Skip to main content

Authentication

CivicPulse uses Google OAuth 2.0 for identity and a dual-token JWT strategy for session management: a short-lived access token stored in memory (or sessionStorage) and a long-lived refresh token stored in an httpOnly cookie.


Token Design​

TokenAlgorithmExpiryStorage
Access tokenJWT HS25615 minutessessionStorage / memory
Refresh tokenJWT HS2567 dayshttpOnly cookie (refresh_token)

The access token is sent in the Authorization header. The refresh token is sent automatically by the browser on requests to /api/auth/refresh (same-origin or configured CORS with credentials: 'include').

Access token payload:

{
"sub": "usr_01HN5K...",
"email": "priya@example.com",
"displayName": "Priya S.",
"role": "citizen",
"city": "bangalore",
"iat": 1705312200,
"exp": 1705313100
}

Roles: citizen | official | admin


Endpoints​

POST /api/auth/google​

Exchange a Google ID token for a CivicPulse access token and refresh cookie.

Request:

POST /api/auth/google
Content-Type: application/json

{
"idToken": "<Google ID token from Google Sign-In>"
}

The idToken is obtained from the Google Sign-In JavaScript library after the user completes the OAuth consent screen.

Response β€” 200 OK:

{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"userId": "usr_01HN5K...",
"email": "priya@example.com",
"displayName": "Priya S.",
"avatarUrl": "https://lh3.googleusercontent.com/...",
"role": "citizen",
"city": "bangalore",
"createdAt": "2025-01-01T00:00:00.000Z"
}
}
}

The Set-Cookie response header sets the httpOnly refresh cookie:

Set-Cookie: refresh_token=eyJhbGci...; HttpOnly; Secure; SameSite=Strict; Max-Age=604800; Path=/api/auth/refresh

Response β€” 401 (invalid Google token):

{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid Google ID token"
}
}

POST /api/auth/refresh​

Obtain a new access token using the refresh token cookie. Call this when you receive a 401 response on any authenticated request.

Request:

POST /api/auth/refresh

No body is required. The browser sends the refresh_token cookie automatically.

Response β€” 200 OK:

{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}

A new Set-Cookie header is sent with the rotated refresh token. The old refresh token is invalidated server-side (stored revocation list in DynamoDB).

Response β€” 401 (missing or expired cookie):

{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"message": "Refresh token missing or expired"
}
}

POST /api/auth/logout​

Revoke the refresh token and clear the cookie. Call this when the user explicitly signs out.

Request:

POST /api/auth/logout
Authorization: Bearer <accessToken>

Response β€” 200 OK:

{
"success": true,
"data": {
"message": "Logged out successfully"
}
}

The Set-Cookie response header clears the cookie:

Set-Cookie: refresh_token=; HttpOnly; Secure; Max-Age=0; Path=/api/auth/refresh

Silent Token Refresh (Client Pattern)​

The recommended client-side pattern uses an interceptor to retry failed requests after refreshing the token:

async function apiFetch(url, options = {}) {
let token = sessionStorage.getItem('accessToken');

let response = await fetch(url, {
...options,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
});

if (response.status === 401) {
// Attempt silent refresh
const refreshRes = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include',
});

if (refreshRes.ok) {
const { data } = await refreshRes.json();
sessionStorage.setItem('accessToken', data.accessToken);
token = data.accessToken;

// Retry the original request once
response = await fetch(url, {
...options,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
...options.headers,
},
});
} else {
// Refresh also failed β€” user must log in again
sessionStorage.removeItem('accessToken');
window.dispatchEvent(new Event('auth:expired'));
}
}

return response;
}

Role-Based Access​

RoleDescriptionElevated Permissions
citizenRegular registered userReport issues, upvote, comment
officialGovernment official accountAssign issues, update status
adminPlatform administratorAll actions + bulk operations, user management

Role is encoded in the JWT and enforced by middleware on protected routes:

// Middleware usage example
router.post('/issues/bulk/status', authenticateToken, requireRole('admin'), bulkUpdateStatus);
router.post('/issues/:id/assign', authenticateToken, requireRole('official', 'admin'), assignIssue);