providers/oauth2: OpenID conformance (#4758)
* don't open inspector by default when debug is enabled Signed-off-by: Jens Langhammer <jens@goauthentik.io> * encode error in fragment when using hybrid grant_type Signed-off-by: Jens Langhammer <jens@goauthentik.io> * require nonce for all response_types that get an id_token from the authorization endpoint Signed-off-by: Jens Langhammer <jens@goauthentik.io> * don't set empty family_name Signed-off-by: Jens Langhammer <jens@goauthentik.io> * only set at_hash when response has token Signed-off-by: Jens Langhammer <jens@goauthentik.io> * cleaner way to get login time Signed-off-by: Jens Langhammer <jens@goauthentik.io> * remove authentication requirement from authentication flow Signed-off-by: Jens Langhammer <jens@goauthentik.io> * use wrapper Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix auth_time not being handled correctly Signed-off-by: Jens Langhammer <jens@goauthentik.io> * minor cleanup Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add test files Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * remove USER_LOGIN_AUTHENTICATED Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rework prompt=login handling Signed-off-by: Jens Langhammer <jens@goauthentik.io> * also set last login uid for max_age check to prevent double login when max_age and prompt=login is set Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
c6a14fa4f1
commit
80f4fccd35
|
@ -4,6 +4,7 @@ from base64 import b64encode
|
|||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
|
||||
from authentik.api.authentication import bearer_auth
|
||||
|
@ -68,6 +69,7 @@ class TestAPIAuth(TestCase):
|
|||
user=create_test_admin_user(),
|
||||
provider=provider,
|
||||
token=generate_id(),
|
||||
auth_time=timezone.now(),
|
||||
_scope=SCOPE_AUTHENTIK_API,
|
||||
_id_token=json.dumps({}),
|
||||
)
|
||||
|
@ -82,6 +84,7 @@ class TestAPIAuth(TestCase):
|
|||
user=create_test_admin_user(),
|
||||
provider=provider,
|
||||
token=generate_id(),
|
||||
auth_time=timezone.now(),
|
||||
_scope="",
|
||||
_id_token=json.dumps({}),
|
||||
)
|
||||
|
|
|
@ -37,7 +37,6 @@ from authentik.lib.utils.file import (
|
|||
from authentik.policies.api.exec import PolicyTestResultSerializer
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
from authentik.policies.types import PolicyResult
|
||||
from authentik.stages.user_login.stage import USER_LOGIN_AUTHENTICATED
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
@ -186,10 +185,6 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
|||
if superuser_full_list and request.user.is_superuser:
|
||||
return super().list(request)
|
||||
|
||||
# To prevent the user from having to double login when prompt is set to login
|
||||
# and the user has just signed it. This session variable is set in the UserLoginStage
|
||||
# and is (quite hackily) removed from the session in applications's API's List method
|
||||
self.request.session.pop(USER_LOGIN_AUTHENTICATED, None)
|
||||
queryset = self._filter_queryset_for_list(self.get_queryset())
|
||||
self.paginate_queryset(queryset)
|
||||
|
||||
|
|
|
@ -231,7 +231,7 @@ class AccessDeniedChallengeView(ChallengeStageView):
|
|||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||
return AccessDeniedChallenge(
|
||||
data={
|
||||
"error_message": self.error_message or "Unknown error",
|
||||
"error_message": str(self.error_message or "Unknown error"),
|
||||
"type": ChallengeTypes.NATIVE.value,
|
||||
"component": "ak-stage-access-denied",
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""OAuth2Provider API Views"""
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField
|
||||
|
@ -153,6 +154,7 @@ class OAuth2ProviderViewSet(UsedByMixin, ModelViewSet):
|
|||
user=request.user,
|
||||
provider=provider,
|
||||
_scope=" ".join(scope_names),
|
||||
auth_time=timezone.now(),
|
||||
),
|
||||
request,
|
||||
)
|
||||
|
|
|
@ -174,10 +174,12 @@ class AuthorizeError(OAuth2Error):
|
|||
|
||||
# See:
|
||||
# http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthError
|
||||
hash_or_question = "#" if self.grant_type == GrantTypes.IMPLICIT else "?"
|
||||
fragment_or_query = (
|
||||
"#" if self.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID] else "?"
|
||||
)
|
||||
|
||||
uri = (
|
||||
f"{self.redirect_uri}{hash_or_question}error="
|
||||
f"{self.redirect_uri}{fragment_or_query}error="
|
||||
f"{self.error}&error_description={description}"
|
||||
)
|
||||
|
||||
|
|
|
@ -110,12 +110,11 @@ class IDToken:
|
|||
# Convert datetimes into timestamps.
|
||||
now = timezone.now()
|
||||
id_token.iat = int(now.timestamp())
|
||||
id_token.auth_time = int(token.auth_time.timestamp())
|
||||
|
||||
# We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
|
||||
auth_event = get_login_event(request)
|
||||
if auth_event:
|
||||
auth_time = auth_event.created
|
||||
id_token.auth_time = int(auth_time.timestamp())
|
||||
# Also check which method was used for authentication
|
||||
method = auth_event.context.get(PLAN_CONTEXT_METHOD, "")
|
||||
method_args = auth_event.context.get(PLAN_CONTEXT_METHOD_ARGS, {})
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
# Generated by Django 4.1.7 on 2023-02-22 22:23
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("authentik_providers_oauth2", "0014_alter_refreshtoken_options_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="accesstoken",
|
||||
name="auth_time",
|
||||
field=models.DateTimeField(
|
||||
default=django.utils.timezone.now,
|
||||
verbose_name="Authentication time",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="authorizationcode",
|
||||
name="auth_time",
|
||||
field=models.DateTimeField(
|
||||
default=django.utils.timezone.now,
|
||||
verbose_name="Authentication time",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="refreshtoken",
|
||||
name="auth_time",
|
||||
field=models.DateTimeField(
|
||||
default=django.utils.timezone.now,
|
||||
verbose_name="Authentication time",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -226,7 +226,7 @@ class OAuth2Provider(Provider):
|
|||
def get_issuer(self, request: HttpRequest) -> Optional[str]:
|
||||
"""Get issuer, based on request"""
|
||||
if self.issuer_mode == IssuerMode.GLOBAL:
|
||||
return request.build_absolute_uri("/")
|
||||
return request.build_absolute_uri(reverse("authentik_core:root-redirect"))
|
||||
try:
|
||||
url = reverse(
|
||||
"authentik_providers_oauth2:provider-root",
|
||||
|
@ -282,6 +282,7 @@ class BaseGrantModel(models.Model):
|
|||
user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE)
|
||||
revoked = models.BooleanField(default=False)
|
||||
_scope = models.TextField(default="", verbose_name=_("Scopes"))
|
||||
auth_time = models.DateTimeField(verbose_name="Authentication time")
|
||||
|
||||
@property
|
||||
def scope(self) -> list[str]:
|
||||
|
|
|
@ -204,6 +204,7 @@ class TestAuthorize(OAuthTestCase):
|
|||
"redirect_uri": "http://local.invalid/Foo",
|
||||
"scope": "openid",
|
||||
"state": "foo",
|
||||
"nonce": generate_id(),
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
|
@ -325,6 +326,7 @@ class TestAuthorize(OAuthTestCase):
|
|||
"state": state,
|
||||
"scope": "openid",
|
||||
"redirect_uri": "http://localhost",
|
||||
"nonce": generate_id(),
|
||||
},
|
||||
)
|
||||
response = self.client.get(
|
||||
|
@ -378,6 +380,7 @@ class TestAuthorize(OAuthTestCase):
|
|||
"state": state,
|
||||
"scope": "openid",
|
||||
"redirect_uri": "http://localhost",
|
||||
"nonce": generate_id(),
|
||||
},
|
||||
)
|
||||
response = self.client.get(
|
||||
|
|
|
@ -4,6 +4,7 @@ from base64 import b64encode
|
|||
from dataclasses import asdict
|
||||
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||
|
@ -41,6 +42,7 @@ class TesOAuth2Introspection(OAuthTestCase):
|
|||
provider=self.provider,
|
||||
user=self.user,
|
||||
token=generate_id(),
|
||||
auth_time=timezone.now(),
|
||||
_scope="openid user profile",
|
||||
_id_token=json.dumps(
|
||||
asdict(
|
||||
|
@ -72,6 +74,7 @@ class TesOAuth2Introspection(OAuthTestCase):
|
|||
provider=self.provider,
|
||||
user=self.user,
|
||||
token=generate_id(),
|
||||
auth_time=timezone.now(),
|
||||
_scope="openid user profile",
|
||||
_id_token=json.dumps(
|
||||
asdict(
|
||||
|
|
|
@ -4,6 +4,7 @@ from base64 import b64encode
|
|||
from dataclasses import asdict
|
||||
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||
|
@ -40,6 +41,7 @@ class TesOAuth2Revoke(OAuthTestCase):
|
|||
provider=self.provider,
|
||||
user=self.user,
|
||||
token=generate_id(),
|
||||
auth_time=timezone.now(),
|
||||
_scope="openid user profile",
|
||||
_id_token=json.dumps(
|
||||
asdict(
|
||||
|
@ -62,6 +64,7 @@ class TesOAuth2Revoke(OAuthTestCase):
|
|||
provider=self.provider,
|
||||
user=self.user,
|
||||
token=generate_id(),
|
||||
auth_time=timezone.now(),
|
||||
_scope="openid user profile",
|
||||
_id_token=json.dumps(
|
||||
asdict(
|
||||
|
|
|
@ -4,6 +4,7 @@ from json import dumps
|
|||
|
||||
from django.test import RequestFactory
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
|
@ -45,7 +46,9 @@ class TestToken(OAuthTestCase):
|
|||
)
|
||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||
user = create_test_admin_user()
|
||||
code = AuthorizationCode.objects.create(code="foobar", provider=provider, user=user)
|
||||
code = AuthorizationCode.objects.create(
|
||||
code="foobar", provider=provider, user=user, auth_time=timezone.now()
|
||||
)
|
||||
request = self.factory.post(
|
||||
"/",
|
||||
data={
|
||||
|
@ -99,6 +102,7 @@ class TestToken(OAuthTestCase):
|
|||
provider=provider,
|
||||
user=user,
|
||||
token=generate_id(),
|
||||
auth_time=timezone.now(),
|
||||
)
|
||||
request = self.factory.post(
|
||||
"/",
|
||||
|
@ -127,7 +131,9 @@ class TestToken(OAuthTestCase):
|
|||
self.app.save()
|
||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||
user = create_test_admin_user()
|
||||
code = AuthorizationCode.objects.create(code="foobar", provider=provider, user=user)
|
||||
code = AuthorizationCode.objects.create(
|
||||
code="foobar", provider=provider, user=user, auth_time=timezone.now()
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
data={
|
||||
|
@ -173,6 +179,7 @@ class TestToken(OAuthTestCase):
|
|||
user=user,
|
||||
token=generate_id(),
|
||||
_id_token=dumps({}),
|
||||
auth_time=timezone.now(),
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
|
@ -221,6 +228,7 @@ class TestToken(OAuthTestCase):
|
|||
user=user,
|
||||
token=generate_id(),
|
||||
_id_token=dumps({}),
|
||||
auth_time=timezone.now(),
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
|
@ -271,6 +279,7 @@ class TestToken(OAuthTestCase):
|
|||
user=user,
|
||||
token=generate_id(),
|
||||
_id_token=dumps({}),
|
||||
auth_time=timezone.now(),
|
||||
)
|
||||
# Create initial refresh token
|
||||
response = self.client.post(
|
||||
|
|
|
@ -3,6 +3,7 @@ import json
|
|||
from dataclasses import asdict
|
||||
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import Application
|
||||
|
@ -37,6 +38,7 @@ class TestUserinfo(OAuthTestCase):
|
|||
provider=self.provider,
|
||||
user=self.user,
|
||||
token=generate_id(),
|
||||
auth_time=timezone.now(),
|
||||
_scope="openid user profile",
|
||||
_id_token=json.dumps(
|
||||
asdict(
|
||||
|
@ -56,7 +58,6 @@ class TestUserinfo(OAuthTestCase):
|
|||
{
|
||||
"name": self.user.name,
|
||||
"given_name": self.user.name,
|
||||
"family_name": "",
|
||||
"preferred_username": self.user.name,
|
||||
"nickname": self.user.name,
|
||||
"groups": [group.name for group in self.user.ak_groups.all()],
|
||||
|
@ -79,7 +80,6 @@ class TestUserinfo(OAuthTestCase):
|
|||
{
|
||||
"name": self.user.name,
|
||||
"given_name": self.user.name,
|
||||
"family_name": "",
|
||||
"preferred_username": self.user.name,
|
||||
"nickname": self.user.name,
|
||||
"groups": [group.name for group in self.user.ak_groups.all()],
|
||||
|
|
|
@ -17,7 +17,7 @@ from structlog.stdlib import get_logger
|
|||
|
||||
from authentik.core.models import Application
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.utils import get_user
|
||||
from authentik.events.signals import get_login_event
|
||||
from authentik.flows.challenge import (
|
||||
PLAN_CONTEXT_TITLE,
|
||||
AutosubmitChallenge,
|
||||
|
@ -64,12 +64,11 @@ from authentik.stages.consent.stage import (
|
|||
PLAN_CONTEXT_CONSENT_PERMISSIONS,
|
||||
ConsentStageView,
|
||||
)
|
||||
from authentik.stages.user_login.stage import USER_LOGIN_AUTHENTICATED
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
PLAN_CONTEXT_PARAMS = "params"
|
||||
SESSION_KEY_NEEDS_LOGIN = "authentik/providers/oauth2/needs_login"
|
||||
SESSION_KEY_LAST_LOGIN_UID = "authentik/providers/oauth2/last_login_uid"
|
||||
|
||||
ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSENT, PROMPT_LOGIN}
|
||||
|
||||
|
@ -235,19 +234,22 @@ class OAuthAuthorizationParams:
|
|||
|
||||
def check_nonce(self):
|
||||
"""Nonce parameter validation."""
|
||||
# https://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation
|
||||
# Nonce is only required for Implicit flows
|
||||
if self.grant_type != GrantTypes.IMPLICIT:
|
||||
# nonce is required for all flows that return an id_token from the authorization endpoint,
|
||||
# see https://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthRequest or
|
||||
# https://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken and
|
||||
# https://bitbucket.org/openid/connect/issues/972/nonce-requirement-in-hybrid-auth-request
|
||||
if self.response_type not in [
|
||||
ResponseTypes.ID_TOKEN,
|
||||
ResponseTypes.ID_TOKEN_TOKEN,
|
||||
ResponseTypes.CODE_ID_TOKEN,
|
||||
ResponseTypes.CODE_ID_TOKEN_TOKEN,
|
||||
]:
|
||||
return
|
||||
if SCOPE_OPENID not in self.scope:
|
||||
return
|
||||
if not self.nonce:
|
||||
self.nonce = self.state
|
||||
LOGGER.warning("Using state as nonce for OpenID Request")
|
||||
if not self.nonce:
|
||||
if SCOPE_OPENID in self.scope:
|
||||
LOGGER.warning("Missing nonce for OpenID Request")
|
||||
raise AuthorizeError(
|
||||
self.redirect_uri, "invalid_request", self.grant_type, self.state
|
||||
)
|
||||
raise AuthorizeError(self.redirect_uri, "invalid_request", self.grant_type, self.state)
|
||||
|
||||
def check_code_challenge(self):
|
||||
"""PKCE validation of the transformation method."""
|
||||
|
@ -262,19 +264,24 @@ class OAuthAuthorizationParams:
|
|||
|
||||
def create_code(self, request: HttpRequest) -> AuthorizationCode:
|
||||
"""Create an AuthorizationCode object for the request"""
|
||||
code = AuthorizationCode()
|
||||
code.user = request.user
|
||||
code.provider = self.provider
|
||||
auth_event = get_login_event(request)
|
||||
|
||||
code.code = uuid4().hex
|
||||
now = timezone.now()
|
||||
|
||||
code = AuthorizationCode(
|
||||
user=request.user,
|
||||
provider=self.provider,
|
||||
auth_time=auth_event.created if auth_event else now,
|
||||
code=uuid4().hex,
|
||||
expires=now + timedelta_from_string(self.provider.access_code_validity),
|
||||
scope=self.scope,
|
||||
nonce=self.nonce,
|
||||
)
|
||||
|
||||
if self.code_challenge and self.code_challenge_method:
|
||||
code.code_challenge = self.code_challenge
|
||||
code.code_challenge_method = self.code_challenge_method
|
||||
|
||||
code.expires = timezone.now() + timedelta_from_string(self.provider.access_code_validity)
|
||||
code.scope = self.scope
|
||||
code.nonce = self.nonce
|
||||
return code
|
||||
|
||||
|
||||
|
@ -309,7 +316,6 @@ class AuthorizationFlowInitView(PolicyAccessView):
|
|||
self.params.grant_type,
|
||||
self.params.state,
|
||||
)
|
||||
error.to_event(redirect_uri=error.redirect_uri).from_http(self.request)
|
||||
raise RequestValidationError(error.get_response(self.request))
|
||||
|
||||
def resolve_provider_application(self):
|
||||
|
@ -329,27 +335,39 @@ class AuthorizationFlowInitView(PolicyAccessView):
|
|||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""Start FlowPLanner, return to flow executor shell"""
|
||||
# Require a login event to be set, otherwise make the user re-login
|
||||
login_event = get_login_event(request)
|
||||
if not login_event:
|
||||
LOGGER.warning("request with no login event")
|
||||
return self.handle_no_permission()
|
||||
login_uid = str(login_event.pk)
|
||||
# After we've checked permissions, and the user has access, check if we need
|
||||
# to re-authenticate the user
|
||||
if self.params.max_age:
|
||||
current_age: timedelta = (
|
||||
timezone.now()
|
||||
- Event.objects.filter(action=EventAction.LOGIN, user=get_user(self.request.user))
|
||||
.latest("created")
|
||||
.created
|
||||
)
|
||||
# Attempt to check via the session's login event if set, otherwise we can't
|
||||
# check
|
||||
login_time = login_event.created
|
||||
current_age: timedelta = timezone.now() - login_time
|
||||
if current_age.total_seconds() > self.params.max_age:
|
||||
LOGGER.debug(
|
||||
"Triggering authentication as max_age requirement",
|
||||
max_age=self.params.max_age,
|
||||
ago=int(current_age.total_seconds()),
|
||||
)
|
||||
# Since we already need to re-authenticate the user, set the old login UID
|
||||
# in case this request has both max_age and prompt=login
|
||||
self.request.session[SESSION_KEY_LAST_LOGIN_UID] = login_uid
|
||||
return self.handle_no_permission()
|
||||
# If prompt=login, we need to re-authenticate the user regardless
|
||||
# Check if we're not already doing the re-authentication
|
||||
if PROMPT_LOGIN in self.params.prompt:
|
||||
# No previous login UID saved, so save the current uid and trigger
|
||||
# re-login, or previous login UID matches current one, so no re-login happened yet
|
||||
if (
|
||||
PROMPT_LOGIN in self.params.prompt
|
||||
and SESSION_KEY_NEEDS_LOGIN not in self.request.session
|
||||
# To prevent the user from having to double login when prompt is set to login
|
||||
# and the user has just signed it. This session variable is set in the UserLoginStage
|
||||
# and is (quite hackily) removed from the session in applications's API's List method
|
||||
and USER_LOGIN_AUTHENTICATED not in self.request.session
|
||||
SESSION_KEY_LAST_LOGIN_UID not in self.request.session
|
||||
or login_uid == self.request.session[SESSION_KEY_LAST_LOGIN_UID]
|
||||
):
|
||||
self.request.session[SESSION_KEY_NEEDS_LOGIN] = True
|
||||
self.request.session[SESSION_KEY_LAST_LOGIN_UID] = login_uid
|
||||
return self.handle_no_permission()
|
||||
scope_descriptions = UserInfoView().get_scope_descriptions(self.params.scope)
|
||||
# Regardless, we start the planner and return to it
|
||||
|
@ -525,6 +543,7 @@ class OAuthFulfillmentStage(StageView):
|
|||
def create_implicit_response(self, code: Optional[AuthorizationCode]) -> dict:
|
||||
"""Create implicit response's URL Fragment dictionary"""
|
||||
query_fragment = {}
|
||||
auth_event = get_login_event(self.request)
|
||||
|
||||
now = timezone.now()
|
||||
access_token_expiry = now + timedelta_from_string(self.provider.access_token_validity)
|
||||
|
@ -533,6 +552,7 @@ class OAuthFulfillmentStage(StageView):
|
|||
scope=self.params.scope,
|
||||
expires=access_token_expiry,
|
||||
provider=self.provider,
|
||||
auth_time=auth_event.created if auth_event else now,
|
||||
)
|
||||
|
||||
id_token = IDToken.new(self.provider, token, self.request)
|
||||
|
@ -553,6 +573,8 @@ class OAuthFulfillmentStage(StageView):
|
|||
ResponseTypes.CODE_TOKEN,
|
||||
]:
|
||||
query_fragment["access_token"] = token.token
|
||||
# Get at_hash of the current token and update the id_token
|
||||
id_token.at_hash = token.at_hash
|
||||
|
||||
# Check if response_type must include id_token in the response.
|
||||
if self.params.response_type in [
|
||||
|
@ -561,8 +583,6 @@ class OAuthFulfillmentStage(StageView):
|
|||
ResponseTypes.CODE_ID_TOKEN,
|
||||
ResponseTypes.CODE_ID_TOKEN_TOKEN,
|
||||
]:
|
||||
# Get at_hash of the current token and update the id_token
|
||||
id_token.at_hash = token.at_hash
|
||||
query_fragment["id_token"] = self.provider.encode(id_token.to_dict())
|
||||
token._id_token = dumps(id_token.to_dict())
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ from authentik.core.models import (
|
|||
User,
|
||||
)
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.signals import get_login_event
|
||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
|
@ -479,6 +480,7 @@ class TokenView(View):
|
|||
expires=access_token_expiry,
|
||||
# Keep same scopes as previous token
|
||||
scope=self.params.authorization_code.scope,
|
||||
auth_time=self.params.authorization_code.auth_time,
|
||||
)
|
||||
access_token.id_token = IDToken.new(
|
||||
self.provider,
|
||||
|
@ -493,6 +495,7 @@ class TokenView(View):
|
|||
scope=self.params.authorization_code.scope,
|
||||
expires=refresh_token_expiry,
|
||||
provider=self.provider,
|
||||
auth_time=self.params.authorization_code.auth_time,
|
||||
)
|
||||
id_token = IDToken.new(
|
||||
self.provider,
|
||||
|
@ -521,7 +524,6 @@ class TokenView(View):
|
|||
unauthorized_scopes = set(self.params.scope) - set(self.params.refresh_token.scope)
|
||||
if unauthorized_scopes:
|
||||
raise TokenError("invalid_scope")
|
||||
|
||||
now = timezone.now()
|
||||
access_token_expiry = now + timedelta_from_string(self.provider.access_token_validity)
|
||||
access_token = AccessToken(
|
||||
|
@ -530,6 +532,7 @@ class TokenView(View):
|
|||
expires=access_token_expiry,
|
||||
# Keep same scopes as previous token
|
||||
scope=self.params.refresh_token.scope,
|
||||
auth_time=self.params.refresh_token.auth_time,
|
||||
)
|
||||
access_token.id_token = IDToken.new(
|
||||
self.provider,
|
||||
|
@ -544,6 +547,7 @@ class TokenView(View):
|
|||
scope=self.params.refresh_token.scope,
|
||||
expires=refresh_token_expiry,
|
||||
provider=self.provider,
|
||||
auth_time=self.params.refresh_token.auth_time,
|
||||
)
|
||||
id_token = IDToken.new(
|
||||
self.provider,
|
||||
|
@ -578,6 +582,7 @@ class TokenView(View):
|
|||
user=self.params.user,
|
||||
expires=access_token_expiry,
|
||||
scope=self.params.scope,
|
||||
auth_time=now,
|
||||
)
|
||||
access_token.id_token = IDToken.new(
|
||||
self.provider,
|
||||
|
@ -600,11 +605,13 @@ class TokenView(View):
|
|||
raise DeviceCodeError("authorization_pending")
|
||||
now = timezone.now()
|
||||
access_token_expiry = now + timedelta_from_string(self.provider.access_token_validity)
|
||||
auth_event = get_login_event(self.request)
|
||||
access_token = AccessToken(
|
||||
provider=self.provider,
|
||||
user=self.params.device_code.user,
|
||||
expires=access_token_expiry,
|
||||
scope=self.params.device_code.scope,
|
||||
auth_time=auth_event.created if auth_event else now,
|
||||
)
|
||||
access_token.id_token = IDToken.new(
|
||||
self.provider,
|
||||
|
@ -619,6 +626,7 @@ class TokenView(View):
|
|||
scope=self.params.device_code.scope,
|
||||
expires=refresh_token_expiry,
|
||||
provider=self.provider,
|
||||
auth_time=auth_event.created if auth_event else now,
|
||||
)
|
||||
id_token = IDToken.new(
|
||||
self.provider,
|
||||
|
|
|
@ -11,8 +11,6 @@ from authentik.lib.utils.time import timedelta_from_string
|
|||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
|
||||
USER_LOGIN_AUTHENTICATED = "user_login_authenticated"
|
||||
|
||||
|
||||
class UserLoginStageView(StageView):
|
||||
"""Finalise Authentication flow by logging the user in"""
|
||||
|
@ -51,7 +49,6 @@ class UserLoginStageView(StageView):
|
|||
flow_slug=self.executor.flow.slug,
|
||||
session_duration=self.executor.current_stage.session_duration,
|
||||
)
|
||||
self.request.session[USER_LOGIN_AUTHENTICATED] = True
|
||||
# Only show success message if we don't have a source in the flow
|
||||
# as sources show their own success messages
|
||||
if not self.executor.plan.context.get(PLAN_CONTEXT_SOURCE, None):
|
||||
|
|
|
@ -11,7 +11,7 @@ entries:
|
|||
designation: authentication
|
||||
name: Welcome to authentik!
|
||||
title: Welcome to authentik!
|
||||
authentication: require_unauthenticated
|
||||
authentication: none
|
||||
identifiers:
|
||||
slug: default-authentication-flow
|
||||
model: authentik_flows.flow
|
||||
|
|
|
@ -40,7 +40,6 @@ entries:
|
|||
# You can override this behaviour in custom mappings, i.e. `request.user.name.split(" ")`
|
||||
"name": request.user.name,
|
||||
"given_name": request.user.name,
|
||||
"family_name": "",
|
||||
"preferred_username": request.user.username,
|
||||
"nickname": request.user.username,
|
||||
# groups is not part of the official userinfo schema, but is a quasi-standard
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
# #Test files for OpenID Conformance testing.
|
||||
|
||||
These config files assume testing is being done using the [OpenID Conformance Suite
|
||||
](https://openid.net/certification/about-conformance-suite/), locally.
|
||||
|
||||
See https://gitlab.com/openid/conformance-suite/-/wikis/Developers/Build-&-Run for running the conformance suite locally.
|
||||
|
||||
Requires docker containers to be able to access the host via `host.docker.internal` and an entry in the hosts file that maps `host.docker.internal` to localhost.
|
|
@ -0,0 +1,81 @@
|
|||
version: 1
|
||||
metadata:
|
||||
name: OIDC conformance testing
|
||||
entries:
|
||||
- identifiers:
|
||||
managed: goauthentik.io/providers/oauth2/scope-address
|
||||
model: authentik_providers_oauth2.scopemapping
|
||||
attrs:
|
||||
name: "authentik default OAuth Mapping: OpenID 'address'"
|
||||
scope_name: address
|
||||
description: "General Address Information"
|
||||
expression: |
|
||||
return {
|
||||
"address": {
|
||||
"formatted": "foo",
|
||||
}
|
||||
}
|
||||
- identifiers:
|
||||
managed: goauthentik.io/providers/oauth2/scope-phone
|
||||
model: authentik_providers_oauth2.scopemapping
|
||||
attrs:
|
||||
name: "authentik default OAuth Mapping: OpenID 'phone'"
|
||||
scope_name: phone
|
||||
description: "General phone Information"
|
||||
expression: |
|
||||
return {
|
||||
"phone_number": "+1234",
|
||||
"phone_number_verified": True,
|
||||
}
|
||||
|
||||
- model: authentik_providers_oauth2.oauth2provider
|
||||
id: provider
|
||||
identifiers:
|
||||
name: provider
|
||||
attrs:
|
||||
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
|
||||
issuer_mode: global
|
||||
client_id: 4054d882aff59755f2f279968b97ce8806a926e1
|
||||
client_secret: 4c7e4933009437fb486b5389d15b173109a0555dc47e0cc0949104f1925bcc6565351cb1dffd7e6818cf074f5bd50c210b565121a7328ee8bd40107fc4bbd867
|
||||
redirect_uris: |
|
||||
https://localhost:8443/test/a/authentik/callback
|
||||
https://localhost.emobix.co.uk:8443/test/a/authentik/callback
|
||||
property_mappings:
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]]
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]]
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile]]
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-address]]
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-phone]]
|
||||
signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]]
|
||||
- model: authentik_core.application
|
||||
identifiers:
|
||||
slug: conformance
|
||||
attrs:
|
||||
provider: !KeyOf provider
|
||||
name: Conformance
|
||||
|
||||
- model: authentik_providers_oauth2.oauth2provider
|
||||
id: oidc-conformance-2
|
||||
identifiers:
|
||||
name: oidc-conformance-2
|
||||
attrs:
|
||||
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
|
||||
issuer_mode: global
|
||||
client_id: ad64aeaf1efe388ecf4d28fcc537e8de08bcae26
|
||||
client_secret: ff2e34a5b04c99acaf7241e25a950e7f6134c86936923d8c698d8f38bd57647750d661069612c0ee55045e29fe06aa101804bdae38e8360647d595e771fea789
|
||||
redirect_uris: |
|
||||
https://localhost:8443/test/a/authentik/callback
|
||||
https://localhost.emobix.co.uk:8443/test/a/authentik/callback
|
||||
property_mappings:
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]]
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]]
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile]]
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-address]]
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-phone]]
|
||||
signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]]
|
||||
- model: authentik_core.application
|
||||
identifiers:
|
||||
slug: oidc-conformance-2
|
||||
attrs:
|
||||
provider: !KeyOf oidc-conformance-2
|
||||
name: OIDC Conformance
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"alias": "authentik",
|
||||
"description": "authentik",
|
||||
"server": {
|
||||
"discoveryUrl": "http://host.docker.internal:9000/application/o/conformance/.well-known/openid-configuration"
|
||||
},
|
||||
"client": {
|
||||
"client_id": "4054d882aff59755f2f279968b97ce8806a926e1",
|
||||
"client_secret": "4c7e4933009437fb486b5389d15b173109a0555dc47e0cc0949104f1925bcc6565351cb1dffd7e6818cf074f5bd50c210b565121a7328ee8bd40107fc4bbd867"
|
||||
},
|
||||
"client_secret_post": {
|
||||
"client_id": "4054d882aff59755f2f279968b97ce8806a926e1",
|
||||
"client_secret": "4c7e4933009437fb486b5389d15b173109a0555dc47e0cc0949104f1925bcc6565351cb1dffd7e6818cf074f5bd50c210b565121a7328ee8bd40107fc4bbd867"
|
||||
},
|
||||
"client2": {
|
||||
"client_id": "ad64aeaf1efe388ecf4d28fcc537e8de08bcae26",
|
||||
"client_secret": "ff2e34a5b04c99acaf7241e25a950e7f6134c86936923d8c698d8f38bd57647750d661069612c0ee55045e29fe06aa101804bdae38e8360647d595e771fea789"
|
||||
},
|
||||
"consent": {}
|
||||
}
|
|
@ -41,7 +41,6 @@ import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
|||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import {
|
||||
CapabilitiesEnum,
|
||||
ChallengeChoices,
|
||||
ChallengeTypes,
|
||||
ContextualFlowInfo,
|
||||
|
@ -97,7 +96,7 @@ export class FlowExecutor extends AKElement implements StageHost {
|
|||
tenant!: CurrentTenant;
|
||||
|
||||
@state()
|
||||
inspectorOpen: boolean;
|
||||
inspectorOpen = false;
|
||||
|
||||
_flowInfo?: ContextualFlowInfo;
|
||||
|
||||
|
@ -177,8 +176,6 @@ export class FlowExecutor extends AKElement implements StageHost {
|
|||
super();
|
||||
this.ws = new WebsocketClient();
|
||||
this.flowSlug = window.location.pathname.split("/")[3];
|
||||
this.inspectorOpen =
|
||||
globalAK()?.config.capabilities.includes(CapabilitiesEnum.Debug) || false;
|
||||
if (window.location.search.includes("inspector")) {
|
||||
this.inspectorOpen = !this.inspectorOpen;
|
||||
}
|
||||
|
|
Reference in New Issue