authentik.providers.oauth2.id_token

id_token utils

  1"""id_token utils"""
  2
  3from dataclasses import asdict, dataclass, field
  4from hashlib import sha256
  5from typing import TYPE_CHECKING, Any
  6
  7from django.http import HttpRequest
  8from django.utils import timezone
  9
 10from authentik.common.oauth.constants import (
 11    ACR_AUTHENTIK_DEFAULT,
 12    AMR_MFA,
 13    AMR_PASSWORD,
 14    AMR_SMART_CARD,
 15    AMR_WEBAUTHN,
 16    SubModes,
 17)
 18from authentik.core.models import default_token_duration
 19from authentik.events.signals import get_login_event
 20from authentik.lib.generators import generate_id
 21from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
 22
 23if TYPE_CHECKING:
 24    from authentik.providers.oauth2.models import BaseGrantModel, OAuth2Provider
 25
 26
 27def hash_session_key(session_key: str) -> str:
 28    """Hash the session key for inclusion in JWTs as `sid`"""
 29    return sha256(session_key.encode("ascii")).hexdigest()
 30
 31
 32@dataclass(slots=True)
 33class IDToken:
 34    """The primary extension that OpenID Connect makes to OAuth 2.0 to enable End-Users to be
 35    Authenticated is the ID Token data structure. The ID Token is a security token that contains
 36    Claims about the Authentication of an End-User by an Authorization Server when using a Client,
 37    and potentially other requested Claims. The ID Token is represented as a
 38    JSON Web Token (JWT) [JWT].
 39
 40    https://openid.net/specs/openid-connect-core-1_0.html#IDToken
 41    https://www.iana.org/assignments/jwt/jwt.xhtml"""
 42
 43    # Issuer, https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.1
 44    iss: str | None = None
 45    # Subject, https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.2
 46    sub: str | None = None
 47    # Audience, https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.3
 48    aud: str | list[str] | None = None
 49    # Expiration time, https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.4
 50    exp: int | None = None
 51    # Issued at, https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.6
 52    iat: int | None = None
 53    # Time when the authentication occurred,
 54    # https://openid.net/specs/openid-connect-core-1_0.html#IDToken
 55    auth_time: int | None = None
 56    # Authentication Context Class Reference,
 57    # https://openid.net/specs/openid-connect-core-1_0.html#IDToken
 58    acr: str | None = ACR_AUTHENTIK_DEFAULT
 59    # Authentication Methods References,
 60    # https://openid.net/specs/openid-connect-core-1_0.html#IDToken
 61    amr: list[str] | None = None
 62    # Code hash value, http://openid.net/specs/openid-connect-core-1_0.html
 63    c_hash: str | None = None
 64    # Value used to associate a Client session with an ID Token,
 65    # http://openid.net/specs/openid-connect-core-1_0.html
 66    nonce: str | None = None
 67    # Access Token hash value, http://openid.net/specs/openid-connect-core-1_0.html
 68    at_hash: str | None = None
 69    # Session ID, https://openid.net/specs/openid-connect-frontchannel-1_0.html#ClaimsContents
 70    sid: str | None = None
 71    # JWT ID, https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.7
 72    jti: str | None = None
 73
 74    claims: dict[str, Any] = field(default_factory=dict)
 75
 76    @staticmethod
 77    def new(
 78        provider: OAuth2Provider, token: BaseGrantModel, request: HttpRequest, **kwargs
 79    ) -> IDToken:
 80        """Create ID Token"""
 81        id_token = IDToken(provider, token, **kwargs)
 82        id_token.exp = int(
 83            (token.expires if token.expires is not None else default_token_duration()).timestamp()
 84        )
 85        id_token.iss = provider.get_issuer(request)
 86        id_token.jti = generate_id()
 87        id_token.aud = provider.client_id
 88        id_token.claims = {}
 89
 90        if provider.sub_mode == SubModes.HASHED_USER_ID:
 91            id_token.sub = token.user.uid
 92        elif provider.sub_mode == SubModes.USER_ID:
 93            id_token.sub = str(token.user.pk)
 94        elif provider.sub_mode == SubModes.USER_UUID:
 95            id_token.sub = str(token.user.uuid)
 96        elif provider.sub_mode == SubModes.USER_EMAIL:
 97            id_token.sub = token.user.email
 98        elif provider.sub_mode == SubModes.USER_USERNAME:
 99            id_token.sub = token.user.username
100        elif provider.sub_mode == SubModes.USER_UPN:
101            id_token.sub = token.user.attributes.get("upn", token.user.uid)
102        else:
103            raise ValueError(
104                f"Provider {provider} has invalid sub_mode selected: {provider.sub_mode}"
105            )
106
107        # Convert datetimes into timestamps.
108        now = timezone.now()
109        id_token.iat = int(now.timestamp())
110        id_token.auth_time = int(token.auth_time.timestamp())
111        if token.session:
112            id_token.sid = hash_session_key(token.session.session.session_key)
113
114        # We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
115        auth_event = get_login_event(token.session)
116        if auth_event:
117            # Also check which method was used for authentication
118            method = auth_event.context.get(PLAN_CONTEXT_METHOD, "")
119            method_args = auth_event.context.get(PLAN_CONTEXT_METHOD_ARGS, {})
120            amr = []
121            if method == "password":
122                amr.append(AMR_PASSWORD)
123            if method == "auth_webauthn_pwl":
124                amr.append(AMR_WEBAUTHN)
125            if "certificate" in method_args:
126                amr.append(AMR_SMART_CARD)
127            if "mfa_devices" in method_args:
128                amr.append(AMR_MFA)
129            if amr:
130                id_token.amr = amr
131
132        # Include (or not) user standard claims in the id_token.
133        if provider.include_claims_in_id_token:
134            from authentik.providers.oauth2.views.userinfo import UserInfoView
135
136            user_info = UserInfoView()
137            user_info.request = request
138            id_token.claims = user_info.get_claims(token.provider, token)
139        return id_token
140
141    def to_dict(self) -> dict[str, Any]:
142        """Convert dataclass to dict, and update with keys from `claims`"""
143        id_dict = asdict(self)
144        # All items without a value should be removed instead being set to None/null
145        # https://openid.net/specs/openid-connect-core-1_0.html#JSONSerialization
146        for key in list(id_dict.keys()):
147            if id_dict[key] is None:
148                id_dict.pop(key)
149        id_dict.pop("claims")
150        id_dict.update(self.claims)
151        return id_dict
152
153    def to_access_token(self, provider: OAuth2Provider, token: BaseGrantModel) -> str:
154        """Encode id_token for use as access token, adding fields"""
155        final = self.to_dict()
156        final["azp"] = provider.client_id
157        final["uid"] = generate_id()
158        final.setdefault("scope", " ".join(token.scope))
159        return provider.encode(final)
160
161    def to_jwt(self, provider: OAuth2Provider) -> str:
162        """Shortcut to encode id_token to jwt, signed by self.provider"""
163        return provider.encode(self.to_dict())
def hash_session_key(session_key: str) -> str:
28def hash_session_key(session_key: str) -> str:
29    """Hash the session key for inclusion in JWTs as `sid`"""
30    return sha256(session_key.encode("ascii")).hexdigest()

Hash the session key for inclusion in JWTs as sid

@dataclass(slots=True)
class IDToken:
 33@dataclass(slots=True)
 34class IDToken:
 35    """The primary extension that OpenID Connect makes to OAuth 2.0 to enable End-Users to be
 36    Authenticated is the ID Token data structure. The ID Token is a security token that contains
 37    Claims about the Authentication of an End-User by an Authorization Server when using a Client,
 38    and potentially other requested Claims. The ID Token is represented as a
 39    JSON Web Token (JWT) [JWT].
 40
 41    https://openid.net/specs/openid-connect-core-1_0.html#IDToken
 42    https://www.iana.org/assignments/jwt/jwt.xhtml"""
 43
 44    # Issuer, https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.1
 45    iss: str | None = None
 46    # Subject, https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.2
 47    sub: str | None = None
 48    # Audience, https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.3
 49    aud: str | list[str] | None = None
 50    # Expiration time, https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.4
 51    exp: int | None = None
 52    # Issued at, https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.6
 53    iat: int | None = None
 54    # Time when the authentication occurred,
 55    # https://openid.net/specs/openid-connect-core-1_0.html#IDToken
 56    auth_time: int | None = None
 57    # Authentication Context Class Reference,
 58    # https://openid.net/specs/openid-connect-core-1_0.html#IDToken
 59    acr: str | None = ACR_AUTHENTIK_DEFAULT
 60    # Authentication Methods References,
 61    # https://openid.net/specs/openid-connect-core-1_0.html#IDToken
 62    amr: list[str] | None = None
 63    # Code hash value, http://openid.net/specs/openid-connect-core-1_0.html
 64    c_hash: str | None = None
 65    # Value used to associate a Client session with an ID Token,
 66    # http://openid.net/specs/openid-connect-core-1_0.html
 67    nonce: str | None = None
 68    # Access Token hash value, http://openid.net/specs/openid-connect-core-1_0.html
 69    at_hash: str | None = None
 70    # Session ID, https://openid.net/specs/openid-connect-frontchannel-1_0.html#ClaimsContents
 71    sid: str | None = None
 72    # JWT ID, https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.7
 73    jti: str | None = None
 74
 75    claims: dict[str, Any] = field(default_factory=dict)
 76
 77    @staticmethod
 78    def new(
 79        provider: OAuth2Provider, token: BaseGrantModel, request: HttpRequest, **kwargs
 80    ) -> IDToken:
 81        """Create ID Token"""
 82        id_token = IDToken(provider, token, **kwargs)
 83        id_token.exp = int(
 84            (token.expires if token.expires is not None else default_token_duration()).timestamp()
 85        )
 86        id_token.iss = provider.get_issuer(request)
 87        id_token.jti = generate_id()
 88        id_token.aud = provider.client_id
 89        id_token.claims = {}
 90
 91        if provider.sub_mode == SubModes.HASHED_USER_ID:
 92            id_token.sub = token.user.uid
 93        elif provider.sub_mode == SubModes.USER_ID:
 94            id_token.sub = str(token.user.pk)
 95        elif provider.sub_mode == SubModes.USER_UUID:
 96            id_token.sub = str(token.user.uuid)
 97        elif provider.sub_mode == SubModes.USER_EMAIL:
 98            id_token.sub = token.user.email
 99        elif provider.sub_mode == SubModes.USER_USERNAME:
100            id_token.sub = token.user.username
101        elif provider.sub_mode == SubModes.USER_UPN:
102            id_token.sub = token.user.attributes.get("upn", token.user.uid)
103        else:
104            raise ValueError(
105                f"Provider {provider} has invalid sub_mode selected: {provider.sub_mode}"
106            )
107
108        # Convert datetimes into timestamps.
109        now = timezone.now()
110        id_token.iat = int(now.timestamp())
111        id_token.auth_time = int(token.auth_time.timestamp())
112        if token.session:
113            id_token.sid = hash_session_key(token.session.session.session_key)
114
115        # We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
116        auth_event = get_login_event(token.session)
117        if auth_event:
118            # Also check which method was used for authentication
119            method = auth_event.context.get(PLAN_CONTEXT_METHOD, "")
120            method_args = auth_event.context.get(PLAN_CONTEXT_METHOD_ARGS, {})
121            amr = []
122            if method == "password":
123                amr.append(AMR_PASSWORD)
124            if method == "auth_webauthn_pwl":
125                amr.append(AMR_WEBAUTHN)
126            if "certificate" in method_args:
127                amr.append(AMR_SMART_CARD)
128            if "mfa_devices" in method_args:
129                amr.append(AMR_MFA)
130            if amr:
131                id_token.amr = amr
132
133        # Include (or not) user standard claims in the id_token.
134        if provider.include_claims_in_id_token:
135            from authentik.providers.oauth2.views.userinfo import UserInfoView
136
137            user_info = UserInfoView()
138            user_info.request = request
139            id_token.claims = user_info.get_claims(token.provider, token)
140        return id_token
141
142    def to_dict(self) -> dict[str, Any]:
143        """Convert dataclass to dict, and update with keys from `claims`"""
144        id_dict = asdict(self)
145        # All items without a value should be removed instead being set to None/null
146        # https://openid.net/specs/openid-connect-core-1_0.html#JSONSerialization
147        for key in list(id_dict.keys()):
148            if id_dict[key] is None:
149                id_dict.pop(key)
150        id_dict.pop("claims")
151        id_dict.update(self.claims)
152        return id_dict
153
154    def to_access_token(self, provider: OAuth2Provider, token: BaseGrantModel) -> str:
155        """Encode id_token for use as access token, adding fields"""
156        final = self.to_dict()
157        final["azp"] = provider.client_id
158        final["uid"] = generate_id()
159        final.setdefault("scope", " ".join(token.scope))
160        return provider.encode(final)
161
162    def to_jwt(self, provider: OAuth2Provider) -> str:
163        """Shortcut to encode id_token to jwt, signed by self.provider"""
164        return provider.encode(self.to_dict())

The primary extension that OpenID Connect makes to OAuth 2.0 to enable End-Users to be Authenticated is the ID Token data structure. The ID Token is a security token that contains Claims about the Authentication of an End-User by an Authorization Server when using a Client, and potentially other requested Claims. The ID Token is represented as a JSON Web Token (JWT) [JWT].

https://openid.net/specs/openid-connect-core-1_0.html#IDToken https://www.iana.org/assignments/jwt/jwt.xhtml

IDToken( iss: str | None = None, sub: str | None = None, aud: str | list[str] | None = None, exp: int | None = None, iat: int | None = None, auth_time: int | None = None, acr: str | None = 'goauthentik.io/providers/oauth2/default', amr: list[str] | None = None, c_hash: str | None = None, nonce: str | None = None, at_hash: str | None = None, sid: str | None = None, jti: str | None = None, claims: dict[str, typing.Any] = <factory>)
iss: str | None
sub: str | None
aud: str | list[str] | None
exp: int | None
iat: int | None
auth_time: int | None
acr: str | None
amr: list[str] | None
c_hash: str | None
nonce: str | None
at_hash: str | None
sid: str | None
jti: str | None
claims: dict[str, typing.Any]
@staticmethod
def new(unknown):
 77    @staticmethod
 78    def new(
 79        provider: OAuth2Provider, token: BaseGrantModel, request: HttpRequest, **kwargs
 80    ) -> IDToken:
 81        """Create ID Token"""
 82        id_token = IDToken(provider, token, **kwargs)
 83        id_token.exp = int(
 84            (token.expires if token.expires is not None else default_token_duration()).timestamp()
 85        )
 86        id_token.iss = provider.get_issuer(request)
 87        id_token.jti = generate_id()
 88        id_token.aud = provider.client_id
 89        id_token.claims = {}
 90
 91        if provider.sub_mode == SubModes.HASHED_USER_ID:
 92            id_token.sub = token.user.uid
 93        elif provider.sub_mode == SubModes.USER_ID:
 94            id_token.sub = str(token.user.pk)
 95        elif provider.sub_mode == SubModes.USER_UUID:
 96            id_token.sub = str(token.user.uuid)
 97        elif provider.sub_mode == SubModes.USER_EMAIL:
 98            id_token.sub = token.user.email
 99        elif provider.sub_mode == SubModes.USER_USERNAME:
100            id_token.sub = token.user.username
101        elif provider.sub_mode == SubModes.USER_UPN:
102            id_token.sub = token.user.attributes.get("upn", token.user.uid)
103        else:
104            raise ValueError(
105                f"Provider {provider} has invalid sub_mode selected: {provider.sub_mode}"
106            )
107
108        # Convert datetimes into timestamps.
109        now = timezone.now()
110        id_token.iat = int(now.timestamp())
111        id_token.auth_time = int(token.auth_time.timestamp())
112        if token.session:
113            id_token.sid = hash_session_key(token.session.session.session_key)
114
115        # We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
116        auth_event = get_login_event(token.session)
117        if auth_event:
118            # Also check which method was used for authentication
119            method = auth_event.context.get(PLAN_CONTEXT_METHOD, "")
120            method_args = auth_event.context.get(PLAN_CONTEXT_METHOD_ARGS, {})
121            amr = []
122            if method == "password":
123                amr.append(AMR_PASSWORD)
124            if method == "auth_webauthn_pwl":
125                amr.append(AMR_WEBAUTHN)
126            if "certificate" in method_args:
127                amr.append(AMR_SMART_CARD)
128            if "mfa_devices" in method_args:
129                amr.append(AMR_MFA)
130            if amr:
131                id_token.amr = amr
132
133        # Include (or not) user standard claims in the id_token.
134        if provider.include_claims_in_id_token:
135            from authentik.providers.oauth2.views.userinfo import UserInfoView
136
137            user_info = UserInfoView()
138            user_info.request = request
139            id_token.claims = user_info.get_claims(token.provider, token)
140        return id_token

Create ID Token

def to_dict(self) -> dict[str, typing.Any]:
142    def to_dict(self) -> dict[str, Any]:
143        """Convert dataclass to dict, and update with keys from `claims`"""
144        id_dict = asdict(self)
145        # All items without a value should be removed instead being set to None/null
146        # https://openid.net/specs/openid-connect-core-1_0.html#JSONSerialization
147        for key in list(id_dict.keys()):
148            if id_dict[key] is None:
149                id_dict.pop(key)
150        id_dict.pop("claims")
151        id_dict.update(self.claims)
152        return id_dict

Convert dataclass to dict, and update with keys from claims

def to_access_token(unknown):
154    def to_access_token(self, provider: OAuth2Provider, token: BaseGrantModel) -> str:
155        """Encode id_token for use as access token, adding fields"""
156        final = self.to_dict()
157        final["azp"] = provider.client_id
158        final["uid"] = generate_id()
159        final.setdefault("scope", " ".join(token.scope))
160        return provider.encode(final)

Encode id_token for use as access token, adding fields

def to_jwt(unknown):
162    def to_jwt(self, provider: OAuth2Provider) -> str:
163        """Shortcut to encode id_token to jwt, signed by self.provider"""
164        return provider.encode(self.to_dict())

Shortcut to encode id_token to jwt, signed by self.provider