A ``TokenParser`` to use an OIDC server to retrieve the user details. Server settings are retrieved from the ``auth`` configuration of the Feature store. Incoming tokens that contain a ``kubernetes.io`` claim (i.e. Kubernetes service-account tokens) are handled via a lightweight To
| 21 | |
| 22 | |
| 23 | class OidcTokenParser(TokenParser): |
| 24 | """ |
| 25 | A ``TokenParser`` to use an OIDC server to retrieve the user details. |
| 26 | Server settings are retrieved from the ``auth`` configuration of the Feature store. |
| 27 | |
| 28 | Incoming tokens that contain a ``kubernetes.io`` claim (i.e. Kubernetes |
| 29 | service-account tokens) are handled via a lightweight TokenReview that |
| 30 | extracts only the namespace — no RBAC queries needed. All other tokens |
| 31 | follow the standard OIDC/Keycloak JWKS validation path. |
| 32 | """ |
| 33 | |
| 34 | _auth_config: OidcAuthConfig |
| 35 | |
| 36 | def __init__(self, auth_config: OidcAuthConfig): |
| 37 | self._auth_config = auth_config |
| 38 | self.oidc_discovery_service = OIDCDiscoveryService( |
| 39 | self._auth_config.auth_discovery_url, |
| 40 | verify_ssl=self._auth_config.verify_ssl, |
| 41 | ca_cert_path=self._auth_config.ca_cert_path, |
| 42 | ) |
| 43 | self._k8s_auth_api = None |
| 44 | |
| 45 | async def _validate_token(self, access_token: str): |
| 46 | """ |
| 47 | Validate the token extracted from the header of the user request against the OAuth2 server. |
| 48 | """ |
| 49 | # FastAPI's OAuth2AuthorizationCodeBearer requires a Request type but actually uses only the headers field |
| 50 | # https://github.com/tiangolo/fastapi/blob/eca465f4c96acc5f6a22e92fd2211675ca8a20c8/fastapi/security/oauth2.py#L380 |
| 51 | request = Mock(spec=Request) |
| 52 | request.headers = {"Authorization": f"Bearer {access_token}"} |
| 53 | |
| 54 | oauth_2_scheme = OAuth2AuthorizationCodeBearer( |
| 55 | tokenUrl=self.oidc_discovery_service.get_token_url(), |
| 56 | authorizationUrl=self.oidc_discovery_service.get_authorization_url(), |
| 57 | refreshUrl=self.oidc_discovery_service.get_refresh_url(), |
| 58 | ) |
| 59 | |
| 60 | await oauth_2_scheme(request=request) |
| 61 | |
| 62 | @staticmethod |
| 63 | def _extract_username_or_raise_error(data: dict) -> str: |
| 64 | """Extract the username from the decoded JWT. Raises if missing — identity is mandatory. |
| 65 | |
| 66 | Checks ``preferred_username`` first (Keycloak default), then falls back |
| 67 | to ``upn`` (Azure AD / Entra ID). |
| 68 | """ |
| 69 | if "preferred_username" in data: |
| 70 | return data["preferred_username"] |
| 71 | if "upn" in data: |
| 72 | return data["upn"] |
| 73 | raise AuthenticationError( |
| 74 | "Missing preferred_username or upn field in access token." |
| 75 | ) |
| 76 | |
| 77 | @staticmethod |
| 78 | def _extract_claim(data: dict, *keys: str, expected_type: type = list): |
| 79 | """Walk *keys* into *data* and return the leaf value, or ``expected_type()`` if any key is missing or the wrong type.""" |
| 80 | node = data |
no outgoing calls