Compare commits
	
		
			10 Commits
		
	
	
		
			version/20
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2a3d2cd262 | |||
| d9aab79c62 | |||
| 1516fe86da | |||
| abad6c181f | |||
| 312eb70349 | |||
| 3af77ab382 | |||
| 72d67f65e5 | |||
| ea75741ec2 | |||
| aaa9b398f4 | |||
| d54d01b118 | 
| @ -1,5 +1,5 @@ | |||||||
| [bumpversion] | [bumpversion] | ||||||
| current_version = 2023.8.3 | current_version = 2023.8.6 | ||||||
| tag = True | tag = True | ||||||
| commit = True | commit = True | ||||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+) | parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+) | ||||||
|  | |||||||
							
								
								
									
										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.6" | ||||||
| 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), | ||||||
|  |     ] | ||||||
| @ -85,6 +85,25 @@ class TestAuthorize(OAuthTestCase): | |||||||
|             ) |             ) | ||||||
|             OAuthAuthorizationParams.from_request(request) |             OAuthAuthorizationParams.from_request(request) | ||||||
|  |  | ||||||
|  |     def test_blocked_redirect_uri(self): | ||||||
|  |         """test missing/invalid redirect URI""" | ||||||
|  |         OAuth2Provider.objects.create( | ||||||
|  |             name=generate_id(), | ||||||
|  |             client_id="test", | ||||||
|  |             authorization_flow=create_test_flow(), | ||||||
|  |             redirect_uris="data:local.invalid", | ||||||
|  |         ) | ||||||
|  |         with self.assertRaises(RedirectUriError): | ||||||
|  |             request = self.factory.get( | ||||||
|  |                 "/", | ||||||
|  |                 data={ | ||||||
|  |                     "response_type": "code", | ||||||
|  |                     "client_id": "test", | ||||||
|  |                     "redirect_uri": "data:localhost", | ||||||
|  |                 }, | ||||||
|  |             ) | ||||||
|  |             OAuthAuthorizationParams.from_request(request) | ||||||
|  |  | ||||||
|     def test_invalid_redirect_uri_empty(self): |     def test_invalid_redirect_uri_empty(self): | ||||||
|         """test missing/invalid redirect URI""" |         """test missing/invalid redirect URI""" | ||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|  | |||||||
							
								
								
									
										187
									
								
								authentik/providers/oauth2/tests/test_token_pkce.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								authentik/providers/oauth2/tests/test_token_pkce.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,187 @@ | |||||||
|  | """Test token view""" | ||||||
|  | from base64 import b64encode, urlsafe_b64encode | ||||||
|  | from hashlib import sha256 | ||||||
|  |  | ||||||
|  | from django.test import RequestFactory | ||||||
|  | from django.urls import reverse | ||||||
|  |  | ||||||
|  | from authentik.core.models import Application | ||||||
|  | from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||||
|  | from authentik.flows.challenge import ChallengeTypes | ||||||
|  | from authentik.lib.generators import generate_id | ||||||
|  | from authentik.providers.oauth2.constants import GRANT_TYPE_AUTHORIZATION_CODE | ||||||
|  | from authentik.providers.oauth2.models import AuthorizationCode, OAuth2Provider | ||||||
|  | from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestTokenPKCE(OAuthTestCase): | ||||||
|  |     """Test token view""" | ||||||
|  |  | ||||||
|  |     def setUp(self) -> None: | ||||||
|  |         super().setUp() | ||||||
|  |         self.factory = RequestFactory() | ||||||
|  |         self.app = Application.objects.create(name=generate_id(), slug="test") | ||||||
|  |  | ||||||
|  |     def test_pkce_missing_in_token(self): | ||||||
|  |         """Test full with pkce""" | ||||||
|  |         flow = create_test_flow() | ||||||
|  |         provider = OAuth2Provider.objects.create( | ||||||
|  |             name=generate_id(), | ||||||
|  |             client_id="test", | ||||||
|  |             authorization_flow=flow, | ||||||
|  |             redirect_uris="foo://localhost", | ||||||
|  |             access_code_validity="seconds=100", | ||||||
|  |         ) | ||||||
|  |         Application.objects.create(name="app", slug="app", provider=provider) | ||||||
|  |         state = generate_id() | ||||||
|  |         user = create_test_admin_user() | ||||||
|  |         self.client.force_login(user) | ||||||
|  |         challenge = generate_id() | ||||||
|  |         header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode() | ||||||
|  |         # Step 1, initiate params and get redirect to flow | ||||||
|  |         self.client.get( | ||||||
|  |             reverse("authentik_providers_oauth2:authorize"), | ||||||
|  |             data={ | ||||||
|  |                 "response_type": "code", | ||||||
|  |                 "client_id": "test", | ||||||
|  |                 "state": state, | ||||||
|  |                 "redirect_uri": "foo://localhost", | ||||||
|  |                 "code_challenge": challenge, | ||||||
|  |                 "code_challenge_method": "S256", | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), | ||||||
|  |         ) | ||||||
|  |         code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first() | ||||||
|  |         self.assertJSONEqual( | ||||||
|  |             response.content.decode(), | ||||||
|  |             { | ||||||
|  |                 "component": "xak-flow-redirect", | ||||||
|  |                 "type": ChallengeTypes.REDIRECT.value, | ||||||
|  |                 "to": f"foo://localhost?code={code.code}&state={state}", | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         response = self.client.post( | ||||||
|  |             reverse("authentik_providers_oauth2:token"), | ||||||
|  |             data={ | ||||||
|  |                 "grant_type": GRANT_TYPE_AUTHORIZATION_CODE, | ||||||
|  |                 "code": code.code, | ||||||
|  |                 # Missing the code_verifier here | ||||||
|  |                 "redirect_uri": "foo://localhost", | ||||||
|  |             }, | ||||||
|  |             HTTP_AUTHORIZATION=f"Basic {header}", | ||||||
|  |         ) | ||||||
|  |         self.assertJSONEqual( | ||||||
|  |             response.content, | ||||||
|  |             {"error": "invalid_request", "error_description": "The request is otherwise malformed"}, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, 400) | ||||||
|  |  | ||||||
|  |     def test_pkce_correct_s256(self): | ||||||
|  |         """Test full with pkce""" | ||||||
|  |         flow = create_test_flow() | ||||||
|  |         provider = OAuth2Provider.objects.create( | ||||||
|  |             name=generate_id(), | ||||||
|  |             client_id="test", | ||||||
|  |             authorization_flow=flow, | ||||||
|  |             redirect_uris="foo://localhost", | ||||||
|  |             access_code_validity="seconds=100", | ||||||
|  |         ) | ||||||
|  |         Application.objects.create(name="app", slug="app", provider=provider) | ||||||
|  |         state = generate_id() | ||||||
|  |         user = create_test_admin_user() | ||||||
|  |         self.client.force_login(user) | ||||||
|  |         verifier = generate_id() | ||||||
|  |         challenge = ( | ||||||
|  |             urlsafe_b64encode(sha256(verifier.encode("ascii")).digest()) | ||||||
|  |             .decode("utf-8") | ||||||
|  |             .replace("=", "") | ||||||
|  |         ) | ||||||
|  |         header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode() | ||||||
|  |         # Step 1, initiate params and get redirect to flow | ||||||
|  |         self.client.get( | ||||||
|  |             reverse("authentik_providers_oauth2:authorize"), | ||||||
|  |             data={ | ||||||
|  |                 "response_type": "code", | ||||||
|  |                 "client_id": "test", | ||||||
|  |                 "state": state, | ||||||
|  |                 "redirect_uri": "foo://localhost", | ||||||
|  |                 "code_challenge": challenge, | ||||||
|  |                 "code_challenge_method": "S256", | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), | ||||||
|  |         ) | ||||||
|  |         code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first() | ||||||
|  |         self.assertJSONEqual( | ||||||
|  |             response.content.decode(), | ||||||
|  |             { | ||||||
|  |                 "component": "xak-flow-redirect", | ||||||
|  |                 "type": ChallengeTypes.REDIRECT.value, | ||||||
|  |                 "to": f"foo://localhost?code={code.code}&state={state}", | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         response = self.client.post( | ||||||
|  |             reverse("authentik_providers_oauth2:token"), | ||||||
|  |             data={ | ||||||
|  |                 "grant_type": GRANT_TYPE_AUTHORIZATION_CODE, | ||||||
|  |                 "code": code.code, | ||||||
|  |                 "code_verifier": verifier, | ||||||
|  |                 "redirect_uri": "foo://localhost", | ||||||
|  |             }, | ||||||
|  |             HTTP_AUTHORIZATION=f"Basic {header}", | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|  |     def test_pkce_correct_plain(self): | ||||||
|  |         """Test full with pkce""" | ||||||
|  |         flow = create_test_flow() | ||||||
|  |         provider = OAuth2Provider.objects.create( | ||||||
|  |             name=generate_id(), | ||||||
|  |             client_id="test", | ||||||
|  |             authorization_flow=flow, | ||||||
|  |             redirect_uris="foo://localhost", | ||||||
|  |             access_code_validity="seconds=100", | ||||||
|  |         ) | ||||||
|  |         Application.objects.create(name="app", slug="app", provider=provider) | ||||||
|  |         state = generate_id() | ||||||
|  |         user = create_test_admin_user() | ||||||
|  |         self.client.force_login(user) | ||||||
|  |         verifier = generate_id() | ||||||
|  |         header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode() | ||||||
|  |         # Step 1, initiate params and get redirect to flow | ||||||
|  |         self.client.get( | ||||||
|  |             reverse("authentik_providers_oauth2:authorize"), | ||||||
|  |             data={ | ||||||
|  |                 "response_type": "code", | ||||||
|  |                 "client_id": "test", | ||||||
|  |                 "state": state, | ||||||
|  |                 "redirect_uri": "foo://localhost", | ||||||
|  |                 "code_challenge": verifier, | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), | ||||||
|  |         ) | ||||||
|  |         code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first() | ||||||
|  |         self.assertJSONEqual( | ||||||
|  |             response.content.decode(), | ||||||
|  |             { | ||||||
|  |                 "component": "xak-flow-redirect", | ||||||
|  |                 "type": ChallengeTypes.REDIRECT.value, | ||||||
|  |                 "to": f"foo://localhost?code={code.code}&state={state}", | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         response = self.client.post( | ||||||
|  |             reverse("authentik_providers_oauth2:token"), | ||||||
|  |             data={ | ||||||
|  |                 "grant_type": GRANT_TYPE_AUTHORIZATION_CODE, | ||||||
|  |                 "code": code.code, | ||||||
|  |                 "code_verifier": verifier, | ||||||
|  |                 "redirect_uri": "foo://localhost", | ||||||
|  |             }, | ||||||
|  |             HTTP_AUTHORIZATION=f"Basic {header}", | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
| @ -74,6 +74,7 @@ PLAN_CONTEXT_PARAMS = "goauthentik.io/providers/oauth2/params" | |||||||
| SESSION_KEY_LAST_LOGIN_UID = "authentik/providers/oauth2/last_login_uid" | SESSION_KEY_LAST_LOGIN_UID = "authentik/providers/oauth2/last_login_uid" | ||||||
|  |  | ||||||
| ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSENT, PROMPT_LOGIN} | ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSENT, PROMPT_LOGIN} | ||||||
|  | FORBIDDEN_URI_SCHEMES = {"javascript", "data", "vbscript"} | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(slots=True) | @dataclass(slots=True) | ||||||
| @ -174,6 +175,10 @@ class OAuthAuthorizationParams: | |||||||
|         self.check_scope() |         self.check_scope() | ||||||
|         self.check_nonce() |         self.check_nonce() | ||||||
|         self.check_code_challenge() |         self.check_code_challenge() | ||||||
|  |         if self.request: | ||||||
|  |             raise AuthorizeError( | ||||||
|  |                 self.redirect_uri, "request_not_supported", self.grant_type, self.state | ||||||
|  |             ) | ||||||
|  |  | ||||||
|     def check_redirect_uri(self): |     def check_redirect_uri(self): | ||||||
|         """Redirect URI validation.""" |         """Redirect URI validation.""" | ||||||
| @ -211,10 +216,9 @@ class OAuthAuthorizationParams: | |||||||
|                     expected=allowed_redirect_urls, |                     expected=allowed_redirect_urls, | ||||||
|                 ) |                 ) | ||||||
|                 raise RedirectUriError(self.redirect_uri, allowed_redirect_urls) |                 raise RedirectUriError(self.redirect_uri, allowed_redirect_urls) | ||||||
|         if self.request: |         # Check against forbidden schemes | ||||||
|             raise AuthorizeError( |         if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES: | ||||||
|                 self.redirect_uri, "request_not_supported", self.grant_type, self.state |             raise RedirectUriError(self.redirect_uri, allowed_redirect_urls) | ||||||
|             ) |  | ||||||
|  |  | ||||||
|     def check_scope(self): |     def check_scope(self): | ||||||
|         """Ensure openid scope is set in Hybrid flows, or when requesting an id_token""" |         """Ensure openid scope is set in Hybrid flows, or when requesting an id_token""" | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ from hashlib import sha256 | |||||||
| from re import error as RegexError | from re import error as RegexError | ||||||
| from re import fullmatch | from re import fullmatch | ||||||
| from typing import Any, Optional | from typing import Any, Optional | ||||||
|  | from urllib.parse import urlparse | ||||||
|  |  | ||||||
| from django.http import HttpRequest, HttpResponse | from django.http import HttpRequest, HttpResponse | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| @ -53,6 +54,7 @@ from authentik.providers.oauth2.models import ( | |||||||
|     RefreshToken, |     RefreshToken, | ||||||
| ) | ) | ||||||
| from authentik.providers.oauth2.utils import TokenResponse, cors_allow, extract_client_auth | from authentik.providers.oauth2.utils import TokenResponse, cors_allow, extract_client_auth | ||||||
|  | from authentik.providers.oauth2.views.authorize import FORBIDDEN_URI_SCHEMES | ||||||
| from authentik.sources.oauth.models import OAuthSource | from authentik.sources.oauth.models import OAuthSource | ||||||
| from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS | from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS | ||||||
|  |  | ||||||
| @ -204,6 +206,10 @@ class TokenParams: | |||||||
|                 ).from_http(request) |                 ).from_http(request) | ||||||
|                 raise TokenError("invalid_client") |                 raise TokenError("invalid_client") | ||||||
|  |  | ||||||
|  |         # Check against forbidden schemes | ||||||
|  |         if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES: | ||||||
|  |             raise TokenError("invalid_request") | ||||||
|  |  | ||||||
|         self.authorization_code = AuthorizationCode.objects.filter(code=raw_code).first() |         self.authorization_code = AuthorizationCode.objects.filter(code=raw_code).first() | ||||||
|         if not self.authorization_code: |         if not self.authorization_code: | ||||||
|             LOGGER.warning("Code does not exist", code=raw_code) |             LOGGER.warning("Code does not exist", code=raw_code) | ||||||
| @ -221,7 +227,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.6} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: server |     command: server | ||||||
|     environment: |     environment: | ||||||
| @ -53,7 +53,7 @@ services: | |||||||
|       - postgresql |       - postgresql | ||||||
|       - redis |       - redis | ||||||
|   worker: |   worker: | ||||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.8.3} |     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.8.6} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: worker |     command: worker | ||||||
|     environment: |     environment: | ||||||
|  | |||||||
| @ -29,4 +29,4 @@ func UserAgent() string { | |||||||
| 	return fmt.Sprintf("authentik@%s", FullVersion()) | 	return fmt.Sprintf("authentik@%s", FullVersion()) | ||||||
| } | } | ||||||
|  |  | ||||||
| const VERSION = "2023.8.3" | const VERSION = "2023.8.6" | ||||||
|  | |||||||
| @ -113,7 +113,7 @@ filterwarnings = [ | |||||||
|  |  | ||||||
| [tool.poetry] | [tool.poetry] | ||||||
| name = "authentik" | name = "authentik" | ||||||
| version = "2023.8.3" | version = "2023.8.6" | ||||||
| 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.6 | ||||||
|   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.6"; | ||||||
| export const TITLE_DEFAULT = "authentik"; | export const TITLE_DEFAULT = "authentik"; | ||||||
| export const ROUTE_SEPARATOR = ";"; | export const ROUTE_SEPARATOR = ";"; | ||||||
|  |  | ||||||
|  | |||||||
| @ -155,6 +155,18 @@ 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) | ||||||
|  |  | ||||||
|  | ## Fixed in 2023.8.5 | ||||||
|  |  | ||||||
|  | -   security: fix [CVE-2023-48228](../../security/CVE-2023-48228.md), Reported by [@Sapd](https://github.com/Sapd) (#7666) | ||||||
|  |  | ||||||
|  | ## Fixed in 2023.8.6 | ||||||
|  |  | ||||||
|  | -   providers/oauth2: fix [CVE-2024-21637](../../security/CVE-2024-21637.md), Reported by [@lauritzh](https://github.com/lauritzh) (#8104) | ||||||
|  |  | ||||||
| ## 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) | ||||||
							
								
								
									
										39
									
								
								website/docs/security/CVE-2024-21637.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								website/docs/security/CVE-2024-21637.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | |||||||
|  | # CVE-2024-21637 | ||||||
|  |  | ||||||
|  | _Reported by [@lauritzh](https://github.com/lauritzh)_ | ||||||
|  |  | ||||||
|  | ## XSS in Authentik via JavaScript-URI as Redirect URI and form_post Response Mode | ||||||
|  |  | ||||||
|  | ### Summary | ||||||
|  |  | ||||||
|  | Given an OAuth2 provider configured with allowed redirect URIs set to `*` or `.*`, an attacker can send an OAuth Authorization request using `response_mode=form_post` and setting `redirect_uri` to a malicious URI, to capture authentik's session token. | ||||||
|  |  | ||||||
|  | ### Patches | ||||||
|  |  | ||||||
|  | authentik 2023.8.6 and 2023.10.6 fix this issue. | ||||||
|  |  | ||||||
|  | ### Impact | ||||||
|  |  | ||||||
|  | The impact depends on the attack scenario. In the following I will describe the two scenario that were identified for Authentik. | ||||||
|  |  | ||||||
|  | #### Redirect URI Misconfiguration | ||||||
|  |  | ||||||
|  | While advising that this may cause security issues, Authentik generally allows wildcards as Redirect URI. Therefore, using a wildcard-only effectively allowing arbitrary URLS is possible misconfiguration that may be present in real-world instances. | ||||||
|  |  | ||||||
|  | In such cases, unauthenticated and unprivileged attackers can perform the above described actions. | ||||||
|  |  | ||||||
|  | ### User with (only) App Administration Permissions | ||||||
|  |  | ||||||
|  | A more likely scenario is an administrative user (e.g. a normal developer) having only permissions to manage applications. | ||||||
|  |  | ||||||
|  | This relatively user could use the described attacks to perform a privilege escalation. | ||||||
|  |  | ||||||
|  | ### Workaround | ||||||
|  |  | ||||||
|  | It is recommended to upgrade to the patched version of authentik. If not possible, ensure that OAuth2 providers do not use a wildcard (`*` or `.*`) value as allowed redirect URI setting. (This is _not_ exploitable if part of the redirect URI has a wildcard, for example `https://foo-.*\.bar\.com`) | ||||||
|  |  | ||||||
|  | ### 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,9 @@ const docsSidebar = { | |||||||
|             }, |             }, | ||||||
|             items: [ |             items: [ | ||||||
|                 "security/policy", |                 "security/policy", | ||||||
|  |                 "security/CVE-2024-21637", | ||||||
|  |                 "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
	