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>)
@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