feat: Enhance NetID OAuth integration with token validation and configuration options
This commit is contained in:
@@ -66,6 +66,8 @@ AUTHENTIK_CONFIG_URL=https://authentik.example.com/application/o/leagueledger/.w
|
|||||||
# NetID
|
# NetID
|
||||||
NETID_CLIENT_ID=leagueledger-netid-client
|
NETID_CLIENT_ID=leagueledger-netid-client
|
||||||
NETID_CLIENT_SECRET=netid-client-secret-abcdef1234567890
|
NETID_CLIENT_SECRET=netid-client-secret-abcdef1234567890
|
||||||
|
NETID_TOKEN_SIGNING_ALG=RS256
|
||||||
|
NETID_VALIDATE_TOKENS=true
|
||||||
|
|
||||||
# Feature Flags
|
# Feature Flags
|
||||||
ENABLE_REGISTRATION=True
|
ENABLE_REGISTRATION=True
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ AUTHENTIK_CONFIG_URL=
|
|||||||
# NetID
|
# NetID
|
||||||
NETID_CLIENT_ID=
|
NETID_CLIENT_ID=
|
||||||
NETID_CLIENT_SECRET=
|
NETID_CLIENT_SECRET=
|
||||||
|
NETID_TOKEN_SIGNING_ALG=RS256
|
||||||
|
NETID_VALIDATE_TOKENS=true
|
||||||
|
|
||||||
# Feature Flags
|
# Feature Flags
|
||||||
ENABLE_REGISTRATION=True
|
ENABLE_REGISTRATION=True
|
||||||
|
|||||||
+52
-2
@@ -830,10 +830,12 @@ class NetIDOAuth(OAuthProvider):
|
|||||||
AUTHORIZATION_URL = "https://broker.netid.de/authorize"
|
AUTHORIZATION_URL = "https://broker.netid.de/authorize"
|
||||||
TOKEN_URL = "https://broker.netid.de/token"
|
TOKEN_URL = "https://broker.netid.de/token"
|
||||||
USERINFO_URL = "https://broker.netid.de/userinfo"
|
USERINFO_URL = "https://broker.netid.de/userinfo"
|
||||||
|
JWKS_URL = "https://broker.netid.de/jwks" # JWKS endpoint for token validation
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.client_id = os.getenv("NETID_CLIENT_ID", "")
|
self.client_id = os.getenv("NETID_CLIENT_ID", "")
|
||||||
self.client_secret = os.getenv("NETID_CLIENT_SECRET", "")
|
self.client_secret = os.getenv("NETID_CLIENT_SECRET", "")
|
||||||
|
self.token_signing_alg = os.getenv("NETID_TOKEN_SIGNING_ALG", "RS256")
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def initialize_client(self):
|
def initialize_client(self):
|
||||||
@@ -864,10 +866,17 @@ class NetIDOAuth(OAuthProvider):
|
|||||||
raise HTTPException(status_code=500, detail="NetID OAuth client could not be initialized")
|
raise HTTPException(status_code=500, detail="NetID OAuth client could not be initialized")
|
||||||
|
|
||||||
try:
|
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(
|
authorization_url = await self.client.get_authorization_url(
|
||||||
redirect_uri=redirect_uri,
|
redirect_uri=redirect_uri,
|
||||||
scope=["openid", "email", "profile"],
|
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
|
return authorization_url
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -883,16 +892,26 @@ class NetIDOAuth(OAuthProvider):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Exchange code for token
|
# Exchange code for token
|
||||||
|
token_params = {
|
||||||
|
"id_token_signed_response_alg": self.token_signing_alg
|
||||||
|
}
|
||||||
|
|
||||||
token = await self.client.get_access_token(
|
token = await self.client.get_access_token(
|
||||||
code=code,
|
code=code,
|
||||||
redirect_uri=redirect_uri
|
redirect_uri=redirect_uri,
|
||||||
|
extra_params=token_params
|
||||||
)
|
)
|
||||||
|
|
||||||
access_token = token.get("access_token")
|
access_token = token.get("access_token")
|
||||||
|
id_token = token.get("id_token") # JWT containing identity information
|
||||||
|
|
||||||
if not access_token:
|
if not access_token:
|
||||||
raise HTTPException(status_code=400, detail="Could not get NetID 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
|
# Get user info from NetID UserInfo endpoint
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
headers = {"Authorization": f"Bearer {access_token}"}
|
headers = {"Authorization": f"Bearer {access_token}"}
|
||||||
@@ -913,6 +932,37 @@ class NetIDOAuth(OAuthProvider):
|
|||||||
print(f"Error getting NetID user info: {str(e)}")
|
print(f"Error getting NetID user info: {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail=f"OAuth error: {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]:
|
def get_normalized_user_data(self, user_info: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
# NetID OpenID Connect response normalization
|
# NetID OpenID Connect response normalization
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ python-multipart>=0.0.6
|
|||||||
passlib>=1.7.4
|
passlib>=1.7.4
|
||||||
itsdangerous>=2.1.2
|
itsdangerous>=2.1.2
|
||||||
bcrypt>=4.0.1
|
bcrypt>=4.0.1
|
||||||
|
PyJWT>=2.6.0 # Added for NetID token validation
|
||||||
|
|
||||||
# OAuth client
|
# OAuth client
|
||||||
httpx-oauth>=0.10.0
|
httpx-oauth>=0.10.0
|
||||||
|
|||||||
Reference in New Issue
Block a user