Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions AUTHENTICATION.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -11,32 +11,39 @@ 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": "<JWT>", "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": "<JWT>", "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": "<JWT>", "token_type": "bearer" }`.

## 4. Using the Token in Swagger UI

- Open `/docs` (Swagger UI).
- Click the "Authorize" button.
- **Paste only the JWT token** (do NOT include the `Bearer ` prefix).
- 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: <your_token>
```

## 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 <token>`) 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.

---

Expand Down
18 changes: 17 additions & 1 deletion app/api/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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)
24 changes: 24 additions & 0 deletions app/config/google.py
Original file line number Diff line number Diff line change
@@ -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",
)
44 changes: 44 additions & 0 deletions app/services/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
)
4 changes: 4 additions & 0 deletions app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down