Skip to main content

Authentication & Authorization Flow

This document explains how users log in, how their identity is verified across different services, and how we keep the system secure.


1. Executive Summary

Goal: Ensure that only the right people can access the right data.

  • Secure Login: We use industry-standard encryption to protect passwords and user sessions.
  • Hybrid Token Strategy: We support both Bearer tokens (localStorage) and HttpOnly cookies to handle modern browser restrictions on third-party cookies.
  • Data Privacy: A practitioner from "Clinic A" can never accidentally see patient records from "Clinic B". This is enforced automatically by the system.
  • Session Safety: If a user logs out or is banned, their access is revoked immediately across the entire platform.

2. The Login Flow (Frontend to Auth Service)

The Basics

When a user logs in on the website:

  1. They enter their email and password (case-insensitive email matching).
  2. The frontend sends a POST request to the Auth Service (https://auth.skinclubpro.com).
  3. The Auth Service checks the password. If it's correct, it creates a "Session Token" (JWT).
  4. Dual Token Delivery: The Auth Service:
    • Sets the token as an HttpOnly Cookie (for same-origin requests)
    • Returns the token in the response body (for cross-origin Bearer auth)
  5. The frontend stores the token in localStorage and includes it as a Bearer token in the Authorization header for all subsequent requests.

Why Both Methods?

Modern browsers increasingly block third-party cookies (cookies set by a different origin than the page). Since our frontend (skinclubpro.com) calls APIs on subdomains (auth.skinclubpro.com, clinic.skinclubpro.com), cookies may not be sent reliably. The Bearer token approach solves this.

Architectural Deep Dive

  • Credential Exchange: Standard username/password exchange over TLS.
  • Token Generation: The Auth Service signs a JWT (JSON Web Token) using an RSA Private Key (RS256). This asymmetric signing is key to our distributed architecture.
  • Cookie Security (Backup Method): The cookie is configured with:
    • HttpOnly: Prevents client-side script access.
    • Secure: Sent only over HTTPS (in production).
    • SameSite=None: Required for cross-origin cookie sending (with Secure).
    • Domain: Set to .skinclubpro.com to allow subdomains to share the session.
  • Bearer Token (Primary Method):
    • Stored in localStorage under key access_token.
    • Attached to every request via Authorization: Bearer <token> header.
    • Axios interceptors automatically add this header to all API clients.

3. Accessing Protected Data (Frontend to Clinic/CMS)

The Basics

Now the user wants to view their appointments (Clinic Service) or edit a blog post (CMS Service).

  1. The frontend makes a request (e.g., GET /appointments).
  2. The Axios client automatically attaches the Authorization: Bearer <token> header.
  3. The Service (Clinic or CMS) receives the request and extracts the token from:
    • First: The Authorization header (Bearer token)
    • Fallback: The access_token cookie
  4. It extracts the token and verifies: "Is this token valid?" and "Who is this user?".
  5. If valid, the request is allowed.

Token Extraction Order

jwtFromRequest: ExtractJwt.fromExtractors([
ExtractJwt.fromAuthHeaderAsBearerToken(), // Primary
(req) => req.cookies?.access_token, // Fallback
])

4. Automated Tenant Isolation (Anti-IDOR)

The Basics

In a multi-tenant system like Skin Club Pro, it is critical that a practitioner from "Clinic A" cannot see data from "Clinic B." We use an automated system to ensure every database query is restricted to the correct tenant.

Implementation Details

  • Tenant Context: We use Node.js AsyncLocalStorage to store the tenantId for the duration of an API request. This context is "thread-safe" and accessible anywhere in the code without passing parameters.
  • Global Interceptor: A TenantInterceptor runs on every request. It extracts the userId (our tenant key) from the JWT and "runs" the request inside a tenant-scoped context.
  • Base Entity: All tenant-specific database tables extend a TenantBaseEntity.
    • Auto-Save: When a new record is created, the tenantId is automatically pulled from the context and saved to the database.
    • Auto-Filter: We use a withTenant() utility to automatically inject the tenantId into database queries, ensuring developers cannot accidentally leak data.

5. Verifying the Token (Service-to-Service Trust)

The Basics

How does the Clinic Service know the token is real? It didn't create it! It uses a system called JWKS (JSON Web Key Set).

  1. The Auth Service publishes a public "lock" (Public Key) at a known URL (https://auth.skinclubpro.com/.well-known/jwks.json).
  2. The Clinic Service downloads this public key on startup.
  3. When a token arrives, the Clinic Service uses this public key to verify the signature.
  4. If the signature matches, it proves the Auth Service created the token, and it hasn't been tampered with.

Architectural Deep Dive

  • Zero Trust / Shared Nothing: The services do not share a secret key (like JWT_SECRET). This prevents a compromised Clinic Service from minting valid tokens for other services.
  • Asymmetric Cryptography (RS256):
    • Auth Service: Holds the Private Key (can sign tokens). Stored in AWS Secrets Manager as {Env}/AuthService/JwtPrivateKey.
    • Auth Service: Also holds the Public Key for its own verification. Stored as {Env}/AuthService/JwtPublicKey.
    • Other Services: Fetch the Public Key dynamically via JWKS endpoint (can only verify tokens).
  • Key Management:
    • Keys are stored base64-encoded in AWS Secrets Manager.
    • The infrastructure (CDK) automatically injects these as environment variables into ECS containers.
  • Caching: The jwks-rsa library caches the public key to prevent making an HTTP request to the Auth Service for every single API call, ensuring high performance.

6. Token Revocation (Logout)

The Basics

JWTs are valid until they expire (e.g., 1 hour). What if a user logs out or their account is banned? We need to stop the token now.

  1. When a user clicks "Logout", we send the token's ID to the Auth Service.
  2. The Auth Service saves this ID in a "Blocklist" (using Redis).
  3. The frontend clears the token from localStorage.
  4. Now, whenever any service checks a token, it asks the Auth Service (or checks Redis directly): "Is this token blocked?"
  5. If it is, the request is rejected, even if the token signature is valid.

Architectural Deep Dive

  • The Revocation Problem: Stateless JWTs cannot be revoked by design. We re-introduce state via a Redis Blocklist.
  • JTI (JWT ID): Every token has a unique jti claim.
  • Redis Strategy:
    • Key: blocklist:<jti>
    • Value: true
    • TTL: The key is set to expire when the JWT itself expires. This keeps Redis memory usage efficient (self-cleaning).
  • Distributed Check:
    • The JwtStrategy in the Auth Service queries Redis directly to check for revocation.
    • Other services (Clinic, CMS) rely on the Auth Service for token verification via JWKS.