Skip to main content

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​

CodeDescriptionFrontend Action
NO_TOKENNo access token providedRedirect to login
TOKEN_EXPIREDAccess token expired, refresh availableAttempt refresh
TOKEN_INVALID_WITH_REFRESHInvalid access token, refresh availableAttempt refresh
AUTHENTICATION_FAILEDInvalid access token, no refreshRedirect 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​

  • parentCompany included in JWT payload
  • Ensures users only access their organization's data

Testing the Authentication System​

Manual Testing​

  1. 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
  2. Protected Endpoint:

    curl -X GET http://localhost:3001/api/v1/account/current \
    -b cookies.txt
  3. 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​

  1. Login and inspect Network tab:

    • Look for Set-Cookie headers with both tokens
    • Verify cookies are HttpOnly and Secure in production
  2. 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
  3. Check refresh token replacement:

    • Compare token values before/after refresh
    • Both access and refresh tokens should be updated

Troubleshooting​

Common Issues​

  1. 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
  2. Tokens expiring too quickly:

    • Check system clock synchronization
    • Verify token expiry constants in constants/index.js
  3. CORS issues with cookies:

    • Ensure credentials: "include" in frontend requests
    • Configure CORS middleware to allow credentials
    • Check sameSite cookie settings
  4. 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​

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
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​

  1. Generate new COOKIE_KEY and REFRESH_KEY
  2. Update environment variables
  3. All existing tokens will be invalidated
  4. 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`);
};