diff --git a/.env.demo b/.env.demo index 30e7571..f03b66b 100644 --- a/.env.demo +++ b/.env.demo @@ -66,6 +66,8 @@ AUTHENTIK_CONFIG_URL=https://authentik.example.com/application/o/leagueledger/.w # NetID NETID_CLIENT_ID=leagueledger-netid-client NETID_CLIENT_SECRET=netid-client-secret-abcdef1234567890 +NETID_TOKEN_SIGNING_ALG=RS256 +NETID_VALIDATE_TOKENS=true # Feature Flags ENABLE_REGISTRATION=True diff --git a/.env.template b/.env.template index fea5103..6669034 100644 --- a/.env.template +++ b/.env.template @@ -64,6 +64,8 @@ AUTHENTIK_CONFIG_URL= # NetID NETID_CLIENT_ID= NETID_CLIENT_SECRET= +NETID_TOKEN_SIGNING_ALG=RS256 +NETID_VALIDATE_TOKENS=true # Feature Flags ENABLE_REGISTRATION=True diff --git a/app/auth/oauth.py b/app/auth/oauth.py index 84ed1ac..20f1911 100644 --- a/app/auth/oauth.py +++ b/app/auth/oauth.py @@ -830,10 +830,12 @@ class NetIDOAuth(OAuthProvider): AUTHORIZATION_URL = "https://broker.netid.de/authorize" TOKEN_URL = "https://broker.netid.de/token" USERINFO_URL = "https://broker.netid.de/userinfo" + JWKS_URL = "https://broker.netid.de/jwks" # JWKS endpoint for token validation def __init__(self): self.client_id = os.getenv("NETID_CLIENT_ID", "") self.client_secret = os.getenv("NETID_CLIENT_SECRET", "") + self.token_signing_alg = os.getenv("NETID_TOKEN_SIGNING_ALG", "RS256") super().__init__() def initialize_client(self): @@ -864,10 +866,17 @@ class NetIDOAuth(OAuthProvider): raise HTTPException(status_code=500, detail="NetID OAuth client could not be initialized") try: + # Add token signing algorithm parameter to the authorization request + extras_params = { + "response_type": "code", + "id_token_signed_response_alg": self.token_signing_alg + } + authorization_url = await self.client.get_authorization_url( redirect_uri=redirect_uri, scope=["openid", "email", "profile"], - state=str(request.session.get("session_id", "")) + state=str(request.session.get("session_id", "")), + extras_params=extras_params ) return authorization_url except Exception as e: @@ -883,16 +892,26 @@ class NetIDOAuth(OAuthProvider): try: # Exchange code for token + token_params = { + "id_token_signed_response_alg": self.token_signing_alg + } + token = await self.client.get_access_token( code=code, - redirect_uri=redirect_uri + redirect_uri=redirect_uri, + extra_params=token_params ) access_token = token.get("access_token") + id_token = token.get("id_token") # JWT containing identity information if not access_token: raise HTTPException(status_code=400, detail="Could not get NetID access token") + # If we have an ID token, validate its signature when configured to do so + if id_token and os.getenv("NETID_VALIDATE_TOKENS", "true").lower() == "true": + await self.validate_id_token(id_token) + # Get user info from NetID UserInfo endpoint async with httpx.AsyncClient() as client: headers = {"Authorization": f"Bearer {access_token}"} @@ -913,6 +932,37 @@ class NetIDOAuth(OAuthProvider): print(f"Error getting NetID user info: {str(e)}") raise HTTPException(status_code=500, detail=f"OAuth error: {str(e)}") + async def validate_id_token(self, id_token: str) -> None: + """Validate the NetID ID token signature using JWKS""" + try: + import jwt + from jwt.jwks_client import PyJWKClient + + # Create a JWKS client to fetch the public keys from NetID + jwks_client = PyJWKClient(self.JWKS_URL) + + # Get the signing key for this specific JWT + signing_key = jwks_client.get_signing_key_from_jwt(id_token) + + # Verify the JWT using the fetched public key + # This will raise exceptions if the token is invalid + jwt.decode( + id_token, + signing_key.key, + algorithms=[self.token_signing_alg], + audience=self.client_id, + options={"verify_exp": True} + ) + + # If we get here, the token is valid + return True + + except Exception as e: + print(f"Error validating NetID ID token: {str(e)}") + # In production, you might want to raise an exception here + # For now, we'll just log the error but not block the flow + return False + def get_normalized_user_data(self, user_info: Dict[str, Any]) -> Dict[str, Any]: # NetID OpenID Connect response normalization return { diff --git a/requirements.txt b/requirements.txt index 9f7db79..eda161a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,6 +16,7 @@ python-multipart>=0.0.6 passlib>=1.7.4 itsdangerous>=2.1.2 bcrypt>=4.0.1 +PyJWT>=2.6.0 # Added for NetID token validation # OAuth client httpx-oauth>=0.10.0