Authentication System

JWT-based authentication and authorization system - login flow, token management, role-based access control

Authentication System

Introduction

JWT-based authentication with dual paths: primary auth in Apollo context factory and fallback in authChecker. Three user roles (USER, ADMIN, SUPER_ADMIN) with role-based access control. Integrates with GraphQL API and Socket.io.

Authentication Architecture

Primary Path: Apollo context factory authenticates every GraphQL request, populates context.user.

Fallback Path: AuthChecker re-authenticates when resolvers have @Authorized decorator.

Both paths use shared verifyRequestAuth function for consistent JWT verification. Authentication metadata tracks which path was used.

User Model and Roles

model User {
  id        String   @id @default(uuid())
  email     String   @unique
  password  String   // bcrypt hashed
  firstName String
  lastName  String
  role      UserRole @default(USER)
  deleted   Boolean  @default(false)
  token     String?  // Legacy, not actively used
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

enum UserRole {
  USER         // Manage own VMs and resources
  ADMIN        // Manage all users and resources
  SUPER_ADMIN  // Can create/modify other SUPER_ADMIN users
}

SafeUser type excludes password and token fields: export type SafeUser = Omit<User, 'password' | 'token'>

Login Flow

mutation Login($email: String!, $password: String!) {
  login(email: $email, password: $password) {
    token
    user { id email firstName lastName role }
  }
}

Process: Find user by email → verify password with bcrypt → generate JWT → return user and token. Returns generic AuthenticationError('Invalid credentials') on failure to prevent user enumeration.

Token generation: Payload { userId, userRole }, signed with TOKENKEY env var, no expiration (long-lived), HS256 algorithm.

⚠️ Security: Login resolver uses insecure fallback 'secret' if TOKENKEY missing. Must set TOKENKEY in all environments and remove fallback.

Client storage: Token stored in browser (localStorage/sessionStorage), included in Authorization: Bearer <token> header for all requests.

Token Verification

Token extracted from Authorization header (supports Bearer <token> format and direct token). JWT secret from TOKENKEY env var (required in production, fallback in dev if ALLOW_INSECURE_JWT_FALLBACK=1).

Verification: Extract token → get JWT secret → verify signature → validate payload (userId, userRole) → check expiration → fetch user from database → check user exists and not deleted → verify role matches → return SafeUser.

Payload validation ensures userId and userRole fields exist. User validation checks user exists, not deleted, role matches token.

Apollo Context

Context factory runs for every GraphQL request, calls verifyRequestAuth, returns context with user (SafeUser | null), auth metadata, userHelpers, prisma, req, res.

Authentication metadata tracks: method (context/fallback/none), status (authenticated/unauthenticated/token_expired/token_invalid/user_not_found/user_deleted/role_mismatch), warnings, timestamp, tokenExpiration, tokenRole.

User validation helpers: isAuthenticated(), hasRole(role), isAdmin(), getDisplayName(), isAuthenticationFresh().

AuthChecker Fallback

Triggers when resolver has @Authorized decorator and context.user is null. Re-authenticates via verifyRequestAuth, updates context if successful, checks role-based access.

Access checking: No roles required → allow. SETUP_MODE role → check context.setupMode. No user → deny. Otherwise check if user has required role (USER = any authenticated user, ADMIN = ADMIN or SUPER_ADMIN).

@Authorized Decorator

@Query(() => User)
@Authorized('USER')
async currentUser(@Ctx() ctx: InfinibayContext) { return ctx.user!; }

@Query(() => [User])
@Authorized('ADMIN')
async users(@Ctx() ctx: InfinibayContext) { return ctx.prisma.user.findMany(); }

Supports multiple roles (OR logic): @Authorized('USER', 'ADMIN'). Returns UNAUTHORIZED error if access denied.

Role Capabilities

USER: Manage own VMs, view own department, update own profile. Cannot create users or access other users' resources.

ADMIN: All USER capabilities + create/manage users (except SUPER_ADMIN), manage all VMs/departments, system settings.

SUPER_ADMIN: All ADMIN capabilities + create/modify SUPER_ADMIN users, system-level configuration.

SUPER_ADMIN protections: Only SUPER_ADMIN can create other SUPER_ADMIN users, cannot demote SUPER_ADMIN users, role mismatch detection prevents privilege escalation.

Department-Scoped Authorization

ADMIN/SUPER_ADMIN: Access all departments. USER: Access only assigned departments. Utilities: getUserAccessibleDepartments, validateDepartmentAccess, getDepartmentScopedWhereClause.

Error Handling

Error flow: Token verification categorizes errors (logged server-side) → authChecker returns true/false → resolvers throw typed errors (AuthenticationError, UserInputError, ForbiddenError) → Apollo formatError maps to client codes (UNAUTHORIZED, FORBIDDEN, BAD_USER_INPUT).

Error categories (server-side only): token_expired, invalid_signature, invalid_payload_*, user_not_found, user_deleted, role_mismatch.

Password Management

bcrypt with 10 rounds, passwords validated for length/complexity, update requires current password verification.

Token Storage

Client stores token in localStorage/sessionStorage, includes in Authorization: Bearer <token> header. Secure transmission over HTTPS.

Socket.io Authentication

JWT authentication on WebSocket connection via socket.handshake.auth.token. Auto-join user-specific room user:{userId}.

Environment Variables

  • TOKENKEY (required): JWT signing secret
  • DEBUG_AUTH (optional): Enable auth debugging (0/1)
  • ALLOW_INSECURE_JWT_FALLBACK (optional): Allow fallback secret in dev (0/1)

Summary

JWT-based auth with dual paths (context + authChecker fallback), role-based access control (USER/ADMIN/SUPER_ADMIN), department-scoped authorization, comprehensive error handling. Key files: app/utils/jwtAuth.ts, app/utils/authChecker.ts, app/config/apollo.ts, app/index.ts.