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.