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,17 +89,22 @@ 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, | ||||||
|                     "name": {"familyName": "", "formatted": "", "givenName": ""}, |                     "name": {"familyName": " ", "formatted": " ", "givenName": ""}, | ||||||
|                     "displayName": "", |                     "displayName": "", | ||||||
|                     "userName": user.username, |                     "userName": user.username, | ||||||
|                 }, |                 }, | ||||||
|             ) |             ) | ||||||
|             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,17 +179,22 @@ 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": [], | ||||||
|                     "externalId": user.uid, |                     "externalId": user.uid, | ||||||
|                     "name": {"familyName": "", "formatted": "", "givenName": ""}, |                     "name": {"familyName": " ", "formatted": " ", "givenName": ""}, | ||||||
|                     "userName": user.username, |                     "userName": user.username, | ||||||
|                 }, |                 }, | ||||||
|             ) |             ) | ||||||
|             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, | ||||||
|  | |||||||
| @ -11,13 +11,15 @@ entries: | |||||||
|       name: "authentik default SCIM Mapping: User" |       name: "authentik default SCIM Mapping: User" | ||||||
|       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 | ||||||
| } | } | ||||||
|  | |||||||
| @ -62,7 +62,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL) | |||||||
| 	// https://github.com/markbates/goth/commit/7276be0fdf719ddff753f3574ef0f967e4a5a5f7 | 	// https://github.com/markbates/goth/commit/7276be0fdf719ddff753f3574ef0f967e4a5a5f7 | ||||||
| 	// set the maxLength of the cookies stored on the disk to a larger number to prevent issues with: | 	// set the maxLength of the cookies stored on the disk to a larger number to prevent issues with: | ||||||
| 	// securecookie: the value is too long | 	// securecookie: the value is too long | ||||||
| 	// when using OpenID Connect , since this can contain a large amount of extra information in the id_token | 	// when using OpenID Connect, since this can contain a large amount of extra information in the id_token | ||||||
|  |  | ||||||
| 	// Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk | 	// Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk | ||||||
| 	cs.MaxLength(math.MaxInt) | 	cs.MaxLength(math.MaxInt) | ||||||
| @ -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
	