Authentication

ForgeStack ships JWT-based authentication out of the box: short-lived access tokens, longer-lived refresh tokens, a guard that validates and checks revocation, role-based access, and optional admin impersonation. Google OAuth is wired in alongside email/password.

Tokens

JwtTokenService issues and verifies tokens. Access and refresh tokens use separate secrets and expiries, and each token gets a unique id to make revocation and replay-protection possible:

// libs/common/src/auth/jwt-token.service.ts
@Injectable()
export class JwtTokenService {
  constructor(private readonly jwtService: JwtService) {
    if (!process.env.JWT_SECRET) throw new Error('JWT_SECRET is required');
    if (!process.env.JWT_REFRESH_SECRET) throw new Error('JWT_REFRESH_SECRET is required');
    this.accessTokenSecret = process.env.JWT_SECRET;
    this.accessTokenExpiry = (process.env.JWT_EXPIRES_IN ?? '5m');
    this.refreshTokenSecret = process.env.JWT_REFRESH_SECRET;
    this.refreshTokenExpiry = (process.env.JWT_REFRESH_EXPIRES_IN ?? '7d');
  }

  generateAccessToken(payload: TokenPayload): string {
    return this.jwtService.sign({ ...payload, uniqueId: uuid() },
      { secret: this.accessTokenSecret, expiresIn: this.accessTokenExpiry });
  }
  generateRefreshToken(payload: TokenPayload): string {
    return this.jwtService.sign({ ...payload, uniqueId: uuid() },
      { secret: this.refreshTokenSecret, expiresIn: this.refreshTokenExpiry });
  }
}

The payload carried in every token:

// libs/common/src/auth/jwt-auth.types.ts
export class TokenPayload {
  userId: string;
  email: string;
  role: string;
  // impersonation
  isImpersonating?: boolean;
  originalUserId?: string;
  originalEmail?: string;
}
export type JwtTokenPayload = JwtPayload & TokenPayload;   // adds uniqueId, iat, exp

Logging in

Authentication is a query (it reads and returns tokens; it doesn't mutate an aggregate). The handler loads the user, verifies the password, runs the domain rule user.canAuthenticate(), and returns a token pair:

// …/queries/get-new-tokens-from-user-credentials/…query-handler.ts
async handle(query) {
  const user = await this.userRepository.findByEmail(new Email(query.email));
  if (!user) throw new UnauthorizedException();
  if (!(await user.password.verify(query.password))) throw new UnauthorizedException();

  user.canAuthenticate();   // domain check on status

  const payload: TokenPayload = {
    userId: user.id.toValue(), email: user.email.toValue(), role: user.role.toValue(),
  };
  return {
    userId: user.id.toValue(),
    accessToken: this.jwtTokenService.generateAccessToken(payload),
    refreshToken: this.jwtTokenService.generateRefreshToken(payload),
  };
}

Passwords are themselves a value object (Password) that hashes on creation and verifies in the domain — the handler never touches a hashing library directly.

Protecting routes

JwtAuthGuard guards endpoints. It does two things: verify the JWT signature and expiry, and check the token still exists in the token store — so logging out (or revoking) genuinely invalidates a token even before it expires:

// libs/common/src/auth/jwt-auth.guard.ts
@Injectable()
export class JwtAuthGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
    const token = this.extractTokenFromHeader(request);
    if (!token) throw new UnauthorizedException();

    try {
      const payload = await this.jwtService.verifyAsync<JwtTokenPayload>(token);
      await this.queryBus.execute(new GetUserTokenByToken_Query(token)); // revocation check
      request.tokenData = payload;
      return true;
    } catch {
      throw new UnauthorizedException();
    }
  }
}

Apply it on a controller and you have the authenticated user on the request:

@UseGuards(JwtAuthGuard)
@Controller('dashboard')
export class DashboardController { /* request.tokenData.userId, .role … */ }

Refresh and revocation

  • Refresh — the client exchanges a valid refresh token for a new access (and refresh) token via a dedicated query, without re-entering credentials.
  • Revocation — because the guard checks the token store on every request, removing a token there immediately blocks it. Logout removes the token.

Roles and impersonation

The payload carries a role, used for role-based authorization (e.g. admin-only endpoints). For support workflows, an admin can mint impersonation tokens (shorter-lived: 1h access / 4h refresh) that carry isImpersonating plus the originalUserId/originalEmail, so the UI can show an impersonation banner and the action is auditable.

Google OAuth

Google sign-in is supported alongside email/password. The OAuth client lives in the auth context's infrastructure/services/google-oauth, behind a domain port, so the verification flow stays swappable and testable. A Google sign-up creates an active user (no email-verification step) and the same token pair is issued.

Secrets

JWT_SECRET and JWT_REFRESH_SECRET are required — the service refuses to start without them. Use long random values and keep the access/refresh secrets distinct. See the env templates in Deployment.

Next: Enforced Boundaries.