From 57a93a45fa023c752e2115fc8e944ed56de3b30b Mon Sep 17 00:00:00 2001 From: durgeshninave9 Date: Wed, 30 Jul 2025 00:02:36 +0530 Subject: [PATCH] feat(auth): added google authentication --- AUTHENTICATION.md | 17 +++++++++++----- app/api/routes/auth.py | 18 ++++++++++++++++- app/config/google.py | 24 +++++++++++++++++++++++ app/services/auth.py | 44 ++++++++++++++++++++++++++++++++++++++++++ app/settings.py | 4 ++++ 5 files changed, 101 insertions(+), 6 deletions(-) create mode 100644 app/config/google.py diff --git a/AUTHENTICATION.md b/AUTHENTICATION.md index 88d398e..c7268c6 100644 --- a/AUTHENTICATION.md +++ b/AUTHENTICATION.md @@ -1,6 +1,6 @@ # Authentication Guide -This project supports authentication via both username/password and GitHub OAuth. All authenticated API endpoints require a valid JWT Bearer token. +This project supports authentication via username/password, GitHub OAuth, and Google OAuth. All authenticated API endpoints require a valid JWT Bearer token. ## 1. Username/Password Login @@ -11,13 +11,20 @@ This project supports authentication via both username/password and GitHub OAuth - `POST /api/v1/users/login` with form data `username` and `password`. - The response will include `{ "access_token": "", "token_type": "bearer" }`. + ## 2. GitHub OAuth Login 1. Go to `/api/v1/auth/github/login` in your browser. 2. Authorize the app on GitHub. 3. You will be redirected back and receive a JSON response with `{ "access_token": "", "token_type": "bearer" }`. -## 3. Using the Token in Swagger UI +## 3. Google OAuth Login + +1. Go to `/api/v1/auth/google/login` in your browser. +2. Authorize the app with your Google account. +3. You will be redirected back and receive a JSON response with `{ "access_token": "", "token_type": "bearer" }`. + +## 4. Using the Token in Swagger UI - Open `/docs` (Swagger UI). - Click the "Authorize" button. @@ -25,18 +32,18 @@ This project supports authentication via both username/password and GitHub OAuth - Click "Authorize". - You can now access all authenticated endpoints. -## 4. Using the Token in API Clients (e.g., Postman) +## 5. Using the Token in API Clients (e.g., Postman) - Set the `Authorization` header: ``` Authorization: ``` -## 5. Notes +## 6. Notes - The same JWT token format is used for both login methods. - If you get a credentials error, ensure you are pasting only the token (not `Bearer `) in Swagger UI. -- For GitHub login, the user is auto-registered on first login if not already present. +- For GitHub and Google login, the user is auto-registered on first login if not already present. --- diff --git a/app/api/routes/auth.py b/app/api/routes/auth.py index 719bf3f..c37b42c 100644 --- a/app/api/routes/auth.py +++ b/app/api/routes/auth.py @@ -4,8 +4,9 @@ from app.config.database import get_db from app.config.github import github +from app.config.google import google from app.schemas.users import Token -from app.services.auth import login_github_user, login_user +from app.services.auth import login_github_user, login_google_user, login_user router = APIRouter() @@ -26,7 +27,22 @@ async def login_via_github(request: Request): return await github.authorize_redirect(request, redirect_uri) +# Google OAuth login route +@router.get("/google/login") +async def login_via_google(request: Request): + """Initiates the Google OAuth login flow.""" + redirect_uri = request.url_for("auth_google_callback") + return await google.authorize_redirect(request, redirect_uri) + + @router.get("/github/callback") async def auth_github_callback(request: Request, db: Session = Depends(get_db)): """Handles the GitHub OAuth callback and logs in or registers the user.""" return await login_github_user(request, db) + + +# Google OAuth callback route +@router.get("/google/callback") +async def auth_google_callback(request: Request, db: Session = Depends(get_db)): + """Handles the Google OAuth callback and logs in or registers the user.""" + return await login_google_user(request, db) diff --git a/app/config/google.py b/app/config/google.py new file mode 100644 index 0000000..55328e3 --- /dev/null +++ b/app/config/google.py @@ -0,0 +1,24 @@ +from authlib.integrations.starlette_client import OAuth +from starlette.config import Config + +from app.settings import settings + +oauth = OAuth( + Config( + environ={ + "GOOGLE_CLIENT_ID": settings.GOOGLE_CLIENT_ID, + "GOOGLE_CLIENT_SECRET": settings.GOOGLE_CLIENT_SECRET, + } + ) +) + +google = oauth.register( + name="google", + client_id=settings.GOOGLE_CLIENT_ID, + client_secret=settings.GOOGLE_CLIENT_SECRET, + access_token_url="https://oauth2.googleapis.com/token", + authorize_url="https://accounts.google.com/o/oauth2/v2/auth", + api_base_url="https://www.googleapis.com/oauth2/v2/", + client_kwargs={"scope": "openid email profile"}, + server_metadata_url="https://accounts.google.com/.well-known/openid-configuration", +) diff --git a/app/services/auth.py b/app/services/auth.py index f3930a7..b2ec8ea 100644 --- a/app/services/auth.py +++ b/app/services/auth.py @@ -5,6 +5,7 @@ from fastapi.responses import JSONResponse from app.config.github import github +from app.config.google import google from app.models.user import User from app.schemas.users import AuthType, UserCreate from app.services.user import create_user @@ -46,6 +47,21 @@ def create_github_user(profile, email: str, db): return user +def create_google_user(profile, email: str, db): + """Create a new user from Google profile data.""" + name = profile.get("name") or profile.get("email") + provider_id = str(profile.get("id")) + user_in = UserCreate( + email=email, + name=name, + is_active=True, + provider_id=provider_id, + auth_type=AuthType.google, + ) + user = create_user(db, user_in) + return user + + async def login_github_user(request, db): """Login user via GitHub OAuth. Returns an access token if successful. Raises HTTPException if authentication fails.""" try: @@ -87,3 +103,31 @@ async def login_github_user(request, db): }, status_code=400, ) + + +async def login_google_user(request, db): + """Login user via Google OAuth. Returns an access token if successful. Raises HTTPException if authentication fails.""" + try: + token = await google.authorize_access_token(request) + resp = await google.get("userinfo", token=token) + profile = resp.json() + email = profile.get("email") + if not email: + raise HTTPException( + status_code=400, detail="Google account has no accessible email." + ) + + user = db.query(User).filter(User.email == email).first() + if not user: + user = create_google_user(profile, email, db) + + access_token = create_access_token({"sub": str(user.id), "email": user.email}) + return JSONResponse({"access_token": access_token, "token_type": "bearer"}) + except OAuthError as e: + logging.error("OAuthError occurred during Google authentication", exc_info=e) + return JSONResponse( + { + "error": "An error occurred during authentication. Please try again later." + }, + status_code=400, + ) diff --git a/app/settings.py b/app/settings.py index e0f42e6..64b9ffb 100644 --- a/app/settings.py +++ b/app/settings.py @@ -35,6 +35,10 @@ class Settings(BaseSettings): GITHUB_CLIENT_ID: str GITHUB_CLIENT_SECRET: str + # google + GOOGLE_CLIENT_ID: str + GOOGLE_CLIENT_SECRET: str + class Config: """Configuration for the settings."""