-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Shenghong/NCKU-70-login-module (#24)
* feat: add login module - Login, Logout - Google oauth - JWT token * modify: user cookie to get file uploader id * modify: fix robot comments * chore: template.env * docs: add comments to environment variables template Add descriptive comments to template.env to improve configuration clarity
- Loading branch information
1 parent
b113cb6
commit efcf967
Showing
16 changed files
with
1,060 additions
and
106 deletions.
There are no files selected for viewing
This file contains 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,37 @@ | ||
from abc import ABC, abstractmethod | ||
from typing import Dict, Protocol, Tuple | ||
|
||
from fastapi import Response | ||
from sqlalchemy.orm import Session | ||
|
||
|
||
class OAuthProvider(ABC): | ||
@abstractmethod | ||
def create_auth_flow(self) -> Tuple[str, str]: | ||
pass | ||
|
||
@abstractmethod | ||
def process_oauth_callback( | ||
self, | ||
db: Session, | ||
code: str, | ||
) -> Dict: | ||
pass | ||
|
||
@abstractmethod | ||
def verify_email_domain(self, email: str) -> bool: | ||
pass | ||
|
||
class TokenService(Protocol): | ||
def create_token(self, data: Dict) -> str: | ||
pass | ||
|
||
def verify_token(self, token: str) -> Dict: | ||
pass | ||
|
||
class CookieService(Protocol): | ||
def set_auth_cookie(self, response: Response, user_id: str) -> None: | ||
pass | ||
|
||
def clear_auth_cookie(self, response: Response) -> None: | ||
pass |
This file contains 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,137 @@ | ||
from typing import Dict, Tuple | ||
|
||
from fastapi import HTTPException | ||
from google.auth.transport import requests | ||
from google.oauth2 import id_token | ||
from google_auth_oauthlib.flow import Flow | ||
from sqlalchemy.orm import Session | ||
|
||
from core.config import get_settings | ||
from core.interfaces import OAuthProvider | ||
from crud.user import UserCRUD | ||
|
||
|
||
class GoogleAuthProvider(OAuthProvider): | ||
def __init__(self): | ||
settings = get_settings() | ||
self.client_config = { | ||
"web": { | ||
"client_id": settings.google_client_id, | ||
"client_secret": settings.google_client_secret, | ||
"auth_uri": "https://accounts.google.com/o/oauth2/auth", | ||
"token_uri": "https://oauth2.googleapis.com/token", | ||
"redirect_uris": [settings.google_redirect_uri] | ||
} | ||
} | ||
self.scopes = [ | ||
'openid', | ||
'https://www.googleapis.com/auth/userinfo.email', | ||
'https://www.googleapis.com/auth/userinfo.profile' | ||
] | ||
self.user_crud = UserCRUD() | ||
|
||
def create_auth_flow(self) -> Tuple[str, str]: | ||
try: | ||
settings = get_settings() | ||
flow = Flow.from_client_config( | ||
self.client_config, | ||
scopes=self.scopes | ||
) | ||
|
||
flow.redirect_uri = settings.google_redirect_uri | ||
|
||
authorization_url, state = flow.authorization_url( | ||
access_type='offline', | ||
include_granted_scopes='true', | ||
prompt='consent', | ||
hd=settings.google_allowed_domains | ||
) | ||
|
||
return authorization_url, state | ||
|
||
except Exception as e: | ||
raise HTTPException( | ||
status_code=500, | ||
detail=f"Failed to create authorization flow: {str(e)}" | ||
) | ||
|
||
def process_oauth_callback( | ||
self, | ||
db: Session, | ||
code: str, | ||
) -> Dict: | ||
try: | ||
settings = get_settings() | ||
flow = Flow.from_client_config( | ||
self.client_config, | ||
scopes=self.scopes | ||
) | ||
flow.redirect_uri = settings.google_redirect_uri | ||
|
||
flow.fetch_token(code=code) | ||
credentials = flow.credentials | ||
|
||
user_info = id_token.verify_oauth2_token( | ||
credentials.id_token, | ||
requests.Request(), | ||
settings.google_client_id, | ||
clock_skew_in_seconds=60 | ||
) | ||
|
||
if not self.verify_email_domain(user_info['email']): | ||
raise HTTPException( | ||
status_code=401, | ||
detail='Only NCKU accounts are allowed to use this service.' | ||
) | ||
|
||
google_user_info = { | ||
'user_id': user_info['sub'], | ||
'email': user_info['email'], | ||
'username': user_info['name'], | ||
'avatar': user_info.get('picture', '') | ||
} | ||
|
||
user_response = self.user_crud.get_or_create_user( | ||
db=db, | ||
google_user_info=google_user_info | ||
) | ||
|
||
return { | ||
'user': user_response.data.model_dump(), | ||
'access_token': credentials.token, | ||
'refresh_token': credentials.refresh_token, | ||
'token_expiry': credentials.expiry.timestamp() if credentials.expiry else None | ||
} | ||
|
||
except Exception: | ||
raise HTTPException( | ||
status_code=400, | ||
detail='Failed to process OAuth callback' | ||
) | ||
|
||
def verify_email_domain(self, email: str) -> bool: | ||
settings = get_settings() | ||
return email.endswith(f'@{settings.google_allowed_domains}') | ||
|
||
def _verify_oauth_token(self, code: str) -> Dict: | ||
settings = get_settings() | ||
flow = Flow.from_client_config( | ||
self.client_config, | ||
scopes=self.scopes | ||
) | ||
flow.redirect_uri = settings.google_redirect_uri | ||
flow.fetch_token(code=code) | ||
|
||
return id_token.verify_oauth2_token( | ||
flow.credentials.id_token, | ||
requests.Request(), | ||
settings.google_client_id | ||
) | ||
|
||
def _extract_user_info(self, id_info: Dict) -> Dict: | ||
return { | ||
'id': id_info['sub'], | ||
'email': id_info['email'], | ||
'name': id_info['name'], | ||
'picture': id_info.get('picture') | ||
} |
This file contains 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
Oops, something went wrong.