Skip to content

Authentication and RBAC Guide

Rajesh Mani edited this page Sep 13, 2026 · 1 revision

StudentAppBackend Banner

Authentication, RBAC & Password Recovery Guide


1. Overview

Authentication in StudentAppBackend is built around stateless JSON Web Tokens (JWT) with stateful real-time revocation, combined with strict Role-Based Access Control (RBAC).

Core Security Rules

  • No Client-Side Privilege Escalation: The client can never specify their own role. Public signup endpoints always assign role = student on the server.
  • Enumeration Resistance: Login and password recovery endpoints return generic responses to prevent attackers from determining whether an email address exists in the system.
  • Resource-Level Authorization (Anti-IDOR): Validating a JWT proves identity; accessing a student resource requires ownership or elevated administrative role.

2. Step-by-Step Feature Implementation

Step 1: Canonical Student Signup (POST /auth/signup/student)

The new canonical signup flow validates input, checks for unique constraint conflicts, normalizes E.164 phone numbers, and hashes passwords:

sequenceDiagram
    autonumber
    actor Client as iOS / Web Client
    participant Auth as AuthController
    participant Val as ValidationUtilities
    participant Svc as StudentService
    participant DB as PostgreSQL 16

    Client->>Auth: POST /auth/signup/student {firstName, lastName, email, password, confirmPassword, countryCode, contactNumber}
    Auth->>Val: validateStudentSignupRequest(...)
    alt Validation Fails
        Val-->>Auth: [FieldValidationError]
        Auth-->>Client: 400 Bad Request (Detailed Field Errors)
    end
    Auth->>Svc: signupStudent(request, on: db)
    Svc->>Svc: Normalize email (lowercase, trim) & phone (E.164)
    Svc->>Svc: Bcrypt.hash(password)
    Svc->>DB: INSERT INTO students (id, name, email, passwordHash, role, status, ...)
    alt Unique Constraint Collision (Email or Phone)
        DB-->>Svc: 23505 Unique Violation
        Svc-->>Auth: Abort(.conflict)
        Auth-->>Client: 409 Conflict ("EMAIL_ALREADY_EXISTS" / "PHONE_ALREADY_EXISTS")
    end
    DB-->>Svc: Saved Student Record
    Svc-->>Auth: Student Entity
    Auth-->>Client: 200 OK (StudentPublicResponse with role: student)
Loading

Verification Rules

  • email: RFC 5322 compliant regex; converted to lowercase before lookup or insertion.
  • password: Minimum 8 characters, must contain at least one digit and one letter.
  • countryCode: Must start with + followed by 1 to 4 digits (e.g. +1, +91, +44).
  • contactNumber: Normalized into E.164 format (+<countryCode><number>), 7–15 digits.

Step 2: User Login & JWT Issuance (POST /auth/login)

sequenceDiagram
    autonumber
    actor Client
    participant Auth as AuthController
    participant Svc as StudentService
    participant Token as TokenService
    participant DB as PostgreSQL 16

    Client->>Auth: POST /auth/login {email, password}
    Auth->>Svc: authenticate(credentials, on: db)
    Svc->>DB: SELECT * FROM students WHERE email = normalizedEmail
    alt Account Not Found OR Password Invalid OR Suspended
        Svc-->>Auth: nil
        Auth-->>Client: 401 Unauthorized ("Invalid email or password")
    end
    Svc->>Svc: Bcrypt.verify(password, against: passwordHash)
    Svc-->>Auth: Authenticated Student (with Role)
    Auth->>Token: signAccessToken(for: student, on: req)
    Token-->>Auth: Signed JWT Token String (HS256)
    Auth-->>Client: 200 OK {user: Student.Public, token: {token: "..."}, status: "ok"}
Loading

Step 3: Real-Time Logout & Token Revocation (POST /auth/logout)

JWTs are stateless by default, which presents a challenge when a user logs out before token expiration. StudentAppBackend solves this using a high-performance Revoked Token Blacklist:

  1. Client sends POST /auth/logout with Authorization: Bearer <JWT>.
  2. JWTAuthMiddleware validates the cryptographic signature.
  3. TokenService.revokeToken(...) computes the SHA-256 hash of the token and writes it into revoked_tokens table with the token's expiration timestamp.
  4. Subsequent requests using this token are immediately rejected with 401 Unauthorized.
  5. An automated cleanup task sweeps expired tokens from revoked_tokens to prevent unbounded table growth.

Step 4: Two-Phase OTP Password Recovery (SendGrid Integration)

sequenceDiagram
    autonumber
    actor User as User
    participant Auth as AuthController
    participant Email as SendGridEmailService
    participant DB as PostgreSQL 16
    participant SendGrid as SendGrid API

    Note over User,SendGrid: Phase 1: Request Code
    User->>Auth: POST /auth/forgot-password {email}
    Auth->>DB: Query student by email
    alt Email Not Found
        Auth-->>User: 200 OK (Enumeration-Safe: "If the account exists, code was sent")
    end
    Auth->>DB: Save PasswordResetToken (6-digit code, 10 min TTL, attempts=0)
    Auth->>Email: send(to: email, code)
    Email->>SendGrid: POST https://api.sendgrid.com/v3/mail/send
    Auth-->>User: 200 OK

    Note over User,SendGrid: Phase 2: Verify Code
    User->>Auth: POST /auth/verify-reset-code {email, code}
    Auth->>DB: Lookup active reset token
    alt Code Mismatch
        Auth->>DB: Increment attempts counter (+1)
        alt attempts >= 3
            Auth->>DB: Mark token used=true (Brute-Force Lockout)
        end
        Auth-->>User: 200 OK {success: false, message: "Invalid code."}
    end
    Auth->>DB: Set verified=true, generate 32-byte sessionToken (15 min TTL)
    Auth-->>User: 200 OK {success: true, sessionToken: "xyz..."}

    Note over User,SendGrid: Phase 3: Update Password
    User->>Auth: POST /auth/reset-password {email, sessionToken, newPassword, confirmPassword}
    Auth->>DB: Verify sessionToken matches and verified=true
    Auth->>DB: Update student passwordHash = Bcrypt.hash(newPassword)
    Auth->>DB: Mark reset token used=true
    Auth-->>User: 200 OK {success: true, message: "Password reset successfully"}
Loading

Step 5: Role-Based Access Control (RBAC) & IDOR Protection

Authorization rules are enforced centrally via AuthorizationService:

Role Permissions Resource Access Rules
admin Full platform management Access, read, update all student and teacher records
principal School-level administration Read student and classroom records within school domain
teacher Classroom instructions Read students assigned to classes
student Self profile only Strict IDOR Barrier: Can only view and update their own record (requester.id == targetID)

IDOR Barrier Implementation Example (StudentController.swift)

let requester = try await TokenService.authenticateStudent(from: req)

guard let studentIDParam = req.parameters.get("studentID", as: UUID.self) else {
    throw Abort(.badRequest, reason: "Invalid student ID format")
}

// Enforce authorization policy BEFORE querying data
guard AuthorizationService.canAccessStudentRecord(requester: requester, targetStudentID: studentIDParam) else {
    throw Abort(.forbidden, reason: "You are not authorized to access this student record")
}

guard let student = try await Student.find(studentIDParam, on: req.db) else {
    throw Abort(.notFound, reason: "Student not found")
}
return student.convertToPublic()

Clone this wiki locally