Compare commits
	
		
			27 Commits
		
	
	
		
			version/20
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | 
@ -1,5 +1,5 @@
 | 
				
			|||||||
[bumpversion]
 | 
					[bumpversion]
 | 
				
			||||||
current_version = 2023.10.3
 | 
					current_version = 2023.10.5
 | 
				
			||||||
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
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										25
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										25
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							@ -48,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
 | 
				
			||||||
@ -76,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:
 | 
				
			||||||
@ -97,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
 | 
				
			||||||
@ -187,6 +204,7 @@ jobs:
 | 
				
			|||||||
    needs: ci-core-mark
 | 
					    needs: ci-core-mark
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
    permissions:
 | 
					    permissions:
 | 
				
			||||||
 | 
					      # Needed to upload contianer images to ghcr.io
 | 
				
			||||||
      packages: write
 | 
					      packages: write
 | 
				
			||||||
    timeout-minutes: 120
 | 
					    timeout-minutes: 120
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
@ -239,6 +257,7 @@ jobs:
 | 
				
			|||||||
    needs: ci-core-mark
 | 
					    needs: ci-core-mark
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
    permissions:
 | 
					    permissions:
 | 
				
			||||||
 | 
					      # Needed to upload contianer images to ghcr.io
 | 
				
			||||||
      packages: write
 | 
					      packages: write
 | 
				
			||||||
    timeout-minutes: 120
 | 
					    timeout-minutes: 120
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										3
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							@ -67,6 +67,7 @@ jobs:
 | 
				
			|||||||
          - radius
 | 
					          - radius
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
    permissions:
 | 
					    permissions:
 | 
				
			||||||
 | 
					      # Needed to upload contianer images to ghcr.io
 | 
				
			||||||
      packages: write
 | 
					      packages: write
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
@ -129,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
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										10
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
								
							@ -24,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 }}/
 | 
				
			||||||
@ -40,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/
 | 
				
			||||||
@ -62,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 }}/
 | 
				
			||||||
@ -78,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/
 | 
				
			||||||
@ -110,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/
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										6
									
								
								.github/workflows/ci-website.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/ci-website.yml
									
									
									
									
										vendored
									
									
								
							@ -18,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/
 | 
				
			||||||
@ -32,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/
 | 
				
			||||||
@ -53,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:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										7
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							@ -8,6 +8,7 @@ jobs:
 | 
				
			|||||||
  build-server:
 | 
					  build-server:
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
    permissions:
 | 
					    permissions:
 | 
				
			||||||
 | 
					      # Needed to upload contianer images to ghcr.io
 | 
				
			||||||
      packages: write
 | 
					      packages: write
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
@ -55,6 +56,7 @@ jobs:
 | 
				
			|||||||
  build-outpost:
 | 
					  build-outpost:
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
    permissions:
 | 
					    permissions:
 | 
				
			||||||
 | 
					      # Needed to upload contianer images to ghcr.io
 | 
				
			||||||
      packages: write
 | 
					      packages: write
 | 
				
			||||||
    strategy:
 | 
					    strategy:
 | 
				
			||||||
      fail-fast: false
 | 
					      fail-fast: false
 | 
				
			||||||
@ -110,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:
 | 
				
			||||||
@ -126,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
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										16
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								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,7 @@ 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 --platform=${BUILDPLATFORM} 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 TARGETOS
 | 
				
			||||||
ARG TARGETARCH
 | 
					ARG TARGETARCH
 | 
				
			||||||
@ -62,8 +64,8 @@ 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 \
 | 
				
			||||||
    GOARM="${TARGETVARIANT#v}" 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
 | 
				
			||||||
@ -89,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.3"
 | 
					__version__ = "2023.10.5"
 | 
				
			||||||
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"):
 | 
				
			||||||
 | 
				
			|||||||
@ -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 = [
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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__,
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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)
 | 
				
			||||||
@ -222,7 +222,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())
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
@ -58,7 +63,6 @@ class EmailStageView(ChallengeStageView):
 | 
				
			|||||||
        query_params = QueryDict(self.request.GET.get(QS_QUERY), mutable=True)
 | 
					        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)
 | 
				
			||||||
        query_params.update(kwargs)
 | 
					        query_params.update(kwargs)
 | 
				
			||||||
        print(query_params)
 | 
					 | 
				
			||||||
        full_url = base_url
 | 
					        full_url = base_url
 | 
				
			||||||
        if len(query_params) > 0:
 | 
					        if len(query_params) > 0:
 | 
				
			||||||
            full_url = f"{full_url}?{query_params.urlencode()}"
 | 
					            full_url = f"{full_url}?{query_params.urlencode()}"
 | 
				
			||||||
@ -71,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():
 | 
				
			||||||
@ -103,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
 | 
				
			||||||
@ -135,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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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"""
 | 
				
			||||||
 | 
				
			|||||||
@ -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.3}
 | 
					    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.5}
 | 
				
			||||||
    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.3}
 | 
					    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.5}
 | 
				
			||||||
    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.3"
 | 
					const VERSION = "2023.10.5"
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,7 @@
 | 
				
			|||||||
 | 
					# syntax=docker/dockerfile:1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 1: Build
 | 
					# Stage 1: Build
 | 
				
			||||||
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.3-bookworm AS builder
 | 
					FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.4-bookworm AS builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ARG TARGETOS
 | 
					ARG TARGETOS
 | 
				
			||||||
ARG TARGETARCH
 | 
					ARG TARGETARCH
 | 
				
			||||||
@ -18,8 +20,8 @@ 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 \
 | 
				
			||||||
    GOARM="${TARGETVARIANT#v}" go build -o /go/ldap ./cmd/ldap
 | 
					    GOARM="${TARGETVARIANT#v}" go build -o /go/ldap ./cmd/ldap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 2: Run
 | 
					# Stage 2: Run
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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,7 @@ COPY web .
 | 
				
			|||||||
RUN npm run build-proxy
 | 
					RUN npm run build-proxy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 2: Build
 | 
					# Stage 2: Build
 | 
				
			||||||
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.3-bookworm AS builder
 | 
					FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.4-bookworm AS builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ARG TARGETOS
 | 
					ARG TARGETOS
 | 
				
			||||||
ARG TARGETARCH
 | 
					ARG TARGETARCH
 | 
				
			||||||
@ -34,8 +36,8 @@ 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 \
 | 
				
			||||||
    GOARM="${TARGETVARIANT#v}" go build -o /go/proxy ./cmd/proxy
 | 
					    GOARM="${TARGETVARIANT#v}" go build -o /go/proxy ./cmd/proxy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 3: Run
 | 
					# Stage 3: Run
 | 
				
			||||||
 | 
				
			|||||||
@ -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.3"
 | 
					version = "2023.10.5"
 | 
				
			||||||
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,7 @@
 | 
				
			|||||||
 | 
					# syntax=docker/dockerfile:1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 1: Build
 | 
					# Stage 1: Build
 | 
				
			||||||
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.3-bookworm AS builder
 | 
					FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.4-bookworm AS builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ARG TARGETOS
 | 
					ARG TARGETOS
 | 
				
			||||||
ARG TARGETARCH
 | 
					ARG TARGETARCH
 | 
				
			||||||
@ -18,8 +20,8 @@ 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 \
 | 
				
			||||||
    GOARM="${TARGETVARIANT#v}" go build -o /go/radius ./cmd/radius
 | 
					    GOARM="${TARGETVARIANT#v}" go build -o /go/radius ./cmd/radius
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 2: Run
 | 
					# Stage 2: Run
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
openapi: 3.0.3
 | 
					openapi: 3.0.3
 | 
				
			||||||
info:
 | 
					info:
 | 
				
			||||||
  title: authentik
 | 
					  title: authentik
 | 
				
			||||||
  version: 2023.10.3
 | 
					  version: 2023.10.5
 | 
				
			||||||
  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"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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>`;
 | 
				
			||||||
 | 
				
			|||||||
@ -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.3";
 | 
					export const VERSION = "2023.10.5";
 | 
				
			||||||
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;
 | 
				
			||||||
 | 
				
			|||||||
@ -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>
 | 
				
			||||||
 | 
				
			|||||||
@ -2561,31 +2561,6 @@ Il y a <x id="0" equiv-text="${ago}"/> jour(s)</target>
 | 
				
			|||||||
        <source>If the password's score is less than or equal this value, the policy will fail.</source>
 | 
					        <source>If the password's score is less than or equal this value, the policy will fail.</source>
 | 
				
			||||||
        <target>Si le score du mot de passe est inférieur ou égal à cette valeur, la politique échoue.</target>
 | 
					        <target>Si le score du mot de passe est inférieur ou égal à cette valeur, la politique échoue.</target>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      </trans-unit>
 | 
					 | 
				
			||||||
      <trans-unit id="s1bfe7505059d164f">
 | 
					 | 
				
			||||||
        <source>0: Too guessable: risky password. (guesses < 10^3)</source>
 | 
					 | 
				
			||||||
        <target>0: Trop prévisible: mot de passe risqué. (essais < 10^3)</target>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      </trans-unit>
 | 
					 | 
				
			||||||
      <trans-unit id="s423d1f2477998d0b">
 | 
					 | 
				
			||||||
        <source>1: Very guessable: protection from throttled online attacks. (guesses < 10^6)</source>
 | 
					 | 
				
			||||||
        <target>1: Très prévisible: protection contre les attaques en ligne limitées. (essais < 10^6)</target>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      </trans-unit>
 | 
					 | 
				
			||||||
      <trans-unit id="s33849cc046eb901d">
 | 
					 | 
				
			||||||
        <source>2: Somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8)</source>
 | 
					 | 
				
			||||||
        <target>2: Quelque peu prévisible: protection contre les attaques en ligne non limitées. (essais < 10^8)</target>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      </trans-unit>
 | 
					 | 
				
			||||||
      <trans-unit id="s578dcce295718e1b">
 | 
					 | 
				
			||||||
        <source>3: Safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10)</source>
 | 
					 | 
				
			||||||
        <target>3: Sûrement imprévisible: protection modérée contre les attaques de hash-lent hors ligne. (essais < 10^10)</target>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      </trans-unit>
 | 
					 | 
				
			||||||
      <trans-unit id="s7a46de49f4eba5d7">
 | 
					 | 
				
			||||||
        <source>4: Very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10)</source>
 | 
					 | 
				
			||||||
        <target>4: Très imprévisible: forte protection control les attaques de hash-lent hors ligne. (essais >= 10^10)</target>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="sd6cd7ce2310a73a4">
 | 
					      <trans-unit id="sd6cd7ce2310a73a4">
 | 
				
			||||||
        <source>Checks the value from the policy request against several rules, mostly used to ensure password strength.</source>
 | 
					        <source>Checks the value from the policy request against several rules, mostly used to ensure password strength.</source>
 | 
				
			||||||
@ -7597,6 +7572,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
 | 
				
			|||||||
  <source>Stage used to configure a WebAuthn authenticator (i.e. Yubikey, FaceID/Windows Hello).</source>
 | 
					  <source>Stage used to configure a WebAuthn authenticator (i.e. Yubikey, FaceID/Windows Hello).</source>
 | 
				
			||||||
  <target>Étape de configuration d'un authentificateur WebAuthn (Yubikey, FaceID/Windows Hello).</target>
 | 
					  <target>Étape de configuration d'un authentificateur WebAuthn (Yubikey, FaceID/Windows Hello).</target>
 | 
				
			||||||
</trans-unit>
 | 
					</trans-unit>
 | 
				
			||||||
 | 
					<<<<<<< HEAD
 | 
				
			||||||
<trans-unit id="s1cffe58249b04669">
 | 
					<trans-unit id="s1cffe58249b04669">
 | 
				
			||||||
  <source>Internal application name used in URLs.</source>
 | 
					  <source>Internal application name used in URLs.</source>
 | 
				
			||||||
  <target>Nom de l'application interne utilisé dans les URLs.</target>
 | 
					  <target>Nom de l'application interne utilisé dans les URLs.</target>
 | 
				
			||||||
@ -7641,14 +7617,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
 | 
				
			|||||||
  <source>Your application has been saved</source>
 | 
					  <source>Your application has been saved</source>
 | 
				
			||||||
  <target>L'application a été sauvegardée</target>
 | 
					  <target>L'application a été sauvegardée</target>
 | 
				
			||||||
</trans-unit>
 | 
					</trans-unit>
 | 
				
			||||||
<trans-unit id="sf60f1e5b76897c93">
 | 
					 | 
				
			||||||
  <source>In the Application:</source>
 | 
					 | 
				
			||||||
  <target>Dans l'application :</target>
 | 
					 | 
				
			||||||
</trans-unit>
 | 
					 | 
				
			||||||
<trans-unit id="s7ce65cf482b7bff0">
 | 
					 | 
				
			||||||
  <source>In the Provider:</source>
 | 
					 | 
				
			||||||
  <target>Dans le fournisseur :</target>
 | 
					 | 
				
			||||||
</trans-unit>
 | 
					 | 
				
			||||||
<trans-unit id="s67d858051b34c38b">
 | 
					<trans-unit id="s67d858051b34c38b">
 | 
				
			||||||
  <source>Method's display Name.</source>
 | 
					  <source>Method's display Name.</source>
 | 
				
			||||||
  <target>Nom d'affichage de la méthode.</target>
 | 
					  <target>Nom d'affichage de la méthode.</target>
 | 
				
			||||||
@ -7923,17 +7891,50 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
 | 
				
			|||||||
  <source><No name set></source>
 | 
					  <source><No name set></source>
 | 
				
			||||||
  <target><No name set></target>
 | 
					  <target><No name set></target>
 | 
				
			||||||
</trans-unit>
 | 
					</trans-unit>
 | 
				
			||||||
<trans-unit id="s32babfed740fd3c1">
 | 
					 | 
				
			||||||
  <source>User type used for newly created users.</source>
 | 
					 | 
				
			||||||
</trans-unit>
 | 
					 | 
				
			||||||
<trans-unit id="sdc9a6ad1af30572c">
 | 
					<trans-unit id="sdc9a6ad1af30572c">
 | 
				
			||||||
  <source>For nginx's auth_request or traefik's forwardAuth</source>
 | 
					  <source>For nginx's auth_request or traefik's forwardAuth</source>
 | 
				
			||||||
 | 
					  <target>Pour nginx auth_request ou traefik forwardAuth</target>
 | 
				
			||||||
</trans-unit>
 | 
					</trans-unit>
 | 
				
			||||||
<trans-unit id="sfc31264ef7ff86ef">
 | 
					<trans-unit id="sfc31264ef7ff86ef">
 | 
				
			||||||
  <source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
 | 
					  <source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
 | 
				
			||||||
 | 
					  <target>Pour nginx auth_request ou traefik forwardAuth par domaine racine</target>
 | 
				
			||||||
</trans-unit>
 | 
					</trans-unit>
 | 
				
			||||||
<trans-unit id="sc615309d10a9228c">
 | 
					<trans-unit id="sc615309d10a9228c">
 | 
				
			||||||
  <source>RBAC is in preview.</source>
 | 
					  <source>RBAC is in preview.</source>
 | 
				
			||||||
 | 
					  <target>RBAC est en aperçu,</target>
 | 
				
			||||||
 | 
					</trans-unit>
 | 
				
			||||||
 | 
					<trans-unit id="s32babfed740fd3c1">
 | 
				
			||||||
 | 
					  <source>User type used for newly created users.</source>
 | 
				
			||||||
 | 
					  <target>Type d'utilisateur pour les utilisateurs nouvellement créés.</target>
 | 
				
			||||||
 | 
					</trans-unit>
 | 
				
			||||||
 | 
					<trans-unit id="sb35c08e3a541188f">
 | 
				
			||||||
 | 
					  <source>Also known as Client ID.</source>
 | 
				
			||||||
 | 
					  <target>Également appelé Client ID.</target>
 | 
				
			||||||
 | 
					</trans-unit>
 | 
				
			||||||
 | 
					<trans-unit id="sd46fd9b647cfea10">
 | 
				
			||||||
 | 
					  <source>Also known as Client Secret.</source>
 | 
				
			||||||
 | 
					  <target>Également appelé Client Secret.</target>
 | 
				
			||||||
 | 
					</trans-unit>
 | 
				
			||||||
 | 
					<trans-unit id="sf60f1e5b76897c93">
 | 
				
			||||||
 | 
					  <source>In the Application:</source>
 | 
				
			||||||
 | 
					</trans-unit>
 | 
				
			||||||
 | 
					<trans-unit id="s7ce65cf482b7bff0">
 | 
				
			||||||
 | 
					  <source>In the Provider:</source>
 | 
				
			||||||
 | 
					</trans-unit>
 | 
				
			||||||
 | 
					<trans-unit id="s1bfe7505059d164f">
 | 
				
			||||||
 | 
					  <source>0: Too guessable: risky password. (guesses < 10^3)</source>
 | 
				
			||||||
 | 
					</trans-unit>
 | 
				
			||||||
 | 
					<trans-unit id="s423d1f2477998d0b">
 | 
				
			||||||
 | 
					  <source>1: Very guessable: protection from throttled online attacks. (guesses < 10^6)</source>
 | 
				
			||||||
 | 
					</trans-unit>
 | 
				
			||||||
 | 
					<trans-unit id="s33849cc046eb901d">
 | 
				
			||||||
 | 
					  <source>2: Somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8)</source>
 | 
				
			||||||
 | 
					</trans-unit>
 | 
				
			||||||
 | 
					<trans-unit id="s578dcce295718e1b">
 | 
				
			||||||
 | 
					  <source>3: Safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10)</source>
 | 
				
			||||||
 | 
					</trans-unit>
 | 
				
			||||||
 | 
					<trans-unit id="s7a46de49f4eba5d7">
 | 
				
			||||||
 | 
					  <source>4: Very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10)</source>
 | 
				
			||||||
</trans-unit>
 | 
					</trans-unit>
 | 
				
			||||||
    </body>
 | 
					    </body>
 | 
				
			||||||
  </file>
 | 
					  </file>
 | 
				
			||||||
 | 
				
			|||||||
@ -6160,6 +6160,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>
 | 
				
			||||||
 | 
				
			|||||||
@ -7848,4 +7848,10 @@ Bindings to groups/users are checked against the user of the event.</source>
 | 
				
			|||||||
<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>
 | 
				
			||||||
 | 
					<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>
 | 
				
			||||||
</body></file></xliff>
 | 
					</body></file></xliff>
 | 
				
			||||||
 | 
				
			|||||||
@ -5945,6 +5945,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>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
<?xml version="1.0" ?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
 | 
					<?xml version="1.0"?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
 | 
				
			||||||
  <file target-language="zh-Hans" source-language="en" original="lit-localize-inputs" datatype="plaintext">
 | 
					  <file target-language="zh-Hans" source-language="en" original="lit-localize-inputs" datatype="plaintext">
 | 
				
			||||||
    <body>
 | 
					    <body>
 | 
				
			||||||
      <trans-unit id="s4caed5b7a7e5d89b">
 | 
					      <trans-unit id="s4caed5b7a7e5d89b">
 | 
				
			||||||
@ -613,9 +613,9 @@
 | 
				
			|||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="saa0e2675da69651b">
 | 
					      <trans-unit id="saa0e2675da69651b">
 | 
				
			||||||
        <source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
 | 
					        <source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
 | 
				
			||||||
        <target>未找到 URL " 
 | 
					        <target>未找到 URL " 
 | 
				
			||||||
        <x id="0" equiv-text="${this.url}"/>"。</target>
 | 
					        <x id="0" equiv-text="${this.url}"/>"。</target>
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="s58cd9c2fe836d9c6">
 | 
					      <trans-unit id="s58cd9c2fe836d9c6">
 | 
				
			||||||
@ -1057,8 +1057,8 @@
 | 
				
			|||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="sa8384c9c26731f83">
 | 
					      <trans-unit id="sa8384c9c26731f83">
 | 
				
			||||||
        <source>To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.</source>
 | 
					        <source>To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.</source>
 | 
				
			||||||
        <target>要允许任何重定向 URI,请将此值设置为 ".*"。请注意这可能带来的安全影响。</target>
 | 
					        <target>要允许任何重定向 URI,请将此值设置为 ".*"。请注意这可能带来的安全影响。</target>
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="s55787f4dfcdce52b">
 | 
					      <trans-unit id="s55787f4dfcdce52b">
 | 
				
			||||||
@ -1799,8 +1799,8 @@
 | 
				
			|||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="sa90b7809586c35ce">
 | 
					      <trans-unit id="sa90b7809586c35ce">
 | 
				
			||||||
        <source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
 | 
					        <source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
 | 
				
			||||||
        <target>输入完整 URL、相对路径,或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。</target>
 | 
					        <target>输入完整 URL、相对路径,或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。</target>
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="s0410779cb47de312">
 | 
					      <trans-unit id="s0410779cb47de312">
 | 
				
			||||||
@ -3013,8 +3013,8 @@ doesn't pass when either or both of the selected options are equal or above the
 | 
				
			|||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="s76768bebabb7d543">
 | 
					      <trans-unit id="s76768bebabb7d543">
 | 
				
			||||||
        <source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source>
 | 
					        <source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source>
 | 
				
			||||||
        <target>包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target>
 | 
					        <target>包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target>
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="s026555347e589f0e">
 | 
					      <trans-unit id="s026555347e589f0e">
 | 
				
			||||||
@ -3806,8 +3806,8 @@ doesn't pass when either or both of the selected options are equal or above the
 | 
				
			|||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="s7b1fba26d245cb1c">
 | 
					      <trans-unit id="s7b1fba26d245cb1c">
 | 
				
			||||||
        <source>When using an external logging solution for archiving, this can be set to "minutes=5".</source>
 | 
					        <source>When using an external logging solution for archiving, this can be set to "minutes=5".</source>
 | 
				
			||||||
        <target>使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。</target>
 | 
					        <target>使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。</target>
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="s44536d20bb5c8257">
 | 
					      <trans-unit id="s44536d20bb5c8257">
 | 
				
			||||||
@ -3816,8 +3816,8 @@ doesn't pass when either or both of the selected options are equal or above the
 | 
				
			|||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="s3bb51cabb02b997e">
 | 
					      <trans-unit id="s3bb51cabb02b997e">
 | 
				
			||||||
        <source>Format: "weeks=3;days=2;hours=3,seconds=2".</source>
 | 
					        <source>Format: "weeks=3;days=2;hours=3,seconds=2".</source>
 | 
				
			||||||
        <target>格式:"weeks=3;days=2;hours=3,seconds=2"。</target>
 | 
					        <target>格式:"weeks=3;days=2;hours=3,seconds=2"。</target>
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="s04bfd02201db5ab8">
 | 
					      <trans-unit id="s04bfd02201db5ab8">
 | 
				
			||||||
@ -4013,10 +4013,10 @@ doesn't pass when either or both of the selected options are equal or above the
 | 
				
			|||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="sa95a538bfbb86111">
 | 
					      <trans-unit id="sa95a538bfbb86111">
 | 
				
			||||||
        <source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
 | 
					        <source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
 | 
				
			||||||
        <target>您确定要更新 
 | 
					        <target>您确定要更新 
 | 
				
			||||||
        <x id="0" equiv-text="${this.objectLabel}"/>" 
 | 
					        <x id="0" equiv-text="${this.objectLabel}"/>" 
 | 
				
			||||||
        <x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target>
 | 
					        <x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target>
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="sc92d7cfb6ee1fec6">
 | 
					      <trans-unit id="sc92d7cfb6ee1fec6">
 | 
				
			||||||
@ -5102,7 +5102,7 @@ doesn't pass when either or both of the selected options are equal or above the
 | 
				
			|||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="sdf1d8edef27236f0">
 | 
					      <trans-unit id="sdf1d8edef27236f0">
 | 
				
			||||||
        <source>A "roaming" authenticator, like a YubiKey</source>
 | 
					        <source>A "roaming" authenticator, like a YubiKey</source>
 | 
				
			||||||
        <target>像 YubiKey 这样的“漫游”身份验证器</target>
 | 
					        <target>像 YubiKey 这样的“漫游”身份验证器</target>
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
@ -5437,10 +5437,10 @@ doesn't pass when either or both of the selected options are equal or above the
 | 
				
			|||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="s2d5f69929bb7221d">
 | 
					      <trans-unit id="s2d5f69929bb7221d">
 | 
				
			||||||
        <source><x id="0" equiv-text="${prompt.name}"/> ("<x id="1" equiv-text="${prompt.fieldKey}"/>", of type <x id="2" equiv-text="${prompt.type}"/>)</source>
 | 
					        <source><x id="0" equiv-text="${prompt.name}"/> ("<x id="1" equiv-text="${prompt.fieldKey}"/>", of type <x id="2" equiv-text="${prompt.type}"/>)</source>
 | 
				
			||||||
        <target>
 | 
					        <target>
 | 
				
			||||||
        <x id="0" equiv-text="${prompt.name}"/>(" 
 | 
					        <x id="0" equiv-text="${prompt.name}"/>(" 
 | 
				
			||||||
        <x id="1" equiv-text="${prompt.fieldKey}"/>",类型为 
 | 
					        <x id="1" equiv-text="${prompt.fieldKey}"/>",类型为 
 | 
				
			||||||
        <x id="2" equiv-text="${prompt.type}"/>)</target>
 | 
					        <x id="2" equiv-text="${prompt.type}"/>)</target>
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
@ -5489,7 +5489,7 @@ doesn't pass when either or both of the selected options are equal or above the
 | 
				
			|||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="s1608b2f94fa0dbd4">
 | 
					      <trans-unit id="s1608b2f94fa0dbd4">
 | 
				
			||||||
        <source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
 | 
					        <source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
 | 
				
			||||||
        <target>如果设置时长大于 0,用户可以选择“保持登录”选项,这将使用户的会话延长此处设置的时间。</target>
 | 
					        <target>如果设置时长大于 0,用户可以选择“保持登录”选项,这将使用户的会话延长此处设置的时间。</target>
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
@ -7941,7 +7941,13 @@ Bindings to groups/users are checked against the user of the event.</source>
 | 
				
			|||||||
<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>
 | 
				
			||||||
  <target>新创建用户使用的用户类型。</target>
 | 
					  <target>新创建用户使用的用户类型。</target>
 | 
				
			||||||
 | 
					</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>
 | 
				
			||||||
</xliff>
 | 
					</xliff>
 | 
				
			||||||
 | 
				
			|||||||
@ -5993,6 +5993,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>
 | 
				
			||||||
 | 
				
			|||||||
@ -5992,6 +5992,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>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										61
									
								
								website/docs/security/CVE-2023-48228.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								website/docs/security/CVE-2023-48228.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,61 @@
 | 
				
			|||||||
 | 
					# CVE-2023-48228
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_Reported by [@Sapd](https://github.com/Sapd)_
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## OAuth2: Insufficient PKCE check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Summary
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					When initialising a OAuth2 flow with a `code_challenge` and `code_method` (thus requesting PKCE), the SSO provider (authentik) **must** check if there is a matching **and** existing `code_verifier` during the token step.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					authentik checks if the contents of code*verifier is matching \*\*\_ONLY*\*\* when it is provided. When it is left out completely, authentik simply accepts the token request with out it; even when the flow was started with a `code_challenge`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Patches
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					authentik 2023.8.5 and 2023.10.4 fix this issue.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Details
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The `code_verifier` is only checked when the user provides it. Note that in line 209 there is a check if the code_parameter is left out. But there is no check if the PKCE parameter simply was omitted WHEN the request was started with a `code_challenge_method`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This oversight likely did not stem from a coding error but from a misinterpretation of the RFC, where the backward compatibility section may be somewhat confusing.
 | 
				
			||||||
 | 
					https://datatracker.ietf.org/doc/html/rfc7636#section-4.5
 | 
				
			||||||
 | 
					RFC7636 explicitly says in Section 4.5:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> The "code_challenge_method" is bound to the Authorization Code when
 | 
				
			||||||
 | 
					> the Authorization Code is issued. That is the method that the token
 | 
				
			||||||
 | 
					> endpoint MUST use to verify the "code_verifier".
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Section 5, Compatibility
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> Server implementations of this specification MAY accept OAuth2.0
 | 
				
			||||||
 | 
					> clients that do not implement this extension. If the "code_verifier"
 | 
				
			||||||
 | 
					> is not received from the client in the Authorization Request, servers
 | 
				
			||||||
 | 
					> supporting backwards compatibility revert to the OAuth 2.0 [[RFC6749](https://datatracker.ietf.org/doc/html/rfc6749)]
 | 
				
			||||||
 | 
					> protocol without this extension.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Section 5, Compatibility, allows server implementations of this specification to accept OAuth 2.0 clients that do not implement this extension. However, if a `code_verifier` is not received from the client in the Authorization Request, servers that support backward compatibility should revert to the standard OAuth 2.0 protocol sans this extension (including all steps).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					It should be noted that this does not mean that the `code_verifier` check can be disregarded at any point if the initial request included `code_challenge` or `code_challenge_method`. Since Authentik supports PKCE, it **MUST** verify the code_verifier as described in Section 4.5 **AND** fail if it was not provided.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Ofc verification can be skipped if the original authorization request did not invoke PKCE (no `code_challenge_method` and no `code_challenge`).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Failure to check the `code_verifier` renders the PKCE flow ineffective. This vulnerability particularly endangers public or hybrid clients, as their `code` is deemed non-confidential.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					While not explicitly stated in the standard, it is generally recommended that OAuth2 flows accepting public clients should enforce PKCE - at least when redirecting to a non HTTPS URL (like http or an app link).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Impact
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The vulnerability poses a high risk to both public and hybrid clients.
 | 
				
			||||||
 | 
					When for example a mobile app implements oauth2, a malicious app can simply also register the same in-app-link (e.g. `mycoolapp://oauth2`) for the redirect callback URL, possibly receiving `code` during callback. With PKCE working, a malicious app would still receive a `code` but the `code` would not work without the correct unhashed code-challenge.
 | 
				
			||||||
 | 
					This is especially problematic, because authentik claims to support PKCE, and a developer can expect that the proper checks are in place. Note that app-links cannot be protected by HTTPS or similar mechanisms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Note also that this vulnerability poses a threat to confidential clients. Many confidential clients act as a proxy for OAuth2 API requests, typically from mobile apps or single-page applications. These proxies relay `code_challenge`, `code_challenge_method` (in auth request, which most libraries force and provide on default settings) and `code_verifier` in the token request unchanged and supplement the CLIENT_SECRET which only the relay knows. The relay can but does not have to check for an existing `code_verifier` as the standard does not define that PKCE can be ignored on confidential clients during the token request when the client requested PKCE during the authorization request.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					An attacker could potentially gain full access to the application. If the code grants access to an admin account, the confidentiality, integrity, and availability of that application are compromised.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### For more information
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you have any questions or comments about this advisory:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-   Email us at [security@goauthentik.io](mailto:security@goauthentik.io)
 | 
				
			||||||
@ -46,5 +46,8 @@
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    "devDependencies": {
 | 
					    "devDependencies": {
 | 
				
			||||||
        "prettier": "3.0.3"
 | 
					        "prettier": "3.0.3"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "engines": {
 | 
				
			||||||
 | 
					        "node": ">=20"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -407,6 +407,7 @@ const docsSidebar = {
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
            items: [
 | 
					            items: [
 | 
				
			||||||
                "security/policy",
 | 
					                "security/policy",
 | 
				
			||||||
 | 
					                "security/CVE-2023-48228",
 | 
				
			||||||
                "security/GHSA-rjvp-29xq-f62w",
 | 
					                "security/GHSA-rjvp-29xq-f62w",
 | 
				
			||||||
                "security/CVE-2023-39522",
 | 
					                "security/CVE-2023-39522",
 | 
				
			||||||
                "security/CVE-2023-36456",
 | 
					                "security/CVE-2023-36456",
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user