Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
74d5cfc
config routes updated
vprashrex Mar 27, 2026
bd48eb8
code update
vprashrex Mar 27, 2026
10fd7c3
made query optional
vprashrex Mar 27, 2026
f73c22a
code updated
vprashrex Mar 27, 2026
0c7175b
added has_more functionality
vprashrex Mar 27, 2026
a363f45
refactor: update config CRUD methods to include query parameter and h…
vprashrex Mar 30, 2026
abd1fc8
Add projects-by-org endpoint and pagination for organizations list
vprashrex Mar 30, 2026
84ce2e8
Enhance organization validation: return 503 status code for inactive …
vprashrex Mar 31, 2026
6144d98
feat(*): google integration flow
Ayush8923 Mar 31, 2026
7f98414
Merge branch 'main' into feat/adding-query-params-to-config
Ayush8923 Mar 31, 2026
a7e9139
Merge branch 'feat/adding-query-params-to-config' of https://github.c…
Ayush8923 Mar 31, 2026
4cf576b
fix(*): update the js comment
Ayush8923 Mar 31, 2026
0b3b30e
fix(*): update the uv.lock
Ayush8923 Mar 31, 2026
6731b1d
Merge branch 'main' of https://github.com/ProjectTech4DevAI/kaapi-bac…
Ayush8923 Apr 1, 2026
6bb875c
fix(*): update the test cases
Ayush8923 Apr 1, 2026
3a1c9c8
fix(*): update test coverage
Ayush8923 Apr 1, 2026
b22588f
fix(*): update the test cases
Ayush8923 Apr 1, 2026
d1c9416
Merge branch 'main' into feat/google-integration-auth-flow
Ayush8923 Apr 4, 2026
5b8af37
fix(*): for the response used the APIResponses utils function
Ayush8923 Apr 4, 2026
b3eb1fd
fix(*): update the test cases
Ayush8923 Apr 4, 2026
8166675
Merge branch 'main' into feat/google-integration-auth-flow
Ayush8923 Apr 7, 2026
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
117 changes: 76 additions & 41 deletions backend/app/api/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@
import jwt
from fastapi import Depends, HTTPException, Request, status
from fastapi.security import APIKeyHeader, OAuth2PasswordBearer
from jwt.exceptions import InvalidTokenError
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
from pydantic import ValidationError
from sqlmodel import Session, select
from sqlmodel import Session

from app.core import security
from app.core.config import settings
from app.core.db import engine
from app.core.security import api_key_manager
from app.crud.organization import validate_organization
from app.crud.project import validate_project
from app.models import (
AuthContext,
Organization,
Project,
TokenPayload,
User,
)
Expand All @@ -35,57 +38,89 @@ def get_db() -> Generator[Session, None, None]:
TokenDep = Annotated[str, Depends(reusable_oauth2)]


def _authenticate_with_jwt(session: Session, token: str) -> AuthContext:
"""Validate a JWT token and return the authenticated user context."""
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
)
token_data = TokenPayload(**payload)
except ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired",
)
except (InvalidTokenError, ValidationError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate credentials",
)

# Reject refresh tokens — they should only be used at /auth/refresh
if token_data.type == "refresh":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Refresh tokens cannot be used for API access",
)

user = session.get(User, token_data.sub)
if not user:
raise HTTPException(status_code=404, detail="User not found")
if not user.is_active:
raise HTTPException(status_code=403, detail="Inactive user")

organization: Organization | None = None
project: Project | None = None

if token_data.org_id:
organization = validate_organization(session=session, org_id=token_data.org_id)
if token_data.project_id:
project = validate_project(session=session, project_id=token_data.project_id)

return AuthContext(user=user, organization=organization, project=project)


def get_auth_context(
request: Request,
session: SessionDep,
token: TokenDep,
api_key: Annotated[str, Depends(api_key_header)],
) -> AuthContext:
"""
Verify valid authentication (API Key or JWT token) and return authenticated user context.
Verify valid authentication (API Key, JWT token, or cookie) and return authenticated user context.
Returns AuthContext with user info, project_id, and organization_id.
Authorization logic should be handled in routes.

Authentication priority:
1. X-API-KEY header
2. Authorization: Bearer <token> header
3. access_token cookie
"""
# 1. Try X-API-KEY header
if api_key:
auth_context = api_key_manager.verify(session, api_key)
if not auth_context:
raise HTTPException(status_code=401, detail="Invalid API Key")

if not auth_context.user.is_active:
raise HTTPException(status_code=403, detail="Inactive user")

if not auth_context.organization.is_active:
raise HTTPException(status_code=403, detail="Inactive Organization")

if not auth_context.project.is_active:
raise HTTPException(status_code=403, detail="Inactive Project")

return auth_context

elif token:
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
)
token_data = TokenPayload(**payload)
except (InvalidTokenError, ValidationError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate credentials",
)

user = session.get(User, token_data.sub)
if not user:
raise HTTPException(status_code=404, detail="User not found")
if not user.is_active:
raise HTTPException(status_code=403, detail="Inactive user")

auth_context = AuthContext(
user=user,
)
return auth_context
if auth_context:
if not auth_context.user.is_active:
raise HTTPException(status_code=403, detail="Inactive user")

if not auth_context.organization.is_active:
raise HTTPException(status_code=403, detail="Inactive Organization")

if not auth_context.project.is_active:
raise HTTPException(status_code=403, detail="Inactive Project")

return auth_context

# 2. Try Authorization: Bearer <token> header
if token:
return _authenticate_with_jwt(session, token)

# 3. Try access_token cookie
cookie_token = request.cookies.get("access_token")
if cookie_token:
return _authenticate_with_jwt(session, cookie_token)

else:
raise HTTPException(status_code=401, detail="Invalid Authorization format")
raise HTTPException(status_code=401, detail="Invalid Authorization format")


AuthContextDep = Annotated[AuthContext, Depends(get_auth_context)]
22 changes: 22 additions & 0 deletions backend/app/api/docs/auth/google.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Google OAuth Authentication

Authenticate a user via Google Sign-In by verifying the Google ID token.

## Request

- **token** (required): The Google ID token obtained from the frontend Google Sign-In flow.

## Behavior

1. Verifies the Google ID token against Google's public keys and the configured `GOOGLE_CLIENT_ID`.
2. Extracts user information (email, name, picture) from the verified token.
3. Looks up the user by email in the database.
4. If the user exists and is active, generates a JWT access token.
5. Sets the access token as an **HTTP-only secure cookie** (`access_token`) in the response.
6. Returns the access token, user details, and Google profile information.

## Error Responses

- **400**: Invalid or expired Google token, or email not verified by Google.
- **401**: No account found for the Google email address.
- **403**: User account is inactive.
2 changes: 2 additions & 0 deletions backend/app/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
config,
doc_transformation_job,
documents,
google_auth,
login,
languages,
llm,
Expand Down Expand Up @@ -39,6 +40,7 @@
api_router.include_router(cron.router)
api_router.include_router(documents.router)
api_router.include_router(doc_transformation_job.router)
api_router.include_router(google_auth.router)
api_router.include_router(evaluations.router)
api_router.include_router(languages.router)
api_router.include_router(llm.router)
Expand Down
Loading
Loading