Skip to main content

Authentication

Serko Northsky uses Firebase Authentication for user identity management.

Overview

The authentication flow:

  1. User authenticates with Firebase (frontend)
  2. Firebase issues an ID token
  3. Frontend sends token in Authorization header
  4. Backend validates token with Firebase Admin SDK
  5. User identity is extracted from validated token

Frontend Authentication

import { getAuth, signInWithEmailAndPassword } from 'firebase/auth';

const auth = getAuth();

// Sign in
const userCredential = await signInWithEmailAndPassword(auth, email, password);
const idToken = await userCredential.user.getIdToken();

// Use token in API requests
const response = await fetch('/api/users/profile', {
headers: {
'Authorization': `Bearer ${idToken}`,
'Content-Type': 'application/json',
},
});

Backend Validation

The backend validates tokens using Firebase Admin SDK:

from firebase_admin import auth as firebase_auth

async def verify_firebase_token(token: str) -> dict[str, Any]:
"""Verify Firebase ID token and return decoded claims."""
try:
decoded = firebase_auth.verify_id_token(token)
return decoded
except firebase_auth.InvalidIdTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
except firebase_auth.ExpiredIdTokenError:
raise HTTPException(status_code=401, detail="Token expired")

Authentication Dependency

Routes requiring authentication use the require_auth dependency:

from serko_northsky.api.v1.middlewares.auth_middleware import require_auth

@router.get("/profile")
async def get_profile(
auth: Annotated[dict[str, Any], Depends(require_auth)],
user_service: Annotated[BaseUserService, Depends(dependencies.get_user_service)],
) -> GetUserProfileResponse:
user = await user_service.get_by_auth_id(auth["uid"])
return GetUserProfileResponse.model_validate(user)

Auth Middleware

async def require_auth(
authorization: Annotated[str | None, Header()] = None,
) -> dict[str, Any]:
"""Require valid Firebase authentication."""
if not authorization:
raise HTTPException(status_code=401, detail="Missing authorization header")

if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Invalid authorization format")

token = authorization[7:] # Remove "Bearer " prefix

try:
decoded = await verify_firebase_token(token)
return decoded
except Exception as e:
raise HTTPException(status_code=401, detail=str(e))

Getting the Current User

async def get_authenticated_user(
auth: Annotated[dict[str, Any], Depends(require_auth)],
uow: Annotated[BaseUnitOfWork, Depends(APIContainer.get_unit_of_work)],
) -> User:
"""Get the authenticated user entity."""
user = await uow.users.get_by_field("auth_id", auth["uid"])
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user

Token Claims

The decoded token contains:

{
"uid": "firebase-user-id",
"email": "user@example.com",
"email_verified": true,
"name": "John Doe",
"iat": 1642687200,
"exp": 1642690800,
"auth_time": 1642687200
}

User Registration

New users are created on first API call:

class UserService(BaseUserService):
async def get_or_create_user(self, auth_id: str, email: str) -> User:
"""Get existing user or create new one."""
existing = await self.uow.users.get_by_field("auth_id", auth_id)
if existing:
return existing

# Create new user
user = User(
auth_id=auth_id,
email=email,
language_id=await self._get_default_language_id(),
)
created = await self.uow.users.create(user)
await self.uow.save_changes()
return created

Token Refresh

Firebase tokens expire after 1 hour. The frontend should refresh proactively:

import { getAuth, onIdTokenChanged } from 'firebase/auth';

const auth = getAuth();

// Listen for token changes
onIdTokenChanged(auth, async (user) => {
if (user) {
const token = await user.getIdToken();
// Update stored token
setAuthToken(token);
}
});

// Force refresh before expiry
const forceRefresh = async () => {
const user = auth.currentUser;
if (user) {
const token = await user.getIdToken(true); // Force refresh
setAuthToken(token);
}
};

Error Responses

StatusErrorDescription
401Missing authorization headerNo token provided
401Invalid authorization formatNot Bearer token
401Invalid tokenToken validation failed
401Token expiredToken has expired
404User not foundNo user with auth_id

Security Best Practices

  1. Always use HTTPS in production
  2. Validate tokens server-side — Never trust client claims
  3. Check email verification for sensitive operations
  4. Implement token refresh before expiry
  5. Log authentication events for security auditing