/**
* JWT Authentication Implementation
* Complete JWT authentication with token refresh and RequestManager integration
*/
import type { AuthConfig, RequestOptions } from "@/types/client.js";
import { AuthenticationError } from "@/types/client.js";
import { AUTH_METHODS } from "@/types/wordpress.js";
import { LoggerFactory } from "@/utils/logger.js";
import type { RequestManager } from "./RequestManager.js";
export interface JWTTokenResponse {
token: string;
user_email: string;
user_nicename: string;
user_display_name: string;
expires_in?: number;
}
export interface JWTValidateResponse {
code: string;
data: {
status: number;
};
}
export interface JWTRefreshResponse {
token: string;
expires_in?: number;
}
export class JWTAuthImplementation {
private logger = LoggerFactory.api();
private jwtToken: string | null = null;
private tokenExpiry: number | null = null;
private refreshTokenValue: string | null = null;
constructor(
private requestManager: RequestManager,
private authConfig: AuthConfig,
) {}
/**
* Complete JWT authentication implementation
*/
async authenticateJWT(): Promise<void> {
if (!this.authConfig.username || !this.authConfig.password) {
throw new AuthenticationError("JWT authentication requires username and password", AUTH_METHODS.JWT);
}
try {
this.logger.info("Starting JWT authentication", {
username: this.authConfig.username,
endpoint: "jwt-auth/v1/token",
});
const response = await this.requestManager.request<JWTTokenResponse>(
"POST",
"jwt-auth/v1/token",
{
username: this.authConfig.username,
password: this.authConfig.password,
},
{
skipAuth: true, // Don't use auth headers for the auth request itself
timeout: 10000,
} as RequestOptions & { skipAuth: boolean },
);
if (!response.token) {
throw new AuthenticationError("JWT token not received in response", AUTH_METHODS.JWT);
}
// Store the JWT token
this.jwtToken = response.token;
// Calculate token expiry (default to 24 hours if not provided)
const expiresInSeconds = response.expires_in || 24 * 60 * 60;
this.tokenExpiry = Date.now() + expiresInSeconds * 1000;
this.logger.info("JWT authentication successful", {
user: response.user_nicename,
expiresAt: new Date(this.tokenExpiry).toISOString(),
});
// Test the token by validating it
await this.validateToken();
} catch (error) {
this.jwtToken = null;
this.tokenExpiry = null;
this.logger.error("JWT authentication failed", {
error: error instanceof Error ? error.message : String(error),
});
if (error instanceof Error) {
throw new AuthenticationError(`JWT authentication failed: ${error.message}`, AUTH_METHODS.JWT);
}
throw new AuthenticationError("JWT authentication failed", AUTH_METHODS.JWT);
}
}
/**
* Validate JWT token
*/
async validateToken(): Promise<boolean> {
if (!this.jwtToken) {
return false;
}
try {
const response = await this.requestManager.request<JWTValidateResponse>(
"POST",
"jwt-auth/v1/token/validate",
{},
{
headers: {
Authorization: `Bearer ${this.jwtToken}`,
},
timeout: 5000,
},
);
const isValid = response.data.status === 200;
if (!isValid) {
this.logger.warn("JWT token validation failed", { response });
this.jwtToken = null;
this.tokenExpiry = null;
}
return isValid;
} catch (error) {
this.logger.warn("JWT token validation error", {
error: error instanceof Error ? error.message : String(error),
});
return false;
}
}
/**
* Refresh JWT token
*/
async refreshToken(): Promise<void> {
if (!this.jwtToken) {
throw new AuthenticationError("No JWT token to refresh", AUTH_METHODS.JWT);
}
try {
this.logger.debug("Refreshing JWT token");
// Some JWT implementations support refresh endpoints
// First try the refresh endpoint if available
try {
const refreshResponse = await this.requestManager.request<JWTRefreshResponse>(
"POST",
"jwt-auth/v1/token/refresh",
{},
{
headers: {
Authorization: `Bearer ${this.jwtToken}`,
},
timeout: 5000,
},
);
if (refreshResponse.token) {
this.jwtToken = refreshResponse.token;
const expiresInSeconds = refreshResponse.expires_in || 24 * 60 * 60;
this.tokenExpiry = Date.now() + expiresInSeconds * 1000;
this.logger.info("JWT token refreshed successfully", {
expiresAt: new Date(this.tokenExpiry).toISOString(),
});
return;
}
} catch (_refreshError) {
this.logger.debug("JWT refresh endpoint not available, falling back to re-authentication");
}
// Fallback: Re-authenticate with original credentials
if (!this.authConfig.username || !this.authConfig.password) {
throw new AuthenticationError("Cannot refresh JWT token: original credentials not available", AUTH_METHODS.JWT);
}
this.logger.debug("Re-authenticating to refresh JWT token");
await this.authenticateJWT();
} catch (error) {
this.jwtToken = null;
this.tokenExpiry = null;
this.logger.error("JWT token refresh failed", {
error: error instanceof Error ? error.message : String(error),
});
throw new AuthenticationError(
`JWT token refresh failed: ${error instanceof Error ? error.message : "Unknown error"}`,
AUTH_METHODS.JWT,
);
}
}
/**
* Check if JWT token is expired
*/
isTokenExpired(): boolean {
if (!this.tokenExpiry) {
return true;
}
// Consider token expired if it expires within 5 minutes (300000ms)
const buffer = 5 * 60 * 1000;
return Date.now() >= this.tokenExpiry - buffer;
}
/**
* Get current JWT token
*/
getToken(): string | null {
return this.jwtToken;
}
/**
* Get token expiry timestamp
*/
getTokenExpiry(): number | null {
return this.tokenExpiry;
}
/**
* Clear JWT token and expiry
*/
clearToken(): void {
this.jwtToken = null;
this.tokenExpiry = null;
this.refreshTokenValue = null;
this.logger.debug("JWT token cleared");
}
/**
* Get JWT auth headers
*/
getAuthHeaders(): Record<string, string> {
if (!this.jwtToken) {
return {};
}
return {
Authorization: `Bearer ${this.jwtToken}`,
};
}
/**
* Check if we have a valid JWT token
*/
hasValidToken(): boolean {
return this.jwtToken !== null && !this.isTokenExpired();
}
/**
* Automatic token refresh if needed
*/
async ensureValidToken(): Promise<void> {
if (!this.jwtToken) {
// No token, need to authenticate
await this.authenticateJWT();
return;
}
if (this.isTokenExpired()) {
// Token expired, need to refresh
await this.refreshToken();
return;
}
// Token is still valid
}
/**
* Logout and invalidate token
*/
async logout(): Promise<void> {
if (!this.jwtToken) {
return;
}
try {
// Try to invalidate the token on the server
await this.requestManager.request(
"POST",
"jwt-auth/v1/token/invalidate",
{},
{
headers: this.getAuthHeaders(),
timeout: 5000,
},
);
this.logger.info("JWT token invalidated on server");
} catch (error) {
this.logger.warn("Failed to invalidate JWT token on server", {
error: error instanceof Error ? error.message : String(error),
});
}
this.clearToken();
}
/**
* Get token information
*/
getTokenInfo(): {
hasToken: boolean;
isExpired: boolean;
expiresAt: string | null;
expiresIn: number | null;
} {
return {
hasToken: this.jwtToken !== null,
isExpired: this.isTokenExpired(),
expiresAt: this.tokenExpiry ? new Date(this.tokenExpiry).toISOString() : null,
expiresIn: this.tokenExpiry ? Math.max(0, this.tokenExpiry - Date.now()) : null,
};
}
}