Compare commits
	
		
			59 Commits
		
	
	
		
			version/20
			...
			version-20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e095e9f694 | |||
| 10e311534f | |||
| 46fdb45273 | |||
| 6d4125cb90 | |||
| bc83176962 | |||
| 0fa8432b72 | |||
| bb9a524b53 | |||
| d31c05625b | |||
| 399223b770 | |||
| 19197d3f9b | |||
| 1cd000dfe2 | |||
| 00ae97944a | |||
| 9f3ccfb7c7 | |||
| 9ed9c39ac8 | |||
| 30b6eeee9f | |||
| afe2621783 | |||
| 8b12c6a01a | |||
| f63adfed96 | |||
| 9c8fec21cf | |||
| 4776d2bcc5 | |||
| a15a040362 | |||
| fcd6dc1d60 | |||
| acc3b59869 | |||
| d9d5ac10e6 | |||
| 750669dcab | |||
| 88a3eed67e | |||
| 6c214fffc4 | |||
| 70100fc105 | |||
| 3c1163fabd | |||
| 539e8242ff | |||
| 2648333590 | |||
| fe828ef993 | |||
| 29a6530742 | |||
| a6b9274c4f | |||
| a2a67161ac | |||
| 2e8263a99b | |||
| 6b9afed21f | |||
| 1eb1f4e0b8 | |||
| 7c3d60ec3a | |||
| a494c6b6e8 | |||
| 6604d3577f | |||
| f8bfa7e16a | |||
| ea6cf6eabf | |||
| 769ce3ce7b | |||
| 3891fb3fa8 | |||
| 41eb965350 | |||
| 8d95612287 | |||
| 82b5274b15 | |||
| af56ce3d78 | |||
| f5c6e7aeb0 | |||
| 3809400e93 | |||
| 1def9865cf | |||
| 3716298639 | |||
| c16317d7cf | |||
| bbb8fa8269 | |||
| e4c251a178 | |||
| 0fefd5f522 | |||
| 88057db0b0 | |||
| 91cb6c9beb | 
@ -1,5 +1,5 @@
 | 
				
			|||||||
[bumpversion]
 | 
					[bumpversion]
 | 
				
			||||||
current_version = 2023.10.2
 | 
					current_version = 2023.10.7
 | 
				
			||||||
tag = True
 | 
					tag = True
 | 
				
			||||||
commit = True
 | 
					commit = True
 | 
				
			||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
 | 
					parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										21
									
								
								.github/actions/setup/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								.github/actions/setup/action.yml
									
									
									
									
										vendored
									
									
								
							@ -2,36 +2,39 @@ name: "Setup authentik testing environment"
 | 
				
			|||||||
description: "Setup authentik testing environment"
 | 
					description: "Setup authentik testing environment"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
inputs:
 | 
					inputs:
 | 
				
			||||||
  postgresql_tag:
 | 
					  postgresql_version:
 | 
				
			||||||
    description: "Optional postgresql image tag"
 | 
					    description: "Optional postgresql image tag"
 | 
				
			||||||
    default: "12"
 | 
					    default: "12"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
runs:
 | 
					runs:
 | 
				
			||||||
  using: "composite"
 | 
					  using: "composite"
 | 
				
			||||||
  steps:
 | 
					  steps:
 | 
				
			||||||
    - name: Install poetry
 | 
					    - name: Install poetry & deps
 | 
				
			||||||
      shell: bash
 | 
					      shell: bash
 | 
				
			||||||
      run: |
 | 
					      run: |
 | 
				
			||||||
        pipx install poetry || true
 | 
					        pipx install poetry || true
 | 
				
			||||||
        sudo apt update
 | 
					        sudo apt-get update
 | 
				
			||||||
        sudo apt install -y libpq-dev openssl libxmlsec1-dev pkg-config gettext
 | 
					        sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext
 | 
				
			||||||
    - name: Setup python and restore poetry
 | 
					    - name: Setup python and restore poetry
 | 
				
			||||||
      uses: actions/setup-python@v3
 | 
					      uses: actions/setup-python@v4
 | 
				
			||||||
      with:
 | 
					      with:
 | 
				
			||||||
        python-version: "3.11"
 | 
					        python-version-file: 'pyproject.toml'
 | 
				
			||||||
        cache: "poetry"
 | 
					        cache: "poetry"
 | 
				
			||||||
    - name: Setup node
 | 
					    - name: Setup node
 | 
				
			||||||
      uses: actions/setup-node@v3
 | 
					      uses: actions/setup-node@v3
 | 
				
			||||||
      with:
 | 
					      with:
 | 
				
			||||||
        node-version: "20"
 | 
					        node-version-file: web/package.json
 | 
				
			||||||
        cache: "npm"
 | 
					        cache: "npm"
 | 
				
			||||||
        cache-dependency-path: web/package-lock.json
 | 
					        cache-dependency-path: web/package-lock.json
 | 
				
			||||||
 | 
					    - name: Setup go
 | 
				
			||||||
 | 
					      uses: actions/setup-go@v4
 | 
				
			||||||
 | 
					      with:
 | 
				
			||||||
 | 
					        go-version-file: "go.mod"
 | 
				
			||||||
    - name: Setup dependencies
 | 
					    - name: Setup dependencies
 | 
				
			||||||
      shell: bash
 | 
					      shell: bash
 | 
				
			||||||
      run: |
 | 
					      run: |
 | 
				
			||||||
        export PSQL_TAG=${{ inputs.postgresql_tag }}
 | 
					        export PSQL_TAG=${{ inputs.postgresql_version }}
 | 
				
			||||||
        docker-compose -f .github/actions/setup/docker-compose.yml up -d
 | 
					        docker-compose -f .github/actions/setup/docker-compose.yml up -d
 | 
				
			||||||
        poetry env use python3.11
 | 
					 | 
				
			||||||
        poetry install
 | 
					        poetry install
 | 
				
			||||||
        cd web && npm ci
 | 
					        cd web && npm ci
 | 
				
			||||||
    - name: Generate config
 | 
					    - name: Generate config
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										30
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										30
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							@ -11,6 +11,7 @@ on:
 | 
				
			|||||||
  pull_request:
 | 
					  pull_request:
 | 
				
			||||||
    branches:
 | 
					    branches:
 | 
				
			||||||
      - main
 | 
					      - main
 | 
				
			||||||
 | 
					      - version-*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
env:
 | 
					env:
 | 
				
			||||||
  POSTGRES_DB: authentik
 | 
					  POSTGRES_DB: authentik
 | 
				
			||||||
@ -47,25 +48,38 @@ jobs:
 | 
				
			|||||||
      - name: run migrations
 | 
					      - name: run migrations
 | 
				
			||||||
        run: poetry run python -m lifecycle.migrate
 | 
					        run: poetry run python -m lifecycle.migrate
 | 
				
			||||||
  test-migrations-from-stable:
 | 
					  test-migrations-from-stable:
 | 
				
			||||||
 | 
					    name: test-migrations-from-stable - PostgreSQL ${{ matrix.psql }}
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
    continue-on-error: true
 | 
					    strategy:
 | 
				
			||||||
 | 
					      fail-fast: false
 | 
				
			||||||
 | 
					      matrix:
 | 
				
			||||||
 | 
					        psql:
 | 
				
			||||||
 | 
					          - 12-alpine
 | 
				
			||||||
 | 
					          - 15-alpine
 | 
				
			||||||
 | 
					          - 16-alpine
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          fetch-depth: 0
 | 
					          fetch-depth: 0
 | 
				
			||||||
      - name: Setup authentik env
 | 
					      - name: Setup authentik env
 | 
				
			||||||
        uses: ./.github/actions/setup
 | 
					        uses: ./.github/actions/setup
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          postgresql_version: ${{ matrix.psql }}
 | 
				
			||||||
      - name: checkout stable
 | 
					      - name: checkout stable
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
 | 
					          # Delete all poetry envs
 | 
				
			||||||
 | 
					          rm -rf /home/runner/.cache/pypoetry
 | 
				
			||||||
          # Copy current, latest config to local
 | 
					          # Copy current, latest config to local
 | 
				
			||||||
          cp authentik/lib/default.yml local.env.yml
 | 
					          cp authentik/lib/default.yml local.env.yml
 | 
				
			||||||
          cp -R .github ..
 | 
					          cp -R .github ..
 | 
				
			||||||
          cp -R scripts ..
 | 
					          cp -R scripts ..
 | 
				
			||||||
          git checkout $(git describe --tags $(git rev-list --tags --max-count=1))
 | 
					          git checkout version/$(python -c "from authentik import __version__; print(__version__)")
 | 
				
			||||||
          rm -rf .github/ scripts/
 | 
					          rm -rf .github/ scripts/
 | 
				
			||||||
          mv ../.github ../scripts .
 | 
					          mv ../.github ../scripts .
 | 
				
			||||||
      - name: Setup authentik env (ensure stable deps are installed)
 | 
					      - name: Setup authentik env (ensure stable deps are installed)
 | 
				
			||||||
        uses: ./.github/actions/setup
 | 
					        uses: ./.github/actions/setup
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          postgresql_version: ${{ matrix.psql }}
 | 
				
			||||||
      - name: run migrations to stable
 | 
					      - name: run migrations to stable
 | 
				
			||||||
        run: poetry run python -m lifecycle.migrate
 | 
					        run: poetry run python -m lifecycle.migrate
 | 
				
			||||||
      - name: checkout current code
 | 
					      - name: checkout current code
 | 
				
			||||||
@ -75,9 +89,13 @@ jobs:
 | 
				
			|||||||
          git reset --hard HEAD
 | 
					          git reset --hard HEAD
 | 
				
			||||||
          git clean -d -fx .
 | 
					          git clean -d -fx .
 | 
				
			||||||
          git checkout $GITHUB_SHA
 | 
					          git checkout $GITHUB_SHA
 | 
				
			||||||
 | 
					          # Delete previous poetry env
 | 
				
			||||||
 | 
					          rm -rf $(poetry env info --path)
 | 
				
			||||||
          poetry install
 | 
					          poetry install
 | 
				
			||||||
      - name: Setup authentik env (ensure latest deps are installed)
 | 
					      - name: Setup authentik env (ensure latest deps are installed)
 | 
				
			||||||
        uses: ./.github/actions/setup
 | 
					        uses: ./.github/actions/setup
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          postgresql_version: ${{ matrix.psql }}
 | 
				
			||||||
      - name: migrate to latest
 | 
					      - name: migrate to latest
 | 
				
			||||||
        run: poetry run python -m lifecycle.migrate
 | 
					        run: poetry run python -m lifecycle.migrate
 | 
				
			||||||
  test-unittest:
 | 
					  test-unittest:
 | 
				
			||||||
@ -96,7 +114,7 @@ jobs:
 | 
				
			|||||||
      - name: Setup authentik env
 | 
					      - name: Setup authentik env
 | 
				
			||||||
        uses: ./.github/actions/setup
 | 
					        uses: ./.github/actions/setup
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          postgresql_tag: ${{ matrix.psql }}
 | 
					          postgresql_version: ${{ matrix.psql }}
 | 
				
			||||||
      - name: run unittest
 | 
					      - name: run unittest
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          poetry run make test
 | 
					          poetry run make test
 | 
				
			||||||
@ -185,6 +203,9 @@ jobs:
 | 
				
			|||||||
  build:
 | 
					  build:
 | 
				
			||||||
    needs: ci-core-mark
 | 
					    needs: ci-core-mark
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    permissions:
 | 
				
			||||||
 | 
					      # Needed to upload contianer images to ghcr.io
 | 
				
			||||||
 | 
					      packages: write
 | 
				
			||||||
    timeout-minutes: 120
 | 
					    timeout-minutes: 120
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
@ -235,6 +256,9 @@ jobs:
 | 
				
			|||||||
  build-arm64:
 | 
					  build-arm64:
 | 
				
			||||||
    needs: ci-core-mark
 | 
					    needs: ci-core-mark
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    permissions:
 | 
				
			||||||
 | 
					      # Needed to upload contianer images to ghcr.io
 | 
				
			||||||
 | 
					      packages: write
 | 
				
			||||||
    timeout-minutes: 120
 | 
					    timeout-minutes: 120
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										6
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							@ -9,6 +9,7 @@ on:
 | 
				
			|||||||
  pull_request:
 | 
					  pull_request:
 | 
				
			||||||
    branches:
 | 
					    branches:
 | 
				
			||||||
      - main
 | 
					      - main
 | 
				
			||||||
 | 
					      - version-*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  lint-golint:
 | 
					  lint-golint:
 | 
				
			||||||
@ -65,6 +66,9 @@ jobs:
 | 
				
			|||||||
          - ldap
 | 
					          - ldap
 | 
				
			||||||
          - radius
 | 
					          - radius
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    permissions:
 | 
				
			||||||
 | 
					      # Needed to upload contianer images to ghcr.io
 | 
				
			||||||
 | 
					      packages: write
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
@ -126,7 +130,7 @@ jobs:
 | 
				
			|||||||
          go-version-file: "go.mod"
 | 
					          go-version-file: "go.mod"
 | 
				
			||||||
      - uses: actions/setup-node@v4
 | 
					      - uses: actions/setup-node@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: "20"
 | 
					          node-version-file: web/package.json
 | 
				
			||||||
          cache: "npm"
 | 
					          cache: "npm"
 | 
				
			||||||
          cache-dependency-path: web/package-lock.json
 | 
					          cache-dependency-path: web/package-lock.json
 | 
				
			||||||
      - name: Generate API
 | 
					      - name: Generate API
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										11
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
								
							@ -9,6 +9,7 @@ on:
 | 
				
			|||||||
  pull_request:
 | 
					  pull_request:
 | 
				
			||||||
    branches:
 | 
					    branches:
 | 
				
			||||||
      - main
 | 
					      - main
 | 
				
			||||||
 | 
					      - version-*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  lint-eslint:
 | 
					  lint-eslint:
 | 
				
			||||||
@ -23,7 +24,7 @@ jobs:
 | 
				
			|||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
      - uses: actions/setup-node@v4
 | 
					      - uses: actions/setup-node@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: "20"
 | 
					          node-version-file: ${{ matrix.project }}/package.json
 | 
				
			||||||
          cache: "npm"
 | 
					          cache: "npm"
 | 
				
			||||||
          cache-dependency-path: ${{ matrix.project }}/package-lock.json
 | 
					          cache-dependency-path: ${{ matrix.project }}/package-lock.json
 | 
				
			||||||
      - working-directory: ${{ matrix.project }}/
 | 
					      - working-directory: ${{ matrix.project }}/
 | 
				
			||||||
@ -39,7 +40,7 @@ jobs:
 | 
				
			|||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
      - uses: actions/setup-node@v4
 | 
					      - uses: actions/setup-node@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: "20"
 | 
					          node-version-file: web/package.json
 | 
				
			||||||
          cache: "npm"
 | 
					          cache: "npm"
 | 
				
			||||||
          cache-dependency-path: web/package-lock.json
 | 
					          cache-dependency-path: web/package-lock.json
 | 
				
			||||||
      - working-directory: web/
 | 
					      - working-directory: web/
 | 
				
			||||||
@ -61,7 +62,7 @@ jobs:
 | 
				
			|||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
      - uses: actions/setup-node@v4
 | 
					      - uses: actions/setup-node@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: "20"
 | 
					          node-version-file: ${{ matrix.project }}/package.json
 | 
				
			||||||
          cache: "npm"
 | 
					          cache: "npm"
 | 
				
			||||||
          cache-dependency-path: ${{ matrix.project }}/package-lock.json
 | 
					          cache-dependency-path: ${{ matrix.project }}/package-lock.json
 | 
				
			||||||
      - working-directory: ${{ matrix.project }}/
 | 
					      - working-directory: ${{ matrix.project }}/
 | 
				
			||||||
@ -77,7 +78,7 @@ jobs:
 | 
				
			|||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
      - uses: actions/setup-node@v4
 | 
					      - uses: actions/setup-node@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: "20"
 | 
					          node-version-file: web/package.json
 | 
				
			||||||
          cache: "npm"
 | 
					          cache: "npm"
 | 
				
			||||||
          cache-dependency-path: web/package-lock.json
 | 
					          cache-dependency-path: web/package-lock.json
 | 
				
			||||||
      - working-directory: web/
 | 
					      - working-directory: web/
 | 
				
			||||||
@ -109,7 +110,7 @@ jobs:
 | 
				
			|||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
      - uses: actions/setup-node@v4
 | 
					      - uses: actions/setup-node@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: "20"
 | 
					          node-version-file: web/package.json
 | 
				
			||||||
          cache: "npm"
 | 
					          cache: "npm"
 | 
				
			||||||
          cache-dependency-path: web/package-lock.json
 | 
					          cache-dependency-path: web/package-lock.json
 | 
				
			||||||
      - working-directory: web/
 | 
					      - working-directory: web/
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										7
									
								
								.github/workflows/ci-website.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/ci-website.yml
									
									
									
									
										vendored
									
									
								
							@ -9,6 +9,7 @@ on:
 | 
				
			|||||||
  pull_request:
 | 
					  pull_request:
 | 
				
			||||||
    branches:
 | 
					    branches:
 | 
				
			||||||
      - main
 | 
					      - main
 | 
				
			||||||
 | 
					      - version-*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  lint-prettier:
 | 
					  lint-prettier:
 | 
				
			||||||
@ -17,7 +18,7 @@ jobs:
 | 
				
			|||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
      - uses: actions/setup-node@v4
 | 
					      - uses: actions/setup-node@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: "20"
 | 
					          node-version-file: website/package.json
 | 
				
			||||||
          cache: "npm"
 | 
					          cache: "npm"
 | 
				
			||||||
          cache-dependency-path: website/package-lock.json
 | 
					          cache-dependency-path: website/package-lock.json
 | 
				
			||||||
      - working-directory: website/
 | 
					      - working-directory: website/
 | 
				
			||||||
@ -31,7 +32,7 @@ jobs:
 | 
				
			|||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
      - uses: actions/setup-node@v4
 | 
					      - uses: actions/setup-node@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: "20"
 | 
					          node-version-file: website/package.json
 | 
				
			||||||
          cache: "npm"
 | 
					          cache: "npm"
 | 
				
			||||||
          cache-dependency-path: website/package-lock.json
 | 
					          cache-dependency-path: website/package-lock.json
 | 
				
			||||||
      - working-directory: website/
 | 
					      - working-directory: website/
 | 
				
			||||||
@ -52,7 +53,7 @@ jobs:
 | 
				
			|||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
      - uses: actions/setup-node@v4
 | 
					      - uses: actions/setup-node@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: "20"
 | 
					          node-version-file: website/package.json
 | 
				
			||||||
          cache: "npm"
 | 
					          cache: "npm"
 | 
				
			||||||
          cache-dependency-path: website/package-lock.json
 | 
					          cache-dependency-path: website/package-lock.json
 | 
				
			||||||
      - working-directory: website/
 | 
					      - working-directory: website/
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								.github/workflows/release-next-branch.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/release-next-branch.yml
									
									
									
									
										vendored
									
									
								
							@ -6,6 +6,7 @@ on:
 | 
				
			|||||||
  workflow_dispatch:
 | 
					  workflow_dispatch:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
permissions:
 | 
					permissions:
 | 
				
			||||||
 | 
					  # Needed to be able to push to the next branch
 | 
				
			||||||
  contents: write
 | 
					  contents: write
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										11
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							@ -7,6 +7,9 @@ on:
 | 
				
			|||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  build-server:
 | 
					  build-server:
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    permissions:
 | 
				
			||||||
 | 
					      # Needed to upload contianer images to ghcr.io
 | 
				
			||||||
 | 
					      packages: write
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
      - name: Set up QEMU
 | 
					      - name: Set up QEMU
 | 
				
			||||||
@ -52,6 +55,9 @@ jobs:
 | 
				
			|||||||
            VERSION_FAMILY=${{ steps.ev.outputs.versionFamily }}
 | 
					            VERSION_FAMILY=${{ steps.ev.outputs.versionFamily }}
 | 
				
			||||||
  build-outpost:
 | 
					  build-outpost:
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    permissions:
 | 
				
			||||||
 | 
					      # Needed to upload contianer images to ghcr.io
 | 
				
			||||||
 | 
					      packages: write
 | 
				
			||||||
    strategy:
 | 
					    strategy:
 | 
				
			||||||
      fail-fast: false
 | 
					      fail-fast: false
 | 
				
			||||||
      matrix:
 | 
					      matrix:
 | 
				
			||||||
@ -106,6 +112,9 @@ jobs:
 | 
				
			|||||||
  build-outpost-binary:
 | 
					  build-outpost-binary:
 | 
				
			||||||
    timeout-minutes: 120
 | 
					    timeout-minutes: 120
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    permissions:
 | 
				
			||||||
 | 
					      # Needed to upload binaries to the release
 | 
				
			||||||
 | 
					      contents: write
 | 
				
			||||||
    strategy:
 | 
					    strategy:
 | 
				
			||||||
      fail-fast: false
 | 
					      fail-fast: false
 | 
				
			||||||
      matrix:
 | 
					      matrix:
 | 
				
			||||||
@ -122,7 +131,7 @@ jobs:
 | 
				
			|||||||
          go-version-file: "go.mod"
 | 
					          go-version-file: "go.mod"
 | 
				
			||||||
      - uses: actions/setup-node@v4
 | 
					      - uses: actions/setup-node@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: "20"
 | 
					          node-version-file: web/package.json
 | 
				
			||||||
          cache: "npm"
 | 
					          cache: "npm"
 | 
				
			||||||
          cache-dependency-path: web/package-lock.json
 | 
					          cache-dependency-path: web/package-lock.json
 | 
				
			||||||
      - name: Build web
 | 
					      - name: Build web
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.github/workflows/repo-stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/repo-stale.yml
									
									
									
									
										vendored
									
									
								
							@ -6,8 +6,8 @@ on:
 | 
				
			|||||||
  workflow_dispatch:
 | 
					  workflow_dispatch:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
permissions:
 | 
					permissions:
 | 
				
			||||||
 | 
					  # Needed to update issues and PRs
 | 
				
			||||||
  issues: write
 | 
					  issues: write
 | 
				
			||||||
  pull-requests: write
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  stale:
 | 
					  stale:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.github/workflows/web-api-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/web-api-publish.yml
									
									
									
									
										vendored
									
									
								
							@ -19,7 +19,7 @@ jobs:
 | 
				
			|||||||
          token: ${{ steps.generate_token.outputs.token }}
 | 
					          token: ${{ steps.generate_token.outputs.token }}
 | 
				
			||||||
      - uses: actions/setup-node@v4
 | 
					      - uses: actions/setup-node@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: "20"
 | 
					          node-version-file: web/package.json
 | 
				
			||||||
          registry-url: "https://registry.npmjs.org"
 | 
					          registry-url: "https://registry.npmjs.org"
 | 
				
			||||||
      - name: Generate API Client
 | 
					      - name: Generate API Client
 | 
				
			||||||
        run: make gen-client-ts
 | 
					        run: make gen-client-ts
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										27
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								Dockerfile
									
									
									
									
									
								
							@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					# syntax=docker/dockerfile:1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 1: Build website
 | 
					# Stage 1: Build website
 | 
				
			||||||
FROM --platform=${BUILDPLATFORM} docker.io/node:21 as website-builder
 | 
					FROM --platform=${BUILDPLATFORM} docker.io/node:21 as website-builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -7,7 +9,7 @@ WORKDIR /work/website
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
RUN --mount=type=bind,target=/work/website/package.json,src=./website/package.json \
 | 
					RUN --mount=type=bind,target=/work/website/package.json,src=./website/package.json \
 | 
				
			||||||
    --mount=type=bind,target=/work/website/package-lock.json,src=./website/package-lock.json \
 | 
					    --mount=type=bind,target=/work/website/package-lock.json,src=./website/package-lock.json \
 | 
				
			||||||
    --mount=type=cache,target=/root/.npm \
 | 
					    --mount=type=cache,id=npm-website,sharing=shared,target=/root/.npm \
 | 
				
			||||||
    npm ci --include=dev
 | 
					    npm ci --include=dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY ./website /work/website/
 | 
					COPY ./website /work/website/
 | 
				
			||||||
@ -25,7 +27,7 @@ WORKDIR /work/web
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
 | 
					RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
 | 
				
			||||||
    --mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
 | 
					    --mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
 | 
				
			||||||
    --mount=type=cache,target=/root/.npm \
 | 
					    --mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
 | 
				
			||||||
    npm ci --include=dev
 | 
					    npm ci --include=dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY ./web /work/web/
 | 
					COPY ./web /work/web/
 | 
				
			||||||
@ -35,7 +37,14 @@ COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
 | 
				
			|||||||
RUN npm run build
 | 
					RUN npm run build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 3: Build go proxy
 | 
					# Stage 3: Build go proxy
 | 
				
			||||||
FROM docker.io/golang:1.21.3-bookworm AS go-builder
 | 
					FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.4-bookworm AS go-builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ARG TARGETOS
 | 
				
			||||||
 | 
					ARG TARGETARCH
 | 
				
			||||||
 | 
					ARG TARGETVARIANT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ARG GOOS=$TARGETOS
 | 
				
			||||||
 | 
					ARG GOARCH=$TARGETARCH
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WORKDIR /go/src/goauthentik.io
 | 
					WORKDIR /go/src/goauthentik.io
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -55,12 +64,12 @@ COPY ./go.sum /go/src/goauthentik.io/go.sum
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
ENV CGO_ENABLED=0
 | 
					ENV CGO_ENABLED=0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUN --mount=type=cache,target=/go/pkg/mod \
 | 
					RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
 | 
				
			||||||
    --mount=type=cache,target=/root/.cache/go-build \
 | 
					    --mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
 | 
				
			||||||
    go build -o /go/authentik ./cmd/server
 | 
					    GOARM="${TARGETVARIANT#v}" go build -o /go/authentik ./cmd/server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 4: MaxMind GeoIP
 | 
					# Stage 4: MaxMind GeoIP
 | 
				
			||||||
FROM ghcr.io/maxmind/geoipupdate:v6.0 as geoip
 | 
					FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v6.0 as geoip
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City"
 | 
					ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City"
 | 
				
			||||||
ENV GEOIPUPDATE_VERBOSE="true"
 | 
					ENV GEOIPUPDATE_VERBOSE="true"
 | 
				
			||||||
@ -82,7 +91,9 @@ ENV VENV_PATH="/ak-root/venv" \
 | 
				
			|||||||
    POETRY_VIRTUALENVS_CREATE=false \
 | 
					    POETRY_VIRTUALENVS_CREATE=false \
 | 
				
			||||||
    PATH="/ak-root/venv/bin:$PATH"
 | 
					    PATH="/ak-root/venv/bin:$PATH"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUN --mount=type=cache,target=/var/cache/apt \
 | 
					RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
 | 
				
			||||||
    apt-get update && \
 | 
					    apt-get update && \
 | 
				
			||||||
    # Required for installing pip packages
 | 
					    # Required for installing pip packages
 | 
				
			||||||
    apt-get install -y --no-install-recommends build-essential pkg-config libxmlsec1-dev zlib1g-dev libpq-dev
 | 
					    apt-get install -y --no-install-recommends build-essential pkg-config libxmlsec1-dev zlib1g-dev libpq-dev
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@
 | 
				
			|||||||
from os import environ
 | 
					from os import environ
 | 
				
			||||||
from typing import Optional
 | 
					from typing import Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__version__ = "2023.10.2"
 | 
					__version__ = "2023.10.7"
 | 
				
			||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
 | 
					ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -21,7 +21,9 @@ _other_urls = []
 | 
				
			|||||||
for _authentik_app in get_apps():
 | 
					for _authentik_app in get_apps():
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        api_urls = import_module(f"{_authentik_app.name}.urls")
 | 
					        api_urls = import_module(f"{_authentik_app.name}.urls")
 | 
				
			||||||
    except (ModuleNotFoundError, ImportError) as exc:
 | 
					    except ModuleNotFoundError:
 | 
				
			||||||
 | 
					        continue
 | 
				
			||||||
 | 
					    except ImportError as exc:
 | 
				
			||||||
        LOGGER.warning("Could not import app's URLs", app_name=_authentik_app.name, exc=exc)
 | 
					        LOGGER.warning("Could not import app's URLs", app_name=_authentik_app.name, exc=exc)
 | 
				
			||||||
        continue
 | 
					        continue
 | 
				
			||||||
    if not hasattr(api_urls, "api_urlpatterns"):
 | 
					    if not hasattr(api_urls, "api_urlpatterns"):
 | 
				
			||||||
 | 
				
			|||||||
@ -40,7 +40,7 @@ class ManagedAppConfig(AppConfig):
 | 
				
			|||||||
                meth()
 | 
					                meth()
 | 
				
			||||||
                self._logger.debug("Successfully reconciled", name=name)
 | 
					                self._logger.debug("Successfully reconciled", name=name)
 | 
				
			||||||
            except (DatabaseError, ProgrammingError, InternalError) as exc:
 | 
					            except (DatabaseError, ProgrammingError, InternalError) as exc:
 | 
				
			||||||
                self._logger.debug("Failed to run reconcile", name=name, exc=exc)
 | 
					                self._logger.warning("Failed to run reconcile", name=name, exc=exc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AuthentikBlueprintsConfig(ManagedAppConfig):
 | 
					class AuthentikBlueprintsConfig(ManagedAppConfig):
 | 
				
			||||||
 | 
				
			|||||||
@ -75,13 +75,13 @@ class BlueprintEventHandler(FileSystemEventHandler):
 | 
				
			|||||||
            return
 | 
					            return
 | 
				
			||||||
        if event.is_directory:
 | 
					        if event.is_directory:
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        if isinstance(event, FileCreatedEvent):
 | 
					 | 
				
			||||||
            LOGGER.debug("new blueprint file created, starting discovery")
 | 
					 | 
				
			||||||
            blueprints_discovery.delay()
 | 
					 | 
				
			||||||
        if isinstance(event, FileModifiedEvent):
 | 
					 | 
				
			||||||
            path = Path(event.src_path)
 | 
					 | 
				
			||||||
        root = Path(CONFIG.get("blueprints_dir")).absolute()
 | 
					        root = Path(CONFIG.get("blueprints_dir")).absolute()
 | 
				
			||||||
 | 
					        path = Path(event.src_path).absolute()
 | 
				
			||||||
        rel_path = str(path.relative_to(root))
 | 
					        rel_path = str(path.relative_to(root))
 | 
				
			||||||
 | 
					        if isinstance(event, FileCreatedEvent):
 | 
				
			||||||
 | 
					            LOGGER.debug("new blueprint file created, starting discovery", path=rel_path)
 | 
				
			||||||
 | 
					            blueprints_discovery.delay(rel_path)
 | 
				
			||||||
 | 
					        if isinstance(event, FileModifiedEvent):
 | 
				
			||||||
            for instance in BlueprintInstance.objects.filter(path=rel_path, enabled=True):
 | 
					            for instance in BlueprintInstance.objects.filter(path=rel_path, enabled=True):
 | 
				
			||||||
                LOGGER.debug("modified blueprint file, starting apply", instance=instance)
 | 
					                LOGGER.debug("modified blueprint file, starting apply", instance=instance)
 | 
				
			||||||
                apply_blueprint.delay(instance.pk.hex)
 | 
					                apply_blueprint.delay(instance.pk.hex)
 | 
				
			||||||
@ -98,39 +98,32 @@ def blueprints_find_dict():
 | 
				
			|||||||
    return blueprints
 | 
					    return blueprints
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def blueprints_find():
 | 
					def blueprints_find() -> list[BlueprintFile]:
 | 
				
			||||||
    """Find blueprints and return valid ones"""
 | 
					    """Find blueprints and return valid ones"""
 | 
				
			||||||
    blueprints = []
 | 
					    blueprints = []
 | 
				
			||||||
    root = Path(CONFIG.get("blueprints_dir"))
 | 
					    root = Path(CONFIG.get("blueprints_dir"))
 | 
				
			||||||
    for path in root.rglob("**/*.yaml"):
 | 
					    for path in root.rglob("**/*.yaml"):
 | 
				
			||||||
 | 
					        rel_path = path.relative_to(root)
 | 
				
			||||||
        # Check if any part in the path starts with a dot and assume a hidden file
 | 
					        # Check if any part in the path starts with a dot and assume a hidden file
 | 
				
			||||||
        if any(part for part in path.parts if part.startswith(".")):
 | 
					        if any(part for part in path.parts if part.startswith(".")):
 | 
				
			||||||
            continue
 | 
					            continue
 | 
				
			||||||
        LOGGER.debug("found blueprint", path=str(path))
 | 
					 | 
				
			||||||
        with open(path, "r", encoding="utf-8") as blueprint_file:
 | 
					        with open(path, "r", encoding="utf-8") as blueprint_file:
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                raw_blueprint = load(blueprint_file.read(), BlueprintLoader)
 | 
					                raw_blueprint = load(blueprint_file.read(), BlueprintLoader)
 | 
				
			||||||
            except YAMLError as exc:
 | 
					            except YAMLError as exc:
 | 
				
			||||||
                raw_blueprint = None
 | 
					                raw_blueprint = None
 | 
				
			||||||
                LOGGER.warning("failed to parse blueprint", exc=exc, path=str(path))
 | 
					                LOGGER.warning("failed to parse blueprint", exc=exc, path=str(rel_path))
 | 
				
			||||||
            if not raw_blueprint:
 | 
					            if not raw_blueprint:
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
            metadata = raw_blueprint.get("metadata", None)
 | 
					            metadata = raw_blueprint.get("metadata", None)
 | 
				
			||||||
            version = raw_blueprint.get("version", 1)
 | 
					            version = raw_blueprint.get("version", 1)
 | 
				
			||||||
            if version != 1:
 | 
					            if version != 1:
 | 
				
			||||||
                LOGGER.warning("invalid blueprint version", version=version, path=str(path))
 | 
					                LOGGER.warning("invalid blueprint version", version=version, path=str(rel_path))
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
        file_hash = sha512(path.read_bytes()).hexdigest()
 | 
					        file_hash = sha512(path.read_bytes()).hexdigest()
 | 
				
			||||||
        blueprint = BlueprintFile(
 | 
					        blueprint = BlueprintFile(str(rel_path), version, file_hash, int(path.stat().st_mtime))
 | 
				
			||||||
            str(path.relative_to(root)), version, file_hash, int(path.stat().st_mtime)
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        blueprint.meta = from_dict(BlueprintMetadata, metadata) if metadata else None
 | 
					        blueprint.meta = from_dict(BlueprintMetadata, metadata) if metadata else None
 | 
				
			||||||
        blueprints.append(blueprint)
 | 
					        blueprints.append(blueprint)
 | 
				
			||||||
        LOGGER.debug(
 | 
					 | 
				
			||||||
            "parsed & loaded blueprint",
 | 
					 | 
				
			||||||
            hash=file_hash,
 | 
					 | 
				
			||||||
            path=str(path),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    return blueprints
 | 
					    return blueprints
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -138,10 +131,12 @@ def blueprints_find():
 | 
				
			|||||||
    throws=(DatabaseError, ProgrammingError, InternalError), base=MonitoredTask, bind=True
 | 
					    throws=(DatabaseError, ProgrammingError, InternalError), base=MonitoredTask, bind=True
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@prefill_task
 | 
					@prefill_task
 | 
				
			||||||
def blueprints_discovery(self: MonitoredTask):
 | 
					def blueprints_discovery(self: MonitoredTask, path: Optional[str] = None):
 | 
				
			||||||
    """Find blueprints and check if they need to be created in the database"""
 | 
					    """Find blueprints and check if they need to be created in the database"""
 | 
				
			||||||
    count = 0
 | 
					    count = 0
 | 
				
			||||||
    for blueprint in blueprints_find():
 | 
					    for blueprint in blueprints_find():
 | 
				
			||||||
 | 
					        if path and blueprint.path != path:
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
        check_blueprint_v1_file(blueprint)
 | 
					        check_blueprint_v1_file(blueprint)
 | 
				
			||||||
        count += 1
 | 
					        count += 1
 | 
				
			||||||
    self.set_status(
 | 
					    self.set_status(
 | 
				
			||||||
@ -171,7 +166,11 @@ def check_blueprint_v1_file(blueprint: BlueprintFile):
 | 
				
			|||||||
            metadata={},
 | 
					            metadata={},
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        instance.save()
 | 
					        instance.save()
 | 
				
			||||||
 | 
					        LOGGER.info(
 | 
				
			||||||
 | 
					            "Creating new blueprint instance from file", instance=instance, path=instance.path
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
    if instance.last_applied_hash != blueprint.hash:
 | 
					    if instance.last_applied_hash != blueprint.hash:
 | 
				
			||||||
 | 
					        LOGGER.info("Applying blueprint due to changed file", instance=instance, path=instance.path)
 | 
				
			||||||
        apply_blueprint.delay(str(instance.pk))
 | 
					        apply_blueprint.delay(str(instance.pk))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -38,7 +38,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    managed = ReadOnlyField()
 | 
					    managed = ReadOnlyField()
 | 
				
			||||||
    component = SerializerMethodField()
 | 
					    component = SerializerMethodField()
 | 
				
			||||||
    icon = ReadOnlyField(source="get_icon")
 | 
					    icon = ReadOnlyField(source="icon_url")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_component(self, obj: Source) -> str:
 | 
					    def get_component(self, obj: Source) -> str:
 | 
				
			||||||
        """Get object component so that we know how to edit the object"""
 | 
					        """Get object component so that we know how to edit the object"""
 | 
				
			||||||
 | 
				
			|||||||
@ -171,6 +171,11 @@ class UserSerializer(ModelSerializer):
 | 
				
			|||||||
            raise ValidationError("Setting a user to internal service account is not allowed.")
 | 
					            raise ValidationError("Setting a user to internal service account is not allowed.")
 | 
				
			||||||
        return user_type
 | 
					        return user_type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def validate(self, attrs: dict) -> dict:
 | 
				
			||||||
 | 
					        if self.instance and self.instance.type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
 | 
				
			||||||
 | 
					            raise ValidationError("Can't modify internal service account users")
 | 
				
			||||||
 | 
					        return super().validate(attrs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = User
 | 
					        model = User
 | 
				
			||||||
        fields = [
 | 
					        fields = [
 | 
				
			||||||
@ -228,9 +233,9 @@ class UserSelfSerializer(ModelSerializer):
 | 
				
			|||||||
    def get_system_permissions(self, user: User) -> list[str]:
 | 
					    def get_system_permissions(self, user: User) -> list[str]:
 | 
				
			||||||
        """Get all system permissions assigned to the user"""
 | 
					        """Get all system permissions assigned to the user"""
 | 
				
			||||||
        return list(
 | 
					        return list(
 | 
				
			||||||
            user.user_permissions.filter(
 | 
					            x.split(".", maxsplit=1)[1]
 | 
				
			||||||
                content_type__app_label="authentik_rbac", content_type__model="systempermission"
 | 
					            for x in user.get_all_permissions()
 | 
				
			||||||
            ).values_list("codename", flat=True)
 | 
					            if x.startswith("authentik_rbac")
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
				
			|||||||
@ -44,6 +44,7 @@ class PropertyMappingEvaluator(BaseEvaluator):
 | 
				
			|||||||
        if request:
 | 
					        if request:
 | 
				
			||||||
            req.http_request = request
 | 
					            req.http_request = request
 | 
				
			||||||
        self._context["request"] = req
 | 
					        self._context["request"] = req
 | 
				
			||||||
 | 
					        req.context.update(**kwargs)
 | 
				
			||||||
        self._context.update(**kwargs)
 | 
					        self._context.update(**kwargs)
 | 
				
			||||||
        self.dry_run = dry_run
 | 
					        self.dry_run = dry_run
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -17,9 +17,15 @@ class Command(BaseCommand):
 | 
				
			|||||||
    """Run worker"""
 | 
					    """Run worker"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_arguments(self, parser):
 | 
					    def add_arguments(self, parser):
 | 
				
			||||||
        parser.add_argument("-b", "--beat", action="store_true")
 | 
					        parser.add_argument(
 | 
				
			||||||
 | 
					            "-b",
 | 
				
			||||||
 | 
					            "--beat",
 | 
				
			||||||
 | 
					            action="store_false",
 | 
				
			||||||
 | 
					            help="When set, this worker will _not_ run Beat (scheduled) tasks",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def handle(self, **options):
 | 
					    def handle(self, **options):
 | 
				
			||||||
 | 
					        LOGGER.debug("Celery options", **options)
 | 
				
			||||||
        close_old_connections()
 | 
					        close_old_connections()
 | 
				
			||||||
        if CONFIG.get_bool("remote_debug"):
 | 
					        if CONFIG.get_bool("remote_debug"):
 | 
				
			||||||
            import debugpy
 | 
					            import debugpy
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,6 @@
 | 
				
			|||||||
        {% block head_before %}
 | 
					        {% block head_before %}
 | 
				
			||||||
        {% endblock %}
 | 
					        {% endblock %}
 | 
				
			||||||
        <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
 | 
					        <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
 | 
				
			||||||
        <link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
 | 
					 | 
				
			||||||
        <link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
 | 
					        <link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
 | 
				
			||||||
        <script src="{% static 'dist/poly.js' %}?version={{ version }}" type="module"></script>
 | 
					        <script src="{% static 'dist/poly.js' %}?version={{ version }}" type="module"></script>
 | 
				
			||||||
        <script src="{% static 'dist/standalone/loading/index.js' %}?version={{ version }}" type="module"></script>
 | 
					        <script src="{% static 'dist/standalone/loading/index.js' %}?version={{ version }}" type="module"></script>
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,7 @@
 | 
				
			|||||||
{% block head_before %}
 | 
					{% block head_before %}
 | 
				
			||||||
<link rel="prefetch" href="/static/dist/assets/images/flow_background.jpg" />
 | 
					<link rel="prefetch" href="/static/dist/assets/images/flow_background.jpg" />
 | 
				
			||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
 | 
					<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
 | 
				
			||||||
 | 
					<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
 | 
				
			||||||
{% include "base/header_js.html" %}
 | 
					{% include "base/header_js.html" %}
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -27,6 +27,7 @@ from authentik.lib.sentry import before_send
 | 
				
			|||||||
from authentik.lib.utils.errors import exception_to_string
 | 
					from authentik.lib.utils.errors import exception_to_string
 | 
				
			||||||
from authentik.outposts.models import OutpostServiceConnection
 | 
					from authentik.outposts.models import OutpostServiceConnection
 | 
				
			||||||
from authentik.policies.models import Policy, PolicyBindingModel
 | 
					from authentik.policies.models import Policy, PolicyBindingModel
 | 
				
			||||||
 | 
					from authentik.policies.reputation.models import Reputation
 | 
				
			||||||
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
 | 
					from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
 | 
				
			||||||
from authentik.providers.scim.models import SCIMGroup, SCIMUser
 | 
					from authentik.providers.scim.models import SCIMGroup, SCIMUser
 | 
				
			||||||
from authentik.stages.authenticator_static.models import StaticToken
 | 
					from authentik.stages.authenticator_static.models import StaticToken
 | 
				
			||||||
@ -52,11 +53,13 @@ IGNORED_MODELS = (
 | 
				
			|||||||
    RefreshToken,
 | 
					    RefreshToken,
 | 
				
			||||||
    SCIMUser,
 | 
					    SCIMUser,
 | 
				
			||||||
    SCIMGroup,
 | 
					    SCIMGroup,
 | 
				
			||||||
 | 
					    Reputation,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def should_log_model(model: Model) -> bool:
 | 
					def should_log_model(model: Model) -> bool:
 | 
				
			||||||
    """Return true if operation on `model` should be logged"""
 | 
					    """Return true if operation on `model` should be logged"""
 | 
				
			||||||
 | 
					    # Check for silk by string so this comparison doesn't fail when silk isn't installed
 | 
				
			||||||
    if model.__module__.startswith("silk"):
 | 
					    if model.__module__.startswith("silk"):
 | 
				
			||||||
        return False
 | 
					        return False
 | 
				
			||||||
    return model.__class__ not in IGNORED_MODELS
 | 
					    return model.__class__ not in IGNORED_MODELS
 | 
				
			||||||
@ -93,21 +96,30 @@ class AuditMiddleware:
 | 
				
			|||||||
    of models"""
 | 
					    of models"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    get_response: Callable[[HttpRequest], HttpResponse]
 | 
					    get_response: Callable[[HttpRequest], HttpResponse]
 | 
				
			||||||
 | 
					    anonymous_user: User = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
 | 
					    def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
 | 
				
			||||||
        self.get_response = get_response
 | 
					        self.get_response = get_response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _ensure_fallback_user(self):
 | 
				
			||||||
 | 
					        """Defer fetching anonymous user until we have to"""
 | 
				
			||||||
 | 
					        if self.anonymous_user:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        from guardian.shortcuts import get_anonymous_user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.anonymous_user = get_anonymous_user()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def connect(self, request: HttpRequest):
 | 
					    def connect(self, request: HttpRequest):
 | 
				
			||||||
        """Connect signal for automatic logging"""
 | 
					        """Connect signal for automatic logging"""
 | 
				
			||||||
        if not hasattr(request, "user"):
 | 
					        self._ensure_fallback_user()
 | 
				
			||||||
            return
 | 
					        user = getattr(request, "user", self.anonymous_user)
 | 
				
			||||||
        if not getattr(request.user, "is_authenticated", False):
 | 
					        if not user.is_authenticated:
 | 
				
			||||||
            return
 | 
					            user = self.anonymous_user
 | 
				
			||||||
        if not hasattr(request, "request_id"):
 | 
					        if not hasattr(request, "request_id"):
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        post_save_handler = partial(self.post_save_handler, user=request.user, request=request)
 | 
					        post_save_handler = partial(self.post_save_handler, user=user, request=request)
 | 
				
			||||||
        pre_delete_handler = partial(self.pre_delete_handler, user=request.user, request=request)
 | 
					        pre_delete_handler = partial(self.pre_delete_handler, user=user, request=request)
 | 
				
			||||||
        m2m_changed_handler = partial(self.m2m_changed_handler, user=request.user, request=request)
 | 
					        m2m_changed_handler = partial(self.m2m_changed_handler, user=user, request=request)
 | 
				
			||||||
        post_save.connect(
 | 
					        post_save.connect(
 | 
				
			||||||
            post_save_handler,
 | 
					            post_save_handler,
 | 
				
			||||||
            dispatch_uid=request.request_id,
 | 
					            dispatch_uid=request.request_id,
 | 
				
			||||||
 | 
				
			|||||||
@ -217,6 +217,7 @@ class Event(SerializerModel, ExpiringModel):
 | 
				
			|||||||
                "path": request.path,
 | 
					                "path": request.path,
 | 
				
			||||||
                "method": request.method,
 | 
					                "method": request.method,
 | 
				
			||||||
                "args": cleanse_dict(QueryDict(request.META.get("QUERY_STRING", ""))),
 | 
					                "args": cleanse_dict(QueryDict(request.META.get("QUERY_STRING", ""))),
 | 
				
			||||||
 | 
					                "user_agent": request.META.get("HTTP_USER_AGENT", ""),
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            # Special case for events created during flow execution
 | 
					            # Special case for events created during flow execution
 | 
				
			||||||
            # since they keep the http query within a wrapped query
 | 
					            # since they keep the http query within a wrapped query
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,7 @@ from authentik.events.tasks import event_notification_handler, gdpr_cleanup
 | 
				
			|||||||
from authentik.flows.models import Stage
 | 
					from authentik.flows.models import Stage
 | 
				
			||||||
from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
 | 
					from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
 | 
				
			||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
 | 
					from authentik.flows.views.executor import SESSION_KEY_PLAN
 | 
				
			||||||
 | 
					from authentik.lib.config import CONFIG
 | 
				
			||||||
from authentik.stages.invitation.models import Invitation
 | 
					from authentik.stages.invitation.models import Invitation
 | 
				
			||||||
from authentik.stages.invitation.signals import invitation_used
 | 
					from authentik.stages.invitation.signals import invitation_used
 | 
				
			||||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
 | 
					from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
 | 
				
			||||||
@ -92,4 +93,5 @@ def event_post_save_notification(sender, instance: Event, **_):
 | 
				
			|||||||
@receiver(pre_delete, sender=User)
 | 
					@receiver(pre_delete, sender=User)
 | 
				
			||||||
def event_user_pre_delete_cleanup(sender, instance: User, **_):
 | 
					def event_user_pre_delete_cleanup(sender, instance: User, **_):
 | 
				
			||||||
    """If gdpr_compliance is enabled, remove all the user's events"""
 | 
					    """If gdpr_compliance is enabled, remove all the user's events"""
 | 
				
			||||||
 | 
					    if CONFIG.get_bool("gdpr_compliance", True):
 | 
				
			||||||
        gdpr_cleanup.delay(instance.pk)
 | 
					        gdpr_cleanup.delay(instance.pk)
 | 
				
			||||||
 | 
				
			|||||||
@ -53,7 +53,15 @@ class TestEvents(TestCase):
 | 
				
			|||||||
        """Test plain from_http"""
 | 
					        """Test plain from_http"""
 | 
				
			||||||
        event = Event.new("unittest").from_http(self.factory.get("/"))
 | 
					        event = Event.new("unittest").from_http(self.factory.get("/"))
 | 
				
			||||||
        self.assertEqual(
 | 
					        self.assertEqual(
 | 
				
			||||||
            event.context, {"http_request": {"args": {}, "method": "GET", "path": "/"}}
 | 
					            event.context,
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "http_request": {
 | 
				
			||||||
 | 
					                    "args": {},
 | 
				
			||||||
 | 
					                    "method": "GET",
 | 
				
			||||||
 | 
					                    "path": "/",
 | 
				
			||||||
 | 
					                    "user_agent": "",
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_from_http_clean_querystring(self):
 | 
					    def test_from_http_clean_querystring(self):
 | 
				
			||||||
@ -67,6 +75,7 @@ class TestEvents(TestCase):
 | 
				
			|||||||
                    "args": {"token": SafeExceptionReporterFilter.cleansed_substitute},
 | 
					                    "args": {"token": SafeExceptionReporterFilter.cleansed_substitute},
 | 
				
			||||||
                    "method": "GET",
 | 
					                    "method": "GET",
 | 
				
			||||||
                    "path": "/",
 | 
					                    "path": "/",
 | 
				
			||||||
 | 
					                    "user_agent": "",
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@ -83,6 +92,7 @@ class TestEvents(TestCase):
 | 
				
			|||||||
                    "args": {"token": SafeExceptionReporterFilter.cleansed_substitute},
 | 
					                    "args": {"token": SafeExceptionReporterFilter.cleansed_substitute},
 | 
				
			||||||
                    "method": "GET",
 | 
					                    "method": "GET",
 | 
				
			||||||
                    "path": "/",
 | 
					                    "path": "/",
 | 
				
			||||||
 | 
					                    "user_agent": "",
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
				
			|||||||
@ -5,12 +5,13 @@ from dataclasses import asdict, is_dataclass
 | 
				
			|||||||
from datetime import date, datetime, time, timedelta
 | 
					from datetime import date, datetime, time, timedelta
 | 
				
			||||||
from enum import Enum
 | 
					from enum import Enum
 | 
				
			||||||
from pathlib import Path
 | 
					from pathlib import Path
 | 
				
			||||||
from types import GeneratorType
 | 
					from types import GeneratorType, NoneType
 | 
				
			||||||
from typing import Any, Optional
 | 
					from typing import Any, Optional
 | 
				
			||||||
from uuid import UUID
 | 
					from uuid import UUID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.contrib.auth.models import AnonymousUser
 | 
					from django.contrib.auth.models import AnonymousUser
 | 
				
			||||||
from django.core.handlers.wsgi import WSGIRequest
 | 
					from django.core.handlers.wsgi import WSGIRequest
 | 
				
			||||||
 | 
					from django.core.serializers.json import DjangoJSONEncoder
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.db.models.base import Model
 | 
					from django.db.models.base import Model
 | 
				
			||||||
from django.http.request import HttpRequest
 | 
					from django.http.request import HttpRequest
 | 
				
			||||||
@ -153,7 +154,20 @@ def sanitize_item(value: Any) -> Any:
 | 
				
			|||||||
        return value.isoformat()
 | 
					        return value.isoformat()
 | 
				
			||||||
    if isinstance(value, timedelta):
 | 
					    if isinstance(value, timedelta):
 | 
				
			||||||
        return str(value.total_seconds())
 | 
					        return str(value.total_seconds())
 | 
				
			||||||
 | 
					    if callable(value):
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            "type": "callable",
 | 
				
			||||||
 | 
					            "name": value.__name__,
 | 
				
			||||||
 | 
					            "module": value.__module__,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    # List taken from the stdlib's JSON encoder (_make_iterencode, encoder.py:415)
 | 
				
			||||||
 | 
					    if isinstance(value, (bool, int, float, NoneType, list, tuple, dict)):
 | 
				
			||||||
        return value
 | 
					        return value
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        return DjangoJSONEncoder().default(value)
 | 
				
			||||||
 | 
					    except TypeError:
 | 
				
			||||||
 | 
					        return str(value)
 | 
				
			||||||
 | 
					    return str(value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def sanitize_dict(source: dict[Any, Any]) -> dict[Any, Any]:
 | 
					def sanitize_dict(source: dict[Any, Any]) -> dict[Any, Any]:
 | 
				
			||||||
 | 
				
			|||||||
@ -167,7 +167,11 @@ class ChallengeStageView(StageView):
 | 
				
			|||||||
                stage_type=self.__class__.__name__, method="get_challenge"
 | 
					                stage_type=self.__class__.__name__, method="get_challenge"
 | 
				
			||||||
            ).time(),
 | 
					            ).time(),
 | 
				
			||||||
        ):
 | 
					        ):
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
                challenge = self.get_challenge(*args, **kwargs)
 | 
					                challenge = self.get_challenge(*args, **kwargs)
 | 
				
			||||||
 | 
					            except StageInvalidException as exc:
 | 
				
			||||||
 | 
					                self.logger.debug("Got StageInvalidException", exc=exc)
 | 
				
			||||||
 | 
					                return self.executor.stage_invalid()
 | 
				
			||||||
        with Hub.current.start_span(
 | 
					        with Hub.current.start_span(
 | 
				
			||||||
            op="authentik.flow.stage._get_challenge",
 | 
					            op="authentik.flow.stage._get_challenge",
 | 
				
			||||||
            description=self.__class__.__name__,
 | 
					            description=self.__class__.__name__,
 | 
				
			||||||
 | 
				
			|||||||
@ -18,7 +18,7 @@ from authentik.core.api.used_by import UsedByMixin
 | 
				
			|||||||
from authentik.core.api.utils import PassiveSerializer, is_dict
 | 
					from authentik.core.api.utils import PassiveSerializer, is_dict
 | 
				
			||||||
from authentik.core.models import Provider
 | 
					from authentik.core.models import Provider
 | 
				
			||||||
from authentik.outposts.api.service_connections import ServiceConnectionSerializer
 | 
					from authentik.outposts.api.service_connections import ServiceConnectionSerializer
 | 
				
			||||||
from authentik.outposts.apps import MANAGED_OUTPOST
 | 
					from authentik.outposts.apps import MANAGED_OUTPOST, MANAGED_OUTPOST_NAME
 | 
				
			||||||
from authentik.outposts.models import (
 | 
					from authentik.outposts.models import (
 | 
				
			||||||
    Outpost,
 | 
					    Outpost,
 | 
				
			||||||
    OutpostConfig,
 | 
					    OutpostConfig,
 | 
				
			||||||
@ -47,6 +47,16 @@ class OutpostSerializer(ModelSerializer):
 | 
				
			|||||||
        source="service_connection", read_only=True
 | 
					        source="service_connection", read_only=True
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def validate_name(self, name: str) -> str:
 | 
				
			||||||
 | 
					        """Validate name (especially for embedded outpost)"""
 | 
				
			||||||
 | 
					        if not self.instance:
 | 
				
			||||||
 | 
					            return name
 | 
				
			||||||
 | 
					        if self.instance.managed == MANAGED_OUTPOST and name != MANAGED_OUTPOST_NAME:
 | 
				
			||||||
 | 
					            raise ValidationError("Embedded outpost's name cannot be changed")
 | 
				
			||||||
 | 
					        if self.instance.name == MANAGED_OUTPOST_NAME:
 | 
				
			||||||
 | 
					            self.instance.managed = MANAGED_OUTPOST
 | 
				
			||||||
 | 
					        return name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def validate_providers(self, providers: list[Provider]) -> list[Provider]:
 | 
					    def validate_providers(self, providers: list[Provider]) -> list[Provider]:
 | 
				
			||||||
        """Check that all providers match the type of the outpost"""
 | 
					        """Check that all providers match the type of the outpost"""
 | 
				
			||||||
        type_map = {
 | 
					        type_map = {
 | 
				
			||||||
 | 
				
			|||||||
@ -15,6 +15,7 @@ GAUGE_OUTPOSTS_LAST_UPDATE = Gauge(
 | 
				
			|||||||
    ["outpost", "uid", "version"],
 | 
					    ["outpost", "uid", "version"],
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
MANAGED_OUTPOST = "goauthentik.io/outposts/embedded"
 | 
					MANAGED_OUTPOST = "goauthentik.io/outposts/embedded"
 | 
				
			||||||
 | 
					MANAGED_OUTPOST_NAME = "authentik Embedded Outpost"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AuthentikOutpostConfig(ManagedAppConfig):
 | 
					class AuthentikOutpostConfig(ManagedAppConfig):
 | 
				
			||||||
@ -35,14 +36,17 @@ class AuthentikOutpostConfig(ManagedAppConfig):
 | 
				
			|||||||
            DockerServiceConnection,
 | 
					            DockerServiceConnection,
 | 
				
			||||||
            KubernetesServiceConnection,
 | 
					            KubernetesServiceConnection,
 | 
				
			||||||
            Outpost,
 | 
					            Outpost,
 | 
				
			||||||
            OutpostConfig,
 | 
					 | 
				
			||||||
            OutpostType,
 | 
					            OutpostType,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if outpost := Outpost.objects.filter(name=MANAGED_OUTPOST_NAME, managed="").first():
 | 
				
			||||||
 | 
					            outpost.managed = MANAGED_OUTPOST
 | 
				
			||||||
 | 
					            outpost.save()
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
        outpost, updated = Outpost.objects.update_or_create(
 | 
					        outpost, updated = Outpost.objects.update_or_create(
 | 
				
			||||||
            defaults={
 | 
					            defaults={
 | 
				
			||||||
                "name": "authentik Embedded Outpost",
 | 
					 | 
				
			||||||
                "type": OutpostType.PROXY,
 | 
					                "type": OutpostType.PROXY,
 | 
				
			||||||
 | 
					                "name": MANAGED_OUTPOST_NAME,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            managed=MANAGED_OUTPOST,
 | 
					            managed=MANAGED_OUTPOST,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@ -51,10 +55,4 @@ class AuthentikOutpostConfig(ManagedAppConfig):
 | 
				
			|||||||
                outpost.service_connection = KubernetesServiceConnection.objects.first()
 | 
					                outpost.service_connection = KubernetesServiceConnection.objects.first()
 | 
				
			||||||
            elif DockerServiceConnection.objects.exists():
 | 
					            elif DockerServiceConnection.objects.exists():
 | 
				
			||||||
                outpost.service_connection = DockerServiceConnection.objects.first()
 | 
					                outpost.service_connection = DockerServiceConnection.objects.first()
 | 
				
			||||||
            outpost.config = OutpostConfig(
 | 
					 | 
				
			||||||
                kubernetes_disabled_components=[
 | 
					 | 
				
			||||||
                    "deployment",
 | 
					 | 
				
			||||||
                    "secret",
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            outpost.save()
 | 
					            outpost.save()
 | 
				
			||||||
 | 
				
			|||||||
@ -43,6 +43,10 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
 | 
				
			|||||||
        self.api = AppsV1Api(controller.client)
 | 
					        self.api = AppsV1Api(controller.client)
 | 
				
			||||||
        self.outpost = self.controller.outpost
 | 
					        self.outpost = self.controller.outpost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def noop(self) -> bool:
 | 
				
			||||||
 | 
					        return self.is_embedded
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def reconciler_name() -> str:
 | 
					    def reconciler_name() -> str:
 | 
				
			||||||
        return "deployment"
 | 
					        return "deployment"
 | 
				
			||||||
 | 
				
			|||||||
@ -24,6 +24,10 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
 | 
				
			|||||||
        super().__init__(controller)
 | 
					        super().__init__(controller)
 | 
				
			||||||
        self.api = CoreV1Api(controller.client)
 | 
					        self.api = CoreV1Api(controller.client)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def noop(self) -> bool:
 | 
				
			||||||
 | 
					        return self.is_embedded
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def reconciler_name() -> str:
 | 
					    def reconciler_name() -> str:
 | 
				
			||||||
        return "secret"
 | 
					        return "secret"
 | 
				
			||||||
 | 
				
			|||||||
@ -77,7 +77,10 @@ class PrometheusServiceMonitorReconciler(KubernetesObjectReconciler[PrometheusSe
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def noop(self) -> bool:
 | 
					    def noop(self) -> bool:
 | 
				
			||||||
        return (not self._crd_exists()) or (self.is_embedded)
 | 
					        if not self._crd_exists():
 | 
				
			||||||
 | 
					            self.logger.debug("CRD doesn't exist")
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					        return self.is_embedded
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _crd_exists(self) -> bool:
 | 
					    def _crd_exists(self) -> bool:
 | 
				
			||||||
        """Check if the Prometheus ServiceMonitor exists"""
 | 
					        """Check if the Prometheus ServiceMonitor exists"""
 | 
				
			||||||
 | 
				
			|||||||
@ -344,11 +344,21 @@ class Outpost(SerializerModel, ManagedModel):
 | 
				
			|||||||
        user_created = False
 | 
					        user_created = False
 | 
				
			||||||
        if not user:
 | 
					        if not user:
 | 
				
			||||||
            user: User = User.objects.create(username=self.user_identifier)
 | 
					            user: User = User.objects.create(username=self.user_identifier)
 | 
				
			||||||
            user.set_unusable_password()
 | 
					 | 
				
			||||||
            user_created = True
 | 
					            user_created = True
 | 
				
			||||||
        user.type = UserTypes.INTERNAL_SERVICE_ACCOUNT
 | 
					        attrs = {
 | 
				
			||||||
        user.name = f"Outpost {self.name} Service-Account"
 | 
					            "type": UserTypes.INTERNAL_SERVICE_ACCOUNT,
 | 
				
			||||||
        user.path = USER_PATH_OUTPOSTS
 | 
					            "name": f"Outpost {self.name} Service-Account",
 | 
				
			||||||
 | 
					            "path": USER_PATH_OUTPOSTS,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        dirty = False
 | 
				
			||||||
 | 
					        for key, value in attrs.items():
 | 
				
			||||||
 | 
					            if getattr(user, key) != value:
 | 
				
			||||||
 | 
					                dirty = True
 | 
				
			||||||
 | 
					                setattr(user, key, value)
 | 
				
			||||||
 | 
					        if user.has_usable_password():
 | 
				
			||||||
 | 
					            user.set_unusable_password()
 | 
				
			||||||
 | 
					            dirty = True
 | 
				
			||||||
 | 
					        if dirty:
 | 
				
			||||||
            user.save()
 | 
					            user.save()
 | 
				
			||||||
        if user_created:
 | 
					        if user_created:
 | 
				
			||||||
            self.build_user_permissions(user)
 | 
					            self.build_user_permissions(user)
 | 
				
			||||||
 | 
				
			|||||||
@ -2,11 +2,13 @@
 | 
				
			|||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
from rest_framework.test import APITestCase
 | 
					from rest_framework.test import APITestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.blueprints.tests import reconcile_app
 | 
				
			||||||
from authentik.core.models import PropertyMapping
 | 
					from authentik.core.models import PropertyMapping
 | 
				
			||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
 | 
					from authentik.core.tests.utils import create_test_admin_user, create_test_flow
 | 
				
			||||||
from authentik.lib.generators import generate_id
 | 
					from authentik.lib.generators import generate_id
 | 
				
			||||||
from authentik.outposts.api.outposts import OutpostSerializer
 | 
					from authentik.outposts.api.outposts import OutpostSerializer
 | 
				
			||||||
from authentik.outposts.models import OutpostType, default_outpost_config
 | 
					from authentik.outposts.apps import MANAGED_OUTPOST
 | 
				
			||||||
 | 
					from authentik.outposts.models import Outpost, OutpostType, default_outpost_config
 | 
				
			||||||
from authentik.providers.ldap.models import LDAPProvider
 | 
					from authentik.providers.ldap.models import LDAPProvider
 | 
				
			||||||
from authentik.providers.proxy.models import ProxyProvider
 | 
					from authentik.providers.proxy.models import ProxyProvider
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -22,7 +24,36 @@ class TestOutpostServiceConnectionsAPI(APITestCase):
 | 
				
			|||||||
        self.user = create_test_admin_user()
 | 
					        self.user = create_test_admin_user()
 | 
				
			||||||
        self.client.force_login(self.user)
 | 
					        self.client.force_login(self.user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_outpost_validaton(self):
 | 
					    @reconcile_app("authentik_outposts")
 | 
				
			||||||
 | 
					    def test_managed_name_change(self):
 | 
				
			||||||
 | 
					        """Test name change for embedded outpost"""
 | 
				
			||||||
 | 
					        embedded_outpost = Outpost.objects.filter(managed=MANAGED_OUTPOST).first()
 | 
				
			||||||
 | 
					        self.assertIsNotNone(embedded_outpost)
 | 
				
			||||||
 | 
					        response = self.client.patch(
 | 
				
			||||||
 | 
					            reverse("authentik_api:outpost-detail", kwargs={"pk": embedded_outpost.pk}),
 | 
				
			||||||
 | 
					            {"name": "foo"},
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 400)
 | 
				
			||||||
 | 
					        self.assertJSONEqual(
 | 
				
			||||||
 | 
					            response.content, {"name": ["Embedded outpost's name cannot be changed"]}
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @reconcile_app("authentik_outposts")
 | 
				
			||||||
 | 
					    def test_managed_without_managed(self):
 | 
				
			||||||
 | 
					        """Test name change for embedded outpost"""
 | 
				
			||||||
 | 
					        embedded_outpost = Outpost.objects.filter(managed=MANAGED_OUTPOST).first()
 | 
				
			||||||
 | 
					        self.assertIsNotNone(embedded_outpost)
 | 
				
			||||||
 | 
					        embedded_outpost.managed = ""
 | 
				
			||||||
 | 
					        embedded_outpost.save()
 | 
				
			||||||
 | 
					        response = self.client.patch(
 | 
				
			||||||
 | 
					            reverse("authentik_api:outpost-detail", kwargs={"pk": embedded_outpost.pk}),
 | 
				
			||||||
 | 
					            {"name": "foo"},
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					        embedded_outpost.refresh_from_db()
 | 
				
			||||||
 | 
					        self.assertEqual(embedded_outpost.managed, MANAGED_OUTPOST)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_outpost_validation(self):
 | 
				
			||||||
        """Test Outpost validation"""
 | 
					        """Test Outpost validation"""
 | 
				
			||||||
        valid = OutpostSerializer(
 | 
					        valid = OutpostSerializer(
 | 
				
			||||||
            data={
 | 
					            data={
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.0 on 2023-12-22 23:20
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_providers_oauth2", "0016_alter_refreshtoken_token"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="accesstoken",
 | 
				
			||||||
 | 
					            name="session_id",
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, default=""),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="authorizationcode",
 | 
				
			||||||
 | 
					            name="session_id",
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, default=""),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="refreshtoken",
 | 
				
			||||||
 | 
					            name="session_id",
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, default=""),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -296,6 +296,7 @@ class BaseGrantModel(models.Model):
 | 
				
			|||||||
    revoked = models.BooleanField(default=False)
 | 
					    revoked = models.BooleanField(default=False)
 | 
				
			||||||
    _scope = models.TextField(default="", verbose_name=_("Scopes"))
 | 
					    _scope = models.TextField(default="", verbose_name=_("Scopes"))
 | 
				
			||||||
    auth_time = models.DateTimeField(verbose_name="Authentication time")
 | 
					    auth_time = models.DateTimeField(verbose_name="Authentication time")
 | 
				
			||||||
 | 
					    session_id = models.CharField(default="", blank=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def scope(self) -> list[str]:
 | 
					    def scope(self) -> list[str]:
 | 
				
			||||||
 | 
				
			|||||||
@ -85,6 +85,25 @@ class TestAuthorize(OAuthTestCase):
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
            OAuthAuthorizationParams.from_request(request)
 | 
					            OAuthAuthorizationParams.from_request(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_blocked_redirect_uri(self):
 | 
				
			||||||
 | 
					        """test missing/invalid redirect URI"""
 | 
				
			||||||
 | 
					        OAuth2Provider.objects.create(
 | 
				
			||||||
 | 
					            name=generate_id(),
 | 
				
			||||||
 | 
					            client_id="test",
 | 
				
			||||||
 | 
					            authorization_flow=create_test_flow(),
 | 
				
			||||||
 | 
					            redirect_uris="data:local.invalid",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        with self.assertRaises(RedirectUriError):
 | 
				
			||||||
 | 
					            request = self.factory.get(
 | 
				
			||||||
 | 
					                "/",
 | 
				
			||||||
 | 
					                data={
 | 
				
			||||||
 | 
					                    "response_type": "code",
 | 
				
			||||||
 | 
					                    "client_id": "test",
 | 
				
			||||||
 | 
					                    "redirect_uri": "data:localhost",
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            OAuthAuthorizationParams.from_request(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_invalid_redirect_uri_empty(self):
 | 
					    def test_invalid_redirect_uri_empty(self):
 | 
				
			||||||
        """test missing/invalid redirect URI"""
 | 
					        """test missing/invalid redirect URI"""
 | 
				
			||||||
        provider = OAuth2Provider.objects.create(
 | 
					        provider = OAuth2Provider.objects.create(
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										258
									
								
								authentik/providers/oauth2/tests/test_token_pkce.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										258
									
								
								authentik/providers/oauth2/tests/test_token_pkce.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,258 @@
 | 
				
			|||||||
 | 
					"""Test token view"""
 | 
				
			||||||
 | 
					from base64 import b64encode, urlsafe_b64encode
 | 
				
			||||||
 | 
					from hashlib import sha256
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.test import RequestFactory
 | 
				
			||||||
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.core.models import Application
 | 
				
			||||||
 | 
					from authentik.core.tests.utils import create_test_admin_user, create_test_flow
 | 
				
			||||||
 | 
					from authentik.flows.challenge import ChallengeTypes
 | 
				
			||||||
 | 
					from authentik.lib.generators import generate_id
 | 
				
			||||||
 | 
					from authentik.providers.oauth2.constants import GRANT_TYPE_AUTHORIZATION_CODE
 | 
				
			||||||
 | 
					from authentik.providers.oauth2.models import AuthorizationCode, OAuth2Provider
 | 
				
			||||||
 | 
					from authentik.providers.oauth2.tests.utils import OAuthTestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestTokenPKCE(OAuthTestCase):
 | 
				
			||||||
 | 
					    """Test token view"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def setUp(self) -> None:
 | 
				
			||||||
 | 
					        super().setUp()
 | 
				
			||||||
 | 
					        self.factory = RequestFactory()
 | 
				
			||||||
 | 
					        self.app = Application.objects.create(name=generate_id(), slug="test")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_pkce_missing_in_authorize(self):
 | 
				
			||||||
 | 
					        """Test PKCE with code_challenge in authorize request
 | 
				
			||||||
 | 
					        and missing verifier in token request"""
 | 
				
			||||||
 | 
					        flow = create_test_flow()
 | 
				
			||||||
 | 
					        provider = OAuth2Provider.objects.create(
 | 
				
			||||||
 | 
					            name=generate_id(),
 | 
				
			||||||
 | 
					            client_id="test",
 | 
				
			||||||
 | 
					            authorization_flow=flow,
 | 
				
			||||||
 | 
					            redirect_uris="foo://localhost",
 | 
				
			||||||
 | 
					            access_code_validity="seconds=100",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        Application.objects.create(name="app", slug="app", provider=provider)
 | 
				
			||||||
 | 
					        state = generate_id()
 | 
				
			||||||
 | 
					        user = create_test_admin_user()
 | 
				
			||||||
 | 
					        self.client.force_login(user)
 | 
				
			||||||
 | 
					        challenge = generate_id()
 | 
				
			||||||
 | 
					        header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
 | 
				
			||||||
 | 
					        # Step 1, initiate params and get redirect to flow
 | 
				
			||||||
 | 
					        self.client.get(
 | 
				
			||||||
 | 
					            reverse("authentik_providers_oauth2:authorize"),
 | 
				
			||||||
 | 
					            data={
 | 
				
			||||||
 | 
					                "response_type": "code",
 | 
				
			||||||
 | 
					                "client_id": "test",
 | 
				
			||||||
 | 
					                "state": state,
 | 
				
			||||||
 | 
					                "redirect_uri": "foo://localhost",
 | 
				
			||||||
 | 
					                "code_challenge": challenge,
 | 
				
			||||||
 | 
					                "code_challenge_method": "S256",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response = self.client.get(
 | 
				
			||||||
 | 
					            reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
 | 
				
			||||||
 | 
					        self.assertJSONEqual(
 | 
				
			||||||
 | 
					            response.content.decode(),
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "component": "xak-flow-redirect",
 | 
				
			||||||
 | 
					                "type": ChallengeTypes.REDIRECT.value,
 | 
				
			||||||
 | 
					                "to": f"foo://localhost?code={code.code}&state={state}",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response = self.client.post(
 | 
				
			||||||
 | 
					            reverse("authentik_providers_oauth2:token"),
 | 
				
			||||||
 | 
					            data={
 | 
				
			||||||
 | 
					                "grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
 | 
				
			||||||
 | 
					                "code": code.code,
 | 
				
			||||||
 | 
					                # Missing the code_verifier here
 | 
				
			||||||
 | 
					                "redirect_uri": "foo://localhost",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            HTTP_AUTHORIZATION=f"Basic {header}",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertJSONEqual(
 | 
				
			||||||
 | 
					            response.content,
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "error": "invalid_grant",
 | 
				
			||||||
 | 
					                "error_description": (
 | 
				
			||||||
 | 
					                    "The provided authorization grant or refresh token is invalid, expired, "
 | 
				
			||||||
 | 
					                    "revoked, does not match the redirection URI used in the authorization "
 | 
				
			||||||
 | 
					                    "request, or was issued to another client"
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 400)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_pkce_missing_in_token(self):
 | 
				
			||||||
 | 
					        """Test PKCE with missing code_challenge in authorization request but verifier
 | 
				
			||||||
 | 
					        set in token request"""
 | 
				
			||||||
 | 
					        flow = create_test_flow()
 | 
				
			||||||
 | 
					        provider = OAuth2Provider.objects.create(
 | 
				
			||||||
 | 
					            name=generate_id(),
 | 
				
			||||||
 | 
					            client_id="test",
 | 
				
			||||||
 | 
					            authorization_flow=flow,
 | 
				
			||||||
 | 
					            redirect_uris="foo://localhost",
 | 
				
			||||||
 | 
					            access_code_validity="seconds=100",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        Application.objects.create(name="app", slug="app", provider=provider)
 | 
				
			||||||
 | 
					        state = generate_id()
 | 
				
			||||||
 | 
					        user = create_test_admin_user()
 | 
				
			||||||
 | 
					        self.client.force_login(user)
 | 
				
			||||||
 | 
					        header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
 | 
				
			||||||
 | 
					        # Step 1, initiate params and get redirect to flow
 | 
				
			||||||
 | 
					        self.client.get(
 | 
				
			||||||
 | 
					            reverse("authentik_providers_oauth2:authorize"),
 | 
				
			||||||
 | 
					            data={
 | 
				
			||||||
 | 
					                "response_type": "code",
 | 
				
			||||||
 | 
					                "client_id": "test",
 | 
				
			||||||
 | 
					                "state": state,
 | 
				
			||||||
 | 
					                "redirect_uri": "foo://localhost",
 | 
				
			||||||
 | 
					                # "code_challenge": challenge,
 | 
				
			||||||
 | 
					                # "code_challenge_method": "S256",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response = self.client.get(
 | 
				
			||||||
 | 
					            reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
 | 
				
			||||||
 | 
					        self.assertJSONEqual(
 | 
				
			||||||
 | 
					            response.content.decode(),
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "component": "xak-flow-redirect",
 | 
				
			||||||
 | 
					                "type": ChallengeTypes.REDIRECT.value,
 | 
				
			||||||
 | 
					                "to": f"foo://localhost?code={code.code}&state={state}",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response = self.client.post(
 | 
				
			||||||
 | 
					            reverse("authentik_providers_oauth2:token"),
 | 
				
			||||||
 | 
					            data={
 | 
				
			||||||
 | 
					                "grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
 | 
				
			||||||
 | 
					                "code": code.code,
 | 
				
			||||||
 | 
					                "code_verifier": generate_id(),
 | 
				
			||||||
 | 
					                "redirect_uri": "foo://localhost",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            HTTP_AUTHORIZATION=f"Basic {header}",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertJSONEqual(
 | 
				
			||||||
 | 
					            response.content,
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "error": "invalid_grant",
 | 
				
			||||||
 | 
					                "error_description": (
 | 
				
			||||||
 | 
					                    "The provided authorization grant or refresh token is invalid, expired, "
 | 
				
			||||||
 | 
					                    "revoked, does not match the redirection URI used in the authorization "
 | 
				
			||||||
 | 
					                    "request, or was issued to another client"
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 400)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_pkce_correct_s256(self):
 | 
				
			||||||
 | 
					        """Test full with pkce"""
 | 
				
			||||||
 | 
					        flow = create_test_flow()
 | 
				
			||||||
 | 
					        provider = OAuth2Provider.objects.create(
 | 
				
			||||||
 | 
					            name=generate_id(),
 | 
				
			||||||
 | 
					            client_id="test",
 | 
				
			||||||
 | 
					            authorization_flow=flow,
 | 
				
			||||||
 | 
					            redirect_uris="foo://localhost",
 | 
				
			||||||
 | 
					            access_code_validity="seconds=100",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        Application.objects.create(name="app", slug="app", provider=provider)
 | 
				
			||||||
 | 
					        state = generate_id()
 | 
				
			||||||
 | 
					        user = create_test_admin_user()
 | 
				
			||||||
 | 
					        self.client.force_login(user)
 | 
				
			||||||
 | 
					        verifier = generate_id()
 | 
				
			||||||
 | 
					        challenge = (
 | 
				
			||||||
 | 
					            urlsafe_b64encode(sha256(verifier.encode("ascii")).digest())
 | 
				
			||||||
 | 
					            .decode("utf-8")
 | 
				
			||||||
 | 
					            .replace("=", "")
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
 | 
				
			||||||
 | 
					        # Step 1, initiate params and get redirect to flow
 | 
				
			||||||
 | 
					        self.client.get(
 | 
				
			||||||
 | 
					            reverse("authentik_providers_oauth2:authorize"),
 | 
				
			||||||
 | 
					            data={
 | 
				
			||||||
 | 
					                "response_type": "code",
 | 
				
			||||||
 | 
					                "client_id": "test",
 | 
				
			||||||
 | 
					                "state": state,
 | 
				
			||||||
 | 
					                "redirect_uri": "foo://localhost",
 | 
				
			||||||
 | 
					                "code_challenge": challenge,
 | 
				
			||||||
 | 
					                "code_challenge_method": "S256",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response = self.client.get(
 | 
				
			||||||
 | 
					            reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
 | 
				
			||||||
 | 
					        self.assertJSONEqual(
 | 
				
			||||||
 | 
					            response.content.decode(),
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "component": "xak-flow-redirect",
 | 
				
			||||||
 | 
					                "type": ChallengeTypes.REDIRECT.value,
 | 
				
			||||||
 | 
					                "to": f"foo://localhost?code={code.code}&state={state}",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response = self.client.post(
 | 
				
			||||||
 | 
					            reverse("authentik_providers_oauth2:token"),
 | 
				
			||||||
 | 
					            data={
 | 
				
			||||||
 | 
					                "grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
 | 
				
			||||||
 | 
					                "code": code.code,
 | 
				
			||||||
 | 
					                "code_verifier": verifier,
 | 
				
			||||||
 | 
					                "redirect_uri": "foo://localhost",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            HTTP_AUTHORIZATION=f"Basic {header}",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_pkce_correct_plain(self):
 | 
				
			||||||
 | 
					        """Test full with pkce"""
 | 
				
			||||||
 | 
					        flow = create_test_flow()
 | 
				
			||||||
 | 
					        provider = OAuth2Provider.objects.create(
 | 
				
			||||||
 | 
					            name=generate_id(),
 | 
				
			||||||
 | 
					            client_id="test",
 | 
				
			||||||
 | 
					            authorization_flow=flow,
 | 
				
			||||||
 | 
					            redirect_uris="foo://localhost",
 | 
				
			||||||
 | 
					            access_code_validity="seconds=100",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        Application.objects.create(name="app", slug="app", provider=provider)
 | 
				
			||||||
 | 
					        state = generate_id()
 | 
				
			||||||
 | 
					        user = create_test_admin_user()
 | 
				
			||||||
 | 
					        self.client.force_login(user)
 | 
				
			||||||
 | 
					        verifier = generate_id()
 | 
				
			||||||
 | 
					        header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
 | 
				
			||||||
 | 
					        # Step 1, initiate params and get redirect to flow
 | 
				
			||||||
 | 
					        self.client.get(
 | 
				
			||||||
 | 
					            reverse("authentik_providers_oauth2:authorize"),
 | 
				
			||||||
 | 
					            data={
 | 
				
			||||||
 | 
					                "response_type": "code",
 | 
				
			||||||
 | 
					                "client_id": "test",
 | 
				
			||||||
 | 
					                "state": state,
 | 
				
			||||||
 | 
					                "redirect_uri": "foo://localhost",
 | 
				
			||||||
 | 
					                "code_challenge": verifier,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response = self.client.get(
 | 
				
			||||||
 | 
					            reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
 | 
				
			||||||
 | 
					        self.assertJSONEqual(
 | 
				
			||||||
 | 
					            response.content.decode(),
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "component": "xak-flow-redirect",
 | 
				
			||||||
 | 
					                "type": ChallengeTypes.REDIRECT.value,
 | 
				
			||||||
 | 
					                "to": f"foo://localhost?code={code.code}&state={state}",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response = self.client.post(
 | 
				
			||||||
 | 
					            reverse("authentik_providers_oauth2:token"),
 | 
				
			||||||
 | 
					            data={
 | 
				
			||||||
 | 
					                "grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
 | 
				
			||||||
 | 
					                "code": code.code,
 | 
				
			||||||
 | 
					                "code_verifier": verifier,
 | 
				
			||||||
 | 
					                "redirect_uri": "foo://localhost",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            HTTP_AUTHORIZATION=f"Basic {header}",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
@ -188,6 +188,7 @@ def authenticate_provider(request: HttpRequest) -> Optional[OAuth2Provider]:
 | 
				
			|||||||
    if client_id != provider.client_id or client_secret != provider.client_secret:
 | 
					    if client_id != provider.client_id or client_secret != provider.client_secret:
 | 
				
			||||||
        LOGGER.debug("(basic) Provider for basic auth does not exist")
 | 
					        LOGGER.debug("(basic) Provider for basic auth does not exist")
 | 
				
			||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					    CTX_AUTH_VIA.set("oauth_client_secret")
 | 
				
			||||||
    return provider
 | 
					    return provider
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
"""authentik OAuth2 Authorization views"""
 | 
					"""authentik OAuth2 Authorization views"""
 | 
				
			||||||
from dataclasses import dataclass, field
 | 
					from dataclasses import dataclass, field
 | 
				
			||||||
from datetime import timedelta
 | 
					from datetime import timedelta
 | 
				
			||||||
 | 
					from hashlib import sha256
 | 
				
			||||||
from json import dumps
 | 
					from json import dumps
 | 
				
			||||||
from re import error as RegexError
 | 
					from re import error as RegexError
 | 
				
			||||||
from re import fullmatch
 | 
					from re import fullmatch
 | 
				
			||||||
@ -74,6 +75,7 @@ PLAN_CONTEXT_PARAMS = "goauthentik.io/providers/oauth2/params"
 | 
				
			|||||||
SESSION_KEY_LAST_LOGIN_UID = "authentik/providers/oauth2/last_login_uid"
 | 
					SESSION_KEY_LAST_LOGIN_UID = "authentik/providers/oauth2/last_login_uid"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSENT, PROMPT_LOGIN}
 | 
					ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSENT, PROMPT_LOGIN}
 | 
				
			||||||
 | 
					FORBIDDEN_URI_SCHEMES = {"javascript", "data", "vbscript"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass(slots=True)
 | 
					@dataclass(slots=True)
 | 
				
			||||||
@ -174,6 +176,10 @@ class OAuthAuthorizationParams:
 | 
				
			|||||||
        self.check_scope()
 | 
					        self.check_scope()
 | 
				
			||||||
        self.check_nonce()
 | 
					        self.check_nonce()
 | 
				
			||||||
        self.check_code_challenge()
 | 
					        self.check_code_challenge()
 | 
				
			||||||
 | 
					        if self.request:
 | 
				
			||||||
 | 
					            raise AuthorizeError(
 | 
				
			||||||
 | 
					                self.redirect_uri, "request_not_supported", self.grant_type, self.state
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def check_redirect_uri(self):
 | 
					    def check_redirect_uri(self):
 | 
				
			||||||
        """Redirect URI validation."""
 | 
					        """Redirect URI validation."""
 | 
				
			||||||
@ -211,10 +217,9 @@ class OAuthAuthorizationParams:
 | 
				
			|||||||
                    expected=allowed_redirect_urls,
 | 
					                    expected=allowed_redirect_urls,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
 | 
					                raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
 | 
				
			||||||
        if self.request:
 | 
					        # Check against forbidden schemes
 | 
				
			||||||
            raise AuthorizeError(
 | 
					        if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
 | 
				
			||||||
                self.redirect_uri, "request_not_supported", self.grant_type, self.state
 | 
					            raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def check_scope(self):
 | 
					    def check_scope(self):
 | 
				
			||||||
        """Ensure openid scope is set in Hybrid flows, or when requesting an id_token"""
 | 
					        """Ensure openid scope is set in Hybrid flows, or when requesting an id_token"""
 | 
				
			||||||
@ -282,6 +287,7 @@ class OAuthAuthorizationParams:
 | 
				
			|||||||
            expires=now + timedelta_from_string(self.provider.access_code_validity),
 | 
					            expires=now + timedelta_from_string(self.provider.access_code_validity),
 | 
				
			||||||
            scope=self.scope,
 | 
					            scope=self.scope,
 | 
				
			||||||
            nonce=self.nonce,
 | 
					            nonce=self.nonce,
 | 
				
			||||||
 | 
					            session_id=sha256(request.session.session_key.encode("ascii")).hexdigest(),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.code_challenge and self.code_challenge_method:
 | 
					        if self.code_challenge and self.code_challenge_method:
 | 
				
			||||||
@ -569,6 +575,7 @@ class OAuthFulfillmentStage(StageView):
 | 
				
			|||||||
            expires=access_token_expiry,
 | 
					            expires=access_token_expiry,
 | 
				
			||||||
            provider=self.provider,
 | 
					            provider=self.provider,
 | 
				
			||||||
            auth_time=auth_event.created if auth_event else now,
 | 
					            auth_time=auth_event.created if auth_event else now,
 | 
				
			||||||
 | 
					            session_id=sha256(self.request.session.session_key.encode("ascii")).hexdigest(),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        id_token = IDToken.new(self.provider, token, self.request)
 | 
					        id_token = IDToken.new(self.provider, token, self.request)
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,7 @@ from hashlib import sha256
 | 
				
			|||||||
from re import error as RegexError
 | 
					from re import error as RegexError
 | 
				
			||||||
from re import fullmatch
 | 
					from re import fullmatch
 | 
				
			||||||
from typing import Any, Optional
 | 
					from typing import Any, Optional
 | 
				
			||||||
 | 
					from urllib.parse import urlparse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.http import HttpRequest, HttpResponse
 | 
					from django.http import HttpRequest, HttpResponse
 | 
				
			||||||
from django.utils import timezone
 | 
					from django.utils import timezone
 | 
				
			||||||
@ -17,6 +18,7 @@ from jwt import PyJWK, PyJWT, PyJWTError, decode
 | 
				
			|||||||
from sentry_sdk.hub import Hub
 | 
					from sentry_sdk.hub import Hub
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.core.middleware import CTX_AUTH_VIA
 | 
				
			||||||
from authentik.core.models import (
 | 
					from authentik.core.models import (
 | 
				
			||||||
    USER_ATTRIBUTE_EXPIRES,
 | 
					    USER_ATTRIBUTE_EXPIRES,
 | 
				
			||||||
    USER_ATTRIBUTE_GENERATED,
 | 
					    USER_ATTRIBUTE_GENERATED,
 | 
				
			||||||
@ -53,6 +55,7 @@ from authentik.providers.oauth2.models import (
 | 
				
			|||||||
    RefreshToken,
 | 
					    RefreshToken,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from authentik.providers.oauth2.utils import TokenResponse, cors_allow, extract_client_auth
 | 
					from authentik.providers.oauth2.utils import TokenResponse, cors_allow, extract_client_auth
 | 
				
			||||||
 | 
					from authentik.providers.oauth2.views.authorize import FORBIDDEN_URI_SCHEMES
 | 
				
			||||||
from authentik.sources.oauth.models import OAuthSource
 | 
					from authentik.sources.oauth.models import OAuthSource
 | 
				
			||||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
 | 
					from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -204,6 +207,10 @@ class TokenParams:
 | 
				
			|||||||
                ).from_http(request)
 | 
					                ).from_http(request)
 | 
				
			||||||
                raise TokenError("invalid_client")
 | 
					                raise TokenError("invalid_client")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Check against forbidden schemes
 | 
				
			||||||
 | 
					        if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
 | 
				
			||||||
 | 
					            raise TokenError("invalid_request")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.authorization_code = AuthorizationCode.objects.filter(code=raw_code).first()
 | 
					        self.authorization_code = AuthorizationCode.objects.filter(code=raw_code).first()
 | 
				
			||||||
        if not self.authorization_code:
 | 
					        if not self.authorization_code:
 | 
				
			||||||
            LOGGER.warning("Code does not exist", code=raw_code)
 | 
					            LOGGER.warning("Code does not exist", code=raw_code)
 | 
				
			||||||
@ -221,7 +228,10 @@ class TokenParams:
 | 
				
			|||||||
            raise TokenError("invalid_grant")
 | 
					            raise TokenError("invalid_grant")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Validate PKCE parameters.
 | 
					        # Validate PKCE parameters.
 | 
				
			||||||
        if self.code_verifier:
 | 
					        if self.authorization_code.code_challenge:
 | 
				
			||||||
 | 
					            # Authorization code had PKCE but we didn't get one
 | 
				
			||||||
 | 
					            if not self.code_verifier:
 | 
				
			||||||
 | 
					                raise TokenError("invalid_grant")
 | 
				
			||||||
            if self.authorization_code.code_challenge_method == PKCE_METHOD_S256:
 | 
					            if self.authorization_code.code_challenge_method == PKCE_METHOD_S256:
 | 
				
			||||||
                new_code_challenge = (
 | 
					                new_code_challenge = (
 | 
				
			||||||
                    urlsafe_b64encode(sha256(self.code_verifier.encode("ascii")).digest())
 | 
					                    urlsafe_b64encode(sha256(self.code_verifier.encode("ascii")).digest())
 | 
				
			||||||
@ -234,6 +244,10 @@ class TokenParams:
 | 
				
			|||||||
            if new_code_challenge != self.authorization_code.code_challenge:
 | 
					            if new_code_challenge != self.authorization_code.code_challenge:
 | 
				
			||||||
                LOGGER.warning("Code challenge not matching")
 | 
					                LOGGER.warning("Code challenge not matching")
 | 
				
			||||||
                raise TokenError("invalid_grant")
 | 
					                raise TokenError("invalid_grant")
 | 
				
			||||||
 | 
					        # Token request had a code_verifier but code did not have a code challenge
 | 
				
			||||||
 | 
					        # Prevent downgrade
 | 
				
			||||||
 | 
					        if not self.authorization_code.code_challenge and self.code_verifier:
 | 
				
			||||||
 | 
					            raise TokenError("invalid_grant")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __post_init_refresh(self, raw_token: str, request: HttpRequest):
 | 
					    def __post_init_refresh(self, raw_token: str, request: HttpRequest):
 | 
				
			||||||
        if not raw_token:
 | 
					        if not raw_token:
 | 
				
			||||||
@ -448,6 +462,7 @@ class TokenView(View):
 | 
				
			|||||||
                if not self.provider:
 | 
					                if not self.provider:
 | 
				
			||||||
                    LOGGER.warning("OAuth2Provider does not exist", client_id=client_id)
 | 
					                    LOGGER.warning("OAuth2Provider does not exist", client_id=client_id)
 | 
				
			||||||
                    raise TokenError("invalid_client")
 | 
					                    raise TokenError("invalid_client")
 | 
				
			||||||
 | 
					                CTX_AUTH_VIA.set("oauth_client_secret")
 | 
				
			||||||
                self.params = TokenParams.parse(request, self.provider, client_id, client_secret)
 | 
					                self.params = TokenParams.parse(request, self.provider, client_id, client_secret)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            with Hub.current.start_span(
 | 
					            with Hub.current.start_span(
 | 
				
			||||||
@ -482,6 +497,7 @@ class TokenView(View):
 | 
				
			|||||||
            # Keep same scopes as previous token
 | 
					            # Keep same scopes as previous token
 | 
				
			||||||
            scope=self.params.authorization_code.scope,
 | 
					            scope=self.params.authorization_code.scope,
 | 
				
			||||||
            auth_time=self.params.authorization_code.auth_time,
 | 
					            auth_time=self.params.authorization_code.auth_time,
 | 
				
			||||||
 | 
					            session_id=self.params.authorization_code.session_id,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        access_token.id_token = IDToken.new(
 | 
					        access_token.id_token = IDToken.new(
 | 
				
			||||||
            self.provider,
 | 
					            self.provider,
 | 
				
			||||||
@ -497,6 +513,7 @@ class TokenView(View):
 | 
				
			|||||||
            expires=refresh_token_expiry,
 | 
					            expires=refresh_token_expiry,
 | 
				
			||||||
            provider=self.provider,
 | 
					            provider=self.provider,
 | 
				
			||||||
            auth_time=self.params.authorization_code.auth_time,
 | 
					            auth_time=self.params.authorization_code.auth_time,
 | 
				
			||||||
 | 
					            session_id=self.params.authorization_code.session_id,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        id_token = IDToken.new(
 | 
					        id_token = IDToken.new(
 | 
				
			||||||
            self.provider,
 | 
					            self.provider,
 | 
				
			||||||
@ -534,6 +551,7 @@ class TokenView(View):
 | 
				
			|||||||
            # Keep same scopes as previous token
 | 
					            # Keep same scopes as previous token
 | 
				
			||||||
            scope=self.params.refresh_token.scope,
 | 
					            scope=self.params.refresh_token.scope,
 | 
				
			||||||
            auth_time=self.params.refresh_token.auth_time,
 | 
					            auth_time=self.params.refresh_token.auth_time,
 | 
				
			||||||
 | 
					            session_id=self.params.refresh_token.session_id,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        access_token.id_token = IDToken.new(
 | 
					        access_token.id_token = IDToken.new(
 | 
				
			||||||
            self.provider,
 | 
					            self.provider,
 | 
				
			||||||
@ -549,6 +567,7 @@ class TokenView(View):
 | 
				
			|||||||
            expires=refresh_token_expiry,
 | 
					            expires=refresh_token_expiry,
 | 
				
			||||||
            provider=self.provider,
 | 
					            provider=self.provider,
 | 
				
			||||||
            auth_time=self.params.refresh_token.auth_time,
 | 
					            auth_time=self.params.refresh_token.auth_time,
 | 
				
			||||||
 | 
					            session_id=self.params.refresh_token.session_id,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        id_token = IDToken.new(
 | 
					        id_token = IDToken.new(
 | 
				
			||||||
            self.provider,
 | 
					            self.provider,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,6 @@
 | 
				
			|||||||
"""proxy provider tasks"""
 | 
					"""proxy provider tasks"""
 | 
				
			||||||
 | 
					from hashlib import sha256
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from asgiref.sync import async_to_sync
 | 
					from asgiref.sync import async_to_sync
 | 
				
			||||||
from channels.layers import get_channel_layer
 | 
					from channels.layers import get_channel_layer
 | 
				
			||||||
from django.db import DatabaseError, InternalError, ProgrammingError
 | 
					from django.db import DatabaseError, InternalError, ProgrammingError
 | 
				
			||||||
@ -23,6 +25,7 @@ def proxy_set_defaults():
 | 
				
			|||||||
def proxy_on_logout(session_id: str):
 | 
					def proxy_on_logout(session_id: str):
 | 
				
			||||||
    """Update outpost instances connected to a single outpost"""
 | 
					    """Update outpost instances connected to a single outpost"""
 | 
				
			||||||
    layer = get_channel_layer()
 | 
					    layer = get_channel_layer()
 | 
				
			||||||
 | 
					    hashed_session_id = sha256(session_id.encode("ascii")).hexdigest()
 | 
				
			||||||
    for outpost in Outpost.objects.filter(type=OutpostType.PROXY):
 | 
					    for outpost in Outpost.objects.filter(type=OutpostType.PROXY):
 | 
				
			||||||
        group = OUTPOST_GROUP % {"outpost_pk": str(outpost.pk)}
 | 
					        group = OUTPOST_GROUP % {"outpost_pk": str(outpost.pk)}
 | 
				
			||||||
        async_to_sync(layer.group_send)(
 | 
					        async_to_sync(layer.group_send)(
 | 
				
			||||||
@ -30,6 +33,6 @@ def proxy_on_logout(session_id: str):
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                "type": "event.provider.specific",
 | 
					                "type": "event.provider.specific",
 | 
				
			||||||
                "sub_type": "logout",
 | 
					                "sub_type": "logout",
 | 
				
			||||||
                "session_id": session_id,
 | 
					                "session_id": hashed_session_id,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
				
			|||||||
@ -46,7 +46,9 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def to_scim(self, obj: Group) -> SCIMGroupSchema:
 | 
					    def to_scim(self, obj: Group) -> SCIMGroupSchema:
 | 
				
			||||||
        """Convert authentik user into SCIM"""
 | 
					        """Convert authentik user into SCIM"""
 | 
				
			||||||
        raw_scim_group = {}
 | 
					        raw_scim_group = {
 | 
				
			||||||
 | 
					            "schemas": ("urn:ietf:params:scim:schemas:core:2.0:Group",),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        for mapping in (
 | 
					        for mapping in (
 | 
				
			||||||
            self.provider.property_mappings_group.all().order_by("name").select_subclasses()
 | 
					            self.provider.property_mappings_group.all().order_by("name").select_subclasses()
 | 
				
			||||||
        ):
 | 
					        ):
 | 
				
			||||||
 | 
				
			|||||||
@ -15,12 +15,14 @@ from pydanticscim.user import User as BaseUser
 | 
				
			|||||||
class User(BaseUser):
 | 
					class User(BaseUser):
 | 
				
			||||||
    """Modified User schema with added externalId field"""
 | 
					    """Modified User schema with added externalId field"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    schemas: tuple[str] = ("urn:ietf:params:scim:schemas:core:2.0:User",)
 | 
				
			||||||
    externalId: Optional[str] = None
 | 
					    externalId: Optional[str] = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Group(BaseGroup):
 | 
					class Group(BaseGroup):
 | 
				
			||||||
    """Modified Group schema with added externalId field"""
 | 
					    """Modified Group schema with added externalId field"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    schemas: tuple[str] = ("urn:ietf:params:scim:schemas:core:2.0:Group",)
 | 
				
			||||||
    externalId: Optional[str] = None
 | 
					    externalId: Optional[str] = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -39,7 +39,9 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def to_scim(self, obj: User) -> SCIMUserSchema:
 | 
					    def to_scim(self, obj: User) -> SCIMUserSchema:
 | 
				
			||||||
        """Convert authentik user into SCIM"""
 | 
					        """Convert authentik user into SCIM"""
 | 
				
			||||||
        raw_scim_user = {}
 | 
					        raw_scim_user = {
 | 
				
			||||||
 | 
					            "schemas": ("urn:ietf:params:scim:schemas:core:2.0:User",),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        for mapping in self.provider.property_mappings.all().order_by("name").select_subclasses():
 | 
					        for mapping in self.provider.property_mappings.all().order_by("name").select_subclasses():
 | 
				
			||||||
            if not isinstance(mapping, SCIMMapping):
 | 
					            if not isinstance(mapping, SCIMMapping):
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
 | 
				
			|||||||
@ -61,7 +61,11 @@ class SCIMGroupTests(TestCase):
 | 
				
			|||||||
        self.assertEqual(mock.request_history[1].method, "POST")
 | 
					        self.assertEqual(mock.request_history[1].method, "POST")
 | 
				
			||||||
        self.assertJSONEqual(
 | 
					        self.assertJSONEqual(
 | 
				
			||||||
            mock.request_history[1].body,
 | 
					            mock.request_history[1].body,
 | 
				
			||||||
            {"externalId": str(group.pk), "displayName": group.name},
 | 
					            {
 | 
				
			||||||
 | 
					                "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
 | 
				
			||||||
 | 
					                "externalId": str(group.pk),
 | 
				
			||||||
 | 
					                "displayName": group.name,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Mocker()
 | 
					    @Mocker()
 | 
				
			||||||
@ -96,7 +100,11 @@ class SCIMGroupTests(TestCase):
 | 
				
			|||||||
            validate(body, loads(schema.read()))
 | 
					            validate(body, loads(schema.read()))
 | 
				
			||||||
        self.assertEqual(
 | 
					        self.assertEqual(
 | 
				
			||||||
            body,
 | 
					            body,
 | 
				
			||||||
            {"externalId": str(group.pk), "displayName": group.name},
 | 
					            {
 | 
				
			||||||
 | 
					                "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
 | 
				
			||||||
 | 
					                "externalId": str(group.pk),
 | 
				
			||||||
 | 
					                "displayName": group.name,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        group.save()
 | 
					        group.save()
 | 
				
			||||||
        self.assertEqual(mock.call_count, 4)
 | 
					        self.assertEqual(mock.call_count, 4)
 | 
				
			||||||
@ -129,7 +137,11 @@ class SCIMGroupTests(TestCase):
 | 
				
			|||||||
        self.assertEqual(mock.request_history[1].method, "POST")
 | 
					        self.assertEqual(mock.request_history[1].method, "POST")
 | 
				
			||||||
        self.assertJSONEqual(
 | 
					        self.assertJSONEqual(
 | 
				
			||||||
            mock.request_history[1].body,
 | 
					            mock.request_history[1].body,
 | 
				
			||||||
            {"externalId": str(group.pk), "displayName": group.name},
 | 
					            {
 | 
				
			||||||
 | 
					                "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
 | 
				
			||||||
 | 
					                "externalId": str(group.pk),
 | 
				
			||||||
 | 
					                "displayName": group.name,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        group.delete()
 | 
					        group.delete()
 | 
				
			||||||
        self.assertEqual(mock.call_count, 4)
 | 
					        self.assertEqual(mock.call_count, 4)
 | 
				
			||||||
 | 
				
			|||||||
@ -89,6 +89,7 @@ class SCIMMembershipTests(TestCase):
 | 
				
			|||||||
            self.assertJSONEqual(
 | 
					            self.assertJSONEqual(
 | 
				
			||||||
                mocker.request_history[3].body,
 | 
					                mocker.request_history[3].body,
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
 | 
					                    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
 | 
				
			||||||
                    "emails": [],
 | 
					                    "emails": [],
 | 
				
			||||||
                    "active": True,
 | 
					                    "active": True,
 | 
				
			||||||
                    "externalId": user.uid,
 | 
					                    "externalId": user.uid,
 | 
				
			||||||
@ -99,7 +100,11 @@ class SCIMMembershipTests(TestCase):
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
            self.assertJSONEqual(
 | 
					            self.assertJSONEqual(
 | 
				
			||||||
                mocker.request_history[5].body,
 | 
					                mocker.request_history[5].body,
 | 
				
			||||||
                {"externalId": str(group.pk), "displayName": group.name},
 | 
					                {
 | 
				
			||||||
 | 
					                    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
 | 
				
			||||||
 | 
					                    "externalId": str(group.pk),
 | 
				
			||||||
 | 
					                    "displayName": group.name,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with Mocker() as mocker:
 | 
					        with Mocker() as mocker:
 | 
				
			||||||
@ -118,6 +123,7 @@ class SCIMMembershipTests(TestCase):
 | 
				
			|||||||
            self.assertJSONEqual(
 | 
					            self.assertJSONEqual(
 | 
				
			||||||
                mocker.request_history[1].body,
 | 
					                mocker.request_history[1].body,
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
 | 
					                    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
 | 
				
			||||||
                    "Operations": [
 | 
					                    "Operations": [
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            "op": "add",
 | 
					                            "op": "add",
 | 
				
			||||||
@ -125,7 +131,6 @@ class SCIMMembershipTests(TestCase):
 | 
				
			|||||||
                            "value": [{"value": user_scim_id}],
 | 
					                            "value": [{"value": user_scim_id}],
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
                    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
 | 
					 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -174,6 +179,7 @@ class SCIMMembershipTests(TestCase):
 | 
				
			|||||||
            self.assertJSONEqual(
 | 
					            self.assertJSONEqual(
 | 
				
			||||||
                mocker.request_history[3].body,
 | 
					                mocker.request_history[3].body,
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
 | 
					                    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
 | 
				
			||||||
                    "active": True,
 | 
					                    "active": True,
 | 
				
			||||||
                    "displayName": "",
 | 
					                    "displayName": "",
 | 
				
			||||||
                    "emails": [],
 | 
					                    "emails": [],
 | 
				
			||||||
@ -184,7 +190,11 @@ class SCIMMembershipTests(TestCase):
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
            self.assertJSONEqual(
 | 
					            self.assertJSONEqual(
 | 
				
			||||||
                mocker.request_history[5].body,
 | 
					                mocker.request_history[5].body,
 | 
				
			||||||
                {"externalId": str(group.pk), "displayName": group.name},
 | 
					                {
 | 
				
			||||||
 | 
					                    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
 | 
				
			||||||
 | 
					                    "externalId": str(group.pk),
 | 
				
			||||||
 | 
					                    "displayName": group.name,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with Mocker() as mocker:
 | 
					        with Mocker() as mocker:
 | 
				
			||||||
@ -203,6 +213,7 @@ class SCIMMembershipTests(TestCase):
 | 
				
			|||||||
            self.assertJSONEqual(
 | 
					            self.assertJSONEqual(
 | 
				
			||||||
                mocker.request_history[1].body,
 | 
					                mocker.request_history[1].body,
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
 | 
					                    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
 | 
				
			||||||
                    "Operations": [
 | 
					                    "Operations": [
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            "op": "add",
 | 
					                            "op": "add",
 | 
				
			||||||
@ -210,7 +221,6 @@ class SCIMMembershipTests(TestCase):
 | 
				
			|||||||
                            "value": [{"value": user_scim_id}],
 | 
					                            "value": [{"value": user_scim_id}],
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
                    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
 | 
					 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -230,6 +240,7 @@ class SCIMMembershipTests(TestCase):
 | 
				
			|||||||
            self.assertJSONEqual(
 | 
					            self.assertJSONEqual(
 | 
				
			||||||
                mocker.request_history[1].body,
 | 
					                mocker.request_history[1].body,
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
 | 
					                    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
 | 
				
			||||||
                    "Operations": [
 | 
					                    "Operations": [
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            "op": "remove",
 | 
					                            "op": "remove",
 | 
				
			||||||
@ -237,6 +248,5 @@ class SCIMMembershipTests(TestCase):
 | 
				
			|||||||
                            "value": [{"value": user_scim_id}],
 | 
					                            "value": [{"value": user_scim_id}],
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
                    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
 | 
					 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
				
			|||||||
@ -57,7 +57,7 @@ class SCIMUserTests(TestCase):
 | 
				
			|||||||
        uid = generate_id()
 | 
					        uid = generate_id()
 | 
				
			||||||
        user = User.objects.create(
 | 
					        user = User.objects.create(
 | 
				
			||||||
            username=uid,
 | 
					            username=uid,
 | 
				
			||||||
            name=uid,
 | 
					            name=f"{uid} {uid}",
 | 
				
			||||||
            email=f"{uid}@goauthentik.io",
 | 
					            email=f"{uid}@goauthentik.io",
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.assertEqual(mock.call_count, 2)
 | 
					        self.assertEqual(mock.call_count, 2)
 | 
				
			||||||
@ -66,6 +66,7 @@ class SCIMUserTests(TestCase):
 | 
				
			|||||||
        self.assertJSONEqual(
 | 
					        self.assertJSONEqual(
 | 
				
			||||||
            mock.request_history[1].body,
 | 
					            mock.request_history[1].body,
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
 | 
					                "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
 | 
				
			||||||
                "active": True,
 | 
					                "active": True,
 | 
				
			||||||
                "emails": [
 | 
					                "emails": [
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
@ -76,11 +77,11 @@ class SCIMUserTests(TestCase):
 | 
				
			|||||||
                ],
 | 
					                ],
 | 
				
			||||||
                "externalId": user.uid,
 | 
					                "externalId": user.uid,
 | 
				
			||||||
                "name": {
 | 
					                "name": {
 | 
				
			||||||
                    "familyName": "",
 | 
					                    "familyName": uid,
 | 
				
			||||||
                    "formatted": uid,
 | 
					                    "formatted": f"{uid} {uid}",
 | 
				
			||||||
                    "givenName": uid,
 | 
					                    "givenName": uid,
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                "displayName": uid,
 | 
					                "displayName": f"{uid} {uid}",
 | 
				
			||||||
                "userName": uid,
 | 
					                "userName": uid,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@ -109,7 +110,7 @@ class SCIMUserTests(TestCase):
 | 
				
			|||||||
        uid = generate_id()
 | 
					        uid = generate_id()
 | 
				
			||||||
        user = User.objects.create(
 | 
					        user = User.objects.create(
 | 
				
			||||||
            username=uid,
 | 
					            username=uid,
 | 
				
			||||||
            name=uid,
 | 
					            name=f"{uid} {uid}",
 | 
				
			||||||
            email=f"{uid}@goauthentik.io",
 | 
					            email=f"{uid}@goauthentik.io",
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.assertEqual(mock.call_count, 2)
 | 
					        self.assertEqual(mock.call_count, 2)
 | 
				
			||||||
@ -121,6 +122,7 @@ class SCIMUserTests(TestCase):
 | 
				
			|||||||
        self.assertEqual(
 | 
					        self.assertEqual(
 | 
				
			||||||
            body,
 | 
					            body,
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
 | 
					                "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
 | 
				
			||||||
                "active": True,
 | 
					                "active": True,
 | 
				
			||||||
                "emails": [
 | 
					                "emails": [
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
@ -129,11 +131,11 @@ class SCIMUserTests(TestCase):
 | 
				
			|||||||
                        "value": f"{uid}@goauthentik.io",
 | 
					                        "value": f"{uid}@goauthentik.io",
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
                "displayName": uid,
 | 
					                "displayName": f"{uid} {uid}",
 | 
				
			||||||
                "externalId": user.uid,
 | 
					                "externalId": user.uid,
 | 
				
			||||||
                "name": {
 | 
					                "name": {
 | 
				
			||||||
                    "familyName": "",
 | 
					                    "familyName": uid,
 | 
				
			||||||
                    "formatted": uid,
 | 
					                    "formatted": f"{uid} {uid}",
 | 
				
			||||||
                    "givenName": uid,
 | 
					                    "givenName": uid,
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                "userName": uid,
 | 
					                "userName": uid,
 | 
				
			||||||
@ -164,7 +166,7 @@ class SCIMUserTests(TestCase):
 | 
				
			|||||||
        uid = generate_id()
 | 
					        uid = generate_id()
 | 
				
			||||||
        user = User.objects.create(
 | 
					        user = User.objects.create(
 | 
				
			||||||
            username=uid,
 | 
					            username=uid,
 | 
				
			||||||
            name=uid,
 | 
					            name=f"{uid} {uid}",
 | 
				
			||||||
            email=f"{uid}@goauthentik.io",
 | 
					            email=f"{uid}@goauthentik.io",
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.assertEqual(mock.call_count, 2)
 | 
					        self.assertEqual(mock.call_count, 2)
 | 
				
			||||||
@ -173,6 +175,7 @@ class SCIMUserTests(TestCase):
 | 
				
			|||||||
        self.assertJSONEqual(
 | 
					        self.assertJSONEqual(
 | 
				
			||||||
            mock.request_history[1].body,
 | 
					            mock.request_history[1].body,
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
 | 
					                "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
 | 
				
			||||||
                "active": True,
 | 
					                "active": True,
 | 
				
			||||||
                "emails": [
 | 
					                "emails": [
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
@ -183,11 +186,11 @@ class SCIMUserTests(TestCase):
 | 
				
			|||||||
                ],
 | 
					                ],
 | 
				
			||||||
                "externalId": user.uid,
 | 
					                "externalId": user.uid,
 | 
				
			||||||
                "name": {
 | 
					                "name": {
 | 
				
			||||||
                    "familyName": "",
 | 
					                    "familyName": uid,
 | 
				
			||||||
                    "formatted": uid,
 | 
					                    "formatted": f"{uid} {uid}",
 | 
				
			||||||
                    "givenName": uid,
 | 
					                    "givenName": uid,
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                "displayName": uid,
 | 
					                "displayName": f"{uid} {uid}",
 | 
				
			||||||
                "userName": uid,
 | 
					                "userName": uid,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@ -227,7 +230,7 @@ class SCIMUserTests(TestCase):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
        user = User.objects.create(
 | 
					        user = User.objects.create(
 | 
				
			||||||
            username=uid,
 | 
					            username=uid,
 | 
				
			||||||
            name=uid,
 | 
					            name=f"{uid} {uid}",
 | 
				
			||||||
            email=f"{uid}@goauthentik.io",
 | 
					            email=f"{uid}@goauthentik.io",
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -240,6 +243,7 @@ class SCIMUserTests(TestCase):
 | 
				
			|||||||
        self.assertJSONEqual(
 | 
					        self.assertJSONEqual(
 | 
				
			||||||
            mock.request_history[1].body,
 | 
					            mock.request_history[1].body,
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
 | 
					                "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
 | 
				
			||||||
                "active": True,
 | 
					                "active": True,
 | 
				
			||||||
                "emails": [
 | 
					                "emails": [
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
@ -250,11 +254,11 @@ class SCIMUserTests(TestCase):
 | 
				
			|||||||
                ],
 | 
					                ],
 | 
				
			||||||
                "externalId": user.uid,
 | 
					                "externalId": user.uid,
 | 
				
			||||||
                "name": {
 | 
					                "name": {
 | 
				
			||||||
                    "familyName": "",
 | 
					                    "familyName": uid,
 | 
				
			||||||
                    "formatted": uid,
 | 
					                    "formatted": f"{uid} {uid}",
 | 
				
			||||||
                    "givenName": uid,
 | 
					                    "givenName": uid,
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                "displayName": uid,
 | 
					                "displayName": f"{uid} {uid}",
 | 
				
			||||||
                "userName": uid,
 | 
					                "userName": uid,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
				
			|||||||
@ -24,7 +24,10 @@ class ExtraRoleObjectPermissionSerializer(RoleObjectPermissionSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def get_app_label_verbose(self, instance: GroupObjectPermission) -> str:
 | 
					    def get_app_label_verbose(self, instance: GroupObjectPermission) -> str:
 | 
				
			||||||
        """Get app label from permission's model"""
 | 
					        """Get app label from permission's model"""
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
            return apps.get_app_config(instance.content_type.app_label).verbose_name
 | 
					            return apps.get_app_config(instance.content_type.app_label).verbose_name
 | 
				
			||||||
 | 
					        except LookupError:
 | 
				
			||||||
 | 
					            return instance.content_type.app_label
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_model_verbose(self, instance: GroupObjectPermission) -> str:
 | 
					    def get_model_verbose(self, instance: GroupObjectPermission) -> str:
 | 
				
			||||||
        """Get model label from permission's model"""
 | 
					        """Get model label from permission's model"""
 | 
				
			||||||
 | 
				
			|||||||
@ -24,7 +24,10 @@ class ExtraUserObjectPermissionSerializer(UserObjectPermissionSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def get_app_label_verbose(self, instance: UserObjectPermission) -> str:
 | 
					    def get_app_label_verbose(self, instance: UserObjectPermission) -> str:
 | 
				
			||||||
        """Get app label from permission's model"""
 | 
					        """Get app label from permission's model"""
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
            return apps.get_app_config(instance.content_type.app_label).verbose_name
 | 
					            return apps.get_app_config(instance.content_type.app_label).verbose_name
 | 
				
			||||||
 | 
					        except LookupError:
 | 
				
			||||||
 | 
					            return instance.content_type.app_label
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_model_verbose(self, instance: UserObjectPermission) -> str:
 | 
					    def get_model_verbose(self, instance: UserObjectPermission) -> str:
 | 
				
			||||||
        """Get model label from permission's model"""
 | 
					        """Get model label from permission's model"""
 | 
				
			||||||
 | 
				
			|||||||
@ -56,6 +56,7 @@ class OAuthSourceSerializer(SourceSerializer):
 | 
				
			|||||||
        """Get source's type configuration"""
 | 
					        """Get source's type configuration"""
 | 
				
			||||||
        return SourceTypeSerializer(instance.source_type).data
 | 
					        return SourceTypeSerializer(instance.source_type).data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # pylint: disable=too-many-locals
 | 
				
			||||||
    def validate(self, attrs: dict) -> dict:
 | 
					    def validate(self, attrs: dict) -> dict:
 | 
				
			||||||
        session = get_http_session()
 | 
					        session = get_http_session()
 | 
				
			||||||
        source_type = registry.find_type(attrs["provider_type"])
 | 
					        source_type = registry.find_type(attrs["provider_type"])
 | 
				
			||||||
@ -73,9 +74,17 @@ class OAuthSourceSerializer(SourceSerializer):
 | 
				
			|||||||
            config = well_known_config.json()
 | 
					            config = well_known_config.json()
 | 
				
			||||||
            if "issuer" not in config:
 | 
					            if "issuer" not in config:
 | 
				
			||||||
                raise ValidationError({"oidc_well_known_url": "Invalid well-known configuration"})
 | 
					                raise ValidationError({"oidc_well_known_url": "Invalid well-known configuration"})
 | 
				
			||||||
            attrs["authorization_url"] = config.get("authorization_endpoint", "")
 | 
					            field_map = {
 | 
				
			||||||
            attrs["access_token_url"] = config.get("token_endpoint", "")
 | 
					                # authentik field to oidc field
 | 
				
			||||||
            attrs["profile_url"] = config.get("userinfo_endpoint", "")
 | 
					                "authorization_url": "authorization_endpoint",
 | 
				
			||||||
 | 
					                "access_token_url": "token_endpoint",
 | 
				
			||||||
 | 
					                "profile_url": "userinfo_endpoint",
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            for ak_key, oidc_key in field_map.items():
 | 
				
			||||||
 | 
					                # Don't overwrite user-set values
 | 
				
			||||||
 | 
					                if ak_key in attrs and attrs[ak_key]:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					                attrs[ak_key] = config.get(oidc_key, "")
 | 
				
			||||||
            inferred_oidc_jwks_url = config.get("jwks_uri", "")
 | 
					            inferred_oidc_jwks_url = config.get("jwks_uri", "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Prefer user-entered URL to inferred URL to default URL
 | 
					        # Prefer user-entered URL to inferred URL to default URL
 | 
				
			||||||
 | 
				
			|||||||
@ -44,3 +44,7 @@ class TestTypeAzureAD(TestCase):
 | 
				
			|||||||
        self.assertEqual(ak_context["username"], AAD_USER["userPrincipalName"])
 | 
					        self.assertEqual(ak_context["username"], AAD_USER["userPrincipalName"])
 | 
				
			||||||
        self.assertEqual(ak_context["email"], AAD_USER["mail"])
 | 
					        self.assertEqual(ak_context["email"], AAD_USER["mail"])
 | 
				
			||||||
        self.assertEqual(ak_context["name"], AAD_USER["displayName"])
 | 
					        self.assertEqual(ak_context["name"], AAD_USER["displayName"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_user_id(self):
 | 
				
			||||||
 | 
					        """Test azure AD user ID"""
 | 
				
			||||||
 | 
					        self.assertEqual(AzureADOAuthCallback().get_user_id(AAD_USER), AAD_USER["id"])
 | 
				
			||||||
 | 
				
			|||||||
@ -69,9 +69,6 @@ class TestOAuthSource(TestCase):
 | 
				
			|||||||
                    "provider_type": "openidconnect",
 | 
					                    "provider_type": "openidconnect",
 | 
				
			||||||
                    "consumer_key": "foo",
 | 
					                    "consumer_key": "foo",
 | 
				
			||||||
                    "consumer_secret": "foo",
 | 
					                    "consumer_secret": "foo",
 | 
				
			||||||
                    "authorization_url": "http://foo",
 | 
					 | 
				
			||||||
                    "access_token_url": "http://foo",
 | 
					 | 
				
			||||||
                    "profile_url": "http://foo",
 | 
					 | 
				
			||||||
                    "oidc_well_known_url": url,
 | 
					                    "oidc_well_known_url": url,
 | 
				
			||||||
                    "oidc_jwks_url": "",
 | 
					                    "oidc_jwks_url": "",
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
 | 
				
			|||||||
@ -4,8 +4,8 @@ from typing import Any
 | 
				
			|||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
 | 
					from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
 | 
				
			||||||
 | 
					from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback
 | 
				
			||||||
from authentik.sources.oauth.types.registry import SourceType, registry
 | 
					from authentik.sources.oauth.types.registry import SourceType, registry
 | 
				
			||||||
from authentik.sources.oauth.views.callback import OAuthCallback
 | 
					 | 
				
			||||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
 | 
					from authentik.sources.oauth.views.redirect import OAuthRedirect
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
@ -20,11 +20,16 @@ class AzureADOAuthRedirect(OAuthRedirect):
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AzureADOAuthCallback(OAuthCallback):
 | 
					class AzureADOAuthCallback(OpenIDConnectOAuth2Callback):
 | 
				
			||||||
    """AzureAD OAuth2 Callback"""
 | 
					    """AzureAD OAuth2 Callback"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    client_class = UserprofileHeaderAuthClient
 | 
					    client_class = UserprofileHeaderAuthClient
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_user_id(self, info: dict[str, str]) -> str:
 | 
				
			||||||
 | 
					        # Default try to get `id` for the Graph API endpoint
 | 
				
			||||||
 | 
					        # fallback to OpenID logic in case the profile URL was changed
 | 
				
			||||||
 | 
					        return info.get("id", super().get_user_id(info))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_user_enroll_context(
 | 
					    def get_user_enroll_context(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        info: dict[str, Any],
 | 
					        info: dict[str, Any],
 | 
				
			||||||
 | 
				
			|||||||
@ -23,7 +23,7 @@ class OpenIDConnectOAuth2Callback(OAuthCallback):
 | 
				
			|||||||
    client_class = UserprofileHeaderAuthClient
 | 
					    client_class = UserprofileHeaderAuthClient
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_user_id(self, info: dict[str, str]) -> str:
 | 
					    def get_user_id(self, info: dict[str, str]) -> str:
 | 
				
			||||||
        return info.get("sub", "")
 | 
					        return info.get("sub", None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_user_enroll_context(
 | 
					    def get_user_enroll_context(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
 | 
				
			|||||||
@ -3,8 +3,8 @@ from typing import Any
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
 | 
					from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
 | 
				
			||||||
from authentik.sources.oauth.models import OAuthSource
 | 
					from authentik.sources.oauth.models import OAuthSource
 | 
				
			||||||
 | 
					from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback
 | 
				
			||||||
from authentik.sources.oauth.types.registry import SourceType, registry
 | 
					from authentik.sources.oauth.types.registry import SourceType, registry
 | 
				
			||||||
from authentik.sources.oauth.views.callback import OAuthCallback
 | 
					 | 
				
			||||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
 | 
					from authentik.sources.oauth.views.redirect import OAuthRedirect
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -17,7 +17,7 @@ class OktaOAuthRedirect(OAuthRedirect):
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class OktaOAuth2Callback(OAuthCallback):
 | 
					class OktaOAuth2Callback(OpenIDConnectOAuth2Callback):
 | 
				
			||||||
    """Okta OAuth2 Callback"""
 | 
					    """Okta OAuth2 Callback"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Okta has the same quirk as azure and throws an error if the access token
 | 
					    # Okta has the same quirk as azure and throws an error if the access token
 | 
				
			||||||
@ -25,9 +25,6 @@ class OktaOAuth2Callback(OAuthCallback):
 | 
				
			|||||||
    # see https://github.com/goauthentik/authentik/issues/1910
 | 
					    # see https://github.com/goauthentik/authentik/issues/1910
 | 
				
			||||||
    client_class = UserprofileHeaderAuthClient
 | 
					    client_class = UserprofileHeaderAuthClient
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_user_id(self, info: dict[str, str]) -> str:
 | 
					 | 
				
			||||||
        return info.get("sub", "")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_user_enroll_context(
 | 
					    def get_user_enroll_context(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        info: dict[str, Any],
 | 
					        info: dict[str, Any],
 | 
				
			||||||
 | 
				
			|||||||
@ -12,8 +12,9 @@ class PatreonOAuthRedirect(OAuthRedirect):
 | 
				
			|||||||
    """Patreon OAuth2 Redirect"""
 | 
					    """Patreon OAuth2 Redirect"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_additional_parameters(self, source: OAuthSource):  # pragma: no cover
 | 
					    def get_additional_parameters(self, source: OAuthSource):  # pragma: no cover
 | 
				
			||||||
 | 
					        # https://docs.patreon.com/#scopes
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
            "scope": ["openid", "email", "profile"],
 | 
					            "scope": ["identity", "identity[email]"],
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -3,8 +3,8 @@ from json import dumps
 | 
				
			|||||||
from typing import Any, Optional
 | 
					from typing import Any, Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
 | 
					from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
 | 
				
			||||||
 | 
					from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback
 | 
				
			||||||
from authentik.sources.oauth.types.registry import SourceType, registry
 | 
					from authentik.sources.oauth.types.registry import SourceType, registry
 | 
				
			||||||
from authentik.sources.oauth.views.callback import OAuthCallback
 | 
					 | 
				
			||||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
 | 
					from authentik.sources.oauth.views.redirect import OAuthRedirect
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -27,14 +27,11 @@ class TwitchOAuthRedirect(OAuthRedirect):
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TwitchOAuth2Callback(OAuthCallback):
 | 
					class TwitchOAuth2Callback(OpenIDConnectOAuth2Callback):
 | 
				
			||||||
    """Twitch OAuth2 Callback"""
 | 
					    """Twitch OAuth2 Callback"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    client_class = TwitchClient
 | 
					    client_class = TwitchClient
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_user_id(self, info: dict[str, str]) -> str:
 | 
					 | 
				
			||||||
        return info.get("sub", "")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_user_enroll_context(
 | 
					    def get_user_enroll_context(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        info: dict[str, Any],
 | 
					        info: dict[str, Any],
 | 
				
			||||||
 | 
				
			|||||||
@ -69,7 +69,6 @@ class AuthenticatorSMSStageView(ChallengeStageView):
 | 
				
			|||||||
        stage: AuthenticatorSMSStage = self.executor.current_stage
 | 
					        stage: AuthenticatorSMSStage = self.executor.current_stage
 | 
				
			||||||
        hashed_number = hash_phone_number(phone_number)
 | 
					        hashed_number = hash_phone_number(phone_number)
 | 
				
			||||||
        query = Q(phone_number=hashed_number) | Q(phone_number=phone_number)
 | 
					        query = Q(phone_number=hashed_number) | Q(phone_number=phone_number)
 | 
				
			||||||
        print(SMSDevice.objects.filter(query, stage=stage.pk))
 | 
					 | 
				
			||||||
        if SMSDevice.objects.filter(query, stage=stage.pk).exists():
 | 
					        if SMSDevice.objects.filter(query, stage=stage.pk).exists():
 | 
				
			||||||
            raise ValidationError(_("Invalid phone number"))
 | 
					            raise ValidationError(_("Invalid phone number"))
 | 
				
			||||||
        # No code yet, but we have a phone number, so send a verification message
 | 
					        # No code yet, but we have a phone number, so send a verification message
 | 
				
			||||||
 | 
				
			|||||||
@ -199,11 +199,9 @@ class AuthenticatorSMSStageTests(FlowTestCase):
 | 
				
			|||||||
                sms_send_mock,
 | 
					                sms_send_mock,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
        ):
 | 
					        ):
 | 
				
			||||||
            print(self.client.session[SESSION_KEY_PLAN])
 | 
					 | 
				
			||||||
            response = self.client.get(
 | 
					            response = self.client.get(
 | 
				
			||||||
                reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
 | 
					                reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        print(response.content.decode())
 | 
					 | 
				
			||||||
        self.assertStageResponse(
 | 
					        self.assertStageResponse(
 | 
				
			||||||
            response,
 | 
					            response,
 | 
				
			||||||
            self.flow,
 | 
					            self.flow,
 | 
				
			||||||
 | 
				
			|||||||
@ -38,4 +38,11 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
            name="statictoken",
 | 
					            name="statictoken",
 | 
				
			||||||
            options={"verbose_name": "Static Token", "verbose_name_plural": "Static Tokens"},
 | 
					            options={"verbose_name": "Static Token", "verbose_name_plural": "Static Tokens"},
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name="authenticatorstaticstage",
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "verbose_name": "Static Authenticator Setup Stage",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Static Authenticator Setup Stages",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
				
			|||||||
@ -46,11 +46,11 @@ class AuthenticatorStaticStage(ConfigurableStage, FriendlyNamedStage, Stage):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self) -> str:
 | 
					    def __str__(self) -> str:
 | 
				
			||||||
        return f"Static Authenticator Stage {self.name}"
 | 
					        return f"Static Authenticator Setup Stage {self.name}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _("Static Authenticator Stage")
 | 
					        verbose_name = _("Static Authenticator Setup Stage")
 | 
				
			||||||
        verbose_name_plural = _("Static Authenticator Stages")
 | 
					        verbose_name_plural = _("Static Authenticator Setup Stages")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StaticDevice(SerializerModel, ThrottlingMixin, Device):
 | 
					class StaticDevice(SerializerModel, ThrottlingMixin, Device):
 | 
				
			||||||
 | 
				
			|||||||
@ -300,8 +300,10 @@ class AuthenticatorValidateStageView(ChallengeStageView):
 | 
				
			|||||||
            serializer = SelectableStageSerializer(
 | 
					            serializer = SelectableStageSerializer(
 | 
				
			||||||
                data={
 | 
					                data={
 | 
				
			||||||
                    "pk": stage.pk,
 | 
					                    "pk": stage.pk,
 | 
				
			||||||
                    "name": stage.name,
 | 
					                    "name": stage.friendly_name or stage.name,
 | 
				
			||||||
                    "verbose_name": str(stage._meta.verbose_name),
 | 
					                    "verbose_name": str(stage._meta.verbose_name)
 | 
				
			||||||
 | 
					                    .replace("Setup Stage", "")
 | 
				
			||||||
 | 
					                    .strip(),
 | 
				
			||||||
                    "meta_model_name": f"{stage._meta.app_label}.{stage._meta.model_name}",
 | 
					                    "meta_model_name": f"{stage._meta.app_label}.{stage._meta.model_name}",
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
				
			|||||||
@ -184,6 +184,7 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
 | 
				
			|||||||
                    "args": {},
 | 
					                    "args": {},
 | 
				
			||||||
                    "method": "GET",
 | 
					                    "method": "GET",
 | 
				
			||||||
                    "path": f"/api/v3/flows/executor/{flow.slug}/",
 | 
					                    "path": f"/api/v3/flows/executor/{flow.slug}/",
 | 
				
			||||||
 | 
					                    "user_agent": "",
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
				
			|||||||
@ -11,6 +11,7 @@ from authentik.flows.tests import FlowTestCase
 | 
				
			|||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
 | 
					from authentik.flows.views.executor import SESSION_KEY_PLAN
 | 
				
			||||||
from authentik.lib.generators import generate_id, generate_key
 | 
					from authentik.lib.generators import generate_id, generate_key
 | 
				
			||||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
 | 
					from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
 | 
				
			||||||
 | 
					from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
 | 
				
			||||||
from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
 | 
					from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
 | 
				
			||||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
 | 
					from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
 | 
				
			||||||
from authentik.stages.authenticator_validate.stage import PLAN_CONTEXT_DEVICE_CHALLENGES
 | 
					from authentik.stages.authenticator_validate.stage import PLAN_CONTEXT_DEVICE_CHALLENGES
 | 
				
			||||||
@ -26,19 +27,22 @@ class AuthenticatorValidateStageTests(FlowTestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def test_not_configured_action(self):
 | 
					    def test_not_configured_action(self):
 | 
				
			||||||
        """Test not_configured_action"""
 | 
					        """Test not_configured_action"""
 | 
				
			||||||
        conf_stage = IdentificationStage.objects.create(
 | 
					        ident_stage = IdentificationStage.objects.create(
 | 
				
			||||||
            name=generate_id(),
 | 
					            name=generate_id(),
 | 
				
			||||||
            user_fields=[
 | 
					            user_fields=[
 | 
				
			||||||
                UserFields.USERNAME,
 | 
					                UserFields.USERNAME,
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					        conf_stage = AuthenticatorStaticStage.objects.create(
 | 
				
			||||||
 | 
					            name=generate_id(),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        stage = AuthenticatorValidateStage.objects.create(
 | 
					        stage = AuthenticatorValidateStage.objects.create(
 | 
				
			||||||
            name=generate_id(),
 | 
					            name=generate_id(),
 | 
				
			||||||
            not_configured_action=NotConfiguredAction.CONFIGURE,
 | 
					            not_configured_action=NotConfiguredAction.CONFIGURE,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        stage.configuration_stages.set([conf_stage])
 | 
					        stage.configuration_stages.set([conf_stage])
 | 
				
			||||||
        flow = create_test_flow()
 | 
					        flow = create_test_flow()
 | 
				
			||||||
        FlowStageBinding.objects.create(target=flow, stage=conf_stage, order=0)
 | 
					        FlowStageBinding.objects.create(target=flow, stage=ident_stage, order=0)
 | 
				
			||||||
        FlowStageBinding.objects.create(target=flow, stage=stage, order=1)
 | 
					        FlowStageBinding.objects.create(target=flow, stage=stage, order=1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.get(
 | 
					        response = self.client.get(
 | 
				
			||||||
@ -57,27 +61,22 @@ class AuthenticatorValidateStageTests(FlowTestCase):
 | 
				
			|||||||
        self.assertStageResponse(
 | 
					        self.assertStageResponse(
 | 
				
			||||||
            response,
 | 
					            response,
 | 
				
			||||||
            flow,
 | 
					            flow,
 | 
				
			||||||
            component="ak-stage-identification",
 | 
					            component="ak-stage-authenticator-static",
 | 
				
			||||||
            password_fields=False,
 | 
					 | 
				
			||||||
            primary_action="Continue",
 | 
					 | 
				
			||||||
            user_fields=["username"],
 | 
					 | 
				
			||||||
            sources=[],
 | 
					 | 
				
			||||||
            show_source_labels=False,
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_not_configured_action_multiple(self):
 | 
					    def test_not_configured_action_multiple(self):
 | 
				
			||||||
        """Test not_configured_action"""
 | 
					        """Test not_configured_action"""
 | 
				
			||||||
        conf_stage = IdentificationStage.objects.create(
 | 
					        ident_stage = IdentificationStage.objects.create(
 | 
				
			||||||
            name=generate_id(),
 | 
					            name=generate_id(),
 | 
				
			||||||
            user_fields=[
 | 
					            user_fields=[
 | 
				
			||||||
                UserFields.USERNAME,
 | 
					                UserFields.USERNAME,
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        conf_stage2 = IdentificationStage.objects.create(
 | 
					        conf_stage = AuthenticatorStaticStage.objects.create(
 | 
				
			||||||
 | 
					            name=generate_id(),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        conf_stage2 = AuthenticatorStaticStage.objects.create(
 | 
				
			||||||
            name=generate_id(),
 | 
					            name=generate_id(),
 | 
				
			||||||
            user_fields=[
 | 
					 | 
				
			||||||
                UserFields.USERNAME,
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        stage = AuthenticatorValidateStage.objects.create(
 | 
					        stage = AuthenticatorValidateStage.objects.create(
 | 
				
			||||||
            name=generate_id(),
 | 
					            name=generate_id(),
 | 
				
			||||||
@ -85,7 +84,7 @@ class AuthenticatorValidateStageTests(FlowTestCase):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
        stage.configuration_stages.set([conf_stage, conf_stage2])
 | 
					        stage.configuration_stages.set([conf_stage, conf_stage2])
 | 
				
			||||||
        flow = create_test_flow()
 | 
					        flow = create_test_flow()
 | 
				
			||||||
        FlowStageBinding.objects.create(target=flow, stage=conf_stage, order=0)
 | 
					        FlowStageBinding.objects.create(target=flow, stage=ident_stage, order=0)
 | 
				
			||||||
        FlowStageBinding.objects.create(target=flow, stage=stage, order=1)
 | 
					        FlowStageBinding.objects.create(target=flow, stage=stage, order=1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Get initial identification stage
 | 
					        # Get initial identification stage
 | 
				
			||||||
@ -118,12 +117,7 @@ class AuthenticatorValidateStageTests(FlowTestCase):
 | 
				
			|||||||
        self.assertStageResponse(
 | 
					        self.assertStageResponse(
 | 
				
			||||||
            response,
 | 
					            response,
 | 
				
			||||||
            flow,
 | 
					            flow,
 | 
				
			||||||
            component="ak-stage-identification",
 | 
					            component="ak-stage-authenticator-static",
 | 
				
			||||||
            password_fields=False,
 | 
					 | 
				
			||||||
            primary_action="Continue",
 | 
					 | 
				
			||||||
            user_fields=["username"],
 | 
					 | 
				
			||||||
            sources=[],
 | 
					 | 
				
			||||||
            show_source_labels=False,
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_stage_validation(self):
 | 
					    def test_stage_validation(self):
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,11 @@
 | 
				
			|||||||
"""authentik multi-stage authentication engine"""
 | 
					"""authentik multi-stage authentication engine"""
 | 
				
			||||||
from datetime import timedelta
 | 
					from datetime import timedelta
 | 
				
			||||||
 | 
					from uuid import uuid4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.contrib import messages
 | 
					from django.contrib import messages
 | 
				
			||||||
from django.http import HttpRequest, HttpResponse
 | 
					from django.http import HttpRequest, HttpResponse
 | 
				
			||||||
from django.http.request import QueryDict
 | 
					from django.http.request import QueryDict
 | 
				
			||||||
 | 
					from django.template.exceptions import TemplateSyntaxError
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
from django.utils.text import slugify
 | 
					from django.utils.text import slugify
 | 
				
			||||||
from django.utils.timezone import now
 | 
					from django.utils.timezone import now
 | 
				
			||||||
@ -11,11 +13,14 @@ from django.utils.translation import gettext as _
 | 
				
			|||||||
from rest_framework.fields import CharField
 | 
					from rest_framework.fields import CharField
 | 
				
			||||||
from rest_framework.serializers import ValidationError
 | 
					from rest_framework.serializers import ValidationError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
 | 
					from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
 | 
				
			||||||
 | 
					from authentik.flows.exceptions import StageInvalidException
 | 
				
			||||||
from authentik.flows.models import FlowDesignation, FlowToken
 | 
					from authentik.flows.models import FlowDesignation, FlowToken
 | 
				
			||||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
 | 
					from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
 | 
				
			||||||
from authentik.flows.stage import ChallengeStageView
 | 
					from authentik.flows.stage import ChallengeStageView
 | 
				
			||||||
from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY
 | 
					from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY
 | 
				
			||||||
 | 
					from authentik.lib.utils.errors import exception_to_string
 | 
				
			||||||
from authentik.stages.email.models import EmailStage
 | 
					from authentik.stages.email.models import EmailStage
 | 
				
			||||||
from authentik.stages.email.tasks import send_mails
 | 
					from authentik.stages.email.tasks import send_mails
 | 
				
			||||||
from authentik.stages.email.utils import TemplateEmailMessage
 | 
					from authentik.stages.email.utils import TemplateEmailMessage
 | 
				
			||||||
@ -52,16 +57,11 @@ class EmailStageView(ChallengeStageView):
 | 
				
			|||||||
            kwargs={"flow_slug": self.executor.flow.slug},
 | 
					            kwargs={"flow_slug": self.executor.flow.slug},
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        # Parse query string from current URL (full query string)
 | 
					        # Parse query string from current URL (full query string)
 | 
				
			||||||
        query_params = QueryDict(self.request.META.get("QUERY_STRING", ""), mutable=True)
 | 
					        # this view is only run within a flow executor, where we need to get the query string
 | 
				
			||||||
 | 
					        # from the query= parameter (double encoded); but for the redirect
 | 
				
			||||||
 | 
					        # we need to expand it since it'll go through the flow interface
 | 
				
			||||||
 | 
					        query_params = QueryDict(self.request.GET.get(QS_QUERY), mutable=True)
 | 
				
			||||||
        query_params.pop(QS_KEY_TOKEN, None)
 | 
					        query_params.pop(QS_KEY_TOKEN, None)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Check for nested query string used by flow executor, and remove any
 | 
					 | 
				
			||||||
        # kind of flow token from that
 | 
					 | 
				
			||||||
        if QS_QUERY in query_params:
 | 
					 | 
				
			||||||
            inner_query_params = QueryDict(query_params.get(QS_QUERY), mutable=True)
 | 
					 | 
				
			||||||
            inner_query_params.pop(QS_KEY_TOKEN, None)
 | 
					 | 
				
			||||||
            query_params[QS_QUERY] = inner_query_params.urlencode()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        query_params.update(kwargs)
 | 
					        query_params.update(kwargs)
 | 
				
			||||||
        full_url = base_url
 | 
					        full_url = base_url
 | 
				
			||||||
        if len(query_params) > 0:
 | 
					        if len(query_params) > 0:
 | 
				
			||||||
@ -75,7 +75,7 @@ class EmailStageView(ChallengeStageView):
 | 
				
			|||||||
        valid_delta = timedelta(
 | 
					        valid_delta = timedelta(
 | 
				
			||||||
            minutes=current_stage.token_expiry + 1
 | 
					            minutes=current_stage.token_expiry + 1
 | 
				
			||||||
        )  # + 1 because django timesince always rounds down
 | 
					        )  # + 1 because django timesince always rounds down
 | 
				
			||||||
        identifier = slugify(f"ak-email-stage-{current_stage.name}-{pending_user}")
 | 
					        identifier = slugify(f"ak-email-stage-{current_stage.name}-{str(uuid4())}")
 | 
				
			||||||
        # Don't check for validity here, we only care if the token exists
 | 
					        # Don't check for validity here, we only care if the token exists
 | 
				
			||||||
        tokens = FlowToken.objects.filter(identifier=identifier)
 | 
					        tokens = FlowToken.objects.filter(identifier=identifier)
 | 
				
			||||||
        if not tokens.exists():
 | 
					        if not tokens.exists():
 | 
				
			||||||
@ -107,6 +107,7 @@ class EmailStageView(ChallengeStageView):
 | 
				
			|||||||
        current_stage: EmailStage = self.executor.current_stage
 | 
					        current_stage: EmailStage = self.executor.current_stage
 | 
				
			||||||
        token = self.get_token()
 | 
					        token = self.get_token()
 | 
				
			||||||
        # Send mail to user
 | 
					        # Send mail to user
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
            message = TemplateEmailMessage(
 | 
					            message = TemplateEmailMessage(
 | 
				
			||||||
                subject=_(current_stage.subject),
 | 
					                subject=_(current_stage.subject),
 | 
				
			||||||
                to=[email],
 | 
					                to=[email],
 | 
				
			||||||
@ -119,6 +120,14 @@ class EmailStageView(ChallengeStageView):
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            send_mails(current_stage, message)
 | 
					            send_mails(current_stage, message)
 | 
				
			||||||
 | 
					        except TemplateSyntaxError as exc:
 | 
				
			||||||
 | 
					            Event.new(
 | 
				
			||||||
 | 
					                EventAction.CONFIGURATION_ERROR,
 | 
				
			||||||
 | 
					                message=_("Exception occurred while rendering E-mail template"),
 | 
				
			||||||
 | 
					                error=exception_to_string(exc),
 | 
				
			||||||
 | 
					                template=current_stage.template,
 | 
				
			||||||
 | 
					            ).from_http(self.request)
 | 
				
			||||||
 | 
					            raise StageInvalidException from exc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 | 
					    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 | 
				
			||||||
        # Check if the user came back from the email link to verify
 | 
					        # Check if the user came back from the email link to verify
 | 
				
			||||||
@ -139,7 +148,11 @@ class EmailStageView(ChallengeStageView):
 | 
				
			|||||||
            return self.executor.stage_invalid()
 | 
					            return self.executor.stage_invalid()
 | 
				
			||||||
        # Check if we've already sent the initial e-mail
 | 
					        # Check if we've already sent the initial e-mail
 | 
				
			||||||
        if PLAN_CONTEXT_EMAIL_SENT not in self.executor.plan.context:
 | 
					        if PLAN_CONTEXT_EMAIL_SENT not in self.executor.plan.context:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
                self.send_email()
 | 
					                self.send_email()
 | 
				
			||||||
 | 
					            except StageInvalidException as exc:
 | 
				
			||||||
 | 
					                self.logger.debug("Got StageInvalidException", exc=exc)
 | 
				
			||||||
 | 
					                return self.executor.stage_invalid()
 | 
				
			||||||
            self.executor.plan.context[PLAN_CONTEXT_EMAIL_SENT] = True
 | 
					            self.executor.plan.context[PLAN_CONTEXT_EMAIL_SENT] = True
 | 
				
			||||||
        return super().get(request, *args, **kwargs)
 | 
					        return super().get(request, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -259,7 +259,7 @@ class TestEmailStage(FlowTestCase):
 | 
				
			|||||||
        session.save()
 | 
					        session.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
 | 
					        url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
 | 
				
			||||||
        url += "?foo=bar"
 | 
					        url += "?query=" + urlencode({"foo": "bar"})
 | 
				
			||||||
        request = self.factory.get(url)
 | 
					        request = self.factory.get(url)
 | 
				
			||||||
        stage_view = EmailStageView(
 | 
					        stage_view = EmailStageView(
 | 
				
			||||||
            FlowExecutorView(
 | 
					            FlowExecutorView(
 | 
				
			||||||
@ -273,31 +273,3 @@ class TestEmailStage(FlowTestCase):
 | 
				
			|||||||
            stage_view.get_full_url(**{QS_KEY_TOKEN: token}),
 | 
					            stage_view.get_full_url(**{QS_KEY_TOKEN: token}),
 | 
				
			||||||
            f"http://testserver/if/flow/{self.flow.slug}/?foo=bar&flow_token={token}",
 | 
					            f"http://testserver/if/flow/{self.flow.slug}/?foo=bar&flow_token={token}",
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_url_existing_params_nested(self):
 | 
					 | 
				
			||||||
        """Test to ensure that URL params are preserved in the URL being sent (including nested)"""
 | 
					 | 
				
			||||||
        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
 | 
					 | 
				
			||||||
        plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
 | 
					 | 
				
			||||||
        session = self.client.session
 | 
					 | 
				
			||||||
        session[SESSION_KEY_PLAN] = plan
 | 
					 | 
				
			||||||
        session.save()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
 | 
					 | 
				
			||||||
        url += "?foo=bar&"
 | 
					 | 
				
			||||||
        url += "query=" + urlencode({"nested": "value"})
 | 
					 | 
				
			||||||
        request = self.factory.get(url)
 | 
					 | 
				
			||||||
        stage_view = EmailStageView(
 | 
					 | 
				
			||||||
            FlowExecutorView(
 | 
					 | 
				
			||||||
                request=request,
 | 
					 | 
				
			||||||
                flow=self.flow,
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            request=request,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        token = generate_id()
 | 
					 | 
				
			||||||
        self.assertEqual(
 | 
					 | 
				
			||||||
            stage_view.get_full_url(**{QS_KEY_TOKEN: token}),
 | 
					 | 
				
			||||||
            (
 | 
					 | 
				
			||||||
                f"http://testserver/if/flow/{self.flow.slug}"
 | 
					 | 
				
			||||||
                f"/?foo=bar&query=nested%3Dvalue&flow_token={token}"
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -4,11 +4,20 @@ from pathlib import Path
 | 
				
			|||||||
from shutil import rmtree
 | 
					from shutil import rmtree
 | 
				
			||||||
from tempfile import mkdtemp, mkstemp
 | 
					from tempfile import mkdtemp, mkstemp
 | 
				
			||||||
from typing import Any
 | 
					from typing import Any
 | 
				
			||||||
 | 
					from unittest.mock import PropertyMock, patch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.test import TestCase
 | 
					from django.core.mail.backends.locmem import EmailBackend
 | 
				
			||||||
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.stages.email.models import get_template_choices
 | 
					from authentik.core.tests.utils import create_test_admin_user, create_test_flow
 | 
				
			||||||
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
 | 
					from authentik.flows.markers import StageMarker
 | 
				
			||||||
 | 
					from authentik.flows.models import FlowDesignation, FlowStageBinding
 | 
				
			||||||
 | 
					from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
 | 
				
			||||||
 | 
					from authentik.flows.tests import FlowTestCase
 | 
				
			||||||
 | 
					from authentik.flows.views.executor import SESSION_KEY_PLAN
 | 
				
			||||||
 | 
					from authentik.stages.email.models import EmailStage, get_template_choices
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_templates_setting(temp_dir: str) -> dict[str, Any]:
 | 
					def get_templates_setting(temp_dir: str) -> dict[str, Any]:
 | 
				
			||||||
@ -18,11 +27,18 @@ def get_templates_setting(temp_dir: str) -> dict[str, Any]:
 | 
				
			|||||||
    return templates_setting
 | 
					    return templates_setting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestEmailStageTemplates(TestCase):
 | 
					class TestEmailStageTemplates(FlowTestCase):
 | 
				
			||||||
    """Email tests"""
 | 
					    """Email tests"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def setUp(self) -> None:
 | 
					    def setUp(self) -> None:
 | 
				
			||||||
        self.dir = mkdtemp()
 | 
					        self.dir = Path(mkdtemp())
 | 
				
			||||||
 | 
					        self.user = create_test_admin_user()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.flow = create_test_flow(FlowDesignation.AUTHENTICATION)
 | 
				
			||||||
 | 
					        self.stage = EmailStage.objects.create(
 | 
				
			||||||
 | 
					            name="email",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def tearDown(self) -> None:
 | 
					    def tearDown(self) -> None:
 | 
				
			||||||
        rmtree(self.dir)
 | 
					        rmtree(self.dir)
 | 
				
			||||||
@ -38,3 +54,37 @@ class TestEmailStageTemplates(TestCase):
 | 
				
			|||||||
            self.assertEqual(len(choices), 3)
 | 
					            self.assertEqual(len(choices), 3)
 | 
				
			||||||
            unlink(file)
 | 
					            unlink(file)
 | 
				
			||||||
            unlink(file2)
 | 
					            unlink(file2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_custom_template_invalid_syntax(self):
 | 
				
			||||||
 | 
					        """Test with custom template"""
 | 
				
			||||||
 | 
					        with open(self.dir / Path("invalid.html"), "w+", encoding="utf-8") as _invalid:
 | 
				
			||||||
 | 
					            _invalid.write("{% blocktranslate %}")
 | 
				
			||||||
 | 
					        with self.settings(TEMPLATES=get_templates_setting(self.dir)):
 | 
				
			||||||
 | 
					            self.stage.template = "invalid.html"
 | 
				
			||||||
 | 
					            plan = FlowPlan(
 | 
				
			||||||
 | 
					                flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
 | 
				
			||||||
 | 
					            session = self.client.session
 | 
				
			||||||
 | 
					            session[SESSION_KEY_PLAN] = plan
 | 
				
			||||||
 | 
					            session.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
 | 
				
			||||||
 | 
					            with patch(
 | 
				
			||||||
 | 
					                "authentik.stages.email.models.EmailStage.backend_class",
 | 
				
			||||||
 | 
					                PropertyMock(return_value=EmailBackend),
 | 
				
			||||||
 | 
					            ):
 | 
				
			||||||
 | 
					                response = self.client.get(url)
 | 
				
			||||||
 | 
					                self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					                self.assertStageResponse(
 | 
				
			||||||
 | 
					                    response,
 | 
				
			||||||
 | 
					                    self.flow,
 | 
				
			||||||
 | 
					                    error_message="Unknown error",
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                events = Event.objects.filter(action=EventAction.CONFIGURATION_ERROR)
 | 
				
			||||||
 | 
					                self.assertEqual(len(events), 1)
 | 
				
			||||||
 | 
					                event = events.first()
 | 
				
			||||||
 | 
					                self.assertEqual(
 | 
				
			||||||
 | 
					                    event.context["message"], "Exception occurred while rendering E-mail template"
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                self.assertEqual(event.context["template"], "invalid.html")
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,7 @@ from django.urls import reverse
 | 
				
			|||||||
from authentik.core.models import USER_ATTRIBUTE_SOURCES, Group, Source, User, UserSourceConnection
 | 
					from authentik.core.models import USER_ATTRIBUTE_SOURCES, Group, Source, User, UserSourceConnection
 | 
				
			||||||
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION
 | 
					from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION
 | 
				
			||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
 | 
					from authentik.core.tests.utils import create_test_admin_user, create_test_flow
 | 
				
			||||||
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
from authentik.flows.markers import StageMarker
 | 
					from authentik.flows.markers import StageMarker
 | 
				
			||||||
from authentik.flows.models import FlowStageBinding
 | 
					from authentik.flows.models import FlowStageBinding
 | 
				
			||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
 | 
					from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
 | 
				
			||||||
@ -58,11 +59,33 @@ class TestUserWriteStage(FlowTestCase):
 | 
				
			|||||||
        self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
 | 
					        self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
 | 
				
			||||||
        user_qs = User.objects.filter(username=plan.context[PLAN_CONTEXT_PROMPT]["username"])
 | 
					        user_qs = User.objects.filter(username=plan.context[PLAN_CONTEXT_PROMPT]["username"])
 | 
				
			||||||
        self.assertTrue(user_qs.exists())
 | 
					        self.assertTrue(user_qs.exists())
 | 
				
			||||||
        self.assertTrue(user_qs.first().check_password(password))
 | 
					        user = user_qs.first()
 | 
				
			||||||
        self.assertEqual(
 | 
					        self.assertTrue(user.check_password(password))
 | 
				
			||||||
            list(user_qs.first().ak_groups.order_by("name")), [self.other_group, self.group]
 | 
					        self.assertEqual(list(user.ak_groups.order_by("name")), [self.other_group, self.group])
 | 
				
			||||||
 | 
					        self.assertEqual(user.attributes, {USER_ATTRIBUTE_SOURCES: [self.source.name]})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertTrue(
 | 
				
			||||||
 | 
					            Event.objects.filter(
 | 
				
			||||||
 | 
					                action=EventAction.MODEL_CREATED,
 | 
				
			||||||
 | 
					                context__model={
 | 
				
			||||||
 | 
					                    "app": "authentik_core",
 | 
				
			||||||
 | 
					                    "model_name": "user",
 | 
				
			||||||
 | 
					                    "pk": user.pk,
 | 
				
			||||||
 | 
					                    "name": "name",
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertTrue(
 | 
				
			||||||
 | 
					            Event.objects.filter(
 | 
				
			||||||
 | 
					                action=EventAction.MODEL_UPDATED,
 | 
				
			||||||
 | 
					                context__model={
 | 
				
			||||||
 | 
					                    "app": "authentik_core",
 | 
				
			||||||
 | 
					                    "model_name": "user",
 | 
				
			||||||
 | 
					                    "pk": user.pk,
 | 
				
			||||||
 | 
					                    "name": "name",
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.assertEqual(user_qs.first().attributes, {USER_ATTRIBUTE_SOURCES: [self.source.name]})
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_user_update(self):
 | 
					    def test_user_update(self):
 | 
				
			||||||
        """Test update of existing user"""
 | 
					        """Test update of existing user"""
 | 
				
			||||||
 | 
				
			|||||||
@ -14,6 +14,7 @@ entries:
 | 
				
			|||||||
- attrs:
 | 
					- attrs:
 | 
				
			||||||
    configure_flow: !KeyOf flow
 | 
					    configure_flow: !KeyOf flow
 | 
				
			||||||
    token_count: 6
 | 
					    token_count: 6
 | 
				
			||||||
 | 
					    friendly_name: Static tokens
 | 
				
			||||||
  identifiers:
 | 
					  identifiers:
 | 
				
			||||||
    name: default-authenticator-static-setup
 | 
					    name: default-authenticator-static-setup
 | 
				
			||||||
  id: default-authenticator-static-setup
 | 
					  id: default-authenticator-static-setup
 | 
				
			||||||
 | 
				
			|||||||
@ -14,6 +14,7 @@ entries:
 | 
				
			|||||||
- attrs:
 | 
					- attrs:
 | 
				
			||||||
    configure_flow: !KeyOf flow
 | 
					    configure_flow: !KeyOf flow
 | 
				
			||||||
    digits: 6
 | 
					    digits: 6
 | 
				
			||||||
 | 
					    friendly_name: TOTP Device
 | 
				
			||||||
  identifiers:
 | 
					  identifiers:
 | 
				
			||||||
    name: default-authenticator-totp-setup
 | 
					    name: default-authenticator-totp-setup
 | 
				
			||||||
  id: default-authenticator-totp-setup
 | 
					  id: default-authenticator-totp-setup
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,7 @@ entries:
 | 
				
			|||||||
  id: flow
 | 
					  id: flow
 | 
				
			||||||
- attrs:
 | 
					- attrs:
 | 
				
			||||||
    configure_flow: !KeyOf flow
 | 
					    configure_flow: !KeyOf flow
 | 
				
			||||||
 | 
					    friendly_name: WebAuthn device
 | 
				
			||||||
  identifiers:
 | 
					  identifiers:
 | 
				
			||||||
    name: default-authenticator-webauthn-setup
 | 
					    name: default-authenticator-webauthn-setup
 | 
				
			||||||
  id: default-authenticator-webauthn-setup
 | 
					  id: default-authenticator-webauthn-setup
 | 
				
			||||||
 | 
				
			|||||||
@ -14,8 +14,11 @@ entries:
 | 
				
			|||||||
      expression: |
 | 
					      expression: |
 | 
				
			||||||
        # This mapping is used by the authentik proxy. It passes extra user attributes,
 | 
					        # This mapping is used by the authentik proxy. It passes extra user attributes,
 | 
				
			||||||
        # which are used for example for the HTTP-Basic Authentication mapping.
 | 
					        # which are used for example for the HTTP-Basic Authentication mapping.
 | 
				
			||||||
 | 
					        session_id = None
 | 
				
			||||||
 | 
					        if "token" in request.context:
 | 
				
			||||||
 | 
					            session_id = request.context.get("token").session_id
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
            "sid": request.http_request.session.session_key,
 | 
					            "sid": session_id,
 | 
				
			||||||
            "ak_proxy": {
 | 
					            "ak_proxy": {
 | 
				
			||||||
                "user_attributes": request.user.group_attributes(request),
 | 
					                "user_attributes": request.user.group_attributes(request),
 | 
				
			||||||
                "is_superuser": request.user.is_superuser,
 | 
					                "is_superuser": request.user.is_superuser,
 | 
				
			||||||
 | 
				
			|||||||
@ -12,12 +12,14 @@ entries:
 | 
				
			|||||||
      expression: |
 | 
					      expression: |
 | 
				
			||||||
        # Some implementations require givenName and familyName to be set
 | 
					        # Some implementations require givenName and familyName to be set
 | 
				
			||||||
        givenName, familyName = request.user.name, " "
 | 
					        givenName, familyName = request.user.name, " "
 | 
				
			||||||
 | 
					        formatted = request.user.name + " "
 | 
				
			||||||
        # This default sets givenName to the name before the first space
 | 
					        # This default sets givenName to the name before the first space
 | 
				
			||||||
        # and the remainder as family name
 | 
					        # and the remainder as family name
 | 
				
			||||||
        # if the user's name has no space the givenName is the entire name
 | 
					        # if the user's name has no space the givenName is the entire name
 | 
				
			||||||
        # (this might cause issues with some SCIM implementations)
 | 
					        # (this might cause issues with some SCIM implementations)
 | 
				
			||||||
        if " " in request.user.name:
 | 
					        if " " in request.user.name:
 | 
				
			||||||
            givenName, _, familyName = request.user.name.partition(" ")
 | 
					            givenName, _, familyName = request.user.name.partition(" ")
 | 
				
			||||||
 | 
					            formatted = request.user.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # photos supports URLs to images, however authentik might return data URIs
 | 
					        # photos supports URLs to images, however authentik might return data URIs
 | 
				
			||||||
        avatar = request.user.avatar
 | 
					        avatar = request.user.avatar
 | 
				
			||||||
@ -39,7 +41,7 @@ entries:
 | 
				
			|||||||
        return {
 | 
					        return {
 | 
				
			||||||
            "userName": request.user.username,
 | 
					            "userName": request.user.username,
 | 
				
			||||||
            "name": {
 | 
					            "name": {
 | 
				
			||||||
                "formatted": request.user.name,
 | 
					                "formatted": formatted,
 | 
				
			||||||
                "givenName": givenName,
 | 
					                "givenName": givenName,
 | 
				
			||||||
                "familyName": familyName,
 | 
					                "familyName": familyName,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
				
			|||||||
@ -32,7 +32,7 @@ services:
 | 
				
			|||||||
    volumes:
 | 
					    volumes:
 | 
				
			||||||
      - redis:/data
 | 
					      - redis:/data
 | 
				
			||||||
  server:
 | 
					  server:
 | 
				
			||||||
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.2}
 | 
					    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.7}
 | 
				
			||||||
    restart: unless-stopped
 | 
					    restart: unless-stopped
 | 
				
			||||||
    command: server
 | 
					    command: server
 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
@ -53,7 +53,7 @@ services:
 | 
				
			|||||||
      - postgresql
 | 
					      - postgresql
 | 
				
			||||||
      - redis
 | 
					      - redis
 | 
				
			||||||
  worker:
 | 
					  worker:
 | 
				
			||||||
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.2}
 | 
					    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.7}
 | 
				
			||||||
    restart: unless-stopped
 | 
					    restart: unless-stopped
 | 
				
			||||||
    command: worker
 | 
					    command: worker
 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							@ -4,7 +4,6 @@ go 1.21
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
require (
 | 
					require (
 | 
				
			||||||
	beryju.io/ldap v0.1.0
 | 
						beryju.io/ldap v0.1.0
 | 
				
			||||||
	github.com/Netflix/go-env v0.0.0-20210215222557-e437a7e7f9fb
 | 
					 | 
				
			||||||
	github.com/coreos/go-oidc v2.2.1+incompatible
 | 
						github.com/coreos/go-oidc v2.2.1+incompatible
 | 
				
			||||||
	github.com/getsentry/sentry-go v0.25.0
 | 
						github.com/getsentry/sentry-go v0.25.0
 | 
				
			||||||
	github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
 | 
						github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
 | 
				
			||||||
@ -24,6 +23,7 @@ require (
 | 
				
			|||||||
	github.com/pires/go-proxyproto v0.7.0
 | 
						github.com/pires/go-proxyproto v0.7.0
 | 
				
			||||||
	github.com/prometheus/client_golang v1.17.0
 | 
						github.com/prometheus/client_golang v1.17.0
 | 
				
			||||||
	github.com/redis/go-redis/v9 v9.2.1
 | 
						github.com/redis/go-redis/v9 v9.2.1
 | 
				
			||||||
 | 
						github.com/sethvargo/go-envconfig v1.0.0
 | 
				
			||||||
	github.com/sirupsen/logrus v1.9.3
 | 
						github.com/sirupsen/logrus v1.9.3
 | 
				
			||||||
	github.com/spf13/cobra v1.7.0
 | 
						github.com/spf13/cobra v1.7.0
 | 
				
			||||||
	github.com/stretchr/testify v1.8.4
 | 
						github.com/stretchr/testify v1.8.4
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										8
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.sum
									
									
									
									
									
								
							@ -37,8 +37,6 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+
 | 
				
			|||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
 | 
					github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
 | 
				
			||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 | 
					github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 | 
				
			||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 | 
					github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 | 
				
			||||||
github.com/Netflix/go-env v0.0.0-20210215222557-e437a7e7f9fb h1:w9IDEB7P1VzNcBpOG7kMpFkZp2DkyJIUt0gDx5MBhRU=
 | 
					 | 
				
			||||||
github.com/Netflix/go-env v0.0.0-20210215222557-e437a7e7f9fb/go.mod h1:9XMFaCeRyW7fC9XJOWQ+NdAv8VLG7ys7l3x4ozEGLUQ=
 | 
					 | 
				
			||||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
 | 
					github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
 | 
				
			||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 | 
					github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 | 
				
			||||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
 | 
					github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
 | 
				
			||||||
@ -198,8 +196,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 | 
				
			|||||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
					github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
				
			||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
					github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
				
			||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
					github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
				
			||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 | 
					github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 | 
				
			||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 | 
					github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 | 
				
			||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 | 
					github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 | 
				
			||||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
 | 
					github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
 | 
				
			||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 | 
					github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 | 
				
			||||||
@ -303,6 +301,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
 | 
				
			|||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
 | 
					github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
 | 
				
			||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
 | 
					github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
 | 
				
			||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 | 
					github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 | 
				
			||||||
 | 
					github.com/sethvargo/go-envconfig v1.0.0 h1:1C66wzy4QrROf5ew4KdVw942CQDa55qmlYmw9FZxZdU=
 | 
				
			||||||
 | 
					github.com/sethvargo/go-envconfig v1.0.0/go.mod h1:Lzc75ghUn5ucmcRGIdGQ33DKJrcjk4kihFYgSTBmjIc=
 | 
				
			||||||
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 | 
					github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 | 
				
			||||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 | 
					github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 | 
				
			||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 | 
					github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
package config
 | 
					package config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
	_ "embed"
 | 
						_ "embed"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
@ -10,10 +11,11 @@ import (
 | 
				
			|||||||
	"reflect"
 | 
						"reflect"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	env "github.com/Netflix/go-env"
 | 
						env "github.com/sethvargo/go-envconfig"
 | 
				
			||||||
	log "github.com/sirupsen/logrus"
 | 
						log "github.com/sirupsen/logrus"
 | 
				
			||||||
	"goauthentik.io/authentik/lib"
 | 
					 | 
				
			||||||
	"gopkg.in/yaml.v2"
 | 
						"gopkg.in/yaml.v2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"goauthentik.io/authentik/lib"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var cfg *Config
 | 
					var cfg *Config
 | 
				
			||||||
@ -113,7 +115,8 @@ func (c *Config) LoadConfigFromFile(path string) error {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (c *Config) fromEnv() error {
 | 
					func (c *Config) fromEnv() error {
 | 
				
			||||||
	_, err := env.UnmarshalFromEnviron(c)
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
						err := env.Process(ctx, c)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("failed to load environment variables: %w", err)
 | 
							return fmt.Errorf("failed to load environment variables: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -3,17 +3,17 @@ package config
 | 
				
			|||||||
type Config struct {
 | 
					type Config struct {
 | 
				
			||||||
	// Core specific config
 | 
						// Core specific config
 | 
				
			||||||
	Paths          PathsConfig          `yaml:"paths"`
 | 
						Paths          PathsConfig          `yaml:"paths"`
 | 
				
			||||||
	LogLevel       string               `yaml:"log_level" env:"AUTHENTIK_LOG_LEVEL"`
 | 
						LogLevel       string               `yaml:"log_level" env:"AUTHENTIK_LOG_LEVEL, overwrite"`
 | 
				
			||||||
	ErrorReporting ErrorReportingConfig `yaml:"error_reporting"`
 | 
						ErrorReporting ErrorReportingConfig `yaml:"error_reporting" env:", prefix=AUTHENTIK_ERROR_REPORTING__"`
 | 
				
			||||||
	Redis          RedisConfig          `yaml:"redis"`
 | 
						Redis          RedisConfig          `yaml:"redis" env:", prefix=AUTHENTIK_REDIS__"`
 | 
				
			||||||
	Outposts       OutpostConfig        `yaml:"outposts"`
 | 
						Outposts       OutpostConfig        `yaml:"outposts" env:", prefix=AUTHENTIK_OUTPOSTS__"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Config for core and embedded outpost
 | 
						// Config for core and embedded outpost
 | 
				
			||||||
	SecretKey string `yaml:"secret_key" env:"AUTHENTIK_SECRET_KEY"`
 | 
						SecretKey string `yaml:"secret_key" env:"AUTHENTIK_SECRET_KEY, overwrite"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Config for both core and outposts
 | 
						// Config for both core and outposts
 | 
				
			||||||
	Debug  bool         `yaml:"debug" env:"AUTHENTIK_DEBUG"`
 | 
						Debug  bool         `yaml:"debug" env:"AUTHENTIK_DEBUG, overwrite"`
 | 
				
			||||||
	Listen ListenConfig `yaml:"listen"`
 | 
						Listen ListenConfig `yaml:"listen" env:", prefix=AUTHENTIK_LISTEN__"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Outpost specific config
 | 
						// Outpost specific config
 | 
				
			||||||
	// These are only relevant for proxy/ldap outposts, and cannot be set via YAML
 | 
						// These are only relevant for proxy/ldap outposts, and cannot be set via YAML
 | 
				
			||||||
@ -25,27 +25,28 @@ type Config struct {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type RedisConfig struct {
 | 
					type RedisConfig struct {
 | 
				
			||||||
	Host                   string `yaml:"host" env:"AUTHENTIK_REDIS__HOST"`
 | 
						Host                   string `yaml:"host" env:"HOST, overwrite"`
 | 
				
			||||||
	Port                   int    `yaml:"port" env:"AUTHENTIK_REDIS__PORT"`
 | 
						Port                   int    `yaml:"port" env:"PORT, overwrite"`
 | 
				
			||||||
	Password               string `yaml:"password" env:"AUTHENTIK_REDIS__PASSWORD"`
 | 
						DB                     int    `yaml:"db" env:"DB, overwrite"`
 | 
				
			||||||
	TLS                    bool   `yaml:"tls" env:"AUTHENTIK_REDIS__TLS"`
 | 
						Username               string `yaml:"username" env:"USERNAME, overwrite"`
 | 
				
			||||||
	TLSReqs                string `yaml:"tls_reqs" env:"AUTHENTIK_REDIS__TLS_REQS"`
 | 
						Password               string `yaml:"password" env:"PASSWORD, overwrite"`
 | 
				
			||||||
	DB                     int    `yaml:"cache_db" env:"AUTHENTIK_REDIS__DB"`
 | 
						TLS                    bool   `yaml:"tls" env:"TLS, overwrite"`
 | 
				
			||||||
	CacheTimeout           int    `yaml:"cache_timeout" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT"`
 | 
						TLSReqs                string `yaml:"tls_reqs" env:"TLS_REQS, overwrite"`
 | 
				
			||||||
	CacheTimeoutFlows      int    `yaml:"cache_timeout_flows" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT_FLOWS"`
 | 
						CacheTimeout           int    `yaml:"cache_timeout" env:"CACHE_TIMEOUT, overwrite"`
 | 
				
			||||||
	CacheTimeoutPolicies   int    `yaml:"cache_timeout_policies" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT_POLICIES"`
 | 
						CacheTimeoutFlows      int    `yaml:"cache_timeout_flows" env:"CACHE_TIMEOUT_FLOWS, overwrite"`
 | 
				
			||||||
	CacheTimeoutReputation int    `yaml:"cache_timeout_reputation" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT_REPUTATION"`
 | 
						CacheTimeoutPolicies   int    `yaml:"cache_timeout_policies" env:"CACHE_TIMEOUT_POLICIES, overwrite"`
 | 
				
			||||||
 | 
						CacheTimeoutReputation int    `yaml:"cache_timeout_reputation" env:"CACHE_TIMEOUT_REPUTATION, overwrite"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ListenConfig struct {
 | 
					type ListenConfig struct {
 | 
				
			||||||
	HTTP              string   `yaml:"listen_http" env:"AUTHENTIK_LISTEN__HTTP"`
 | 
						HTTP              string   `yaml:"listen_http" env:"HTTP, overwrite"`
 | 
				
			||||||
	HTTPS             string   `yaml:"listen_https" env:"AUTHENTIK_LISTEN__HTTPS"`
 | 
						HTTPS             string   `yaml:"listen_https" env:"HTTPS, overwrite"`
 | 
				
			||||||
	LDAP              string   `yaml:"listen_ldap" env:"AUTHENTIK_LISTEN__LDAP"`
 | 
						LDAP              string   `yaml:"listen_ldap" env:"LDAP, overwrite"`
 | 
				
			||||||
	LDAPS             string   `yaml:"listen_ldaps" env:"AUTHENTIK_LISTEN__LDAPS"`
 | 
						LDAPS             string   `yaml:"listen_ldaps" env:"LDAPS, overwrite"`
 | 
				
			||||||
	Radius            string   `yaml:"listen_radius" env:"AUTHENTIK_LISTEN__RADIUS"`
 | 
						Radius            string   `yaml:"listen_radius" env:"RADIUS, overwrite"`
 | 
				
			||||||
	Metrics           string   `yaml:"listen_metrics" env:"AUTHENTIK_LISTEN__METRICS"`
 | 
						Metrics           string   `yaml:"listen_metrics" env:"METRICS, overwrite"`
 | 
				
			||||||
	Debug             string   `yaml:"listen_debug" env:"AUTHENTIK_LISTEN__DEBUG"`
 | 
						Debug             string   `yaml:"listen_debug" env:"DEBUG, overwrite"`
 | 
				
			||||||
	TrustedProxyCIDRs []string `yaml:"trusted_proxy_cidrs" env:"AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS"`
 | 
						TrustedProxyCIDRs []string `yaml:"trusted_proxy_cidrs" env:"TRUSTED_PROXY_CIDRS, overwrite"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type PathsConfig struct {
 | 
					type PathsConfig struct {
 | 
				
			||||||
@ -53,15 +54,15 @@ type PathsConfig struct {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ErrorReportingConfig struct {
 | 
					type ErrorReportingConfig struct {
 | 
				
			||||||
	Enabled     bool    `yaml:"enabled" env:"AUTHENTIK_ERROR_REPORTING__ENABLED"`
 | 
						Enabled     bool    `yaml:"enabled" env:"ENABLED, overwrite"`
 | 
				
			||||||
	SentryDSN   string  `yaml:"sentry_dsn" env:"AUTHENTIK_ERROR_REPORTING__SENTRY_DSN"`
 | 
						SentryDSN   string  `yaml:"sentry_dsn" env:"SENTRY_DSN, overwrite"`
 | 
				
			||||||
	Environment string  `yaml:"environment" env:"AUTHENTIK_ERROR_REPORTING__ENVIRONMENT"`
 | 
						Environment string  `yaml:"environment" env:"ENVIRONMENT, overwrite"`
 | 
				
			||||||
	SendPII     bool    `yaml:"send_pii" env:"AUTHENTIK_ERROR_REPORTING__SEND_PII"`
 | 
						SendPII     bool    `yaml:"send_pii" env:"SEND_PII, overwrite"`
 | 
				
			||||||
	SampleRate  float64 `yaml:"sample_rate" env:"AUTHENTIK_ERROR_REPORTING__SAMPLE_RATE"`
 | 
						SampleRate  float64 `yaml:"sample_rate" env:"SAMPLE_RATE, overwrite"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type OutpostConfig struct {
 | 
					type OutpostConfig struct {
 | 
				
			||||||
	ContainerImageBase     string `yaml:"container_image_base" env:"AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE"`
 | 
						ContainerImageBase     string `yaml:"container_image_base" env:"CONTAINER_IMAGE_BASE, overwrite"`
 | 
				
			||||||
	Discover               bool   `yaml:"discover" env:"AUTHENTIK_OUTPOSTS__DISCOVER"`
 | 
						Discover               bool   `yaml:"discover" env:"DISCOVER, overwrite"`
 | 
				
			||||||
	DisableEmbeddedOutpost bool   `yaml:"disable_embedded_outpost" env:"AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST"`
 | 
						DisableEmbeddedOutpost bool   `yaml:"disable_embedded_outpost" env:"DISABLE_EMBEDDED_OUTPOST, overwrite"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -29,4 +29,4 @@ func UserAgent() string {
 | 
				
			|||||||
	return fmt.Sprintf("authentik@%s", FullVersion())
 | 
						return fmt.Sprintf("authentik@%s", FullVersion())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const VERSION = "2023.10.2"
 | 
					const VERSION = "2023.10.7"
 | 
				
			||||||
 | 
				
			|||||||
@ -31,16 +31,11 @@ func (a *Application) redeemCallback(savedState string, u *url.URL, c context.Co
 | 
				
			|||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Extract the ID Token from OAuth2 token.
 | 
						jwt := oauth2Token.AccessToken
 | 
				
			||||||
	rawIDToken, ok := oauth2Token.Extra("id_token").(string)
 | 
						a.log.WithField("jwt", jwt).Trace("access_token")
 | 
				
			||||||
	if !ok {
 | 
					 | 
				
			||||||
		return nil, fmt.Errorf("missing id_token")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	a.log.WithField("id_token", rawIDToken).Trace("id_token")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Parse and verify ID Token payload.
 | 
						// Parse and verify ID Token payload.
 | 
				
			||||||
	idToken, err := a.tokenVerifier.Verify(ctx, rawIDToken)
 | 
						idToken, err := a.tokenVerifier.Verify(ctx, jwt)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -53,6 +48,6 @@ func (a *Application) redeemCallback(savedState string, u *url.URL, c context.Co
 | 
				
			|||||||
	if claims.Proxy == nil {
 | 
						if claims.Proxy == nil {
 | 
				
			||||||
		claims.Proxy = &ProxyClaims{}
 | 
							claims.Proxy = &ProxyClaims{}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	claims.RawToken = rawIDToken
 | 
						claims.RawToken = jwt
 | 
				
			||||||
	return claims, nil
 | 
						return claims, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -71,7 +71,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
 | 
				
			|||||||
	cs.Options.Domain = *p.CookieDomain
 | 
						cs.Options.Domain = *p.CookieDomain
 | 
				
			||||||
	cs.Options.SameSite = http.SameSiteLaxMode
 | 
						cs.Options.SameSite = http.SameSiteLaxMode
 | 
				
			||||||
	cs.Options.MaxAge = maxAge
 | 
						cs.Options.MaxAge = maxAge
 | 
				
			||||||
	cs.Options.Path = externalHost.Path
 | 
						cs.Options.Path = "/"
 | 
				
			||||||
	a.log.WithField("dir", dir).Trace("using filesystem session backend")
 | 
						a.log.WithField("dir", dir).Trace("using filesystem session backend")
 | 
				
			||||||
	return cs
 | 
						return cs
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -131,7 +131,6 @@ func (a *Application) Logout(ctx context.Context, filter func(c Claims) bool) er
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	if rs, ok := a.sessions.(*redisstore.RedisStore); ok {
 | 
						if rs, ok := a.sessions.(*redisstore.RedisStore); ok {
 | 
				
			||||||
		client := rs.Client()
 | 
							client := rs.Client()
 | 
				
			||||||
		defer client.Close()
 | 
					 | 
				
			||||||
		keys, err := client.Keys(ctx, fmt.Sprintf("%s*", RedisKeyPrefix)).Result()
 | 
							keys, err := client.Keys(ctx, fmt.Sprintf("%s*", RedisKeyPrefix)).Result()
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
 | 
				
			|||||||
@ -36,6 +36,7 @@ func (ps *ProxyServer) handleWSMessage(ctx context.Context, args map[string]inte
 | 
				
			|||||||
	switch msg.SubType {
 | 
						switch msg.SubType {
 | 
				
			||||||
	case WSProviderSubTypeLogout:
 | 
						case WSProviderSubTypeLogout:
 | 
				
			||||||
		for _, p := range ps.apps {
 | 
							for _, p := range ps.apps {
 | 
				
			||||||
 | 
								ps.log.WithField("provider", p.Host).Debug("Logging out")
 | 
				
			||||||
			err := p.Logout(ctx, func(c application.Claims) bool {
 | 
								err := p.Logout(ctx, func(c application.Claims) bool {
 | 
				
			||||||
				return c.Sid == msg.SessionID
 | 
									return c.Sid == msg.SessionID
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,14 @@
 | 
				
			|||||||
 | 
					# syntax=docker/dockerfile:1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 1: Build
 | 
					# Stage 1: Build
 | 
				
			||||||
FROM docker.io/golang:1.21.3-bookworm AS builder
 | 
					FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.4-bookworm AS builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ARG TARGETOS
 | 
				
			||||||
 | 
					ARG TARGETARCH
 | 
				
			||||||
 | 
					ARG TARGETVARIANT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ARG GOOS=$TARGETOS
 | 
				
			||||||
 | 
					ARG GOARCH=$TARGETARCH
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WORKDIR /go/src/goauthentik.io
 | 
					WORKDIR /go/src/goauthentik.io
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -11,9 +20,9 @@ RUN --mount=type=bind,target=/go/src/goauthentik.io/go.mod,src=./go.mod \
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
ENV CGO_ENABLED=0
 | 
					ENV CGO_ENABLED=0
 | 
				
			||||||
COPY . .
 | 
					COPY . .
 | 
				
			||||||
RUN --mount=type=cache,target=/go/pkg/mod \
 | 
					RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
 | 
				
			||||||
    --mount=type=cache,target=/root/.cache/go-build \
 | 
					    --mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
 | 
				
			||||||
    go build -o /go/ldap ./cmd/ldap
 | 
					    GOARM="${TARGETVARIANT#v}" go build -o /go/ldap ./cmd/ldap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 2: Run
 | 
					# Stage 2: Run
 | 
				
			||||||
FROM gcr.io/distroless/static-debian11:debug
 | 
					FROM gcr.io/distroless/static-debian11:debug
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										31
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										31
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							@ -1,4 +1,4 @@
 | 
				
			|||||||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
 | 
					# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "aiohttp"
 | 
					name = "aiohttp"
 | 
				
			||||||
@ -2096,16 +2096,6 @@ files = [
 | 
				
			|||||||
    {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
 | 
					    {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
 | 
				
			||||||
    {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
 | 
					    {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
 | 
				
			||||||
    {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
 | 
					    {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
 | 
				
			||||||
    {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
 | 
					 | 
				
			||||||
    {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
 | 
					 | 
				
			||||||
    {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
 | 
					 | 
				
			||||||
    {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
 | 
					 | 
				
			||||||
    {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
 | 
					 | 
				
			||||||
    {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
 | 
					 | 
				
			||||||
    {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
 | 
					 | 
				
			||||||
    {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
 | 
					 | 
				
			||||||
    {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
 | 
					 | 
				
			||||||
    {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
 | 
					 | 
				
			||||||
    {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
 | 
					    {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
 | 
				
			||||||
    {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
 | 
					    {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
 | 
				
			||||||
    {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
 | 
					    {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
 | 
				
			||||||
@ -2840,10 +2830,7 @@ files = [
 | 
				
			|||||||
[package.dependencies]
 | 
					[package.dependencies]
 | 
				
			||||||
astroid = ">=3.0.1,<=3.1.0-dev0"
 | 
					astroid = ">=3.0.1,<=3.1.0-dev0"
 | 
				
			||||||
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
 | 
					colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
 | 
				
			||||||
dill = [
 | 
					dill = {version = ">=0.3.6", markers = "python_version >= \"3.11\""}
 | 
				
			||||||
    {version = ">=0.3.7", markers = "python_version >= \"3.12\""},
 | 
					 | 
				
			||||||
    {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
isort = ">=4.2.5,<6"
 | 
					isort = ">=4.2.5,<6"
 | 
				
			||||||
mccabe = ">=0.6,<0.8"
 | 
					mccabe = ">=0.6,<0.8"
 | 
				
			||||||
platformdirs = ">=2.2.0"
 | 
					platformdirs = ">=2.2.0"
 | 
				
			||||||
@ -3096,7 +3083,6 @@ files = [
 | 
				
			|||||||
    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
 | 
					    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
 | 
					    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
 | 
					    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
 | 
					 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
 | 
					    {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
 | 
					    {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
 | 
					    {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
 | 
				
			||||||
@ -3104,15 +3090,8 @@ files = [
 | 
				
			|||||||
    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
 | 
					    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
 | 
					    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
 | 
					    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
 | 
					 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
 | 
					    {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
 | 
					    {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
 | 
					 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
 | 
					 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
 | 
					 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
 | 
					 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
 | 
					 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
 | 
					 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
 | 
					    {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
 | 
					    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
 | 
					    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
 | 
				
			||||||
@ -3129,7 +3108,6 @@ files = [
 | 
				
			|||||||
    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
 | 
					    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
 | 
					    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
 | 
					    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
 | 
					 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
 | 
					    {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
 | 
					    {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
 | 
					    {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
 | 
				
			||||||
@ -3137,7 +3115,6 @@ files = [
 | 
				
			|||||||
    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
 | 
					    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
 | 
					    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
 | 
					    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
 | 
					 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
 | 
					    {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
 | 
					    {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
 | 
				
			||||||
    {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
 | 
					    {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
 | 
				
			||||||
@ -4331,5 +4308,5 @@ files = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[metadata]
 | 
					[metadata]
 | 
				
			||||||
lock-version = "2.0"
 | 
					lock-version = "2.0"
 | 
				
			||||||
python-versions = "^3.11"
 | 
					python-versions = "~3.11"
 | 
				
			||||||
content-hash = "2fc746976187f4674f04575cffd6a367744723bf78c356b6951c2370bc47ceae"
 | 
					content-hash = "5a57dede617d149e0f307fc42580dcfd0d4b76161009dc447d6f10b048426c98"
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					# syntax=docker/dockerfile:1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 1: Build website
 | 
					# Stage 1: Build website
 | 
				
			||||||
FROM --platform=${BUILDPLATFORM} docker.io/node:21 as web-builder
 | 
					FROM --platform=${BUILDPLATFORM} docker.io/node:21 as web-builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -15,7 +17,14 @@ COPY web .
 | 
				
			|||||||
RUN npm run build-proxy
 | 
					RUN npm run build-proxy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 2: Build
 | 
					# Stage 2: Build
 | 
				
			||||||
FROM docker.io/golang:1.21.3-bookworm AS builder
 | 
					FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.4-bookworm AS builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ARG TARGETOS
 | 
				
			||||||
 | 
					ARG TARGETARCH
 | 
				
			||||||
 | 
					ARG TARGETVARIANT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ARG GOOS=$TARGETOS
 | 
				
			||||||
 | 
					ARG GOARCH=$TARGETARCH
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WORKDIR /go/src/goauthentik.io
 | 
					WORKDIR /go/src/goauthentik.io
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -27,9 +36,9 @@ RUN --mount=type=bind,target=/go/src/goauthentik.io/go.mod,src=./go.mod \
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
ENV CGO_ENABLED=0
 | 
					ENV CGO_ENABLED=0
 | 
				
			||||||
COPY . .
 | 
					COPY . .
 | 
				
			||||||
RUN --mount=type=cache,target=/go/pkg/mod \
 | 
					RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
 | 
				
			||||||
    --mount=type=cache,target=/root/.cache/go-build \
 | 
					    --mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
 | 
				
			||||||
    go build -o /go/proxy ./cmd/proxy
 | 
					    GOARM="${TARGETVARIANT#v}" go build -o /go/proxy ./cmd/proxy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 3: Run
 | 
					# Stage 3: Run
 | 
				
			||||||
FROM gcr.io/distroless/static-debian11:debug
 | 
					FROM gcr.io/distroless/static-debian11:debug
 | 
				
			||||||
 | 
				
			|||||||
@ -97,7 +97,7 @@ const-rgx = "[a-zA-Z0-9_]{1,40}$"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
ignored-modules = ["binascii", "socket", "zlib"]
 | 
					ignored-modules = ["binascii", "socket", "zlib"]
 | 
				
			||||||
generated-members = ["xmlsec.constants.*", "xmlsec.tree.*", "xmlsec.template.*"]
 | 
					generated-members = ["xmlsec.constants.*", "xmlsec.tree.*", "xmlsec.template.*"]
 | 
				
			||||||
ignore = "migrations"
 | 
					ignore = ["migrations", "tests"]
 | 
				
			||||||
max-attributes = 12
 | 
					max-attributes = 12
 | 
				
			||||||
max-branches = 20
 | 
					max-branches = 20
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -113,7 +113,7 @@ filterwarnings = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[tool.poetry]
 | 
					[tool.poetry]
 | 
				
			||||||
name = "authentik"
 | 
					name = "authentik"
 | 
				
			||||||
version = "2023.10.2"
 | 
					version = "2023.10.7"
 | 
				
			||||||
description = ""
 | 
					description = ""
 | 
				
			||||||
authors = ["authentik Team <hello@goauthentik.io>"]
 | 
					authors = ["authentik Team <hello@goauthentik.io>"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -151,10 +151,10 @@ packaging = "*"
 | 
				
			|||||||
paramiko = "*"
 | 
					paramiko = "*"
 | 
				
			||||||
psycopg = { extras = ["c"], version = "*" }
 | 
					psycopg = { extras = ["c"], version = "*" }
 | 
				
			||||||
pycryptodome = "*"
 | 
					pycryptodome = "*"
 | 
				
			||||||
pydantic = "<3.0.0"
 | 
					pydantic = "*"
 | 
				
			||||||
pydantic-scim = "^0.0.8"
 | 
					pydantic-scim = "*"
 | 
				
			||||||
pyjwt = "*"
 | 
					pyjwt = "*"
 | 
				
			||||||
python = "^3.11"
 | 
					python = "~3.11"
 | 
				
			||||||
pyyaml = "*"
 | 
					pyyaml = "*"
 | 
				
			||||||
requests-oauthlib = "*"
 | 
					requests-oauthlib = "*"
 | 
				
			||||||
sentry-sdk = "*"
 | 
					sentry-sdk = "*"
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,14 @@
 | 
				
			|||||||
 | 
					# syntax=docker/dockerfile:1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 1: Build
 | 
					# Stage 1: Build
 | 
				
			||||||
FROM docker.io/golang:1.21.3-bookworm AS builder
 | 
					FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.4-bookworm AS builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ARG TARGETOS
 | 
				
			||||||
 | 
					ARG TARGETARCH
 | 
				
			||||||
 | 
					ARG TARGETVARIANT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ARG GOOS=$TARGETOS
 | 
				
			||||||
 | 
					ARG GOARCH=$TARGETARCH
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WORKDIR /go/src/goauthentik.io
 | 
					WORKDIR /go/src/goauthentik.io
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -11,9 +20,9 @@ RUN --mount=type=bind,target=/go/src/goauthentik.io/go.mod,src=./go.mod \
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
ENV CGO_ENABLED=0
 | 
					ENV CGO_ENABLED=0
 | 
				
			||||||
COPY . .
 | 
					COPY . .
 | 
				
			||||||
RUN --mount=type=cache,target=/go/pkg/mod \
 | 
					RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
 | 
				
			||||||
    --mount=type=cache,target=/root/.cache/go-build \
 | 
					    --mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
 | 
				
			||||||
    go build -o /go/radius ./cmd/radius
 | 
					    GOARM="${TARGETVARIANT#v}" go build -o /go/radius ./cmd/radius
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 2: Run
 | 
					# Stage 2: Run
 | 
				
			||||||
FROM gcr.io/distroless/static-debian11:debug
 | 
					FROM gcr.io/distroless/static-debian11:debug
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
openapi: 3.0.3
 | 
					openapi: 3.0.3
 | 
				
			||||||
info:
 | 
					info:
 | 
				
			||||||
  title: authentik
 | 
					  title: authentik
 | 
				
			||||||
  version: 2023.10.2
 | 
					  version: 2023.10.7
 | 
				
			||||||
  description: Making authentication simple.
 | 
					  description: Making authentication simple.
 | 
				
			||||||
  contact:
 | 
					  contact:
 | 
				
			||||||
    email: hello@goauthentik.io
 | 
					    email: hello@goauthentik.io
 | 
				
			||||||
 | 
				
			|||||||
@ -36,8 +36,8 @@ class TestProviderOAuth2Github(SeleniumTestCase):
 | 
				
			|||||||
            "auto_remove": True,
 | 
					            "auto_remove": True,
 | 
				
			||||||
            "healthcheck": Healthcheck(
 | 
					            "healthcheck": Healthcheck(
 | 
				
			||||||
                test=["CMD", "wget", "--spider", "http://localhost:3000"],
 | 
					                test=["CMD", "wget", "--spider", "http://localhost:3000"],
 | 
				
			||||||
                interval=5 * 100 * 1000000,
 | 
					                interval=5 * 1_000 * 1_000_000,
 | 
				
			||||||
                start_period=1 * 100 * 1000000,
 | 
					                start_period=1 * 1_000 * 1_000_000,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            "environment": {
 | 
					            "environment": {
 | 
				
			||||||
                "GF_AUTH_GITHUB_ENABLED": "true",
 | 
					                "GF_AUTH_GITHUB_ENABLED": "true",
 | 
				
			||||||
 | 
				
			|||||||
@ -42,8 +42,8 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
 | 
				
			|||||||
            "auto_remove": True,
 | 
					            "auto_remove": True,
 | 
				
			||||||
            "healthcheck": Healthcheck(
 | 
					            "healthcheck": Healthcheck(
 | 
				
			||||||
                test=["CMD", "wget", "--spider", "http://localhost:3000"],
 | 
					                test=["CMD", "wget", "--spider", "http://localhost:3000"],
 | 
				
			||||||
                interval=5 * 100 * 1000000,
 | 
					                interval=5 * 1_000 * 1_000_000,
 | 
				
			||||||
                start_period=1 * 100 * 1000000,
 | 
					                start_period=1 * 1_000 * 1_000_000,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            "environment": {
 | 
					            "environment": {
 | 
				
			||||||
                "GF_AUTH_GENERIC_OAUTH_ENABLED": "true",
 | 
					                "GF_AUTH_GENERIC_OAUTH_ENABLED": "true",
 | 
				
			||||||
 | 
				
			|||||||
@ -113,8 +113,8 @@ class TestSourceOAuth2(SeleniumTestCase):
 | 
				
			|||||||
            "command": "dex serve /config.yml",
 | 
					            "command": "dex serve /config.yml",
 | 
				
			||||||
            "healthcheck": Healthcheck(
 | 
					            "healthcheck": Healthcheck(
 | 
				
			||||||
                test=["CMD", "wget", "--spider", "http://localhost:5556/dex/healthz"],
 | 
					                test=["CMD", "wget", "--spider", "http://localhost:5556/dex/healthz"],
 | 
				
			||||||
                interval=5 * 100 * 1000000,
 | 
					                interval=5 * 1_000 * 1_000_000,
 | 
				
			||||||
                start_period=1 * 100 * 1000000,
 | 
					                start_period=1 * 1_000 * 1_000_000,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            "volumes": {str(Path(CONFIG_PATH).absolute()): {"bind": "/config.yml", "mode": "ro"}},
 | 
					            "volumes": {str(Path(CONFIG_PATH).absolute()): {"bind": "/config.yml", "mode": "ro"}},
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -83,8 +83,8 @@ class TestSourceSAML(SeleniumTestCase):
 | 
				
			|||||||
            "auto_remove": True,
 | 
					            "auto_remove": True,
 | 
				
			||||||
            "healthcheck": Healthcheck(
 | 
					            "healthcheck": Healthcheck(
 | 
				
			||||||
                test=["CMD", "curl", "http://localhost:8080"],
 | 
					                test=["CMD", "curl", "http://localhost:8080"],
 | 
				
			||||||
                interval=5 * 100 * 1000000,
 | 
					                interval=5 * 1_000 * 1_000_000,
 | 
				
			||||||
                start_period=1 * 100 * 1000000,
 | 
					                start_period=1 * 1_000 * 1_000_000,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            "environment": {
 | 
					            "environment": {
 | 
				
			||||||
                "SIMPLESAMLPHP_SP_ENTITY_ID": "entity-id",
 | 
					                "SIMPLESAMLPHP_SP_ENTITY_ID": "entity-id",
 | 
				
			||||||
 | 
				
			|||||||
@ -34,8 +34,8 @@ class OutpostDockerTests(DockerTestCase, ChannelsLiveServerTestCase):
 | 
				
			|||||||
            privileged=True,
 | 
					            privileged=True,
 | 
				
			||||||
            healthcheck=Healthcheck(
 | 
					            healthcheck=Healthcheck(
 | 
				
			||||||
                test=["CMD", "docker", "info"],
 | 
					                test=["CMD", "docker", "info"],
 | 
				
			||||||
                interval=5 * 100 * 1000000,
 | 
					                interval=5 * 1_000 * 1_000_000,
 | 
				
			||||||
                start_period=5 * 100 * 1000000,
 | 
					                start_period=5 * 1_000 * 1_000_000,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            environment={"DOCKER_TLS_CERTDIR": "/ssl"},
 | 
					            environment={"DOCKER_TLS_CERTDIR": "/ssl"},
 | 
				
			||||||
            volumes={
 | 
					            volumes={
 | 
				
			||||||
 | 
				
			|||||||
@ -34,8 +34,8 @@ class TestProxyDocker(DockerTestCase, ChannelsLiveServerTestCase):
 | 
				
			|||||||
            privileged=True,
 | 
					            privileged=True,
 | 
				
			||||||
            healthcheck=Healthcheck(
 | 
					            healthcheck=Healthcheck(
 | 
				
			||||||
                test=["CMD", "docker", "info"],
 | 
					                test=["CMD", "docker", "info"],
 | 
				
			||||||
                interval=5 * 100 * 1000000,
 | 
					                interval=5 * 1_000 * 1_000_000,
 | 
				
			||||||
                start_period=5 * 100 * 1000000,
 | 
					                start_period=5 * 1_000 * 1_000_000,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            environment={"DOCKER_TLS_CERTDIR": "/ssl"},
 | 
					            environment={"DOCKER_TLS_CERTDIR": "/ssl"},
 | 
				
			||||||
            volumes={
 | 
					            volumes={
 | 
				
			||||||
 | 
				
			|||||||
@ -27,5 +27,8 @@
 | 
				
			|||||||
        "precommit": "run-s lint:precommit lint:spelling prettier",
 | 
					        "precommit": "run-s lint:precommit lint:spelling prettier",
 | 
				
			||||||
        "prettier-check": "prettier --check .",
 | 
					        "prettier-check": "prettier --check .",
 | 
				
			||||||
        "prettier": "prettier --write ."
 | 
					        "prettier": "prettier --write ."
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "engines": {
 | 
				
			||||||
 | 
					        "node": ">=20"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										3
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -100,6 +100,9 @@
 | 
				
			|||||||
                "typescript": "^5.2.2",
 | 
					                "typescript": "^5.2.2",
 | 
				
			||||||
                "vite-tsconfig-paths": "^4.2.1"
 | 
					                "vite-tsconfig-paths": "^4.2.1"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            "engines": {
 | 
				
			||||||
 | 
					                "node": ">=20"
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
            "optionalDependencies": {
 | 
					            "optionalDependencies": {
 | 
				
			||||||
                "@esbuild/darwin-arm64": "^0.19.5",
 | 
					                "@esbuild/darwin-arm64": "^0.19.5",
 | 
				
			||||||
                "@esbuild/linux-amd64": "^0.18.11",
 | 
					                "@esbuild/linux-amd64": "^0.18.11",
 | 
				
			||||||
 | 
				
			|||||||
@ -125,5 +125,8 @@
 | 
				
			|||||||
        "@esbuild/darwin-arm64": "^0.19.5",
 | 
					        "@esbuild/darwin-arm64": "^0.19.5",
 | 
				
			||||||
        "@esbuild/linux-amd64": "^0.18.11",
 | 
					        "@esbuild/linux-amd64": "^0.18.11",
 | 
				
			||||||
        "@esbuild/linux-arm64": "^0.19.5"
 | 
					        "@esbuild/linux-arm64": "^0.19.5"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "engines": {
 | 
				
			||||||
 | 
					        "node": ">=20"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -116,7 +116,7 @@ export class ApplicationForm extends ModelForm<Application, string> {
 | 
				
			|||||||
        return app;
 | 
					        return app;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    handleConfirmBackchannelProviders({ items }: { items: Provider[] }) {
 | 
					    handleConfirmBackchannelProviders(items: Provider[]) {
 | 
				
			||||||
        this.backchannelProviders = items;
 | 
					        this.backchannelProviders = items;
 | 
				
			||||||
        this.requestUpdate();
 | 
					        this.requestUpdate();
 | 
				
			||||||
        return Promise.resolve();
 | 
					        return Promise.resolve();
 | 
				
			||||||
 | 
				
			|||||||
@ -63,7 +63,7 @@ export class AkBackchannelProvidersInput extends AKElement {
 | 
				
			|||||||
        return html`
 | 
					        return html`
 | 
				
			||||||
            <ak-form-element-horizontal label=${this.label} name=${name}>
 | 
					            <ak-form-element-horizontal label=${this.label} name=${name}>
 | 
				
			||||||
                <div class="pf-c-input-group">
 | 
					                <div class="pf-c-input-group">
 | 
				
			||||||
                    <ak-provider-select-table ?backchannelOnly=${true} .confirm=${confirm}>
 | 
					                    <ak-provider-select-table ?backchannelOnly=${true} .confirm=${this.confirm}>
 | 
				
			||||||
                        <button slot="trigger" class="pf-c-button pf-m-control" type="button">
 | 
					                        <button slot="trigger" class="pf-c-button pf-m-control" type="button">
 | 
				
			||||||
                            ${this.tooltip ? this.tooltip : nothing}
 | 
					                            ${this.tooltip ? this.tooltip : nothing}
 | 
				
			||||||
                            <i class="fas fa-plus" aria-hidden="true"></i>
 | 
					                            <i class="fas fa-plus" aria-hidden="true"></i>
 | 
				
			||||||
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user