Compare commits
10 Commits
trustchain
...
version-20
Author | SHA1 | Date |
---|---|---|
Jens Langhammer | 2a3d2cd262 | |
gcp-cherry-pick-bot[bot] | d9aab79c62 | |
Jens Langhammer | 1516fe86da | |
gcp-cherry-pick-bot[bot] | abad6c181f | |
gcp-cherry-pick-bot[bot] | 312eb70349 | |
gcp-cherry-pick-bot[bot] | 3af77ab382 | |
Jens Langhammer | 72d67f65e5 | |
Jens L | ea75741ec2 | |
gcp-cherry-pick-bot[bot] | aaa9b398f4 | |
gcp-cherry-pick-bot[bot] | d54d01b118 |
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 2023.8.3
|
||||
current_version = 2023.8.6
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
|
||||
|
|
|
@ -11,6 +11,7 @@ on:
|
|||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- version-*
|
||||
|
||||
env:
|
||||
POSTGRES_DB: authentik
|
||||
|
@ -184,6 +185,9 @@ jobs:
|
|||
build:
|
||||
needs: ci-core-mark
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload contianer images to ghcr.io
|
||||
packages: write
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
@ -229,6 +233,9 @@ jobs:
|
|||
build-arm64:
|
||||
needs: ci-core-mark
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload contianer images to ghcr.io
|
||||
packages: write
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
|
|
@ -9,6 +9,7 @@ on:
|
|||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- version-*
|
||||
|
||||
jobs:
|
||||
lint-golint:
|
||||
|
@ -63,6 +64,9 @@ jobs:
|
|||
- ldap
|
||||
- radius
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload contianer images to ghcr.io
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
|
|
@ -9,6 +9,7 @@ on:
|
|||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- version-*
|
||||
|
||||
jobs:
|
||||
lint-eslint:
|
||||
|
|
|
@ -9,6 +9,7 @@ on:
|
|||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- version-*
|
||||
|
||||
jobs:
|
||||
lint-prettier:
|
||||
|
|
|
@ -6,6 +6,7 @@ on:
|
|||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
# Needed to be able to push to the next branch
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
|
|
|
@ -7,6 +7,9 @@ on:
|
|||
jobs:
|
||||
build-server:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload contianer images to ghcr.io
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
|
@ -47,6 +50,9 @@ jobs:
|
|||
VERSION_FAMILY=${{ steps.ev.outputs.versionFamily }}
|
||||
build-outpost:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload contianer images to ghcr.io
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
@ -96,6 +102,9 @@ jobs:
|
|||
build-outpost-binary:
|
||||
timeout-minutes: 120
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload binaries to the release
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
|
|
@ -6,8 +6,8 @@ on:
|
|||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
# Needed to update issues and PRs
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from os import environ
|
||||
from typing import Optional
|
||||
|
||||
__version__ = "2023.8.3"
|
||||
__version__ = "2023.8.6"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
||||
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):
|
||||
"""test missing/invalid redirect URI"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
|
|
|
@ -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"
|
||||
|
||||
ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSENT, PROMPT_LOGIN}
|
||||
FORBIDDEN_URI_SCHEMES = {"javascript", "data", "vbscript"}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
|
@ -174,6 +175,10 @@ class OAuthAuthorizationParams:
|
|||
self.check_scope()
|
||||
self.check_nonce()
|
||||
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):
|
||||
"""Redirect URI validation."""
|
||||
|
@ -211,10 +216,9 @@ class OAuthAuthorizationParams:
|
|||
expected=allowed_redirect_urls,
|
||||
)
|
||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
||||
if self.request:
|
||||
raise AuthorizeError(
|
||||
self.redirect_uri, "request_not_supported", self.grant_type, self.state
|
||||
)
|
||||
# Check against forbidden schemes
|
||||
if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
|
||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
||||
|
||||
def check_scope(self):
|
||||
"""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 fullmatch
|
||||
from typing import Any, Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils import timezone
|
||||
|
@ -53,6 +54,7 @@ from authentik.providers.oauth2.models import (
|
|||
RefreshToken,
|
||||
)
|
||||
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.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||
|
||||
|
@ -204,6 +206,10 @@ class TokenParams:
|
|||
).from_http(request)
|
||||
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()
|
||||
if not self.authorization_code:
|
||||
LOGGER.warning("Code does not exist", code=raw_code)
|
||||
|
@ -221,7 +227,10 @@ class TokenParams:
|
|||
raise TokenError("invalid_grant")
|
||||
|
||||
# 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:
|
||||
new_code_challenge = (
|
||||
urlsafe_b64encode(sha256(self.code_verifier.encode("ascii")).digest())
|
||||
|
|
|
@ -171,6 +171,8 @@ class MetadataProcessor:
|
|||
entity_descriptor, f"{{{NS_SAML_METADATA}}}IDPSSODescriptor"
|
||||
)
|
||||
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()
|
||||
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.processors.metadata import MetadataProcessor
|
||||
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):
|
||||
|
@ -55,6 +55,24 @@ class TestServiceProviderMetadataParser(TestCase):
|
|||
schema = etree.XMLSchema(etree.parse("schemas/saml-schema-metadata-2.0.xsd")) # nosec
|
||||
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):
|
||||
"""Test simple metadata without Signing"""
|
||||
metadata = ServiceProviderMetadataParser().parse(load_fixture("fixtures/simple.xml"))
|
||||
|
|
|
@ -47,9 +47,11 @@ class FreeIPA(BaseLDAPSynchronizer):
|
|||
return
|
||||
# For some reason, nsaccountlock is not defined properly in the schema as bool
|
||||
# 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
|
||||
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:
|
||||
user.is_active = is_active
|
||||
user.save()
|
||||
|
|
|
@ -137,6 +137,7 @@ class LDAPSyncTests(TestCase):
|
|||
user_sync.sync_full()
|
||||
self.assertTrue(User.objects.filter(username="user0_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):
|
||||
"""Test group sync"""
|
||||
|
|
|
@ -85,6 +85,19 @@ entries:
|
|||
identifiers:
|
||||
name: default-oobe-password-usable
|
||||
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:
|
||||
fields:
|
||||
- !KeyOf prompt-field-header
|
||||
|
@ -129,6 +142,7 @@ entries:
|
|||
evaluate_on_plan: true
|
||||
invalid_response_action: retry
|
||||
re_evaluate_policies: false
|
||||
id: binding-login
|
||||
identifiers:
|
||||
order: 100
|
||||
stage: !KeyOf stage-default-authentication-login
|
||||
|
@ -144,3 +158,8 @@ entries:
|
|||
policy: !KeyOf policy-default-oobe-prefill-user
|
||||
target: !KeyOf binding-password-write
|
||||
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
|
||||
attrs:
|
||||
key: !Context token
|
||||
- model: authentik_blueprints.blueprintinstance
|
||||
identifiers:
|
||||
metadata:
|
||||
labels:
|
||||
blueprints.goauthentik.io/system-bootstrap: "true"
|
||||
state: absent
|
||||
|
|
|
@ -32,7 +32,7 @@ services:
|
|||
volumes:
|
||||
- redis:/data
|
||||
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
|
||||
command: server
|
||||
environment:
|
||||
|
@ -53,7 +53,7 @@ services:
|
|||
- postgresql
|
||||
- redis
|
||||
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
|
||||
command: worker
|
||||
environment:
|
||||
|
|
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
|||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2023.8.3"
|
||||
const VERSION = "2023.8.6"
|
||||
|
|
|
@ -113,7 +113,7 @@ filterwarnings = [
|
|||
|
||||
[tool.poetry]
|
||||
name = "authentik"
|
||||
version = "2023.8.3"
|
||||
version = "2023.8.6"
|
||||
description = ""
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2023.8.3
|
||||
version: 2023.8.6
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
|
|
|
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
|||
export const ERROR_CLASS = "pf-m-danger";
|
||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||
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 ROUTE_SEPARATOR = ";";
|
||||
|
||||
|
|
|
@ -155,6 +155,18 @@ image:
|
|||
- web: don't import entire SourceViewPage in flow and user interface (#6761)
|
||||
- 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
|
||||
|
||||
#### What's New
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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: [
|
||||
"security/policy",
|
||||
"security/CVE-2024-21637",
|
||||
"security/CVE-2023-48228",
|
||||
"security/GHSA-rjvp-29xq-f62w",
|
||||
"security/CVE-2023-39522",
|
||||
"security/CVE-2023-36456",
|
||||
"security/2023-06-cure53",
|
||||
|
|
Reference in New Issue