providers/oauth2: set amr values based on login event
closes #4070 Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
1fa9b3a996
commit
a9909fcf6d
|
@ -11,7 +11,6 @@ from authentik.core.middleware import CTX_AUTH_VIA
|
||||||
from authentik.core.models import Token, TokenIntents, User
|
from authentik.core.models import Token, TokenIntents, User
|
||||||
from authentik.outposts.models import Outpost
|
from authentik.outposts.models import Outpost
|
||||||
from authentik.providers.oauth2.constants import SCOPE_AUTHENTIK_API
|
from authentik.providers.oauth2.constants import SCOPE_AUTHENTIK_API
|
||||||
from authentik.providers.oauth2.models import RefreshToken
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
@ -33,6 +32,8 @@ def validate_auth(header: bytes) -> Optional[str]:
|
||||||
|
|
||||||
def bearer_auth(raw_header: bytes) -> Optional[User]:
|
def bearer_auth(raw_header: bytes) -> Optional[User]:
|
||||||
"""raw_header in the Format of `Bearer ....`"""
|
"""raw_header in the Format of `Bearer ....`"""
|
||||||
|
from authentik.providers.oauth2.models import RefreshToken
|
||||||
|
|
||||||
auth_credentials = validate_auth(raw_header)
|
auth_credentials = validate_auth(raw_header)
|
||||||
if not auth_credentials:
|
if not auth_credentials:
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -31,3 +31,9 @@ SCOPE_GITHUB_USER_EMAIL = "user:email"
|
||||||
SCOPE_GITHUB_ORG_READ = "read:org"
|
SCOPE_GITHUB_ORG_READ = "read:org"
|
||||||
|
|
||||||
ACR_AUTHENTIK_DEFAULT = "goauthentik.io/providers/oauth2/default"
|
ACR_AUTHENTIK_DEFAULT = "goauthentik.io/providers/oauth2/default"
|
||||||
|
|
||||||
|
# https://datatracker.ietf.org/doc/html/draft-ietf-oauth-amr-values-06#section-2
|
||||||
|
AMR_PASSWORD = "pwd" # nosec
|
||||||
|
AMR_MFA = "mfa"
|
||||||
|
AMR_OTP = "otp"
|
||||||
|
AMR_WEBAUTHN = "user"
|
||||||
|
|
|
@ -20,14 +20,21 @@ from rest_framework.serializers import Serializer
|
||||||
|
|
||||||
from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User
|
from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event
|
||||||
from authentik.events.utils import get_user
|
from authentik.events.signals import SESSION_LOGIN_EVENT
|
||||||
from authentik.lib.generators import generate_code_fixed_length, generate_id, generate_key
|
from authentik.lib.generators import generate_code_fixed_length, generate_id, generate_key
|
||||||
from authentik.lib.models import SerializerModel
|
from authentik.lib.models import SerializerModel
|
||||||
from authentik.lib.utils.time import timedelta_string_validator
|
from authentik.lib.utils.time import timedelta_string_validator
|
||||||
from authentik.providers.oauth2.apps import AuthentikProviderOAuth2Config
|
from authentik.providers.oauth2.apps import AuthentikProviderOAuth2Config
|
||||||
from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT
|
from authentik.providers.oauth2.constants import (
|
||||||
|
ACR_AUTHENTIK_DEFAULT,
|
||||||
|
AMR_MFA,
|
||||||
|
AMR_OTP,
|
||||||
|
AMR_PASSWORD,
|
||||||
|
AMR_WEBAUTHN,
|
||||||
|
)
|
||||||
from authentik.sources.oauth.models import OAuthSource
|
from authentik.sources.oauth.models import OAuthSource
|
||||||
|
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||||
|
|
||||||
|
|
||||||
class ClientTypes(models.TextChoices):
|
class ClientTypes(models.TextChoices):
|
||||||
|
@ -392,6 +399,7 @@ class IDToken:
|
||||||
iat: Optional[int] = None
|
iat: Optional[int] = None
|
||||||
auth_time: Optional[int] = None
|
auth_time: Optional[int] = None
|
||||||
acr: Optional[str] = ACR_AUTHENTIK_DEFAULT
|
acr: Optional[str] = ACR_AUTHENTIK_DEFAULT
|
||||||
|
amr: Optional[list[str]] = None
|
||||||
|
|
||||||
c_hash: Optional[str] = None
|
c_hash: Optional[str] = None
|
||||||
nonce: Optional[str] = None
|
nonce: Optional[str] = None
|
||||||
|
@ -485,21 +493,27 @@ class RefreshToken(SerializerModel, ExpiringModel, BaseGrantModel):
|
||||||
f"selected: {self.provider.sub_mode}"
|
f"selected: {self.provider.sub_mode}"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
amr = []
|
||||||
# Convert datetimes into timestamps.
|
# Convert datetimes into timestamps.
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
iat_time = int(now.timestamp())
|
iat_time = int(now.timestamp())
|
||||||
exp_time = int(self.expires.timestamp())
|
exp_time = int(self.expires.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 = (
|
|
||||||
Event.objects.filter(action=EventAction.LOGIN, user=get_user(user))
|
|
||||||
.order_by("-created")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
# Fallback in case we can't find any login events
|
# Fallback in case we can't find any login events
|
||||||
auth_time = now
|
auth_time = now
|
||||||
if auth_event:
|
if SESSION_LOGIN_EVENT in request.session:
|
||||||
|
auth_event: Event = request.session[SESSION_LOGIN_EVENT]
|
||||||
auth_time = auth_event.created
|
auth_time = auth_event.created
|
||||||
|
# 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, {})
|
||||||
|
if method == "password":
|
||||||
|
amr.append(AMR_PASSWORD)
|
||||||
|
if method == "auth_webauthn_pwl":
|
||||||
|
amr.append(AMR_WEBAUTHN)
|
||||||
|
if "mfa_devices" in method_args:
|
||||||
|
if len(amr) > 0:
|
||||||
|
amr.append(AMR_MFA)
|
||||||
|
|
||||||
auth_timestamp = int(auth_time.timestamp())
|
auth_timestamp = int(auth_time.timestamp())
|
||||||
|
|
||||||
|
|
Reference in New Issue