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.conf import settings
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.utils import timezone
|
||||||
from rest_framework.exceptions import AuthenticationFailed
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
|
|
||||||
from authentik.api.authentication import bearer_auth
|
from authentik.api.authentication import bearer_auth
|
||||||
|
@ -68,6 +69,7 @@ class TestAPIAuth(TestCase):
|
||||||
user=create_test_admin_user(),
|
user=create_test_admin_user(),
|
||||||
provider=provider,
|
provider=provider,
|
||||||
token=generate_id(),
|
token=generate_id(),
|
||||||
|
auth_time=timezone.now(),
|
||||||
_scope=SCOPE_AUTHENTIK_API,
|
_scope=SCOPE_AUTHENTIK_API,
|
||||||
_id_token=json.dumps({}),
|
_id_token=json.dumps({}),
|
||||||
)
|
)
|
||||||
|
@ -82,6 +84,7 @@ class TestAPIAuth(TestCase):
|
||||||
user=create_test_admin_user(),
|
user=create_test_admin_user(),
|
||||||
provider=provider,
|
provider=provider,
|
||||||
token=generate_id(),
|
token=generate_id(),
|
||||||
|
auth_time=timezone.now(),
|
||||||
_scope="",
|
_scope="",
|
||||||
_id_token=json.dumps({}),
|
_id_token=json.dumps({}),
|
||||||
)
|
)
|
||||||
|
|
|
@ -37,7 +37,6 @@ from authentik.lib.utils.file import (
|
||||||
from authentik.policies.api.exec import PolicyTestResultSerializer
|
from authentik.policies.api.exec import PolicyTestResultSerializer
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
from authentik.policies.types import PolicyResult
|
from authentik.policies.types import PolicyResult
|
||||||
from authentik.stages.user_login.stage import USER_LOGIN_AUTHENTICATED
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
@ -186,10 +185,6 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||||
if superuser_full_list and request.user.is_superuser:
|
if superuser_full_list and request.user.is_superuser:
|
||||||
return super().list(request)
|
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())
|
queryset = self._filter_queryset_for_list(self.get_queryset())
|
||||||
self.paginate_queryset(queryset)
|
self.paginate_queryset(queryset)
|
||||||
|
|
||||||
|
|
|
@ -231,7 +231,7 @@ class AccessDeniedChallengeView(ChallengeStageView):
|
||||||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||||
return AccessDeniedChallenge(
|
return AccessDeniedChallenge(
|
||||||
data={
|
data={
|
||||||
"error_message": self.error_message or "Unknown error",
|
"error_message": str(self.error_message or "Unknown error"),
|
||||||
"type": ChallengeTypes.NATIVE.value,
|
"type": ChallengeTypes.NATIVE.value,
|
||||||
"component": "ak-stage-access-denied",
|
"component": "ak-stage-access-denied",
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""OAuth2Provider API Views"""
|
"""OAuth2Provider API Views"""
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import CharField
|
from rest_framework.fields import CharField
|
||||||
|
@ -153,6 +154,7 @@ class OAuth2ProviderViewSet(UsedByMixin, ModelViewSet):
|
||||||
user=request.user,
|
user=request.user,
|
||||||
provider=provider,
|
provider=provider,
|
||||||
_scope=" ".join(scope_names),
|
_scope=" ".join(scope_names),
|
||||||
|
auth_time=timezone.now(),
|
||||||
),
|
),
|
||||||
request,
|
request,
|
||||||
)
|
)
|
||||||
|
|
|
@ -174,10 +174,12 @@ class AuthorizeError(OAuth2Error):
|
||||||
|
|
||||||
# See:
|
# See:
|
||||||
# http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthError
|
# 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 = (
|
uri = (
|
||||||
f"{self.redirect_uri}{hash_or_question}error="
|
f"{self.redirect_uri}{fragment_or_query}error="
|
||||||
f"{self.error}&error_description={description}"
|
f"{self.error}&error_description={description}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -110,12 +110,11 @@ class IDToken:
|
||||||
# Convert datetimes into timestamps.
|
# Convert datetimes into timestamps.
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
id_token.iat = int(now.timestamp())
|
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
|
# We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
|
||||||
auth_event = get_login_event(request)
|
auth_event = get_login_event(request)
|
||||||
if auth_event:
|
if auth_event:
|
||||||
auth_time = auth_event.created
|
|
||||||
id_token.auth_time = int(auth_time.timestamp())
|
|
||||||
# Also check which method was used for authentication
|
# Also check which method was used for authentication
|
||||||
method = auth_event.context.get(PLAN_CONTEXT_METHOD, "")
|
method = auth_event.context.get(PLAN_CONTEXT_METHOD, "")
|
||||||
method_args = auth_event.context.get(PLAN_CONTEXT_METHOD_ARGS, {})
|
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]:
|
def get_issuer(self, request: HttpRequest) -> Optional[str]:
|
||||||
"""Get issuer, based on request"""
|
"""Get issuer, based on request"""
|
||||||
if self.issuer_mode == IssuerMode.GLOBAL:
|
if self.issuer_mode == IssuerMode.GLOBAL:
|
||||||
return request.build_absolute_uri("/")
|
return request.build_absolute_uri(reverse("authentik_core:root-redirect"))
|
||||||
try:
|
try:
|
||||||
url = reverse(
|
url = reverse(
|
||||||
"authentik_providers_oauth2:provider-root",
|
"authentik_providers_oauth2:provider-root",
|
||||||
|
@ -282,6 +282,7 @@ class BaseGrantModel(models.Model):
|
||||||
user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE)
|
user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE)
|
||||||
revoked = models.BooleanField(default=False)
|
revoked = models.BooleanField(default=False)
|
||||||
_scope = models.TextField(default="", verbose_name=_("Scopes"))
|
_scope = models.TextField(default="", verbose_name=_("Scopes"))
|
||||||
|
auth_time = models.DateTimeField(verbose_name="Authentication time")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scope(self) -> list[str]:
|
def scope(self) -> list[str]:
|
||||||
|
|
|
@ -204,6 +204,7 @@ class TestAuthorize(OAuthTestCase):
|
||||||
"redirect_uri": "http://local.invalid/Foo",
|
"redirect_uri": "http://local.invalid/Foo",
|
||||||
"scope": "openid",
|
"scope": "openid",
|
||||||
"state": "foo",
|
"state": "foo",
|
||||||
|
"nonce": generate_id(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
@ -325,6 +326,7 @@ class TestAuthorize(OAuthTestCase):
|
||||||
"state": state,
|
"state": state,
|
||||||
"scope": "openid",
|
"scope": "openid",
|
||||||
"redirect_uri": "http://localhost",
|
"redirect_uri": "http://localhost",
|
||||||
|
"nonce": generate_id(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
|
@ -378,6 +380,7 @@ class TestAuthorize(OAuthTestCase):
|
||||||
"state": state,
|
"state": state,
|
||||||
"scope": "openid",
|
"scope": "openid",
|
||||||
"redirect_uri": "http://localhost",
|
"redirect_uri": "http://localhost",
|
||||||
|
"nonce": generate_id(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
|
|
|
@ -4,6 +4,7 @@ from base64 import b64encode
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
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,
|
provider=self.provider,
|
||||||
user=self.user,
|
user=self.user,
|
||||||
token=generate_id(),
|
token=generate_id(),
|
||||||
|
auth_time=timezone.now(),
|
||||||
_scope="openid user profile",
|
_scope="openid user profile",
|
||||||
_id_token=json.dumps(
|
_id_token=json.dumps(
|
||||||
asdict(
|
asdict(
|
||||||
|
@ -72,6 +74,7 @@ class TesOAuth2Introspection(OAuthTestCase):
|
||||||
provider=self.provider,
|
provider=self.provider,
|
||||||
user=self.user,
|
user=self.user,
|
||||||
token=generate_id(),
|
token=generate_id(),
|
||||||
|
auth_time=timezone.now(),
|
||||||
_scope="openid user profile",
|
_scope="openid user profile",
|
||||||
_id_token=json.dumps(
|
_id_token=json.dumps(
|
||||||
asdict(
|
asdict(
|
||||||
|
|
|
@ -4,6 +4,7 @@ from base64 import b64encode
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
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,
|
provider=self.provider,
|
||||||
user=self.user,
|
user=self.user,
|
||||||
token=generate_id(),
|
token=generate_id(),
|
||||||
|
auth_time=timezone.now(),
|
||||||
_scope="openid user profile",
|
_scope="openid user profile",
|
||||||
_id_token=json.dumps(
|
_id_token=json.dumps(
|
||||||
asdict(
|
asdict(
|
||||||
|
@ -62,6 +64,7 @@ class TesOAuth2Revoke(OAuthTestCase):
|
||||||
provider=self.provider,
|
provider=self.provider,
|
||||||
user=self.user,
|
user=self.user,
|
||||||
token=generate_id(),
|
token=generate_id(),
|
||||||
|
auth_time=timezone.now(),
|
||||||
_scope="openid user profile",
|
_scope="openid user profile",
|
||||||
_id_token=json.dumps(
|
_id_token=json.dumps(
|
||||||
asdict(
|
asdict(
|
||||||
|
|
|
@ -4,6 +4,7 @@ from json import dumps
|
||||||
|
|
||||||
from django.test import RequestFactory
|
from django.test import RequestFactory
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
|
@ -45,7 +46,9 @@ class TestToken(OAuthTestCase):
|
||||||
)
|
)
|
||||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||||
user = create_test_admin_user()
|
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(
|
request = self.factory.post(
|
||||||
"/",
|
"/",
|
||||||
data={
|
data={
|
||||||
|
@ -99,6 +102,7 @@ class TestToken(OAuthTestCase):
|
||||||
provider=provider,
|
provider=provider,
|
||||||
user=user,
|
user=user,
|
||||||
token=generate_id(),
|
token=generate_id(),
|
||||||
|
auth_time=timezone.now(),
|
||||||
)
|
)
|
||||||
request = self.factory.post(
|
request = self.factory.post(
|
||||||
"/",
|
"/",
|
||||||
|
@ -127,7 +131,9 @@ class TestToken(OAuthTestCase):
|
||||||
self.app.save()
|
self.app.save()
|
||||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||||
user = create_test_admin_user()
|
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(
|
response = self.client.post(
|
||||||
reverse("authentik_providers_oauth2:token"),
|
reverse("authentik_providers_oauth2:token"),
|
||||||
data={
|
data={
|
||||||
|
@ -173,6 +179,7 @@ class TestToken(OAuthTestCase):
|
||||||
user=user,
|
user=user,
|
||||||
token=generate_id(),
|
token=generate_id(),
|
||||||
_id_token=dumps({}),
|
_id_token=dumps({}),
|
||||||
|
auth_time=timezone.now(),
|
||||||
)
|
)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_providers_oauth2:token"),
|
reverse("authentik_providers_oauth2:token"),
|
||||||
|
@ -221,6 +228,7 @@ class TestToken(OAuthTestCase):
|
||||||
user=user,
|
user=user,
|
||||||
token=generate_id(),
|
token=generate_id(),
|
||||||
_id_token=dumps({}),
|
_id_token=dumps({}),
|
||||||
|
auth_time=timezone.now(),
|
||||||
)
|
)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_providers_oauth2:token"),
|
reverse("authentik_providers_oauth2:token"),
|
||||||
|
@ -271,6 +279,7 @@ class TestToken(OAuthTestCase):
|
||||||
user=user,
|
user=user,
|
||||||
token=generate_id(),
|
token=generate_id(),
|
||||||
_id_token=dumps({}),
|
_id_token=dumps({}),
|
||||||
|
auth_time=timezone.now(),
|
||||||
)
|
)
|
||||||
# Create initial refresh token
|
# Create initial refresh token
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
|
|
|
@ -3,6 +3,7 @@ import json
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from authentik.blueprints.tests import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
|
@ -37,6 +38,7 @@ class TestUserinfo(OAuthTestCase):
|
||||||
provider=self.provider,
|
provider=self.provider,
|
||||||
user=self.user,
|
user=self.user,
|
||||||
token=generate_id(),
|
token=generate_id(),
|
||||||
|
auth_time=timezone.now(),
|
||||||
_scope="openid user profile",
|
_scope="openid user profile",
|
||||||
_id_token=json.dumps(
|
_id_token=json.dumps(
|
||||||
asdict(
|
asdict(
|
||||||
|
@ -56,7 +58,6 @@ class TestUserinfo(OAuthTestCase):
|
||||||
{
|
{
|
||||||
"name": self.user.name,
|
"name": self.user.name,
|
||||||
"given_name": self.user.name,
|
"given_name": self.user.name,
|
||||||
"family_name": "",
|
|
||||||
"preferred_username": self.user.name,
|
"preferred_username": self.user.name,
|
||||||
"nickname": self.user.name,
|
"nickname": self.user.name,
|
||||||
"groups": [group.name for group in self.user.ak_groups.all()],
|
"groups": [group.name for group in self.user.ak_groups.all()],
|
||||||
|
@ -79,7 +80,6 @@ class TestUserinfo(OAuthTestCase):
|
||||||
{
|
{
|
||||||
"name": self.user.name,
|
"name": self.user.name,
|
||||||
"given_name": self.user.name,
|
"given_name": self.user.name,
|
||||||
"family_name": "",
|
|
||||||
"preferred_username": self.user.name,
|
"preferred_username": self.user.name,
|
||||||
"nickname": self.user.name,
|
"nickname": self.user.name,
|
||||||
"groups": [group.name for group in self.user.ak_groups.all()],
|
"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.core.models import Application
|
||||||
from authentik.events.models import Event, EventAction
|
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 (
|
from authentik.flows.challenge import (
|
||||||
PLAN_CONTEXT_TITLE,
|
PLAN_CONTEXT_TITLE,
|
||||||
AutosubmitChallenge,
|
AutosubmitChallenge,
|
||||||
|
@ -64,12 +64,11 @@ from authentik.stages.consent.stage import (
|
||||||
PLAN_CONTEXT_CONSENT_PERMISSIONS,
|
PLAN_CONTEXT_CONSENT_PERMISSIONS,
|
||||||
ConsentStageView,
|
ConsentStageView,
|
||||||
)
|
)
|
||||||
from authentik.stages.user_login.stage import USER_LOGIN_AUTHENTICATED
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
PLAN_CONTEXT_PARAMS = "params"
|
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}
|
ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSENT, PROMPT_LOGIN}
|
||||||
|
|
||||||
|
@ -235,19 +234,22 @@ class OAuthAuthorizationParams:
|
||||||
|
|
||||||
def check_nonce(self):
|
def check_nonce(self):
|
||||||
"""Nonce parameter validation."""
|
"""Nonce parameter validation."""
|
||||||
# https://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation
|
# nonce is required for all flows that return an id_token from the authorization endpoint,
|
||||||
# Nonce is only required for Implicit flows
|
# see https://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthRequest or
|
||||||
if self.grant_type != GrantTypes.IMPLICIT:
|
# 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
|
return
|
||||||
if not self.nonce:
|
if not self.nonce:
|
||||||
self.nonce = self.state
|
LOGGER.warning("Missing nonce for OpenID Request")
|
||||||
LOGGER.warning("Using state as nonce for OpenID Request")
|
raise AuthorizeError(self.redirect_uri, "invalid_request", self.grant_type, self.state)
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
def check_code_challenge(self):
|
def check_code_challenge(self):
|
||||||
"""PKCE validation of the transformation method."""
|
"""PKCE validation of the transformation method."""
|
||||||
|
@ -262,19 +264,24 @@ class OAuthAuthorizationParams:
|
||||||
|
|
||||||
def create_code(self, request: HttpRequest) -> AuthorizationCode:
|
def create_code(self, request: HttpRequest) -> AuthorizationCode:
|
||||||
"""Create an AuthorizationCode object for the request"""
|
"""Create an AuthorizationCode object for the request"""
|
||||||
code = AuthorizationCode()
|
auth_event = get_login_event(request)
|
||||||
code.user = request.user
|
|
||||||
code.provider = self.provider
|
|
||||||
|
|
||||||
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:
|
if self.code_challenge and self.code_challenge_method:
|
||||||
code.code_challenge = self.code_challenge
|
code.code_challenge = self.code_challenge
|
||||||
code.code_challenge_method = self.code_challenge_method
|
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
|
return code
|
||||||
|
|
||||||
|
|
||||||
|
@ -309,7 +316,6 @@ class AuthorizationFlowInitView(PolicyAccessView):
|
||||||
self.params.grant_type,
|
self.params.grant_type,
|
||||||
self.params.state,
|
self.params.state,
|
||||||
)
|
)
|
||||||
error.to_event(redirect_uri=error.redirect_uri).from_http(self.request)
|
|
||||||
raise RequestValidationError(error.get_response(self.request))
|
raise RequestValidationError(error.get_response(self.request))
|
||||||
|
|
||||||
def resolve_provider_application(self):
|
def resolve_provider_application(self):
|
||||||
|
@ -329,28 +335,40 @@ class AuthorizationFlowInitView(PolicyAccessView):
|
||||||
|
|
||||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
"""Start FlowPLanner, return to flow executor shell"""
|
"""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
|
# After we've checked permissions, and the user has access, check if we need
|
||||||
# to re-authenticate the user
|
# to re-authenticate the user
|
||||||
if self.params.max_age:
|
if self.params.max_age:
|
||||||
current_age: timedelta = (
|
# Attempt to check via the session's login event if set, otherwise we can't
|
||||||
timezone.now()
|
# check
|
||||||
- Event.objects.filter(action=EventAction.LOGIN, user=get_user(self.request.user))
|
login_time = login_event.created
|
||||||
.latest("created")
|
current_age: timedelta = timezone.now() - login_time
|
||||||
.created
|
|
||||||
)
|
|
||||||
if current_age.total_seconds() > self.params.max_age:
|
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()
|
return self.handle_no_permission()
|
||||||
# If prompt=login, we need to re-authenticate the user regardless
|
# If prompt=login, we need to re-authenticate the user regardless
|
||||||
if (
|
# Check if we're not already doing the re-authentication
|
||||||
PROMPT_LOGIN in self.params.prompt
|
if PROMPT_LOGIN in self.params.prompt:
|
||||||
and SESSION_KEY_NEEDS_LOGIN not in self.request.session
|
# No previous login UID saved, so save the current uid and trigger
|
||||||
# To prevent the user from having to double login when prompt is set to login
|
# re-login, or previous login UID matches current one, so no re-login happened yet
|
||||||
# and the user has just signed it. This session variable is set in the UserLoginStage
|
if (
|
||||||
# and is (quite hackily) removed from the session in applications's API's List method
|
SESSION_KEY_LAST_LOGIN_UID not in self.request.session
|
||||||
and USER_LOGIN_AUTHENTICATED 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()
|
return self.handle_no_permission()
|
||||||
scope_descriptions = UserInfoView().get_scope_descriptions(self.params.scope)
|
scope_descriptions = UserInfoView().get_scope_descriptions(self.params.scope)
|
||||||
# Regardless, we start the planner and return to it
|
# Regardless, we start the planner and return to it
|
||||||
planner = FlowPlanner(self.provider.authorization_flow)
|
planner = FlowPlanner(self.provider.authorization_flow)
|
||||||
|
@ -525,6 +543,7 @@ class OAuthFulfillmentStage(StageView):
|
||||||
def create_implicit_response(self, code: Optional[AuthorizationCode]) -> dict:
|
def create_implicit_response(self, code: Optional[AuthorizationCode]) -> dict:
|
||||||
"""Create implicit response's URL Fragment dictionary"""
|
"""Create implicit response's URL Fragment dictionary"""
|
||||||
query_fragment = {}
|
query_fragment = {}
|
||||||
|
auth_event = get_login_event(self.request)
|
||||||
|
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
access_token_expiry = now + timedelta_from_string(self.provider.access_token_validity)
|
access_token_expiry = now + timedelta_from_string(self.provider.access_token_validity)
|
||||||
|
@ -533,6 +552,7 @@ class OAuthFulfillmentStage(StageView):
|
||||||
scope=self.params.scope,
|
scope=self.params.scope,
|
||||||
expires=access_token_expiry,
|
expires=access_token_expiry,
|
||||||
provider=self.provider,
|
provider=self.provider,
|
||||||
|
auth_time=auth_event.created if auth_event else now,
|
||||||
)
|
)
|
||||||
|
|
||||||
id_token = IDToken.new(self.provider, token, self.request)
|
id_token = IDToken.new(self.provider, token, self.request)
|
||||||
|
@ -553,6 +573,8 @@ class OAuthFulfillmentStage(StageView):
|
||||||
ResponseTypes.CODE_TOKEN,
|
ResponseTypes.CODE_TOKEN,
|
||||||
]:
|
]:
|
||||||
query_fragment["access_token"] = token.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.
|
# Check if response_type must include id_token in the response.
|
||||||
if self.params.response_type in [
|
if self.params.response_type in [
|
||||||
|
@ -561,8 +583,6 @@ class OAuthFulfillmentStage(StageView):
|
||||||
ResponseTypes.CODE_ID_TOKEN,
|
ResponseTypes.CODE_ID_TOKEN,
|
||||||
ResponseTypes.CODE_ID_TOKEN_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())
|
query_fragment["id_token"] = self.provider.encode(id_token.to_dict())
|
||||||
token._id_token = dumps(id_token.to_dict())
|
token._id_token = dumps(id_token.to_dict())
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ from authentik.core.models import (
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from authentik.events.models import Event, EventAction
|
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.flows.planner import PLAN_CONTEXT_APPLICATION
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
|
@ -479,6 +480,7 @@ class TokenView(View):
|
||||||
expires=access_token_expiry,
|
expires=access_token_expiry,
|
||||||
# Keep same scopes as previous token
|
# Keep same scopes as previous token
|
||||||
scope=self.params.authorization_code.scope,
|
scope=self.params.authorization_code.scope,
|
||||||
|
auth_time=self.params.authorization_code.auth_time,
|
||||||
)
|
)
|
||||||
access_token.id_token = IDToken.new(
|
access_token.id_token = IDToken.new(
|
||||||
self.provider,
|
self.provider,
|
||||||
|
@ -493,6 +495,7 @@ class TokenView(View):
|
||||||
scope=self.params.authorization_code.scope,
|
scope=self.params.authorization_code.scope,
|
||||||
expires=refresh_token_expiry,
|
expires=refresh_token_expiry,
|
||||||
provider=self.provider,
|
provider=self.provider,
|
||||||
|
auth_time=self.params.authorization_code.auth_time,
|
||||||
)
|
)
|
||||||
id_token = IDToken.new(
|
id_token = IDToken.new(
|
||||||
self.provider,
|
self.provider,
|
||||||
|
@ -521,7 +524,6 @@ class TokenView(View):
|
||||||
unauthorized_scopes = set(self.params.scope) - set(self.params.refresh_token.scope)
|
unauthorized_scopes = set(self.params.scope) - set(self.params.refresh_token.scope)
|
||||||
if unauthorized_scopes:
|
if unauthorized_scopes:
|
||||||
raise TokenError("invalid_scope")
|
raise TokenError("invalid_scope")
|
||||||
|
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
access_token_expiry = now + timedelta_from_string(self.provider.access_token_validity)
|
access_token_expiry = now + timedelta_from_string(self.provider.access_token_validity)
|
||||||
access_token = AccessToken(
|
access_token = AccessToken(
|
||||||
|
@ -530,6 +532,7 @@ class TokenView(View):
|
||||||
expires=access_token_expiry,
|
expires=access_token_expiry,
|
||||||
# Keep same scopes as previous token
|
# Keep same scopes as previous token
|
||||||
scope=self.params.refresh_token.scope,
|
scope=self.params.refresh_token.scope,
|
||||||
|
auth_time=self.params.refresh_token.auth_time,
|
||||||
)
|
)
|
||||||
access_token.id_token = IDToken.new(
|
access_token.id_token = IDToken.new(
|
||||||
self.provider,
|
self.provider,
|
||||||
|
@ -544,6 +547,7 @@ class TokenView(View):
|
||||||
scope=self.params.refresh_token.scope,
|
scope=self.params.refresh_token.scope,
|
||||||
expires=refresh_token_expiry,
|
expires=refresh_token_expiry,
|
||||||
provider=self.provider,
|
provider=self.provider,
|
||||||
|
auth_time=self.params.refresh_token.auth_time,
|
||||||
)
|
)
|
||||||
id_token = IDToken.new(
|
id_token = IDToken.new(
|
||||||
self.provider,
|
self.provider,
|
||||||
|
@ -578,6 +582,7 @@ class TokenView(View):
|
||||||
user=self.params.user,
|
user=self.params.user,
|
||||||
expires=access_token_expiry,
|
expires=access_token_expiry,
|
||||||
scope=self.params.scope,
|
scope=self.params.scope,
|
||||||
|
auth_time=now,
|
||||||
)
|
)
|
||||||
access_token.id_token = IDToken.new(
|
access_token.id_token = IDToken.new(
|
||||||
self.provider,
|
self.provider,
|
||||||
|
@ -600,11 +605,13 @@ class TokenView(View):
|
||||||
raise DeviceCodeError("authorization_pending")
|
raise DeviceCodeError("authorization_pending")
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
access_token_expiry = now + timedelta_from_string(self.provider.access_token_validity)
|
access_token_expiry = now + timedelta_from_string(self.provider.access_token_validity)
|
||||||
|
auth_event = get_login_event(self.request)
|
||||||
access_token = AccessToken(
|
access_token = AccessToken(
|
||||||
provider=self.provider,
|
provider=self.provider,
|
||||||
user=self.params.device_code.user,
|
user=self.params.device_code.user,
|
||||||
expires=access_token_expiry,
|
expires=access_token_expiry,
|
||||||
scope=self.params.device_code.scope,
|
scope=self.params.device_code.scope,
|
||||||
|
auth_time=auth_event.created if auth_event else now,
|
||||||
)
|
)
|
||||||
access_token.id_token = IDToken.new(
|
access_token.id_token = IDToken.new(
|
||||||
self.provider,
|
self.provider,
|
||||||
|
@ -619,6 +626,7 @@ class TokenView(View):
|
||||||
scope=self.params.device_code.scope,
|
scope=self.params.device_code.scope,
|
||||||
expires=refresh_token_expiry,
|
expires=refresh_token_expiry,
|
||||||
provider=self.provider,
|
provider=self.provider,
|
||||||
|
auth_time=auth_event.created if auth_event else now,
|
||||||
)
|
)
|
||||||
id_token = IDToken.new(
|
id_token = IDToken.new(
|
||||||
self.provider,
|
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 import BACKEND_INBUILT
|
||||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||||
|
|
||||||
USER_LOGIN_AUTHENTICATED = "user_login_authenticated"
|
|
||||||
|
|
||||||
|
|
||||||
class UserLoginStageView(StageView):
|
class UserLoginStageView(StageView):
|
||||||
"""Finalise Authentication flow by logging the user in"""
|
"""Finalise Authentication flow by logging the user in"""
|
||||||
|
@ -51,7 +49,6 @@ class UserLoginStageView(StageView):
|
||||||
flow_slug=self.executor.flow.slug,
|
flow_slug=self.executor.flow.slug,
|
||||||
session_duration=self.executor.current_stage.session_duration,
|
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
|
# Only show success message if we don't have a source in the flow
|
||||||
# as sources show their own success messages
|
# as sources show their own success messages
|
||||||
if not self.executor.plan.context.get(PLAN_CONTEXT_SOURCE, None):
|
if not self.executor.plan.context.get(PLAN_CONTEXT_SOURCE, None):
|
||||||
|
|
|
@ -11,7 +11,7 @@ entries:
|
||||||
designation: authentication
|
designation: authentication
|
||||||
name: Welcome to authentik!
|
name: Welcome to authentik!
|
||||||
title: Welcome to authentik!
|
title: Welcome to authentik!
|
||||||
authentication: require_unauthenticated
|
authentication: none
|
||||||
identifiers:
|
identifiers:
|
||||||
slug: default-authentication-flow
|
slug: default-authentication-flow
|
||||||
model: authentik_flows.flow
|
model: authentik_flows.flow
|
||||||
|
|
|
@ -40,7 +40,6 @@ entries:
|
||||||
# You can override this behaviour in custom mappings, i.e. `request.user.name.split(" ")`
|
# You can override this behaviour in custom mappings, i.e. `request.user.name.split(" ")`
|
||||||
"name": request.user.name,
|
"name": request.user.name,
|
||||||
"given_name": request.user.name,
|
"given_name": request.user.name,
|
||||||
"family_name": "",
|
|
||||||
"preferred_username": request.user.username,
|
"preferred_username": request.user.username,
|
||||||
"nickname": request.user.username,
|
"nickname": request.user.username,
|
||||||
# groups is not part of the official userinfo schema, but is a quasi-standard
|
# 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 PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CapabilitiesEnum,
|
|
||||||
ChallengeChoices,
|
ChallengeChoices,
|
||||||
ChallengeTypes,
|
ChallengeTypes,
|
||||||
ContextualFlowInfo,
|
ContextualFlowInfo,
|
||||||
|
@ -97,7 +96,7 @@ export class FlowExecutor extends AKElement implements StageHost {
|
||||||
tenant!: CurrentTenant;
|
tenant!: CurrentTenant;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
inspectorOpen: boolean;
|
inspectorOpen = false;
|
||||||
|
|
||||||
_flowInfo?: ContextualFlowInfo;
|
_flowInfo?: ContextualFlowInfo;
|
||||||
|
|
||||||
|
@ -177,8 +176,6 @@ export class FlowExecutor extends AKElement implements StageHost {
|
||||||
super();
|
super();
|
||||||
this.ws = new WebsocketClient();
|
this.ws = new WebsocketClient();
|
||||||
this.flowSlug = window.location.pathname.split("/")[3];
|
this.flowSlug = window.location.pathname.split("/")[3];
|
||||||
this.inspectorOpen =
|
|
||||||
globalAK()?.config.capabilities.includes(CapabilitiesEnum.Debug) || false;
|
|
||||||
if (window.location.search.includes("inspector")) {
|
if (window.location.search.includes("inspector")) {
|
||||||
this.inspectorOpen = !this.inspectorOpen;
|
this.inspectorOpen = !this.inspectorOpen;
|
||||||
}
|
}
|
||||||
|
|
Reference in New Issue