34 KiB
Backend Auth Module Documentation
Overview
The Auth module provides comprehensive authentication and authorization for the application. It supports local email/password authentication, OAuth 2.0 (Google, Microsoft), JWT-based session management, email verification, and password reset flows.
Files:
| File | Purpose |
|---|---|
src/auth/auth.ts |
Passport.js strategy configurations (JWT, Google, Microsoft) |
src/services/auth.ts |
Auth business logic (signin, password reset/update, email verification) |
src/routes/auth.ts |
REST API endpoints for authentication |
src/helpers.ts |
JWT signing utility (jwtSign) |
src/db/api/users.js |
User database operations (tokens, password updates) |
src/middlewares/rateLimiter.js |
Auth-specific rate limiters |
Architecture Diagram
┌──────────────────────────────────────────────────────────────────────┐
│ Frontend/Client │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────────────┐ │
│ │ Login Form │ │ Signup Form │ │ OAuth Buttons │ │
│ │ (email/pass) │ │ (email/pass) │ │ (Google/Microsoft) │ │
│ └───────┬────────┘ └───────┬────────┘ └───────────┬────────────┘ │
└──────────┼───────────────────┼───────────────────────┼───────────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────────┐
│ Routes Layer (routes/auth.ts) │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Rate Limiters: │ │
│ │ • authLimiter (10 req/15min) → /signin/local │ │
│ │ • passwordResetLimiter (5 req/hour) → /send-password-reset │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────────────┐ │
│ │ POST /signin │ │ PUT /reset │ │ GET /signin/google │ │
│ │ /local │ │ │ │ GET /signin/microsoft │ │
│ └───────┬───────┘ └───────┬───────┘ └───────────┬───────────┘ │
│ │ │ │ │
└──────────┼──────────────────┼──────────────────────┼─────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────────┐
│ Service Layer (services/auth.ts) │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ Auth Class (Static Methods) │ │
│ │ • signup(email, password, options, host) │ │
│ │ • signin(email, password) │ │
│ │ • verifyEmail(token) │ │
│ │ • passwordUpdate(currentPassword, newPassword, options) │ │
│ │ • passwordReset(token, password) │ │
│ │ • sendEmailAddressVerificationEmail(email, host) │ │
│ │ • sendPasswordResetEmail(email, type, host) │ │
│ │ • updateProfile(data, currentUser) │ │
│ └────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────────┐
│ Passport Layer (auth/auth.ts) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ JWT Strategy │ │ Google Strategy │ │ Microsoft Strategy │ │
│ │ (API Auth) │ │ (OAuth 2.0) │ │ (OAuth 2.0) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────────┘ │
│ │ │ │
│ └──────────┬───────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ socialStrategy() │ │
│ │ • findOrCreate │ │
│ │ • Generate JWT │ │
│ └─────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ Database Layer (db/api/users.js) │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ UsersDBApi (Static Methods) │ │
│ │ • findBy({ email }) │ │
│ │ • createFromAuth(data) │ │
│ │ • updatePassword(id, password) │ │
│ │ • generateEmailVerificationToken(email) │ │
│ │ • generatePasswordResetToken(email) │ │
│ │ • findByEmailVerificationToken(token) │ │
│ │ • findByPasswordResetToken(token) │ │
│ │ • markEmailVerified(id) │ │
│ └────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
Authentication Strategies
1. JWT Strategy (Primary)
Used for API authentication on all protected routes.
Configuration (auth/auth.ts):
passport.use(
new JWTstrategy({
passReqToCallback: true,
secretOrKey: config.secret_key,
jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(),
}, async (req, token, done) => {
const user = await UsersDBApi.findBy({ email: token.user.email });
if (user && user.disabled) {
return done(new Error(`User '${user.email}' is disabled`));
}
req.currentUser = user;
return done(null, user);
})
);
Token Structure:
{
user: {
id: "uuid",
email: "user@example.com"
},
iat: 1234567890, // Issued at
exp: 1234589490 // Expires in 6 hours
}
Usage:
// Protect route with JWT
router.get('/me', passport.authenticate('jwt', { session: false }), handler);
// Access user in handler
const currentUser = req.currentUser;
2. Google OAuth Strategy
Configuration (auth/auth.ts):
passport.use(
new GoogleStrategy({
clientID: config.google.clientId,
clientSecret: config.google.clientSecret,
callbackURL: config.apiUrl + '/auth/signin/google/callback',
passReqToCallback: true,
}, (request, accessToken, refreshToken, profile, done) => {
socialStrategy(profile.email, profile, providers.GOOGLE, done);
})
);
Environment Variables:
| Variable | Description |
|---|---|
GOOGLE_CLIENT_ID |
Google OAuth client ID |
GOOGLE_CLIENT_SECRET |
Google OAuth client secret |
OAuth Scopes: profile, email
3. Microsoft OAuth Strategy
Configuration (auth/auth.ts):
passport.use(
new MicrosoftStrategy({
clientID: config.microsoft.clientId,
clientSecret: config.microsoft.clientSecret,
callbackURL: config.apiUrl + '/auth/signin/microsoft/callback',
passReqToCallback: true,
}, (request, accessToken, refreshToken, profile, done) => {
const email = profile._json.mail || profile._json.userPrincipalName;
socialStrategy(email, profile, providers.MICROSOFT, done);
})
);
Environment Variables:
| Variable | Description |
|---|---|
MS_CLIENT_ID |
Microsoft OAuth client ID |
MS_CLIENT_SECRET |
Microsoft OAuth client secret |
OAuth Scopes: https://graph.microsoft.com/user.read, openid
Social Strategy Helper
Common logic for OAuth providers:
function socialStrategy(email, profile, provider, done) {
db.users.findOrCreate({ where: { email, provider } }).then(([user]) => {
const body = {
id: user.id,
email: user.email,
name: profile.displayName,
};
const token = helpers.jwtSign({ user: body });
return done(null, { token });
});
}
File Details
1. services/auth.ts
Core authentication business logic.
Class: Auth
class Auth {
static async signin(email, password)
static async verifyEmail(token, options)
static async passwordUpdate(currentPassword, newPassword, options)
static async passwordReset(token, password, options)
static async sendEmailAddressVerificationEmail(email, host)
static async sendPasswordResetEmail(email, type, host)
static async updateProfile(data, currentUser)
}
Method: signup(email, password, options, host)
Registers a new user or updates password for existing unverified user.
Flow:
1. Check if user exists by email
├── User exists with authenticationUid → Error: emailAlreadyInUse
├── User exists but disabled → Error: userDisabled
└── User exists without authenticationUid → Update password
2. If new user:
├── Hash password (bcrypt, 12 rounds)
├── Create user via UsersDBApi.createFromAuth()
└── Assign default "User" role
3. Send verification email (if EmailSender configured)
4. Return signed JWT token
Returns: JWT token string
Method: signin(email, password)
Authenticates user with email and password.
Flow:
1. Find user by email
└── Not found → Error: userNotFound
2. Check if disabled
└── Disabled → Error: userDisabled
3. Verify password exists
└── No password → Error: wrongPassword
4. Check email verification
└── Not verified (and email configured) → Error: userNotVerified
5. Compare password with bcrypt
└── Mismatch → Error: wrongPassword
6. Return signed JWT token
Returns: JWT token string
Method: verifyEmail(token, options)
Verifies user email address using token.
Flow:
1. Find user by email verification token
└── Not found or expired → Error: invalidToken
2. Mark email as verified
3. Return true
Method: passwordUpdate(currentPassword, newPassword, options)
Updates password for authenticated user.
Flow:
1. Verify currentUser exists
└── Not authenticated → ForbiddenError
2. Verify current password matches
└── Mismatch → Error: wrongPassword
3. Verify new password is different
└── Same → Error: samePassword
4. Hash new password and update
Method: passwordReset(token, password, options)
Resets password using reset token.
Flow:
1. Find user by password reset token
└── Not found or expired → Error: invalidToken
2. Hash new password
3. Update user password
2. routes/auth.ts (327 lines)
REST API endpoints for authentication.
Endpoints Overview
| Method | Path | Auth | Rate Limit | Description |
|---|---|---|---|---|
| POST | /signin/local |
No | authLimiter | Login with email/password |
| GET | /me |
JWT | - | Get current user |
| PUT | /password-reset |
No | - | Reset password with token |
| PUT | /password-update |
JWT | - | Change password |
| PUT | /profile |
JWT | - | Update user profile |
| PUT | /verify-email |
No | - | Verify email with token |
| POST | /send-email-address-verification-email |
JWT | - | Resend verification email |
| POST | /send-password-reset-email |
No | passwordResetLimiter | Send password reset email |
| GET | /email-configured |
No | - | Check if email is configured |
| GET | /signin/google |
No | - | Initiate Google OAuth |
| GET | /signin/google/callback |
No | - | Google OAuth callback |
| GET | /signin/microsoft |
No | - | Initiate Microsoft OAuth |
| GET | /signin/microsoft/callback |
No | - | Microsoft OAuth callback |
POST /api/auth/signin/local
Login with email and password.
Request:
{
"email": "user@example.com",
"password": "securepassword123"
}
Response (200):
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Errors:
| Code | Message | Cause |
|---|---|---|
| 400 | auth.userNotFound |
User doesn't exist |
| 400 | auth.userDisabled |
User account is disabled |
| 400 | auth.wrongPassword |
Invalid password |
| 400 | auth.userNotVerified |
Email not verified |
| 429 | Too Many Requests | Rate limit exceeded |
Self-Registration
Self-registration is disabled. POST /api/auth/signup is not registered.
New users are created through the authenticated Users flow and receive an
invitation/setup link.
GET /api/auth/me
Get current authenticated user.
Headers:
Authorization: Bearer <JWT_TOKEN>
Response (200):
{
"id": "uuid",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe",
"emailVerified": true,
"disabled": false,
"app_role": {
"id": "uuid",
"name": "Administrator",
"permissions": [...]
},
"custom_permissions": [],
"avatar": [...],
"createdAt": "2024-01-01T00:00:00.000Z"
}
Note: Password field is omitted from response.
PUT /api/auth/password-reset
Reset password using token from email.
Request:
{
"token": "abc123...",
"password": "newSecurePassword456"
}
Response (200):
{ "success": true }
PUT /api/auth/password-update
Change password for authenticated user.
Headers:
Authorization: Bearer <JWT_TOKEN>
Request:
{
"currentPassword": "oldPassword123",
"newPassword": "newPassword456"
}
Errors:
| Code | Message | Cause |
|---|---|---|
| 400 | auth.wrongPassword |
Current password incorrect |
| 400 | auth.passwordUpdate.samePassword |
New password same as old |
| 403 | Forbidden | Not authenticated |
PUT /api/auth/profile
Update user profile.
Headers:
Authorization: Bearer <JWT_TOKEN>
Request:
{
"profile": {
"firstName": "John",
"lastName": "Smith",
"phoneNumber": "+1234567890"
}
}
OAuth Endpoints
GET /api/auth/signin/google
- Redirects to Google OAuth consent screen
- Query param:
app(passed as state)
GET /api/auth/signin/google/callback
- Handles Google OAuth callback
- Redirects to:
{uiUrl}/login?token={jwt}
GET /api/auth/signin/microsoft
- Redirects to Microsoft OAuth consent screen
- Query param:
app(passed as state)
GET /api/auth/signin/microsoft/callback
- Handles Microsoft OAuth callback
- Redirects to:
{uiUrl}/login?token={jwt}
3. helpers.js (32 lines)
JWT and utility functions.
Method: jwtSign(data)
Signs JWT token with application secret.
static jwtSign(data) {
return jwt.sign(data, config.secret_key, { expiresIn: '6h' });
}
Configuration:
| Setting | Value | Description |
|---|---|---|
| Secret Key | config.secret_key |
From SECRET_KEY env var |
| Expiration | 6h |
Token valid for 6 hours |
| Algorithm | HS256 |
Default HMAC SHA-256 |
4. db/api/users.js (Authentication Methods)
User database operations related to authentication.
Method: createFromAuth(data)
Creates new user during signup.
static async createFromAuth(data, options) {
const users = await db.users.create({
email: data.email,
firstName: data.firstName,
authenticationUid: data.authenticationUid,
password: data.password,
}, { transaction });
// Assign default "User" role
const app_role = await db.roles.findOne({
where: { name: config.roles?.user || 'User' },
});
await users.setApp_role(app_role?.id);
return users;
}
Method: generateEmailVerificationToken(email)
Generates secure token for email verification.
static async generateEmailVerificationToken(email, options) {
const token = crypto.randomBytes(20).toString('hex');
const tokenExpiresAt = Date.now() + (24 * 60 * 60 * 1000); // 24 hours
await users.update({
emailVerificationToken: token,
emailVerificationTokenExpiresAt: tokenExpiresAt,
});
return token;
}
Token Properties:
| Property | Value |
|---|---|
| Length | 40 hex characters |
| Expiry | 24 hours |
| Storage | emailVerificationToken column |
Method: generatePasswordResetToken(email)
Generates secure token for password reset.
Same implementation as generateEmailVerificationToken but stores in:
passwordResetTokenpasswordResetTokenExpiresAt
Method: findByEmailVerificationToken(token)
Finds user by valid (non-expired) verification token.
static async findByEmailVerificationToken(token, options) {
return db.users.findOne({
where: {
emailVerificationToken: token,
emailVerificationTokenExpiresAt: {
[Op.gt]: Date.now(),
},
},
});
}
Method: markEmailVerified(id)
Marks user email as verified.
static async markEmailVerified(id, options) {
const users = await db.users.findByPk(id);
await users.update({ emailVerified: true });
return true;
}
Rate Limiting
Authentication endpoints have dedicated rate limiters defined in middlewares/rateLimiter.js.
Auth Limiter (Login)
const authLimiter = createRateLimiter({
keyPrefix: 'auth',
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10,
message: 'Too many authentication attempts. Please try again later.',
skipFailedRequests: false, // Count ALL attempts
});
| Setting | Value |
|---|---|
| Window | 15 minutes |
| Max Requests | 10 |
| Applied To | /signin/local |
Signup Limiter
Self-registration is disabled, so no signup limiter is registered.
Password Reset Limiter
const passwordResetLimiter = createRateLimiter({
keyPrefix: 'password-reset',
windowMs: 60 * 60 * 1000, // 1 hour
max: 5,
message: 'Too many password reset requests. Please try again later.',
});
| Setting | Value |
|---|---|
| Window | 1 hour |
| Max Requests | 5 |
| Applied To | /send-password-reset-email |
Rate Limit Response
{
"error": "Too Many Requests",
"message": "Too many authentication attempts. Please try again later.",
"retryAfter": 300
}
Headers:
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 2024-01-01T00:15:00.000Z
Retry-After: 300
Password Security
Hashing Configuration
// config.ts
bcrypt: {
saltRounds: 12,
}
| Setting | Value | Security Impact |
|---|---|---|
| Algorithm | bcrypt | Industry standard |
| Salt Rounds | 12 | ~200ms hash time |
| Salt | Auto-generated | Per-password unique |
Password Validation
Passwords are:
- Hashed before storage (never stored in plain text)
- Compared using
bcrypt.compare()(timing-attack safe) - Required for local authentication
Email Integration
Email functionality is conditional based on configuration.
Email Configuration Check
if (EmailSender.isConfigured) {
await this.sendEmailAddressVerificationEmail(user.email, host);
}
When email is NOT configured:
- Signup succeeds without verification email
- Users are auto-verified on signin
- Password reset emails not sent
Verification Email
Email Class: EmailAddressVerificationEmail
Link Format:
{host}/verify-email?token={token}
Password Reset Email
Email Classes:
PasswordResetEmail- Standard resetInvitationEmail- New user invitation
Link Format:
{host}/password-reset?token={token}
Configuration Reference
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
SECRET_KEY |
Yes | 88dbeaf8-e906-405e-9e41-c3baadeda5c6 |
JWT signing secret |
GOOGLE_CLIENT_ID |
No | - | Google OAuth client ID |
GOOGLE_CLIENT_SECRET |
No | - | Google OAuth client secret |
MS_CLIENT_ID |
No | - | Microsoft OAuth client ID |
MS_CLIENT_SECRET |
No | - | Microsoft OAuth client secret |
ADMIN_EMAIL |
No | admin@flatlogic.com |
Default admin email |
ADMIN_PASS |
No | 88dbeaf8 |
Default admin password |
USER_PASS |
No | c3baadeda5c6 |
Default user password |
config.ts Settings
{
bcrypt: { saltRounds: 12 },
secret_key: env.SECRET_KEY,
providers: {
LOCAL: 'local',
GOOGLE: 'google',
MICROSOFT: 'microsoft',
},
google: {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
},
microsoft: {
clientId: env.MS_CLIENT_ID,
clientSecret: env.MS_CLIENT_SECRET,
},
roles: {
admin: 'Administrator',
user: 'Analytics Viewer',
},
}
Authentication Flows
Local Login Flow
┌────────┐ ┌─────────┐ ┌─────────────┐ ┌──────────┐
│ Client │────▶│ Router │────▶│ AuthService │────▶│ UsersDB │
└────────┘ └─────────┘ └─────────────┘ └──────────┘
│ │ │ │
│ POST /signin │ │ │
│ {email,pass} │ │ │
│──────────────▶│ │ │
│ │ signin() │ │
│ │───────────────▶│ │
│ │ │ findBy({email}) │
│ │ │──────────────────▶│
│ │ │◀──────────────────│
│ │ │ │
│ │ │ bcrypt.compare() │
│ │ │ │
│ │ │ jwtSign() │
│ │◀───────────────│ │
│◀──────────────│ │ │
│ JWT Token │ │ │
OAuth Login Flow
┌────────┐ ┌─────────┐ ┌──────────┐ ┌──────────┐
│ Client │ │ Backend │ │ Provider │ │ UsersDB │
└────────┘ └─────────┘ └──────────┘ └──────────┘
│ │ │ │
│ GET /signin/ │ │ │
│ google │ │ │
│──────────────▶│ │ │
│ │ Redirect to │ │
│◀──────────────│ consent screen │ │
│──────────────────────────────▶│ │
│ │ │ │
│ │ User consents │ │
│◀──────────────────────────────│ │
│ │ │ │
│ GET /callback │ │ │
│ ?code=... │ │ │
│──────────────▶│ │ │
│ │ Exchange code │ │
│ │───────────────▶│ │
│ │ Profile data │ │
│ │◀───────────────│ │
│ │ │ │
│ │ findOrCreate() │ │
│ │───────────────────────────────▶│
│ │◀───────────────────────────────│
│ │ jwtSign() │ │
│ │ │ │
│ Redirect to │ │ │
│ /login?token= │ │ │
│◀──────────────│ │ │
Email Verification Flow
┌────────┐ ┌─────────┐ ┌─────────────┐ ┌───────┐
│ Client │ │ Router │ │ AuthService │ │ Email │
└────────┘ └─────────┘ └─────────────┘ └───────┘
│ │ │ │
│ POST /signup │ │ │
│──────────────▶│ │ │
│ │ signup() │ │
│ │───────────────▶│ │
│ │ │ generateToken()│
│ │ │ sendEmail() │
│ │ │───────────────▶│
│ │◀───────────────│ │
│◀──────────────│ │ │
│ JWT Token │ │ │
│ │ │ │
│ User clicks email link │ │
│ │ │ │
│ PUT /verify- │ │ │
│ email │ │ │
│ {token} │ │ │
│──────────────▶│ │ │
│ │ verifyEmail() │ │
│ │───────────────▶│ │
│ │ │ markVerified() │
│ │◀───────────────│ │
│◀──────────────│ │ │
│ Success │ │ │
Error Codes
| Error Key | HTTP Status | Description |
|---|---|---|
auth.userNotFound |
400 | User with email doesn't exist |
auth.userDisabled |
400 | User account is disabled |
auth.wrongPassword |
400 | Password doesn't match |
auth.userNotVerified |
400 | Email not verified |
auth.emailAlreadyInUse |
400 | Email already registered |
auth.passwordUpdate.samePassword |
400 | New password same as current |
auth.passwordReset.error |
400 | Token generation failed |
auth.passwordReset.invalidToken |
400 | Invalid or expired reset token |
auth.emailAddressVerificationEmail.error |
400 | Verification email failed |
auth.emailAddressVerificationEmail.invalidToken |
400 | Invalid verification token |
Security Considerations
- Password Storage: bcrypt with 12 salt rounds
- JWT Expiration: 6 hours (balances security and UX)
- Token Generation: crypto.randomBytes(20) - 160-bit entropy
- Token Expiry: 24 hours for email/password reset tokens
- Rate Limiting: Prevents brute force attacks
- Disabled User Check: Checked on both JWT validation and login
- Session-less: No server-side session storage (stateless JWT)
- HTTPS Required: OAuth callbacks require HTTPS in production
Testing
Test Local Login
curl -X POST http://localhost:3000/api/auth/signin/local \
-H "Content-Type: application/json" \
-d '{"email": "admin@flatlogic.com", "password": "password"}'
Test Get Current User
curl http://localhost:3000/api/auth/me \
-H "Authorization: Bearer <JWT_TOKEN>"
User Creation
Use the authenticated Users API/UI to create invited users.
Dependencies
| Package | Version | Purpose |
|---|---|---|
passport |
^0.6.0 | Authentication middleware |
passport-jwt |
^4.0.0 | JWT strategy for Passport |
passport-google-oauth2 |
^0.2.0 | Google OAuth strategy |
passport-microsoft |
^2.0.0 | Microsoft OAuth strategy |
@types/passport-jwt |
^4.0.1 | Maintained TypeScript definitions for JWT Passport strategy |
@types/passport-google-oauth2 |
^0.1.10 | Maintained TypeScript definitions for Google OAuth Passport strategy |
@types/passport-microsoft |
^2.1.1 | Maintained TypeScript definitions for Microsoft Passport strategy |
jsonwebtoken |
^9.0.0 | JWT sign/verify |
bcrypt |
^5.1.0 | Password hashing |
crypto |
built-in | Token generation |
Summary
The Auth module provides:
- JWT Authentication - Stateless API authentication with 6-hour tokens
- Local Login - Email/password authentication with bcrypt
- OAuth 2.0 - Google and Microsoft social login
- Email Verification - Token-based email confirmation
- Password Reset - Secure token-based password recovery
- Rate Limiting - Protection against brute force attacks
- Profile Management - User profile updates
- Role Assignment - Default role on signup