feat: Add NetID OAuth provider integration and update login UI

- Implemented NetID OAuth provider in app/auth/oauth.py
- Added styles for NetID login button in app/static/css/styles.css
- Updated login.html to include NetID login option with custom button
- Modified docker-compose.yml to include NetID client credentials
- Enhanced social login setup documentation to include NetID instructions
- Created demo environment configuration file (.env.demo) for easier setup
- Updated .env.template to include NetID configuration options
- Added comprehensive README.md with project features and setup instructions
This commit is contained in:
Christian Krakau-Louis
2025-04-15 14:28:14 +02:00
parent 6306abf6d9
commit 6d796c95ff
8 changed files with 863 additions and 179 deletions
+108
View File
@@ -818,6 +818,113 @@ class LinkedInOAuth(OAuthProvider):
"raw": user_info
}
class NetIDOAuth(OAuthProvider):
"""NetID OAuth provider implementation"""
provider_id = "netid"
display_name = "netID"
icon_class = "fas fa-check" # Could be replaced with a custom netID icon class if available
button_color = "#76b82a"
# NetID OIDC endpoints
AUTHORIZATION_URL = "https://broker.netid.de/authorize"
TOKEN_URL = "https://broker.netid.de/token"
USERINFO_URL = "https://broker.netid.de/userinfo"
def __init__(self):
self.client_id = os.getenv("NETID_CLIENT_ID", "")
self.client_secret = os.getenv("NETID_CLIENT_SECRET", "")
super().__init__()
def initialize_client(self):
if not self.client_id or not self.client_secret:
self.client = None
return
try:
# Create a custom OAuth2 client for NetID OpenID Connect
from httpx_oauth.oauth2 import OAuth2
self.client = OAuth2(
client_id=self.client_id,
client_secret=self.client_secret,
authorize_endpoint=self.AUTHORIZATION_URL,
access_token_endpoint=self.TOKEN_URL,
refresh_token_endpoint=self.TOKEN_URL,
base_scopes=["openid", "email", "profile"]
)
except Exception as e:
print(f"Error initializing NetID OAuth client: {str(e)}")
self.client = None
async def get_login_url(self, request: Request, redirect_uri: str) -> str:
if not self.client:
self.initialize_client()
if not self.client:
raise HTTPException(status_code=500, detail="NetID OAuth client could not be initialized")
try:
authorization_url = await self.client.get_authorization_url(
redirect_uri=redirect_uri,
scope=["openid", "email", "profile"],
state=str(request.session.get("session_id", ""))
)
return authorization_url
except Exception as e:
print(f"Error getting NetID authorization URL: {str(e)}")
raise HTTPException(status_code=500, detail=f"OAuth error: {str(e)}")
async def get_user_info(self, request: Request, redirect_uri: str, code: str) -> Dict[str, Any]:
if not self.client:
self.initialize_client()
if not self.client:
raise HTTPException(status_code=500, detail="NetID OAuth client could not be initialized")
try:
# Exchange code for token
token = await self.client.get_access_token(
code=code,
redirect_uri=redirect_uri
)
access_token = token.get("access_token")
if not access_token:
raise HTTPException(status_code=400, detail="Could not get NetID access token")
# Get user info from NetID UserInfo endpoint
async with httpx.AsyncClient() as client:
headers = {"Authorization": f"Bearer {access_token}"}
response = await client.get(
self.USERINFO_URL,
headers=headers
)
if response.status_code != 200:
raise HTTPException(status_code=500, detail=f"Error fetching NetID user info: {response.text}")
return response.json()
except GetAccessTokenError as e:
error_description = e.args[0]
raise HTTPException(status_code=400, detail=f"NetID OAuth error: {error_description}")
except Exception as e:
print(f"Error getting NetID user info: {str(e)}")
raise HTTPException(status_code=500, detail=f"OAuth error: {str(e)}")
def get_normalized_user_data(self, user_info: Dict[str, Any]) -> Dict[str, Any]:
# NetID OpenID Connect response normalization
return {
"id": user_info.get("sub", ""), # 'sub' is the standard OIDC subject identifier
"email": user_info.get("email"),
"name": f"{user_info.get('given_name', '')} {user_info.get('family_name', '')}".strip(),
"first_name": user_info.get("given_name"),
"last_name": user_info.get("family_name"),
"picture": None, # NetID might not provide profile picture
"raw": user_info
}
class OAuthManager:
"""
Manager class for handling multiple OAuth providers
@@ -836,6 +943,7 @@ class OAuthManager:
self.register_provider(MicrosoftOAuth())
self.register_provider(DiscordOAuth())
self.register_provider(LinkedInOAuth())
self.register_provider(NetIDOAuth()) # Register NetID provider
def register_provider(self, provider: OAuthProvider):
"""Register a new provider"""