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β
| Token | Algorithm | Expiry | Storage |
|---|---|---|---|
| Access token | JWT HS256 | 15 minutes | sessionStorage / memory |
| Refresh token | JWT HS256 | 7 days | httpOnly 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β
| Role | Description | Elevated Permissions |
|---|---|---|
citizen | Regular registered user | Report issues, upvote, comment |
official | Government official account | Assign issues, update status |
admin | Platform administrator | All 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);