Merge pull request #13 from NTumurbars/codex/update-auth-token-structure-and-consumers
Refine auth token handling
This commit is contained in:
commit
a4119a2db8
@ -23,11 +23,14 @@ import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { sanitizeWhmcsRedirectPath } from "@bff/core/utils/sso.util";
|
||||
import { EmailService } from "@bff/infra/email/email.service";
|
||||
import type { AuthTokens, User as SharedUser } from "@customer-portal/domain";
|
||||
|
||||
import { User as SharedUser, type AuthTokens } from "@customer-portal/domain";
|
||||
|
||||
import type { User as PrismaUser } from "@prisma/client";
|
||||
import type { Request } from "express";
|
||||
import { WhmcsClientResponse } from "@bff/integrations/whmcs/types/whmcs-api.types";
|
||||
import { PrismaService } from "@bff/infra/database/prisma.service";
|
||||
import { calculateExpiryDate } from "./utils/jwt-expiry.util";
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@ -721,14 +724,13 @@ export class AuthService {
|
||||
// Helper methods
|
||||
private generateTokens(user: { id: string; email: string; role?: string }): AuthTokens {
|
||||
const payload = { email: user.email, sub: user.id, role: user.role };
|
||||
const accessToken = this.jwtService.sign(payload);
|
||||
|
||||
const tokenType = this.configService.get<string>("JWT_TOKEN_TYPE") ?? this.DEFAULT_TOKEN_TYPE;
|
||||
const expiresIn = this.configService.get<string | number>("JWT_EXPIRES_IN", "7d");
|
||||
const accessToken = this.jwtService.sign(payload, { expiresIn });
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
tokenType,
|
||||
expiresAt: this.resolveAccessTokenExpiry(accessToken),
|
||||
expiresAt: calculateExpiryDate(expiresIn),
|
||||
tokenType: "Bearer",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { Injectable, Inject } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { Redis } from "ioredis";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { parseJwtExpiry } from "../utils/jwt-expiry.util";
|
||||
|
||||
@Injectable()
|
||||
export class TokenBlacklistService {
|
||||
@ -27,7 +28,7 @@ export class TokenBlacklistService {
|
||||
} catch {
|
||||
// If we can't parse the token, blacklist it for the default JWT expiry time
|
||||
try {
|
||||
const defaultTtl = this.parseJwtExpiry(this.configService.get("JWT_EXPIRES_IN", "7d"));
|
||||
const defaultTtl = parseJwtExpiry(this.configService.get("JWT_EXPIRES_IN", "7d"));
|
||||
await this.redis.setex(`blacklist:${token}`, defaultTtl, "1");
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
@ -53,22 +54,4 @@ export class TokenBlacklistService {
|
||||
}
|
||||
}
|
||||
|
||||
private parseJwtExpiry(expiresIn: string): number {
|
||||
// Convert JWT expiry string to seconds
|
||||
const unit = expiresIn.slice(-1);
|
||||
const value = parseInt(expiresIn.slice(0, -1), 10);
|
||||
|
||||
switch (unit) {
|
||||
case "s":
|
||||
return value;
|
||||
case "m":
|
||||
return value * 60;
|
||||
case "h":
|
||||
return value * 60 * 60;
|
||||
case "d":
|
||||
return value * 24 * 60 * 60;
|
||||
default:
|
||||
return 7 * 24 * 60 * 60; // Default 7 days in seconds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
62
apps/bff/src/modules/auth/utils/jwt-expiry.util.ts
Normal file
62
apps/bff/src/modules/auth/utils/jwt-expiry.util.ts
Normal file
@ -0,0 +1,62 @@
|
||||
const DEFAULT_JWT_EXPIRY_SECONDS = 7 * 24 * 60 * 60;
|
||||
|
||||
const isFiniteNumber = (value: unknown): value is number =>
|
||||
typeof value === "number" && Number.isFinite(value) && value > 0;
|
||||
|
||||
/**
|
||||
* Parse a JWT expiry configuration string (e.g. "15m", "7d") into seconds.
|
||||
* Falls back to a sensible default when parsing fails.
|
||||
*/
|
||||
export const parseJwtExpiry = (expiresIn: string | number | undefined | null): number => {
|
||||
if (isFiniteNumber(expiresIn)) {
|
||||
return Math.floor(expiresIn);
|
||||
}
|
||||
|
||||
if (typeof expiresIn !== "string") {
|
||||
return DEFAULT_JWT_EXPIRY_SECONDS;
|
||||
}
|
||||
|
||||
const trimmed = expiresIn.trim();
|
||||
if (!trimmed) {
|
||||
return DEFAULT_JWT_EXPIRY_SECONDS;
|
||||
}
|
||||
|
||||
const unit = trimmed.slice(-1);
|
||||
const valuePortion = trimmed.slice(0, -1);
|
||||
const parsedValue = parseInt(valuePortion, 10);
|
||||
|
||||
const toSeconds = (multiplier: number) => {
|
||||
if (Number.isNaN(parsedValue) || parsedValue <= 0) {
|
||||
return DEFAULT_JWT_EXPIRY_SECONDS;
|
||||
}
|
||||
return parsedValue * multiplier;
|
||||
};
|
||||
|
||||
switch (unit) {
|
||||
case "s":
|
||||
return toSeconds(1);
|
||||
case "m":
|
||||
return toSeconds(60);
|
||||
case "h":
|
||||
return toSeconds(60 * 60);
|
||||
case "d":
|
||||
return toSeconds(24 * 60 * 60);
|
||||
default: {
|
||||
const numeric = Number(trimmed);
|
||||
if (isFiniteNumber(numeric)) {
|
||||
return Math.floor(numeric);
|
||||
}
|
||||
return DEFAULT_JWT_EXPIRY_SECONDS;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateExpiryDate = (
|
||||
expiresIn: string | number | undefined | null,
|
||||
referenceDate: number = Date.now()
|
||||
): string => {
|
||||
const seconds = parseJwtExpiry(expiresIn);
|
||||
return new Date(referenceDate + seconds * 1000).toISOString();
|
||||
};
|
||||
|
||||
export const JWT_EXPIRY_DEFAULT_SECONDS = DEFAULT_JWT_EXPIRY_SECONDS;
|
||||
@ -20,11 +20,24 @@ import type {
|
||||
|
||||
const DEFAULT_TOKEN_TYPE = "Bearer";
|
||||
|
||||
type LegacyAuthTokens = {
|
||||
access_token: string;
|
||||
refresh_token?: string | null;
|
||||
tokenType?: string | null;
|
||||
token_type?: string | null;
|
||||
refreshToken?: string | null;
|
||||
expiresAt?: string | number | Date | null;
|
||||
expires_at?: string | number | Date | null;
|
||||
};
|
||||
|
||||
type RawAuthTokens =
|
||||
| AuthTokens
|
||||
| (Omit<AuthTokens, "expiresAt"> & { expiresAt: IsoDateTimeString | number | Date });
|
||||
|
||||
const toIsoString = (value: string | number | Date): IsoDateTimeString => {
|
||||
| (Omit<AuthTokens, "expiresAt"> & { expiresAt: string | number | Date })
|
||||
| LegacyAuthTokens;
|
||||
|
||||
const toIsoString = (value: string | number | Date | null | undefined) => {
|
||||
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
@ -33,15 +46,40 @@ const toIsoString = (value: string | number | Date): IsoDateTimeString => {
|
||||
return new Date(value).toISOString();
|
||||
}
|
||||
|
||||
return value as IsoDateTimeString;
|
||||
|
||||
if (typeof value === "string" && value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Treat missing expiry as already expired to force re-authentication
|
||||
return new Date(0).toISOString();
|
||||
|
||||
};
|
||||
|
||||
const normalizeAuthTokens = (tokens: RawAuthTokens): AuthTokens => ({
|
||||
accessToken: tokens.accessToken,
|
||||
refreshToken: tokens.refreshToken,
|
||||
tokenType: tokens.tokenType ?? DEFAULT_TOKEN_TYPE,
|
||||
expiresAt: toIsoString(tokens.expiresAt),
|
||||
});
|
||||
const isLegacyTokens = (tokens: RawAuthTokens): tokens is LegacyAuthTokens =>
|
||||
typeof tokens === "object" && tokens !== null && "access_token" in tokens;
|
||||
|
||||
const normalizeAuthTokens = (tokens: RawAuthTokens): AuthTokens => {
|
||||
if (isLegacyTokens(tokens)) {
|
||||
const refreshToken = tokens.refreshToken ?? tokens.refresh_token ?? undefined;
|
||||
const tokenType = tokens.tokenType ?? tokens.token_type ?? undefined;
|
||||
const expiresAt = tokens.expiresAt ?? tokens.expires_at ?? undefined;
|
||||
|
||||
return {
|
||||
accessToken: tokens.access_token,
|
||||
refreshToken: refreshToken ?? undefined,
|
||||
tokenType: tokenType ?? DEFAULT_TOKEN_TYPE,
|
||||
expiresAt: toIsoString(expiresAt),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: tokens.accessToken,
|
||||
refreshToken: tokens.refreshToken,
|
||||
tokenType: tokens.tokenType ?? DEFAULT_TOKEN_TYPE,
|
||||
expiresAt: toIsoString(tokens.expiresAt),
|
||||
};
|
||||
};
|
||||
|
||||
const getExpiryTime = (tokens?: AuthTokens | null) => {
|
||||
if (!tokens?.expiresAt) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user