2026-07-03 16:11:24 +02:00

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:

  • passwordResetToken
  • passwordResetTokenExpiresAt

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:

  1. Hashed before storage (never stored in plain text)
  2. Compared using bcrypt.compare() (timing-attack safe)
  3. 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 reset
  • InvitationEmail - 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

  1. Password Storage: bcrypt with 12 salt rounds
  2. JWT Expiration: 6 hours (balances security and UX)
  3. Token Generation: crypto.randomBytes(20) - 160-bit entropy
  4. Token Expiry: 24 hours for email/password reset tokens
  5. Rate Limiting: Prevents brute force attacks
  6. Disabled User Check: Checked on both JWT validation and login
  7. Session-less: No server-side session storage (stateless JWT)
  8. 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:

  1. JWT Authentication - Stateless API authentication with 6-hour tokens
  2. Local Login - Email/password authentication with bcrypt
  3. OAuth 2.0 - Google and Microsoft social login
  4. Email Verification - Token-based email confirmation
  5. Password Reset - Secure token-based password recovery
  6. Rate Limiting - Protection against brute force attacks
  7. Profile Management - User profile updates
  8. Role Assignment - Default role on signup