Compare commits
	
		
			49 Commits
		
	
	
		
			version-20
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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.6 | ||||||
| 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.6" | ||||||
| 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 | ||||||
|  |         root = Path(CONFIG.get("blueprints_dir")).absolute() | ||||||
|  |         path = Path(event.src_path).absolute() | ||||||
|  |         rel_path = str(path.relative_to(root)) | ||||||
|         if isinstance(event, FileCreatedEvent): |         if isinstance(event, FileCreatedEvent): | ||||||
|             LOGGER.debug("new blueprint file created, starting discovery") |             LOGGER.debug("new blueprint file created, starting discovery", path=rel_path) | ||||||
|             blueprints_discovery.delay() |             blueprints_discovery.delay(rel_path) | ||||||
|         if isinstance(event, FileModifiedEvent): |         if isinstance(event, FileModifiedEvent): | ||||||
|             path = Path(event.src_path) |  | ||||||
|             root = Path(CONFIG.get("blueprints_dir")).absolute() |  | ||||||
|             rel_path = str(path.relative_to(root)) |  | ||||||
|             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 = [ | ||||||
|  | |||||||
| @ -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""" | ||||||
|     gdpr_cleanup.delay(instance.pk) |     if CONFIG.get_bool("gdpr_compliance", True): | ||||||
|  |         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()) | ||||||
|     return value |     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 | ||||||
|  |     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(), | ||||||
|         ): |         ): | ||||||
|             challenge = self.get_challenge(*args, **kwargs) |             try: | ||||||
|  |                 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,12 +344,22 @@ 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", | ||||||
|         user.save() |             "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() | ||||||
|         if user_created: |         if user_created: | ||||||
|             self.build_user_permissions(user) |             self.build_user_permissions(user) | ||||||
|         return user |         return 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( | ||||||
|  | |||||||
							
								
								
									
										187
									
								
								authentik/providers/oauth2/tests/test_token_pkce.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								authentik/providers/oauth2/tests/test_token_pkce.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,187 @@ | |||||||
|  | """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_token(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) | ||||||
|  |         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_request", "error_description": "The request is otherwise malformed"}, | ||||||
|  |         ) | ||||||
|  |         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_request") | ||||||
|             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()) | ||||||
| @ -448,6 +458,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 +493,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 +509,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 +547,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 +563,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""" | ||||||
|         return apps.get_app_config(instance.content_type.app_label).verbose_name |         try: | ||||||
|  |             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""" | ||||||
|         return apps.get_app_config(instance.content_type.app_label).verbose_name |         try: | ||||||
|  |             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""" | ||||||
|  | |||||||
| @ -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,7 +20,7 @@ class AzureADOAuthRedirect(OAuthRedirect): | |||||||
|         } |         } | ||||||
|  |  | ||||||
|  |  | ||||||
| class AzureADOAuthCallback(OAuthCallback): | class AzureADOAuthCallback(OpenIDConnectOAuth2Callback): | ||||||
|     """AzureAD OAuth2 Callback""" |     """AzureAD OAuth2 Callback""" | ||||||
|  |  | ||||||
|     client_class = UserprofileHeaderAuthClient |     client_class = UserprofileHeaderAuthClient | ||||||
| @ -50,7 +50,7 @@ class AzureADType(SourceType): | |||||||
|  |  | ||||||
|     authorization_url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" |     authorization_url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" | ||||||
|     access_token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"  # nosec |     access_token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"  # nosec | ||||||
|     profile_url = "https://graph.microsoft.com/v1.0/me" |     profile_url = "https://login.microsoftonline.com/common/openid/userinfo" | ||||||
|     oidc_well_known_url = ( |     oidc_well_known_url = ( | ||||||
|         "https://login.microsoftonline.com/common/.well-known/openid-configuration" |         "https://login.microsoftonline.com/common/.well-known/openid-configuration" | ||||||
|     ) |     ) | ||||||
|  | |||||||
| @ -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, | ||||||
|  | |||||||
| @ -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": "", | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -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,18 +107,27 @@ 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 | ||||||
|         message = TemplateEmailMessage( |         try: | ||||||
|             subject=_(current_stage.subject), |             message = TemplateEmailMessage( | ||||||
|             to=[email], |                 subject=_(current_stage.subject), | ||||||
|             language=pending_user.locale(self.request), |                 to=[email], | ||||||
|             template_name=current_stage.template, |                 language=pending_user.locale(self.request), | ||||||
|             template_context={ |                 template_name=current_stage.template, | ||||||
|                 "url": self.get_full_url(**{QS_KEY_TOKEN: token.key}), |                 template_context={ | ||||||
|                 "user": pending_user, |                     "url": self.get_full_url(**{QS_KEY_TOKEN: token.key}), | ||||||
|                 "expires": token.expires, |                     "user": pending_user, | ||||||
|             }, |                     "expires": token.expires, | ||||||
|         ) |                 }, | ||||||
|         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: | ||||||
|             self.send_email() |             try: | ||||||
|  |                 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,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.6} | ||||||
|     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.6} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: worker |     command: worker | ||||||
|     environment: |     environment: | ||||||
|  | |||||||
| @ -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.6" | ||||||
|  | |||||||
| @ -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.6" | ||||||
| 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.6 | ||||||
|   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> | ||||||
|  | |||||||
| @ -334,13 +334,14 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> { | |||||||
|                         )} |                         )} | ||||||
|                     > |                     > | ||||||
|                     </ak-radio-input> |                     </ak-radio-input> | ||||||
|                     <ak-switch-input name="includeClaimsInIdToken"> |                     <ak-switch-input | ||||||
|  |                         name="includeClaimsInIdToken" | ||||||
|                         label=${msg("Include claims in id_token")} |                         label=${msg("Include claims in id_token")} | ||||||
|                         ?checked=${first(provider?.includeClaimsInIdToken, true)} |                         ?checked=${first(provider?.includeClaimsInIdToken, true)} | ||||||
|                         help=${msg( |                         help=${msg( | ||||||
|                             "Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.", |                             "Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.", | ||||||
|                         )}></ak-switch-input |                         )} | ||||||
|                     > |                     ></ak-switch-input> | ||||||
|                     <ak-radio-input |                     <ak-radio-input | ||||||
|                         name="issuerMode" |                         name="issuerMode" | ||||||
|                         label=${msg("Issuer mode")} |                         label=${msg("Issuer mode")} | ||||||
|  | |||||||
| @ -184,28 +184,31 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> { | |||||||
|                           </p> |                           </p> | ||||||
|                       </ak-form-element-horizontal> ` |                       </ak-form-element-horizontal> ` | ||||||
|                     : html``} |                     : html``} | ||||||
|                 ${this.providerType.slug === ProviderTypeEnum.Openidconnect |                 ${this.providerType.slug === ProviderTypeEnum.Openidconnect || | ||||||
|                     ? html` |                 this.providerType.oidcWellKnownUrl !== "" | ||||||
|                           <ak-form-element-horizontal |                     ? html`<ak-form-element-horizontal | ||||||
|                               label=${msg("OIDC Well-known URL")} |                           label=${msg("OIDC Well-known URL")} | ||||||
|                               name="oidcWellKnownUrl" |                           name="oidcWellKnownUrl" | ||||||
|                           > |                       > | ||||||
|                               <input |                           <input | ||||||
|                                   type="text" |                               type="text" | ||||||
|                                   value="${first( |                               value="${first( | ||||||
|                                       this.instance?.oidcWellKnownUrl, |                                   this.instance?.oidcWellKnownUrl, | ||||||
|                                       this.providerType.oidcWellKnownUrl, |                                   this.providerType.oidcWellKnownUrl, | ||||||
|                                       "", |                                   "", | ||||||
|                                   )}" |                               )}" | ||||||
|                                   class="pf-c-form-control" |                               class="pf-c-form-control" | ||||||
|                               /> |                           /> | ||||||
|                               <p class="pf-c-form__helper-text"> |                           <p class="pf-c-form__helper-text"> | ||||||
|                                   ${msg( |                               ${msg( | ||||||
|                                       "OIDC well-known configuration URL. Can be used to automatically configure the URLs above.", |                                   "OIDC well-known configuration URL. Can be used to automatically configure the URLs above.", | ||||||
|                                   )} |                               )} | ||||||
|                               </p> |                           </p> | ||||||
|                           </ak-form-element-horizontal> |                       </ak-form-element-horizontal>` | ||||||
|                           <ak-form-element-horizontal |                     : html``} | ||||||
|  |                 ${this.providerType.slug === ProviderTypeEnum.Openidconnect || | ||||||
|  |                 this.providerType.oidcJwksUrl !== "" | ||||||
|  |                     ? html`<ak-form-element-horizontal | ||||||
|                               label=${msg("OIDC JWKS URL")} |                               label=${msg("OIDC JWKS URL")} | ||||||
|                               name="oidcJwksUrl" |                               name="oidcJwksUrl" | ||||||
|                           > |                           > | ||||||
| @ -224,7 +227,6 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> { | |||||||
|                                   )} |                                   )} | ||||||
|                               </p> |                               </p> | ||||||
|                           </ak-form-element-horizontal> |                           </ak-form-element-horizontal> | ||||||
|  |  | ||||||
|                           <ak-form-element-horizontal label=${msg("OIDC JWKS")} name="oidcJwks"> |                           <ak-form-element-horizontal label=${msg("OIDC JWKS")} name="oidcJwks"> | ||||||
|                               <ak-codemirror |                               <ak-codemirror | ||||||
|                                   mode=${CodeMirrorMode.JavaScript} |                                   mode=${CodeMirrorMode.JavaScript} | ||||||
| @ -232,8 +234,7 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> { | |||||||
|                               > |                               > | ||||||
|                               </ak-codemirror> |                               </ak-codemirror> | ||||||
|                               <p class="pf-c-form__helper-text">${msg("Raw JWKS data.")}</p> |                               <p class="pf-c-form__helper-text">${msg("Raw JWKS data.")}</p> | ||||||
|                           </ak-form-element-horizontal> |                           </ak-form-element-horizontal>` | ||||||
|                       ` |  | ||||||
|                     : html``} |                     : html``} | ||||||
|             </div> |             </div> | ||||||
|         </ak-form-group>`; |         </ak-form-group>`; | ||||||
| @ -386,6 +387,7 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> { | |||||||
|                             class="pf-c-form-control" |                             class="pf-c-form-control" | ||||||
|                             required |                             required | ||||||
|                         /> |                         /> | ||||||
|  |                         <p class="pf-c-form__helper-text">${msg("Also known as Client ID.")}</p> | ||||||
|                     </ak-form-element-horizontal> |                     </ak-form-element-horizontal> | ||||||
|                     <ak-form-element-horizontal |                     <ak-form-element-horizontal | ||||||
|                         label=${msg("Consumer secret")} |                         label=${msg("Consumer secret")} | ||||||
| @ -394,6 +396,7 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> { | |||||||
|                         name="consumerSecret" |                         name="consumerSecret" | ||||||
|                     > |                     > | ||||||
|                         <textarea class="pf-c-form-control"></textarea> |                         <textarea class="pf-c-form-control"></textarea> | ||||||
|  |                         <p class="pf-c-form__helper-text">${msg("Also known as Client Secret.")}</p> | ||||||
|                     </ak-form-element-horizontal> |                     </ak-form-element-horizontal> | ||||||
|                     <ak-form-element-horizontal label=${msg("Scopes")} name="additionalScopes"> |                     <ak-form-element-horizontal label=${msg("Scopes")} name="additionalScopes"> | ||||||
|                         <input |                         <input | ||||||
|  | |||||||
| @ -15,6 +15,8 @@ export class UserDeviceTable extends Table<Device> { | |||||||
|     @property({ type: Number }) |     @property({ type: Number }) | ||||||
|     userId?: number; |     userId?: number; | ||||||
|  |  | ||||||
|  |     checkbox = true; | ||||||
|  |  | ||||||
|     async apiEndpoint(): Promise<PaginatedResponse<Device>> { |     async apiEndpoint(): Promise<PaginatedResponse<Device>> { | ||||||
|         return new AuthenticatorsApi(DEFAULT_CONFIG) |         return new AuthenticatorsApi(DEFAULT_CONFIG) | ||||||
|             .authenticatorsAdminAllList({ |             .authenticatorsAdminAllList({ | ||||||
| @ -64,6 +66,21 @@ export class UserDeviceTable extends Table<Device> { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     renderToolbarSelected(): TemplateResult { | ||||||
|  |         const disabled = this.selectedElements.length < 1; | ||||||
|  |         return html`<ak-forms-delete-bulk | ||||||
|  |             objectLabel=${msg("Device(s)")} | ||||||
|  |             .objects=${this.selectedElements} | ||||||
|  |             .delete=${(item: Device) => { | ||||||
|  |                 return this.deleteWrapper(item); | ||||||
|  |             }} | ||||||
|  |         > | ||||||
|  |             <button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger"> | ||||||
|  |                 ${msg("Delete")} | ||||||
|  |             </button> | ||||||
|  |         </ak-forms-delete-bulk>`; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     renderToolbar(): TemplateResult { |     renderToolbar(): TemplateResult { | ||||||
|         return html` <ak-spinner-button |         return html` <ak-spinner-button | ||||||
|             .callAction=${() => { |             .callAction=${() => { | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success"; | |||||||
| export const ERROR_CLASS = "pf-m-danger"; | export const ERROR_CLASS = "pf-m-danger"; | ||||||
| export const PROGRESS_CLASS = "pf-m-in-progress"; | export const PROGRESS_CLASS = "pf-m-in-progress"; | ||||||
| export const CURRENT_CLASS = "pf-m-current"; | export const CURRENT_CLASS = "pf-m-current"; | ||||||
| export const VERSION = "2023.10.2"; | export const VERSION = "2023.10.6"; | ||||||
| export const TITLE_DEFAULT = "authentik"; | export const TITLE_DEFAULT = "authentik"; | ||||||
| export const ROUTE_SEPARATOR = ";"; | export const ROUTE_SEPARATOR = ";"; | ||||||
|  |  | ||||||
|  | |||||||
| @ -310,6 +310,12 @@ select[multiple] option:checked { | |||||||
|     --pf-c-wizard__nav-link--before--BackgroundColor: transparent; |     --pf-c-wizard__nav-link--before--BackgroundColor: transparent; | ||||||
| } | } | ||||||
| /* tree view */ | /* tree view */ | ||||||
|  | .pf-c-tree-view__node { | ||||||
|  |     --pf-c-tree-view__node--Color: var(--ak-dark-foreground); | ||||||
|  | } | ||||||
|  | .pf-c-tree-view__node-toggle { | ||||||
|  |     --pf-c-tree-view__node-toggle--Color: var(--ak-dark-foreground); | ||||||
|  | } | ||||||
| .pf-c-tree-view__node:focus { | .pf-c-tree-view__node:focus { | ||||||
|     --pf-c-tree-view__node--focus--BackgroundColor: var(--ak-dark-background-light-ish); |     --pf-c-tree-view__node--focus--BackgroundColor: var(--ak-dark-background-light-ish); | ||||||
| } | } | ||||||
|  | |||||||
| @ -79,6 +79,7 @@ export class PageHeader extends AKElement { | |||||||
|                 } |                 } | ||||||
|                 .pf-c-page__main-section { |                 .pf-c-page__main-section { | ||||||
|                     flex-grow: 1; |                     flex-grow: 1; | ||||||
|  |                     flex-shrink: 1; | ||||||
|                     display: flex; |                     display: flex; | ||||||
|                     flex-direction: column; |                     flex-direction: column; | ||||||
|                     justify-content: center; |                     justify-content: center; | ||||||
|  | |||||||
| @ -89,6 +89,9 @@ export class AuthenticatorValidateStage | |||||||
|                 display: flex; |                 display: flex; | ||||||
|                 align-items: center; |                 align-items: center; | ||||||
|             } |             } | ||||||
|  |             :host([theme="dark"]) .authenticator-button { | ||||||
|  |                 color: var(--ak-dark-foreground) !important; | ||||||
|  |             } | ||||||
|             i { |             i { | ||||||
|                 font-size: 1.5rem; |                 font-size: 1.5rem; | ||||||
|                 padding: 1rem 0; |                 padding: 1rem 0; | ||||||
|  | |||||||
| @ -80,11 +80,12 @@ export class IdentificationStage extends BaseStage< | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     createHelperForm(): void { |     createHelperForm(): void { | ||||||
|  |         const compatMode = "ShadyDOM" in window; | ||||||
|         this.form = document.createElement("form"); |         this.form = document.createElement("form"); | ||||||
|         document.documentElement.appendChild(this.form); |         document.documentElement.appendChild(this.form); | ||||||
|         // Only add the additional username input if we're in a shadow dom |         // Only add the additional username input if we're in a shadow dom | ||||||
|         // otherwise it just confuses browsers |         // otherwise it just confuses browsers | ||||||
|         if (!("ShadyDOM" in window)) { |         if (!compatMode) { | ||||||
|             // This is a workaround for the fact that we're in a shadow dom |             // This is a workaround for the fact that we're in a shadow dom | ||||||
|             // adapted from https://github.com/home-assistant/frontend/issues/3133 |             // adapted from https://github.com/home-assistant/frontend/issues/3133 | ||||||
|             const username = document.createElement("input"); |             const username = document.createElement("input"); | ||||||
| @ -104,30 +105,33 @@ export class IdentificationStage extends BaseStage< | |||||||
|             }; |             }; | ||||||
|             this.form.appendChild(username); |             this.form.appendChild(username); | ||||||
|         } |         } | ||||||
|         const password = document.createElement("input"); |         // Only add the password field when we don't already show a password field | ||||||
|         password.setAttribute("type", "password"); |         if (!compatMode && !this.challenge.passwordFields) { | ||||||
|         password.setAttribute("name", "password"); |             const password = document.createElement("input"); | ||||||
|         password.setAttribute("autocomplete", "current-password"); |             password.setAttribute("type", "password"); | ||||||
|         password.onkeyup = (ev: KeyboardEvent) => { |             password.setAttribute("name", "password"); | ||||||
|             if (ev.key == "Enter") { |             password.setAttribute("autocomplete", "current-password"); | ||||||
|                 this.submitForm(ev); |             password.onkeyup = (ev: KeyboardEvent) => { | ||||||
|             } |                 if (ev.key == "Enter") { | ||||||
|             const el = ev.target as HTMLInputElement; |                     this.submitForm(ev); | ||||||
|             // Because the password field is not actually on this page, |                 } | ||||||
|             // and we want to 'prefill' the password for the user, |                 const el = ev.target as HTMLInputElement; | ||||||
|             // save it globally |                 // Because the password field is not actually on this page, | ||||||
|             PasswordManagerPrefill.password = el.value; |                 // and we want to 'prefill' the password for the user, | ||||||
|             // Because password managers fill username, then password, |                 // save it globally | ||||||
|             // we need to re-focus the uid_field here too |                 PasswordManagerPrefill.password = el.value; | ||||||
|             (this.shadowRoot || this) |                 // Because password managers fill username, then password, | ||||||
|                 .querySelectorAll<HTMLInputElement>("input[name=uidField]") |                 // we need to re-focus the uid_field here too | ||||||
|                 .forEach((input) => { |                 (this.shadowRoot || this) | ||||||
|                     // Because we assume only one input field exists that matches this |                     .querySelectorAll<HTMLInputElement>("input[name=uidField]") | ||||||
|                     // call focus so the user can press enter |                     .forEach((input) => { | ||||||
|                     input.focus(); |                         // Because we assume only one input field exists that matches this | ||||||
|                 }); |                         // call focus so the user can press enter | ||||||
|         }; |                         input.focus(); | ||||||
|         this.form.appendChild(password); |                     }); | ||||||
|  |             }; | ||||||
|  |             this.form.appendChild(password); | ||||||
|  |         } | ||||||
|         const totp = document.createElement("input"); |         const totp = document.createElement("input"); | ||||||
|         totp.setAttribute("type", "text"); |         totp.setAttribute("type", "text"); | ||||||
|         totp.setAttribute("name", "code"); |         totp.setAttribute("name", "code"); | ||||||
|  | |||||||
| @ -96,7 +96,7 @@ export class LibraryApplication extends AKElement { | |||||||
|             this.application.metaPublisher !== "" || |             this.application.metaPublisher !== "" || | ||||||
|             this.application.metaDescription !== ""; |             this.application.metaDescription !== ""; | ||||||
|  |  | ||||||
|         const classes = { "pf-m-selectable pf-m-selected": this.selected }; |         const classes = { "pf-m-selectable": this.selected, "pf-m-selected": this.selected }; | ||||||
|         const styles = this.background ? { background: this.background } : {}; |         const styles = this.background ? { background: this.background } : {}; | ||||||
|  |  | ||||||
|         return html` <div |         return html` <div | ||||||
|  | |||||||
| @ -38,7 +38,9 @@ export class LibraryPageApplicationList extends AKElement { | |||||||
|     ]; |     ]; | ||||||
|  |  | ||||||
|     @property({ attribute: false }) |     @property({ attribute: false }) | ||||||
|     apps: Application[] = []; |     set apps(value: Application[]) { | ||||||
|  |         this.fuse.setCollection(value); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @property() |     @property() | ||||||
|     query = getURLParam<string | undefined>("search", undefined); |     query = getURLParam<string | undefined>("search", undefined); | ||||||
| @ -63,7 +65,7 @@ export class LibraryPageApplicationList extends AKElement { | |||||||
|             shouldSort: true, |             shouldSort: true, | ||||||
|             ignoreFieldNorm: true, |             ignoreFieldNorm: true, | ||||||
|             useExtendedSearch: true, |             useExtendedSearch: true, | ||||||
|             threshold: 0.5, |             threshold: 0.3, | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @ -77,7 +79,6 @@ export class LibraryPageApplicationList extends AKElement { | |||||||
|  |  | ||||||
|     connectedCallback() { |     connectedCallback() { | ||||||
|         super.connectedCallback(); |         super.connectedCallback(); | ||||||
|         this.fuse.setCollection(this.apps); |  | ||||||
|         if (!this.query) { |         if (!this.query) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -82,9 +82,9 @@ export class UserInterface extends Interface { | |||||||
|                 :host([theme="dark"]) .pf-c-page__header { |                 :host([theme="dark"]) .pf-c-page__header { | ||||||
|                     color: var(--ak-dark-foreground) !important; |                     color: var(--ak-dark-foreground) !important; | ||||||
|                 } |                 } | ||||||
|                 .pf-c-page__header-tools-item .fas, |                 :host([theme="light"]) .pf-c-page__header-tools-item .fas, | ||||||
|                 .pf-c-notification-badge__count, |                 :host([theme="light"]) .pf-c-notification-badge__count, | ||||||
|                 .pf-c-page__header-tools-group .pf-c-button { |                 :host([theme="light"]) .pf-c-page__header-tools-group .pf-c-button { | ||||||
|                     color: var(--ak-global--Color--100) !important; |                     color: var(--ak-global--Color--100) !important; | ||||||
|                 } |                 } | ||||||
|                 .pf-c-page { |                 .pf-c-page { | ||||||
| @ -183,7 +183,7 @@ export class UserInterface extends Interface { | |||||||
|             <ak-enterprise-status interface="user"></ak-enterprise-status> |             <ak-enterprise-status interface="user"></ak-enterprise-status> | ||||||
|             <div class="pf-c-page"> |             <div class="pf-c-page"> | ||||||
|                 <div class="background-wrapper" style="${this.uiConfig.theme.background}"> |                 <div class="background-wrapper" style="${this.uiConfig.theme.background}"> | ||||||
|                     ${this.uiConfig.theme.background === "" |                     ${(this.uiConfig.theme.background || "") === "" | ||||||
|                         ? html`<div class="background-default-slant"></div>` |                         ? html`<div class="background-default-slant"></div>` | ||||||
|                         : html``} |                         : html``} | ||||||
|                 </div> |                 </div> | ||||||
|  | |||||||
| @ -6037,6 +6037,12 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s32babfed740fd3c1"> | <trans-unit id="s32babfed740fd3c1"> | ||||||
|   <source>User type used for newly created users.</source> |   <source>User type used for newly created users.</source> | ||||||
|  | </trans-unit> | ||||||
|  | <trans-unit id="sb35c08e3a541188f"> | ||||||
|  |   <source>Also known as Client ID.</source> | ||||||
|  | </trans-unit> | ||||||
|  | <trans-unit id="sd46fd9b647cfea10"> | ||||||
|  |   <source>Also known as Client Secret.</source> | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -6318,6 +6318,12 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s32babfed740fd3c1"> | <trans-unit id="s32babfed740fd3c1"> | ||||||
|   <source>User type used for newly created users.</source> |   <source>User type used for newly created users.</source> | ||||||
|  | </trans-unit> | ||||||
|  | <trans-unit id="sb35c08e3a541188f"> | ||||||
|  |   <source>Also known as Client ID.</source> | ||||||
|  | </trans-unit> | ||||||
|  | <trans-unit id="sd46fd9b647cfea10"> | ||||||
|  |   <source>Also known as Client Secret.</source> | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -5952,6 +5952,12 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s32babfed740fd3c1"> | <trans-unit id="s32babfed740fd3c1"> | ||||||
|   <source>User type used for newly created users.</source> |   <source>User type used for newly created users.</source> | ||||||
|  | </trans-unit> | ||||||
|  | <trans-unit id="sb35c08e3a541188f"> | ||||||
|  |   <source>Also known as Client ID.</source> | ||||||
|  | </trans-unit> | ||||||
|  | <trans-unit id="sd46fd9b647cfea10"> | ||||||
|  |   <source>Also known as Client Secret.</source> | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	