Authentication System Documentation
Overviewβ
Attune Logic uses a JWT-based authentication system with access tokens and refresh tokens to provide secure, seamless user authentication across web and mobile applications.
Architectureβ
graph TD
A[User Login] --> B[Generate Access + Refresh Tokens]
B --> C[Store Tokens as HTTP-Only Cookies]
C --> D[User Makes API Request]
D --> E{Access Token Valid?}
E -->|Yes| F[Request Proceeds]
E -->|No| G{Refresh Token Valid?}
G -->|Yes| H[Generate New Tokens]
H --> I[Update Cookies]
I --> J[Retry Original Request]
G -->|No| K[Redirect to Login]
Token Types & Configurationβ
Access Tokensβ
- Lifetime: 4 hours
- Purpose: API authentication
- Storage: HTTP-only cookie
- Cookie Name:
Attune-{stage}-Context
Refresh Tokensβ
- Lifetime: 8 hours (web), 10 days (mobile)
- Purpose: Generate new access tokens
- Storage: HTTP-only cookie + database
- Cookie Name:
Attune-{stage}-Refresh
Configuration Locationβ
All token expiry settings are defined in:
// attunelogic-api/constants/index.js
general: {
TOKEN_EXPIRE: "4h", // Access token JWT expiry
REFRESH_TOKEN_EXPIRES_WEB: "8h", // Refresh token JWT expiry (web)
REFRESH_TOKEN_EXPIRES_MOBILE: "10d", // Refresh token JWT expiry (mobile)
TOKEN_EXPIRE_TTL: 4 * 60 * 60 * 1000, // Access token cookie maxAge
REFRESH_TOKEN_EXPIRES_WEB_TTL: 8 * 60 * 60 * 1000, // Refresh token cookie maxAge (web)
REFRESH_TOKEN_EXPIRES_MOBILE_TTL: 10 * 24 * 60 * 60 * 1000, // Refresh token cookie maxAge (mobile)
}
Authentication Flowβ
1. Login Processβ
Frontend (pages/Login/index.jsx):
const handleLogin = async (credentials) => {
const result = await signinMutation({ user: credentials }).unwrap();
// Tokens are automatically stored as cookies by the backend
};
Backend (controllers/account/login.js):
// 1. Validate credentials with Passport
// 2. Generate JWT tokens
const accessToken = user.generateAccessToken();
const refreshToken = user.generateRefreshToken();
// 3. Store refresh token in database
await Token.create({
token: refreshToken,
userId: user._id,
type: "refresh",
jti: decodedRefresh.jti,
expiresAt: new Date(decodedRefresh.exp * 1000),
});
// 4. Set HTTP-only cookies
res.cookie(general.TOKEN_NAME, accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: general.TOKEN_EXPIRE_TTL,
});
res.cookie(general.REFRESH_TOKEN_NAME, refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: general.REFRESH_TOKEN_EXPIRES_WEB_TTL,
});
2. API Request Authenticationβ
Middleware (middlewares/verifyToken.js):
const verifyToken = (req, res, next) => {
const token = req.cookies[general.TOKEN_NAME];
if (!token) {
const refreshToken = req.cookies[general.REFRESH_TOKEN_NAME];
if (refreshToken) {
return res.status(401).json({
code: "TOKEN_EXPIRED",
message: "Access token missing but refresh token available",
});
}
return res.status(403).json({ code: "NO_TOKEN" });
}
jwt.verify(token, COOKIE_KEY, (err, decoded) => {
if (err) {
const refreshToken = req.cookies[general.REFRESH_TOKEN_NAME];
if (refreshToken) {
return res.status(401).json({
code: "TOKEN_INVALID_WITH_REFRESH",
message: "Token validation failed",
});
}
return res.status(401).json({ code: "AUTHENTICATION_FAILED" });
}
req.user = decoded;
next();
});
};
3. Automatic Token Refreshβ
Frontend (redux/api.ts):
const baseQueryWithReauth = async (args, api, extraOptions) => {
let result = await baseQuery(args, api, extraOptions);
// Handle 401 errors with refresh token retry
if (result.error?.status === 401) {
const errorCode = result.error?.data?.code;
if (["TOKEN_EXPIRED", "TOKEN_INVALID_WITH_REFRESH"].includes(errorCode)) {
if (!isRefreshing) {
isRefreshing = true;
const refreshResult = await baseQuery(
{
url: "/account/refresh",
method: "POST",
credentials: "include",
},
api,
extraOptions
);
if (refreshResult.data?.status === "success") {
// Retry original request with new tokens
result = await baseQuery(args, api, extraOptions);
} else {
await clearAuthData();
}
isRefreshing = false;
}
}
}
return result;
};
Backend (controllers/account/tokens.js):
const refresh = async (req, res) => {
const refreshToken = req.cookies[general.REFRESH_TOKEN_NAME];
// 1. Verify refresh token JWT
const decoded = jwt.verify(refreshToken, REFRESH_KEY);
// 2. Check token exists in database and is not revoked
const [tokenDoc, user] = await Promise.all([
Token.findOne({
jti: decoded.jti,
userId: decoded.id,
type: "refresh",
isRevoked: false,
}),
User.findById(decoded.id),
]);
if (!tokenDoc || !user) {
return res.status(401).json({ message: "Invalid refresh token" });
}
// 3. Generate new tokens
const accessToken = user.generateAccessToken();
const newRefreshToken = user.generateRefreshToken();
// 4. Revoke old refresh token and save new one
await Token.findOneAndUpdate({ jti: decoded.jti }, { isRevoked: true });
await Token.create({
token: newRefreshToken,
userId: user._id,
type: "refresh",
jti: jwt.decode(newRefreshToken).jti,
previousJti: decoded.jti,
expiresAt: new Date(jwt.decode(newRefreshToken).exp * 1000),
});
// 5. Set new cookies
res.cookie(general.TOKEN_NAME, accessToken, cookieOptions);
res.cookie(general.REFRESH_TOKEN_NAME, newRefreshToken, cookieOptions);
res.json({
status: "success",
token: accessToken,
refreshToken: newRefreshToken,
});
};
Database Schemaβ
User Model (models/User.js)β
const UserSchema = new Schema({
email: String,
firstName: String,
lastName: String,
fullName: String,
hash: String, // Password hash
authority: String, // User role
active: Boolean, // Account status
parentCompany: ObjectId, // Multi-tenant reference
// ... other fields
});
// Token generation methods
UserSchema.methods.generateAccessToken = function () {
return jwt.sign(
{
id: this._id,
email: this.email,
name: this.fullName,
authority: this.authority,
parentCompany: this.parentCompany,
tokenType: "access",
jti: new mongoose.Types.ObjectId().toString(),
},
COOKIE_KEY,
{ expiresIn: general.TOKEN_EXPIRE }
);
};
UserSchema.methods.generateRefreshToken = function () {
return jwt.sign(
{
// Same payload as access token
tokenType: "refresh",
jti: new mongoose.Types.ObjectId().toString(),
},
REFRESH_KEY,
{ expiresIn: general.REFRESH_TOKEN_EXPIRES_WEB }
);
};
Token Model (models/Token.js)β
const tokenSchema = new mongoose.Schema({
token: String, // The actual JWT token
userId: ObjectId, // Reference to User
type: String, // "refresh", "access", "quote", "proposal"
jti: String, // JWT ID (unique identifier)
previousJti: String, // Previous token's JTI (for refresh chains)
expiresAt: Date, // Token expiration
isRevoked: Boolean, // Revocation status
});
// Indexes for performance
tokenSchema.index({ userId: 1, type: 1 });
tokenSchema.index({ jti: 1 }, { unique: true });
tokenSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); // Auto-cleanup
Error Codesβ
| Code | Description | Frontend Action |
|---|---|---|
NO_TOKEN | No access token provided | Redirect to login |
TOKEN_EXPIRED | Access token expired, refresh available | Attempt refresh |
TOKEN_INVALID_WITH_REFRESH | Invalid access token, refresh available | Attempt refresh |
AUTHENTICATION_FAILED | Invalid access token, no refresh | Redirect to login |
Security Featuresβ
1. HTTP-Only Cookiesβ
- Prevents XSS attacks by making tokens inaccessible to JavaScript
- Automatically sent with requests (CSRF protection via SameSite)
2. Token Rotationβ
- Each refresh generates a new refresh token
- Old refresh tokens are immediately revoked
- Prevents token replay attacks
3. Database Token Trackingβ
- All refresh tokens stored in database with metadata
- Revocation capability for security incidents
- Automatic cleanup of expired tokens
4. Multi-Tenant Contextβ
parentCompanyincluded in JWT payload- Ensures users only access their organization's data
Testing the Authentication Systemβ
Manual Testingβ
-
Login Flow:
curl -X POST http://localhost:3001/api/v1/account/login \
-H "Content-Type: application/json" \
-d '{"user":{"email":"test@example.com","password":"password"}}' \
-c cookies.txt -
Protected Endpoint:
curl -X GET http://localhost:3001/api/v1/account/current \
-b cookies.txt -
Force Token Refresh (delete access token cookie):
# Edit cookies.txt to remove access token, keep refresh token
curl -X GET http://localhost:3001/api/v1/account/current \
-b cookies.txt
# Should automatically refresh and succeed
Frontend Testingβ
-
Login and inspect Network tab:
- Look for
Set-Cookieheaders with both tokens - Verify cookies are
HttpOnlyandSecurein production
- Look for
-
Wait for token expiry or manually delete access token cookie:
- Make an API request
- Should see automatic refresh call in Network tab
- Original request should retry and succeed
-
Check refresh token replacement:
- Compare token values before/after refresh
- Both access and refresh tokens should be updated
Troubleshootingβ
Common Issuesβ
-
Refresh tokens not being replaced:
- Check frontend
credentials: "include"in refresh request - Verify backend is setting new cookies in refresh response
- Ensure error codes match between frontend and backend
- Check frontend
-
Tokens expiring too quickly:
- Check system clock synchronization
- Verify token expiry constants in
constants/index.js
-
CORS issues with cookies:
- Ensure
credentials: "include"in frontend requests - Configure CORS middleware to allow credentials
- Check
sameSitecookie settings
- Ensure
-
Database token cleanup:
// Clean up expired tokens
await Token.deleteMany({
expiresAt: { $lt: new Date() },
});
Debug Loggingβ
Add logging to track token lifecycle:
// In refresh controller
console.log("Refresh attempt:", {
userId: decoded.id,
oldJti: decoded.jti,
newJti: jwt.decode(newRefreshToken).jti,
timestamp: new Date(),
});
Configuration Best Practicesβ
Recommended Token Lifespansβ
For better security, consider shorter access tokens:
// More secure configuration
TOKEN_EXPIRE: "15m", // 15 minutes
REFRESH_TOKEN_EXPIRES_WEB: "7d", // 7 days
REFRESH_TOKEN_EXPIRES_MOBILE: "30d", // 30 days
Environment Variablesβ
NODE_ENV=production # Enables secure cookies
COOKIE_KEY=your-super-secret-key # JWT signing key
REFRESH_KEY=your-refresh-secret-key # Refresh token signing key
Cookie Securityβ
const cookieOptions = {
httpOnly: true, // Prevent XSS
secure: process.env.NODE_ENV === "production", // HTTPS only in prod
sameSite: "strict", // CSRF protection
path: "/", # Available site-wide
};
Migration & Maintenanceβ
Rotating JWT Secretsβ
- Generate new
COOKIE_KEYandREFRESH_KEY - Update environment variables
- All existing tokens will be invalidated
- Users will need to re-login
Token Cleanup Jobβ
// Run daily to clean expired tokens
const cleanupExpiredTokens = async () => {
const result = await Token.deleteMany({
$or: [
{ expiresAt: { $lt: new Date() } },
{
isRevoked: true,
updatedAt: { $lt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) },
},
],
});
console.log(`Cleaned up ${result.deletedCount} expired tokens`);
};