-
Notifications
You must be signed in to change notification settings - Fork 5
feat: ias module with token parsing #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
8dc92fc
feat: IAS module with token parsing
LucasAlvesSoares 4226167
chore: docs
LucasAlvesSoares 4c64990
feat: user_uuid and custom attributes
LucasAlvesSoares 69f56f8
chore: linting
LucasAlvesSoares 7f7a5c0
chore: make claims enums
LucasAlvesSoares 5af3dd6
chore: linting
LucasAlvesSoares 9ccfa2b
chore: version bump
LucasAlvesSoares File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| """SAP Cloud SDK for Python - IAS module | ||
|
|
||
| Utilities for parsing SAP Identity Authentication Service (IAS) JWT tokens. | ||
|
|
||
| Usage: | ||
| from sap_cloud_sdk.ias import parse_token, IASClaims | ||
|
|
||
| claims = parse_token(request.headers["Authorization"]) | ||
| print(claims.app_tid) # tenant ID (multitenant scenarios) | ||
| print(claims.scim_id) # SCIM-based user ID | ||
| print(claims.sub) # OIDC subject identifier | ||
| print(claims.email) # user email (when email scope requested) | ||
| """ | ||
|
|
||
| from sap_cloud_sdk.ias._token import IASClaims, parse_token | ||
| from sap_cloud_sdk.ias.exceptions import IASTokenError | ||
|
|
||
| __all__ = [ | ||
| "IASClaims", | ||
| "parse_token", | ||
| "IASTokenError", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| """Internal IAS JWT claim name definitions.""" | ||
|
|
||
| from enum import Enum | ||
|
|
||
|
|
||
| class _IASClaim(str, Enum): | ||
| APP_TID = "app_tid" | ||
| AT_HASH = "at_hash" | ||
| AUD = "aud" | ||
| AUTH_TIME = "auth_time" | ||
| AZP = "azp" | ||
| EMAIL = "email" | ||
| EMAIL_VERIFIED = "email_verified" | ||
| EXP = "exp" | ||
| FAMILY_NAME = "family_name" | ||
| GIVEN_NAME = "given_name" | ||
| GROUPS = "groups" | ||
| IAS_APIS = "ias_apis" | ||
| IAS_ISS = "ias_iss" | ||
| IAT = "iat" | ||
| ISS = "iss" | ||
| JTI = "jti" | ||
| MIDDLE_NAME = "middle_name" | ||
| NAME = "name" | ||
| NONCE = "nonce" | ||
| PREFERRED_USERNAME = "preferred_username" | ||
| SAP_ID_TYPE = "sap_id_type" | ||
| SCIM_ID = "scim_id" | ||
| SID = "sid" | ||
| SUB = "sub" | ||
| USER_UUID = "user_uuid" | ||
|
|
||
|
|
||
| _KNOWN_CLAIM_VALUES: frozenset[str] = frozenset(c.value for c in _IASClaim) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| """IAS JWT token parsing. | ||
|
|
||
| Decodes SAP IAS JWT tokens without signature verification and maps | ||
| all standard IAS claims to a typed dataclass. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from dataclasses import dataclass, field | ||
| from typing import Any, Dict, List, Optional, Union | ||
|
|
||
| import jwt | ||
|
|
||
| from sap_cloud_sdk.ias._claims import _IASClaim, _KNOWN_CLAIM_VALUES | ||
| from sap_cloud_sdk.ias.exceptions import IASTokenError | ||
|
|
||
|
|
||
| @dataclass | ||
| class IASClaims: | ||
| """Typed representation of SAP IAS JWT token claims. | ||
|
|
||
| All standard fields are optional — a claim absent from the token is None. | ||
| Any claims not in the standard set are collected in ``custom_attributes``. | ||
|
|
||
| Attributes: | ||
| app_tid: SAP claim identifying the tenant of the application. | ||
| Used in multitenant scenarios (e.g. subscribed BTP applications). | ||
| at_hash: Hash of the access token. Can be used to bind the ID token | ||
| to an access token and prevent token substitution attacks. | ||
| aud: Audience — recipient(s) of the token. Can be a string or list | ||
| of client IDs. | ||
| auth_time: Time when the user authenticated (seconds since Unix epoch). | ||
| azp: Authorized party — client ID to which the ID token was issued. | ||
| email: Email address of the user. Included when the email scope is | ||
| requested. | ||
| email_verified: Whether the email address has been verified. | ||
| exp: Expiration time after which the token must not be accepted | ||
| (seconds since Unix epoch). | ||
| family_name: Surname (last name) of the user. Included when the | ||
| profile scope is requested. | ||
| given_name: Given name (first name) of the user. Included when the | ||
| profile scope is requested. | ||
| groups: Groups the user belongs to, associated with authorizations. | ||
| Included when the groups scope is requested. | ||
| ias_apis: SAP claim listing API permission groups or a fixed value | ||
| when all APIs of an application are consumed. | ||
| ias_iss: SAP claim identifying the SAP tenant even when the token was | ||
| issued from a custom domain in a non-SAP domain. | ||
| iat: Time when the token was issued (seconds since Unix epoch). | ||
| iss: Issuer of the token, typically a URL such as | ||
| https://<tenant>.accounts.ondemand.com. | ||
| jti: Unique identifier for the JWT, used to prevent replay attacks. | ||
| Included when the profile scope is requested. | ||
| middle_name: Middle name of the user. | ||
| name: Full display name of the user. | ||
| nonce: String associated with the client session to mitigate replay | ||
| attacks. | ||
| preferred_username: Human-readable display name / username of the user. | ||
| sap_id_type: SAP claim identifying the type of token. | ||
| ``"app"`` for application credentials, ``"user"`` for user | ||
| credentials. | ||
| scim_id: SAP claim identifying the user by their SCIM ID in SAP Cloud | ||
| Identity Services. | ||
| sid: Session ID used to track a user session across applications and | ||
| logout scenarios. | ||
| sub: Subject — unique identifier for the user, scoped to the issuer. | ||
| user_uuid: SAP claim identifying the global user ID. | ||
| custom_attributes: Any claims present in the token that are not part | ||
| of the standard IAS claim set. | ||
| """ | ||
|
|
||
| app_tid: Optional[str] = None | ||
| at_hash: Optional[str] = None | ||
| aud: Optional[Union[str, List[str]]] = None | ||
| auth_time: Optional[int] = None | ||
| azp: Optional[str] = None | ||
| email: Optional[str] = None | ||
| email_verified: Optional[bool] = None | ||
| exp: Optional[int] = None | ||
| family_name: Optional[str] = None | ||
| given_name: Optional[str] = None | ||
| groups: Optional[List[str]] = None | ||
| ias_apis: Optional[Union[str, List[str]]] = None | ||
| ias_iss: Optional[str] = None | ||
| iat: Optional[int] = None | ||
| iss: Optional[str] = None | ||
| jti: Optional[str] = None | ||
| middle_name: Optional[str] = None | ||
| name: Optional[str] = None | ||
| nonce: Optional[str] = None | ||
| preferred_username: Optional[str] = None | ||
| sap_id_type: Optional[str] = None | ||
| scim_id: Optional[str] = None | ||
| sid: Optional[str] = None | ||
| sub: Optional[str] = None | ||
| user_uuid: Optional[str] = None | ||
| custom_attributes: Dict[str, Any] = field(default_factory=dict) | ||
|
|
||
|
|
||
| def parse_token(token: str) -> IASClaims: | ||
| """Parse an SAP IAS JWT token and return its claims. | ||
|
|
||
| Decodes the token without signature verification. The token is not | ||
| validated against a JWKS endpoint — callers are responsible for | ||
| ensuring the token has already been verified by their framework or | ||
| middleware before using the extracted claims. | ||
|
|
||
| Args: | ||
| token: A JWT string, optionally prefixed with ``"Bearer "`` or ``"bearer "``. | ||
|
|
||
| Returns: | ||
| IASClaims with all present token claims populated. Absent standard | ||
| claims are None. Unrecognised claims are collected in | ||
| ``custom_attributes``. | ||
|
|
||
| Raises: | ||
| IASTokenError: If the token is malformed and cannot be decoded. | ||
|
|
||
| Example: | ||
| ```python | ||
| from sap_cloud_sdk.ias import parse_token | ||
|
|
||
| claims = parse_token(request.headers["Authorization"]) | ||
| print(claims.user_uuid) # global user ID | ||
| print(claims.custom_attributes) # any non-standard claims | ||
| ``` | ||
| """ | ||
| raw = token.removeprefix("Bearer ").removeprefix("bearer ").strip() | ||
|
|
||
| try: | ||
| payload: dict = jwt.decode( | ||
| raw, | ||
| options={"verify_signature": False}, | ||
| algorithms=["RS256", "ES256", "HS256"], | ||
| ) | ||
| except jwt.exceptions.DecodeError as e: | ||
| raise IASTokenError(f"Failed to decode IAS token: {e}") from e | ||
|
|
||
| return IASClaims( | ||
| app_tid=payload.get(_IASClaim.APP_TID), | ||
| at_hash=payload.get(_IASClaim.AT_HASH), | ||
| aud=payload.get(_IASClaim.AUD), | ||
| auth_time=payload.get(_IASClaim.AUTH_TIME), | ||
| azp=payload.get(_IASClaim.AZP), | ||
| email=payload.get(_IASClaim.EMAIL), | ||
| email_verified=payload.get(_IASClaim.EMAIL_VERIFIED), | ||
| exp=payload.get(_IASClaim.EXP), | ||
| family_name=payload.get(_IASClaim.FAMILY_NAME), | ||
| given_name=payload.get(_IASClaim.GIVEN_NAME), | ||
| groups=payload.get(_IASClaim.GROUPS), | ||
| ias_apis=payload.get(_IASClaim.IAS_APIS), | ||
| ias_iss=payload.get(_IASClaim.IAS_ISS), | ||
| iat=payload.get(_IASClaim.IAT), | ||
| iss=payload.get(_IASClaim.ISS), | ||
| jti=payload.get(_IASClaim.JTI), | ||
| middle_name=payload.get(_IASClaim.MIDDLE_NAME), | ||
| name=payload.get(_IASClaim.NAME), | ||
| nonce=payload.get(_IASClaim.NONCE), | ||
| preferred_username=payload.get(_IASClaim.PREFERRED_USERNAME), | ||
| sap_id_type=payload.get(_IASClaim.SAP_ID_TYPE), | ||
| scim_id=payload.get(_IASClaim.SCIM_ID), | ||
| sid=payload.get(_IASClaim.SID), | ||
| sub=payload.get(_IASClaim.SUB), | ||
| user_uuid=payload.get(_IASClaim.USER_UUID), | ||
| custom_attributes={ | ||
| k: v for k, v in payload.items() if k not in _KNOWN_CLAIM_VALUES | ||
| }, | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| """Exceptions for the IAS module.""" | ||
|
|
||
|
|
||
| class IASTokenError(Exception): | ||
| """Raised when an IAS JWT token cannot be parsed.""" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| # IAS User Guide | ||
|
|
||
| This module provides utilities for working with SAP Identity Authentication Service (IAS). | ||
|
|
||
| ## Import | ||
|
|
||
| ```python | ||
| from sap_cloud_sdk.ias import parse_token, IASClaims, IASTokenError | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Token Parsing | ||
|
|
||
| Use `parse_token` to decode an IAS JWT token into a typed dataclass. All standard IAS claims are mapped to named attributes. | ||
|
|
||
| > **Note:** `parse_token` does **not** verify the token signature. Validate the token against the IAS JWKS endpoint in your framework or middleware before using the extracted claims for authorization decisions. | ||
|
|
||
| ```python | ||
| from sap_cloud_sdk.ias import parse_token | ||
|
|
||
| claims = parse_token(request.headers["Authorization"]) # accepts "Bearer <token>" or raw token | ||
|
|
||
| print(claims.app_tid) # tenant ID (multitenant scenarios) | ||
| print(claims.scim_id) # SCIM-based user ID in SAP Cloud Identity Services | ||
| print(claims.sub) # OIDC subject identifier | ||
| print(claims.email) # user email (when email scope was requested) | ||
| ``` | ||
|
|
||
| ### Claims Reference | ||
|
|
||
| All fields on `IASClaims` are `Optional` — claims absent from the token are `None`. | ||
|
|
||
| | Attribute | Claim | Description | | ||
| |----------------------|----------------------|-----------------------------------------------------------------------------------------------| | ||
| | `app_tid` | `app_tid` | SAP tenant of the application. Present in multitenant scenarios. | | ||
| | `at_hash` | `at_hash` | Hash of the access token, used to bind the ID token to an access token. | | ||
| | `aud` | `aud` | Audience — recipient(s) of the token. `str` or `List[str]`. | | ||
| | `auth_time` | `auth_time` | Time of user authentication (seconds since Unix epoch). | | ||
| | `azp` | `azp` | Authorized party — client ID to which the ID token was issued. | | ||
| | `email` | `email` | User email address. Requires `email` scope. | | ||
| | `email_verified` | `email_verified` | Whether the email address has been verified. Requires `email` scope. | | ||
| | `exp` | `exp` | Expiration time (seconds since Unix epoch). | | ||
| | `family_name` | `family_name` | Surname. Requires `profile` scope. | | ||
| | `given_name` | `given_name` | Given name. Requires `profile` scope. | | ||
| | `groups` | `groups` | Groups the user belongs to. Requires `groups` scope. | | ||
| | `ias_apis` | `ias_apis` | SAP API permission groups, or a fixed value when all APIs are consumed. `str` or `List[str]`. | | ||
| | `ias_iss` | `ias_iss` | SAP tenant identifier — stable even when using a custom domain. | | ||
| | `iat` | `iat` | Issued-at time (seconds since Unix epoch). | | ||
| | `iss` | `iss` | Issuer URL, e.g. `https://<tenant>.accounts.ondemand.com`. | | ||
| | `jti` | `jti` | Unique JWT identifier, used to prevent replay attacks. Requires `profile` scope. | | ||
| | `middle_name` | `middle_name` | Middle name of the user. | | ||
| | `name` | `name` | Full display name. | | ||
| | `nonce` | `nonce` | Session nonce to mitigate replay attacks. | | ||
| | `preferred_username` | `preferred_username` | Human-readable username. | | ||
| | `sap_id_type` | `sap_id_type` | Token type: `"user"` for user credentials, `"app"` for application credentials. | | ||
| | `scim_id` | `scim_id` | User's SCIM ID in SAP Cloud Identity Services. | | ||
| | `sid` | `sid` | Session ID for tracking a user session across applications. | | ||
| | `sub` | `sub` | Subject — unique identifier for the user, scoped to the issuer. | | ||
| | `user_uuid` | `user_uuid` | SAP claim identifying the global user ID. | | ||
| | `custom_attributes` | *(any)* | Claims not in the standard IAS set. Always a `dict`, empty if no custom claims are present. | | ||
|
|
||
| ### Custom Attributes | ||
|
|
||
| Any claim not in the standard IAS set lands in `custom_attributes` as a plain dict, so nothing is silently dropped: | ||
|
|
||
| ```python | ||
| claims = parse_token(token) | ||
| print(claims.custom_attributes) # {"my_app_claim": "value", ...} | ||
| ``` | ||
|
|
||
|
|
||
| #### With Telemetry | ||
|
|
||
| ```python | ||
| from sap_cloud_sdk.ias import parse_token | ||
| from sap_cloud_sdk.core.telemetry import set_tenant_id, add_span_attribute | ||
|
|
||
| claims = parse_token(token) | ||
| set_tenant_id(claims.app_tid or "") | ||
| add_span_attribute("enduser.id", claims.scim_id or claims.sub or "") | ||
| ``` |
Empty file.
Empty file.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.