Compare commits
	
		
			8 Commits
		
	
	
		
			version/20
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1516fe86da | |||
| abad6c181f | |||
| 312eb70349 | |||
| 3af77ab382 | |||
| 72d67f65e5 | |||
| ea75741ec2 | |||
| aaa9b398f4 | |||
| d54d01b118 | 
@ -1,5 +1,5 @@
 | 
				
			|||||||
[bumpversion]
 | 
					[bumpversion]
 | 
				
			||||||
current_version = 2023.8.3
 | 
					current_version = 2023.8.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+)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										7
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							@ -11,6 +11,7 @@ on:
 | 
				
			|||||||
  pull_request:
 | 
					  pull_request:
 | 
				
			||||||
    branches:
 | 
					    branches:
 | 
				
			||||||
      - main
 | 
					      - main
 | 
				
			||||||
 | 
					      - version-*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
env:
 | 
					env:
 | 
				
			||||||
  POSTGRES_DB: authentik
 | 
					  POSTGRES_DB: authentik
 | 
				
			||||||
@ -184,6 +185,9 @@ jobs:
 | 
				
			|||||||
  build:
 | 
					  build:
 | 
				
			||||||
    needs: ci-core-mark
 | 
					    needs: ci-core-mark
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    permissions:
 | 
				
			||||||
 | 
					      # Needed to upload contianer images to ghcr.io
 | 
				
			||||||
 | 
					      packages: write
 | 
				
			||||||
    timeout-minutes: 120
 | 
					    timeout-minutes: 120
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v3
 | 
					      - uses: actions/checkout@v3
 | 
				
			||||||
@ -229,6 +233,9 @@ jobs:
 | 
				
			|||||||
  build-arm64:
 | 
					  build-arm64:
 | 
				
			||||||
    needs: ci-core-mark
 | 
					    needs: ci-core-mark
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    permissions:
 | 
				
			||||||
 | 
					      # Needed to upload contianer images to ghcr.io
 | 
				
			||||||
 | 
					      packages: write
 | 
				
			||||||
    timeout-minutes: 120
 | 
					    timeout-minutes: 120
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v3
 | 
					      - uses: actions/checkout@v3
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										4
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							@ -9,6 +9,7 @@ on:
 | 
				
			|||||||
  pull_request:
 | 
					  pull_request:
 | 
				
			||||||
    branches:
 | 
					    branches:
 | 
				
			||||||
      - main
 | 
					      - main
 | 
				
			||||||
 | 
					      - version-*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  lint-golint:
 | 
					  lint-golint:
 | 
				
			||||||
@ -63,6 +64,9 @@ jobs:
 | 
				
			|||||||
          - ldap
 | 
					          - ldap
 | 
				
			||||||
          - radius
 | 
					          - radius
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    permissions:
 | 
				
			||||||
 | 
					      # Needed to upload contianer images to ghcr.io
 | 
				
			||||||
 | 
					      packages: write
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v3
 | 
					      - uses: actions/checkout@v3
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
								
							@ -9,6 +9,7 @@ on:
 | 
				
			|||||||
  pull_request:
 | 
					  pull_request:
 | 
				
			||||||
    branches:
 | 
					    branches:
 | 
				
			||||||
      - main
 | 
					      - main
 | 
				
			||||||
 | 
					      - version-*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  lint-eslint:
 | 
					  lint-eslint:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								.github/workflows/ci-website.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/ci-website.yml
									
									
									
									
										vendored
									
									
								
							@ -9,6 +9,7 @@ on:
 | 
				
			|||||||
  pull_request:
 | 
					  pull_request:
 | 
				
			||||||
    branches:
 | 
					    branches:
 | 
				
			||||||
      - main
 | 
					      - main
 | 
				
			||||||
 | 
					      - version-*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  lint-prettier:
 | 
					  lint-prettier:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										9
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							@ -7,6 +7,9 @@ on:
 | 
				
			|||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  build-server:
 | 
					  build-server:
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    permissions:
 | 
				
			||||||
 | 
					      # Needed to upload contianer images to ghcr.io
 | 
				
			||||||
 | 
					      packages: write
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v3
 | 
					      - uses: actions/checkout@v3
 | 
				
			||||||
      - name: Set up QEMU
 | 
					      - name: Set up QEMU
 | 
				
			||||||
@ -47,6 +50,9 @@ jobs:
 | 
				
			|||||||
            VERSION_FAMILY=${{ steps.ev.outputs.versionFamily }}
 | 
					            VERSION_FAMILY=${{ steps.ev.outputs.versionFamily }}
 | 
				
			||||||
  build-outpost:
 | 
					  build-outpost:
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    permissions:
 | 
				
			||||||
 | 
					      # Needed to upload contianer images to ghcr.io
 | 
				
			||||||
 | 
					      packages: write
 | 
				
			||||||
    strategy:
 | 
					    strategy:
 | 
				
			||||||
      fail-fast: false
 | 
					      fail-fast: false
 | 
				
			||||||
      matrix:
 | 
					      matrix:
 | 
				
			||||||
@ -96,6 +102,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:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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,7 +2,7 @@
 | 
				
			|||||||
from os import environ
 | 
					from os import environ
 | 
				
			||||||
from typing import Optional
 | 
					from typing import Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__version__ = "2023.8.3"
 | 
					__version__ = "2023.8.5"
 | 
				
			||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
 | 
					ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										25
									
								
								authentik/flows/migrations/0026_alter_flow_options.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								authentik/flows/migrations/0026_alter_flow_options.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					# Generated by Django 4.2.6 on 2023-10-10 17:18
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_flows", "0025_alter_flowstagebinding_evaluate_on_plan_and_more"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name="flow",
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "permissions": [
 | 
				
			||||||
 | 
					                    ("export_flow", "Can export a Flow"),
 | 
				
			||||||
 | 
					                    ("inspect_flow", "Can inspect a Flow's execution"),
 | 
				
			||||||
 | 
					                    ("view_flow_cache", "View Flow's cache metrics"),
 | 
				
			||||||
 | 
					                    ("clear_flow_cache", "Clear Flow's cache metrics"),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                "verbose_name": "Flow",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Flows",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										34
									
								
								authentik/flows/migrations/0027_auto_20231028_1424.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								authentik/flows/migrations/0027_auto_20231028_1424.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					# Generated by Django 4.2.6 on 2023-10-28 14:24
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.apps.registry import Apps
 | 
				
			||||||
 | 
					from django.db import migrations
 | 
				
			||||||
 | 
					from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def set_oobe_flow_authentication(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    from guardian.shortcuts import get_anonymous_user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Flow = apps.get_model("authentik_flows", "Flow")
 | 
				
			||||||
 | 
					    User = apps.get_model("authentik_core", "User")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    users = User.objects.using(db_alias).exclude(username="akadmin")
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        users = users.exclude(pk=get_anonymous_user().pk)
 | 
				
			||||||
 | 
					    # pylint: disable=broad-except
 | 
				
			||||||
 | 
					    except Exception:  # nosec
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if users.exists():
 | 
				
			||||||
 | 
					        Flow.objects.filter(slug="initial-setup").update(authentication="require_superuser")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_flows", "0026_alter_flow_options"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.RunPython(set_oobe_flow_authentication),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										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)
 | 
				
			||||||
@ -221,7 +221,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())
 | 
				
			||||||
 | 
				
			|||||||
@ -171,6 +171,8 @@ class MetadataProcessor:
 | 
				
			|||||||
            entity_descriptor, f"{{{NS_SAML_METADATA}}}IDPSSODescriptor"
 | 
					            entity_descriptor, f"{{{NS_SAML_METADATA}}}IDPSSODescriptor"
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        idp_sso_descriptor.attrib["protocolSupportEnumeration"] = NS_SAML_PROTOCOL
 | 
					        idp_sso_descriptor.attrib["protocolSupportEnumeration"] = NS_SAML_PROTOCOL
 | 
				
			||||||
 | 
					        if self.provider.verification_kp:
 | 
				
			||||||
 | 
					            idp_sso_descriptor.attrib["WantAuthnRequestsSigned"] = "true"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        signing_descriptor = self.get_signing_key_descriptor()
 | 
					        signing_descriptor = self.get_signing_key_descriptor()
 | 
				
			||||||
        if signing_descriptor is not None:
 | 
					        if signing_descriptor is not None:
 | 
				
			||||||
 | 
				
			|||||||
@ -12,7 +12,7 @@ from authentik.lib.xml import lxml_from_string
 | 
				
			|||||||
from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider
 | 
					from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider
 | 
				
			||||||
from authentik.providers.saml.processors.metadata import MetadataProcessor
 | 
					from authentik.providers.saml.processors.metadata import MetadataProcessor
 | 
				
			||||||
from authentik.providers.saml.processors.metadata_parser import ServiceProviderMetadataParser
 | 
					from authentik.providers.saml.processors.metadata_parser import ServiceProviderMetadataParser
 | 
				
			||||||
from authentik.sources.saml.processors.constants import NS_MAP
 | 
					from authentik.sources.saml.processors.constants import NS_MAP, NS_SAML_METADATA
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestServiceProviderMetadataParser(TestCase):
 | 
					class TestServiceProviderMetadataParser(TestCase):
 | 
				
			||||||
@ -55,6 +55,24 @@ class TestServiceProviderMetadataParser(TestCase):
 | 
				
			|||||||
        schema = etree.XMLSchema(etree.parse("schemas/saml-schema-metadata-2.0.xsd"))  # nosec
 | 
					        schema = etree.XMLSchema(etree.parse("schemas/saml-schema-metadata-2.0.xsd"))  # nosec
 | 
				
			||||||
        self.assertTrue(schema.validate(metadata))
 | 
					        self.assertTrue(schema.validate(metadata))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_schema_want_authn_requests_signed(self):
 | 
				
			||||||
 | 
					        """Test metadata generation with WantAuthnRequestsSigned"""
 | 
				
			||||||
 | 
					        cert = create_test_cert()
 | 
				
			||||||
 | 
					        provider = SAMLProvider.objects.create(
 | 
				
			||||||
 | 
					            name=generate_id(),
 | 
				
			||||||
 | 
					            authorization_flow=self.flow,
 | 
				
			||||||
 | 
					            verification_kp=cert,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        Application.objects.create(
 | 
				
			||||||
 | 
					            name=generate_id(),
 | 
				
			||||||
 | 
					            slug=generate_id(),
 | 
				
			||||||
 | 
					            provider=provider,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        request = self.factory.get("/")
 | 
				
			||||||
 | 
					        metadata = lxml_from_string(MetadataProcessor(provider, request).build_entity_descriptor())
 | 
				
			||||||
 | 
					        idp_sso_descriptor = metadata.findall(f"{{{NS_SAML_METADATA}}}IDPSSODescriptor")[0]
 | 
				
			||||||
 | 
					        self.assertEqual(idp_sso_descriptor.attrib["WantAuthnRequestsSigned"], "true")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_simple(self):
 | 
					    def test_simple(self):
 | 
				
			||||||
        """Test simple metadata without Signing"""
 | 
					        """Test simple metadata without Signing"""
 | 
				
			||||||
        metadata = ServiceProviderMetadataParser().parse(load_fixture("fixtures/simple.xml"))
 | 
					        metadata = ServiceProviderMetadataParser().parse(load_fixture("fixtures/simple.xml"))
 | 
				
			||||||
 | 
				
			|||||||
@ -47,9 +47,11 @@ class FreeIPA(BaseLDAPSynchronizer):
 | 
				
			|||||||
            return
 | 
					            return
 | 
				
			||||||
        # For some reason, nsaccountlock is not defined properly in the schema as bool
 | 
					        # For some reason, nsaccountlock is not defined properly in the schema as bool
 | 
				
			||||||
        # hence we get it as a list of strings
 | 
					        # hence we get it as a list of strings
 | 
				
			||||||
        _is_active = str(self._flatten(attributes.get("nsaccountlock", ["FALSE"])))
 | 
					        _is_locked = str(self._flatten(attributes.get("nsaccountlock", ["FALSE"])))
 | 
				
			||||||
        # So we have to attempt to convert it to a bool
 | 
					        # So we have to attempt to convert it to a bool
 | 
				
			||||||
        is_active = _is_active.lower() == "true"
 | 
					        is_locked = _is_locked.lower() == "true"
 | 
				
			||||||
 | 
					        # And then invert it since freeipa saves locked and we save active
 | 
				
			||||||
 | 
					        is_active = not is_locked
 | 
				
			||||||
        if is_active != user.is_active:
 | 
					        if is_active != user.is_active:
 | 
				
			||||||
            user.is_active = is_active
 | 
					            user.is_active = is_active
 | 
				
			||||||
            user.save()
 | 
					            user.save()
 | 
				
			||||||
 | 
				
			|||||||
@ -137,6 +137,7 @@ class LDAPSyncTests(TestCase):
 | 
				
			|||||||
            user_sync.sync_full()
 | 
					            user_sync.sync_full()
 | 
				
			||||||
            self.assertTrue(User.objects.filter(username="user0_sn").exists())
 | 
					            self.assertTrue(User.objects.filter(username="user0_sn").exists())
 | 
				
			||||||
            self.assertFalse(User.objects.filter(username="user1_sn").exists())
 | 
					            self.assertFalse(User.objects.filter(username="user1_sn").exists())
 | 
				
			||||||
 | 
					            self.assertFalse(User.objects.get(username="user-nsaccountlock").is_active)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_sync_groups_ad(self):
 | 
					    def test_sync_groups_ad(self):
 | 
				
			||||||
        """Test group sync"""
 | 
					        """Test group sync"""
 | 
				
			||||||
 | 
				
			|||||||
@ -85,6 +85,19 @@ entries:
 | 
				
			|||||||
  identifiers:
 | 
					  identifiers:
 | 
				
			||||||
    name: default-oobe-password-usable
 | 
					    name: default-oobe-password-usable
 | 
				
			||||||
  model: authentik_policies_expression.expressionpolicy
 | 
					  model: authentik_policies_expression.expressionpolicy
 | 
				
			||||||
 | 
					- attrs:
 | 
				
			||||||
 | 
					    expression: |
 | 
				
			||||||
 | 
					      # This policy ensures that the setup flow can only be
 | 
				
			||||||
 | 
					      # used one time
 | 
				
			||||||
 | 
					      from authentik.flows.models import Flow, FlowAuthenticationRequirement
 | 
				
			||||||
 | 
					      Flow.objects.filter(slug="initial-setup").update(
 | 
				
			||||||
 | 
					          authentication=FlowAuthenticationRequirement.REQUIRE_SUPERUSER,
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      return True
 | 
				
			||||||
 | 
					  id: policy-default-oobe-flow-set-authentication
 | 
				
			||||||
 | 
					  identifiers:
 | 
				
			||||||
 | 
					    name: default-oobe-flow-set-authentication
 | 
				
			||||||
 | 
					  model: authentik_policies_expression.expressionpolicy
 | 
				
			||||||
- attrs:
 | 
					- attrs:
 | 
				
			||||||
    fields:
 | 
					    fields:
 | 
				
			||||||
    - !KeyOf prompt-field-header
 | 
					    - !KeyOf prompt-field-header
 | 
				
			||||||
@ -129,6 +142,7 @@ entries:
 | 
				
			|||||||
    evaluate_on_plan: true
 | 
					    evaluate_on_plan: true
 | 
				
			||||||
    invalid_response_action: retry
 | 
					    invalid_response_action: retry
 | 
				
			||||||
    re_evaluate_policies: false
 | 
					    re_evaluate_policies: false
 | 
				
			||||||
 | 
					  id: binding-login
 | 
				
			||||||
  identifiers:
 | 
					  identifiers:
 | 
				
			||||||
    order: 100
 | 
					    order: 100
 | 
				
			||||||
    stage: !KeyOf stage-default-authentication-login
 | 
					    stage: !KeyOf stage-default-authentication-login
 | 
				
			||||||
@ -144,3 +158,8 @@ entries:
 | 
				
			|||||||
    policy: !KeyOf policy-default-oobe-prefill-user
 | 
					    policy: !KeyOf policy-default-oobe-prefill-user
 | 
				
			||||||
    target: !KeyOf binding-password-write
 | 
					    target: !KeyOf binding-password-write
 | 
				
			||||||
  model: authentik_policies.policybinding
 | 
					  model: authentik_policies.policybinding
 | 
				
			||||||
 | 
					- identifiers:
 | 
				
			||||||
 | 
					    order: 0
 | 
				
			||||||
 | 
					    policy: !KeyOf policy-default-oobe-flow-set-authentication
 | 
				
			||||||
 | 
					    target: !KeyOf binding-login
 | 
				
			||||||
 | 
					  model: authentik_policies.policybinding
 | 
				
			||||||
 | 
				
			|||||||
@ -42,9 +42,3 @@ entries:
 | 
				
			|||||||
      user: !KeyOf admin-user
 | 
					      user: !KeyOf admin-user
 | 
				
			||||||
    attrs:
 | 
					    attrs:
 | 
				
			||||||
      key: !Context token
 | 
					      key: !Context token
 | 
				
			||||||
  - model: authentik_blueprints.blueprintinstance
 | 
					 | 
				
			||||||
    identifiers:
 | 
					 | 
				
			||||||
      metadata:
 | 
					 | 
				
			||||||
        labels:
 | 
					 | 
				
			||||||
          blueprints.goauthentik.io/system-bootstrap: "true"
 | 
					 | 
				
			||||||
    state: absent
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -32,7 +32,7 @@ services:
 | 
				
			|||||||
    volumes:
 | 
					    volumes:
 | 
				
			||||||
      - redis:/data
 | 
					      - redis:/data
 | 
				
			||||||
  server:
 | 
					  server:
 | 
				
			||||||
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.8.3}
 | 
					    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.8.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.8.3}
 | 
					    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.8.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.8.3"
 | 
					const VERSION = "2023.8.5"
 | 
				
			||||||
 | 
				
			|||||||
@ -113,7 +113,7 @@ filterwarnings = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[tool.poetry]
 | 
					[tool.poetry]
 | 
				
			||||||
name = "authentik"
 | 
					name = "authentik"
 | 
				
			||||||
version = "2023.8.3"
 | 
					version = "2023.8.5"
 | 
				
			||||||
description = ""
 | 
					description = ""
 | 
				
			||||||
authors = ["authentik Team <hello@goauthentik.io>"]
 | 
					authors = ["authentik Team <hello@goauthentik.io>"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
openapi: 3.0.3
 | 
					openapi: 3.0.3
 | 
				
			||||||
info:
 | 
					info:
 | 
				
			||||||
  title: authentik
 | 
					  title: authentik
 | 
				
			||||||
  version: 2023.8.3
 | 
					  version: 2023.8.5
 | 
				
			||||||
  description: Making authentication simple.
 | 
					  description: Making authentication simple.
 | 
				
			||||||
  contact:
 | 
					  contact:
 | 
				
			||||||
    email: hello@goauthentik.io
 | 
					    email: hello@goauthentik.io
 | 
				
			||||||
 | 
				
			|||||||
@ -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.8.3";
 | 
					export const VERSION = "2023.8.5";
 | 
				
			||||||
export const TITLE_DEFAULT = "authentik";
 | 
					export const TITLE_DEFAULT = "authentik";
 | 
				
			||||||
export const ROUTE_SEPARATOR = ";";
 | 
					export const ROUTE_SEPARATOR = ";";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -155,6 +155,10 @@ image:
 | 
				
			|||||||
-   web: don't import entire SourceViewPage in flow and user interface (#6761)
 | 
					-   web: don't import entire SourceViewPage in flow and user interface (#6761)
 | 
				
			||||||
-   web: replace ampersand (#6737)
 | 
					-   web: replace ampersand (#6737)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Fixed in 2023.8.4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-   \*: fix [GHSA-rjvp-29xq-f62w](../security/GHSA-rjvp-29xq-f62w), Reported by [@devSparkle](https://github.com/devSparkle)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## API Changes
 | 
					## API Changes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### What's New
 | 
					#### What's New
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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)
 | 
				
			||||||
							
								
								
									
										27
									
								
								website/docs/security/GHSA-rjvp-29xq-f62w.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								website/docs/security/GHSA-rjvp-29xq-f62w.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					# GHSA-rjvp-29xq-f62w
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_Reported by [@devSparkle](https://github.com/devSparkle)_
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Potential Installation takeover when default admin user is deleted
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Summary
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					In the affected versions, when the default admin user has been deleted, it is potentially possible for an attacker to set the password of the default admin user without any authentication.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Patches
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					authentik 2023.8.4 and 2023.10.2 fix this issue, for other versions the workaround can be used.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Impact
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					authentik uses a blueprint to create the default admin user, which can also optionally set the default admin users' password from an environment variable. When the user is deleted, the `initial-setup` flow used to configure authentik after the first installation becomes available again.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Workarounds
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Ensure the default admin user (Username `akadmin`) exists and has a password set. It is recommended to use a very strong password for this user, and store it in a secure location like a password manager. It is also possible to deactivate the user to prevent any logins as akadmin.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### For more information
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you have any questions or comments about this advisory:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-   Email us at [security@goauthentik.io](mailto:security@goauthentik.io)
 | 
				
			||||||
@ -362,6 +362,8 @@ const docsSidebar = {
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
            items: [
 | 
					            items: [
 | 
				
			||||||
                "security/policy",
 | 
					                "security/policy",
 | 
				
			||||||
 | 
					                "security/CVE-2023-48228",
 | 
				
			||||||
 | 
					                "security/GHSA-rjvp-29xq-f62w",
 | 
				
			||||||
                "security/CVE-2023-39522",
 | 
					                "security/CVE-2023-39522",
 | 
				
			||||||
                "security/CVE-2023-36456",
 | 
					                "security/CVE-2023-36456",
 | 
				
			||||||
                "security/2023-06-cure53",
 | 
					                "security/2023-06-cure53",
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user