@narangcia-oss/cryptic-auth-client-plain-ts
Version:
A TypeScript client for interacting with a cryptic-auth host web server, crafted by Narangcia OSS.
742 lines (734 loc) • 26.7 kB
JavaScript
'use strict';
var axios = require('axios');
/**
* Token utility functions
*/
/**
* Checks if a token is expired based on its timestamp
*/
function isTokenExpired(exp) {
const now = Math.floor(Date.now() / 1000);
const expired = exp < now;
console.log("[Tokens] Checking token expiration:", { exp, now, expired });
return expired;
}
/**
* Extracts tokens from auth response
*/
function extractTokens(response) {
console.log("[Tokens] Extracting tokens from response:", response);
return {
access_token: response.access_token,
refresh_token: response.refresh_token,
};
}
/**
* Stores tokens securely (placeholder for future custom storage)
*/
function storeTokens(tokens, storage = "memory") {
console.log("[Tokens] Storing tokens:", { tokens, storage });
switch (storage) {
case "localStorage":
localStorage.setItem("auth_tokens", JSON.stringify(tokens));
break;
case "sessionStorage":
sessionStorage.setItem("auth_tokens", JSON.stringify(tokens));
break;
}
}
/**
* Retrieves stored tokens
*/
function retrieveTokens(storage = "memory") {
console.log("[Tokens] Retrieving tokens from storage:", storage);
switch (storage) {
case "localStorage": {
const localTokens = localStorage.getItem("auth_tokens");
console.log("[Tokens] Retrieved from localStorage:", localTokens);
return localTokens ? JSON.parse(localTokens) : null;
}
case "sessionStorage": {
const sessionTokens = sessionStorage.getItem("auth_tokens");
console.log("[Tokens] Retrieved from sessionStorage:", sessionTokens);
return sessionTokens ? JSON.parse(sessionTokens) : null;
}
default:
// Memory storage is handled by the AuthClient instance
return null;
}
}
/**
* Clears stored tokens
*/
function clearStoredTokens(storage = "memory") {
console.log("[Tokens] Clearing stored tokens from:", storage);
switch (storage) {
case "localStorage":
localStorage.removeItem("auth_tokens");
break;
case "sessionStorage":
sessionStorage.removeItem("auth_tokens");
break;
}
}
/**
* Extract token expiration time from JWT token
*/
function getTokenExpiration(token) {
try {
// JWT tokens have 3 parts separated by dots
const parts = token.split(".");
if (parts.length !== 3)
return null;
// Decode the payload (middle part)
const payload = JSON.parse(atob(parts[1]));
if (payload.exp) {
// exp is in seconds, Date expects milliseconds
return new Date(payload.exp * 1000);
}
return null;
}
catch (_a) {
return null;
}
}
/**
* Check if a token is expired or will expire within a certain timeframe
*/
function isTokenExpiring(token, bufferMinutes = 5) {
const expiration = getTokenExpiration(token);
if (!expiration)
return true; // Assume expired if we can't parse
const now = new Date();
const bufferTime = bufferMinutes * 60 * 1000; // Convert to milliseconds
return expiration.getTime() - now.getTime() <= bufferTime;
}
/**
* Get token payload without verification (client-side only)
*/
function getTokenPayload(token) {
try {
const parts = token.split(".");
if (parts.length !== 3)
return null;
return JSON.parse(atob(parts[1]));
}
catch (_a) {
return null;
}
}
/**
* Format tokens for secure storage
*/
function formatTokensForStorage(tokens) {
return JSON.stringify({
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
user_id: tokens.user_id,
token_type: tokens.token_type,
expires_in: tokens.expires_in,
timestamp: Date.now(),
});
}
/**
* Parse tokens from secure storage
*/
function parseTokensFromStorage(stored) {
try {
const parsed = JSON.parse(stored);
return {
access_token: parsed.access_token,
refresh_token: parsed.refresh_token,
user_id: parsed.user_id,
token_type: parsed.token_type,
expires_in: parsed.expires_in,
};
}
catch (_a) {
return null;
}
}
/**
* Core authentication client for handling all auth flows
* Handles login, signup, OAuth, token refresh, and validation
*/
class AuthClient {
constructor(config) {
this.accessToken = null;
this.refreshToken = null;
this.isRefreshing = false;
this.failedQueue = [];
console.log("[AuthClient] Initializing with config:", Object.assign(Object.assign({}, config), { baseURL: config.baseURL }));
if (!config.baseURL) {
throw new Error("Base URL is required for AuthClient initialization.");
}
this.config = Object.assign({ enableAutoRefresh: true, tokenStorage: "memory" }, config);
this.api = axios.create({
baseURL: this.config.baseURL,
headers: {
"Content-Type": "application/json",
},
});
this.setupInterceptors();
}
setupInterceptors() {
// Request interceptor: attach access token
this.api.interceptors.request.use((config) => {
if (this.accessToken && config.headers) {
console.log("[AuthClient] Attaching access token to request.");
config.headers.Authorization = `Bearer ${this.accessToken}`;
}
return config;
}, (error) => {
console.error("[AuthClient] Request error:", error);
return Promise.reject(error);
});
this.api.interceptors.response.use((response) => {
console.log("[AuthClient] Response received:", response.status, response.config.url);
return response;
}, async (error) => {
var _a, _b;
const originalRequest = error.config;
if (((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 401 &&
originalRequest &&
!originalRequest._isRetry &&
this.config.enableAutoRefresh) {
console.warn("[AuthClient] 401 detected, attempting token refresh.");
if (this.isRefreshing) {
console.log("[AuthClient] Token refresh already in progress, queueing request.");
return new Promise((resolve, reject) => {
this.failedQueue.push({
resolve,
reject,
config: originalRequest,
});
});
}
this.isRefreshing = true;
originalRequest._isRetry = true;
try {
if (!this.refreshToken) {
console.error("[AuthClient] No refresh token available, clearing tokens.");
this.clearTokens();
this.processQueue(null);
return Promise.reject(new Error("Refresh token missing. Please re-authenticate."));
}
console.log("[AuthClient] Attempting to refresh token...");
const response = await this.refreshTokenFlow(this.refreshToken);
const newAccessToken = response.access_token;
const newRefreshToken = (_b = response.refresh_token) !== null && _b !== void 0 ? _b : "";
console.log("[AuthClient] Token refresh successful. New access token set.");
this.setTokens(newAccessToken, newRefreshToken);
this.processQueue(newAccessToken);
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
}
console.log("[AuthClient] Retrying original request after token refresh.");
return this.api(originalRequest);
}
catch (refreshError) {
console.error("[AuthClient] Token refresh failed:", refreshError);
this.clearTokens();
this.processQueue(null, refreshError);
return Promise.reject(refreshError);
}
finally {
console.log("[AuthClient] Token refresh process finished.");
this.isRefreshing = false;
}
}
console.error("[AuthClient] Response error:", error);
return Promise.reject(error);
});
}
processQueue(accessToken, error = null) {
console.log(`[AuthClient] Processing failed request queue. Queue length: ${this.failedQueue.length}`);
while (this.failedQueue.length) {
const { resolve, reject, config } = this.failedQueue.shift();
if (accessToken) {
if (config.headers) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
console.log("[AuthClient] Retrying queued request with new access token.");
resolve(this.api(config));
}
else {
console.error("[AuthClient] Rejecting queued request due to missing access token.");
reject(error || new axios.AxiosError("Authentication required", "401", config));
}
}
}
setTokens(accessToken, refreshToken) {
console.log("[AuthClient] Setting tokens.");
this.accessToken = accessToken;
this.refreshToken = refreshToken || null;
}
clearTokens() {
console.log("[AuthClient] Clearing all tokens.");
this.accessToken = null;
this.refreshToken = null;
}
getAccessToken() {
return this.accessToken;
}
getRefreshToken() {
return this.refreshToken;
}
isAuthenticated() {
return this.accessToken !== null;
}
async login(credentials) {
console.log("[AuthClient] login called with username:", credentials.username);
try {
const response = await this.api.post("/login", credentials);
console.log("[AuthClient] Login successful for:", credentials.username);
this.setTokens(response.data.access_token, response.data.refresh_token);
return response.data;
}
catch (error) {
console.error("[AuthClient] Login failed for:", credentials.username, error);
throw error;
}
}
async signup(credentials) {
console.log("[AuthClient] signup called with username:", credentials.username);
try {
const response = await this.api.post("/signup", credentials);
console.log("[AuthClient] Signup successful for:", credentials.username);
return response.data;
}
catch (error) {
console.error("[AuthClient] Signup failed for:", credentials.username, error);
throw error;
}
}
async refreshTokenFlow(refreshToken) {
console.log("[AuthClient] refreshTokenFlow called.");
try {
const response = await this.api.post("/token/refresh", {
refresh_token: refreshToken,
});
console.log("[AuthClient] Token refresh API call successful.");
return response.data;
}
catch (error) {
console.error("[AuthClient] Token refresh API call failed:", error);
throw error;
}
}
async validateToken(token) {
console.log("[AuthClient] validateToken called.");
try {
const response = await this.api.post("/token/validate", { token });
console.log("[AuthClient] Token validation response:", response.data);
return response.data;
}
catch (error) {
console.error("[AuthClient] Token validation failed:", error);
throw error;
}
}
async healthCheck() {
console.log("[AuthClient] healthCheck called.");
try {
const response = await this.api.get("/health");
console.log("[AuthClient] Health check response:", response.data);
return response.data;
}
catch (error) {
console.error("[AuthClient] Health check failed:", error);
throw error;
}
}
async generateOAuthAuthUrl(provider, state, scopes) {
console.log(`[AuthClient] generateOAuthAuthUrl called for provider: ${provider}`);
try {
const response = await this.api.get(`/oauth/${provider}/auth`, {
params: {
state,
scopes: scopes.join(" "),
},
});
if (response.data && response.data.auth_url) {
console.log(`[AuthClient] OAuth auth URL generated: ${response.data.auth_url}`);
return response.data.auth_url;
}
console.error("[AuthClient] Invalid response from OAuth auth endpoint.");
throw new Error("Invalid response from OAuth auth endpoint");
}
catch (error) {
console.error(`[AuthClient] Failed to generate OAuth URL for ${provider}:`, error);
throw error;
}
}
async oauthLoginCallback(provider, params) {
console.log(`[AuthClient] oauthLoginCallback called for provider: ${provider}`);
try {
const response = await this.api.post(`/oauth/login`, {
provider,
code: params.code,
state: params.state,
});
console.log(`[AuthClient] OAuth login successful for provider: ${provider}`);
this.setTokens(response.data.access_token, response.data.refresh_token);
return response.data;
}
catch (error) {
console.error(`[AuthClient] OAuth login failed for provider: ${provider}`, error);
throw error;
}
}
async oauthSignupCallback(provider, params) {
console.log(`[AuthClient] oauthSignupCallback called for provider: ${provider}`);
try {
const response = await this.api.post(`/oauth/signup`, {
provider,
code: params.code,
state: params.state,
});
console.log(`[AuthClient] OAuth signup successful for provider: ${provider}`);
this.setTokens(response.data.access_token, response.data.refresh_token);
return response.data;
}
catch (error) {
console.error(`[AuthClient] OAuth signup failed for provider: ${provider}`, error);
throw error;
}
}
getAxiosInstance() {
console.log("[AuthClient] getAxiosInstance called.");
return this.api;
}
}
// Static utility methods
AuthClient.extractTokens = extractTokens;
AuthClient.isTokenExpired = isTokenExpired;
/**
* OAuth utility functions
*/
/**
* Generates a secure random state for OAuth CSRF protection
*/
function generateOAuthState() {
const state = Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
console.log("[OAuth] Generated OAuth state:", state);
return state;
}
/**
* Stores OAuth state securely for validation
*/
function storeOAuthState(state) {
console.log("[OAuth] Storing OAuth state:", state);
sessionStorage.setItem("oauth_state", state);
}
/**
* Retrieves stored OAuth state for validation
*/
function getStoredOAuthState() {
const state = sessionStorage.getItem("oauth_state");
console.log("[OAuth] Retrieved stored OAuth state:", state);
return state;
}
/**
* Clears stored OAuth state
*/
function clearOAuthState() {
console.log("[OAuth] Clearing stored OAuth state");
sessionStorage.removeItem("oauth_state");
}
/**
* Validates OAuth state to prevent CSRF attacks
*/
function validateOAuthState(receivedState) {
const storedState = getStoredOAuthState();
const isValid = receivedState === storedState;
console.log("[OAuth] Validating OAuth state:", {
receivedState,
storedState,
isValid,
});
return isValid;
}
/**
* Extracts OAuth callback parameters from URL
*/
function extractOAuthParams() {
const urlParams = new URLSearchParams(window.location.search);
return {
code: urlParams.get("code"),
state: urlParams.get("state"),
error: urlParams.get("error"),
};
}
/**
* Checks if current URL is an OAuth callback
*/
function isOAuthCallback() {
return (window.location.pathname.includes("/auth/") &&
window.location.search.includes("code="));
}
/**
* Cleans OAuth parameters from URL
*/
function cleanOAuthUrl() {
window.history.replaceState({}, document.title, window.location.origin);
}
/**
* Core OAuth callback handler - framework agnostic
* Handles the OAuth callback flow logic without any UI dependencies
*/
class OAuthCallbackHandler {
constructor(authClient) {
this.authClient = authClient;
}
/**
* Checks if the current URL is an OAuth callback URL
*/
isOAuthCallback() {
return isOAuthCallback();
}
/**
* Processes the OAuth callback from the current URL
* Returns the result without any UI side effects
*/
async processCallback() {
try {
if (!this.isOAuthCallback()) {
return {
success: false,
error: "Not an OAuth callback URL",
};
}
const { code, state, error: oauthError } = extractOAuthParams();
if (oauthError) {
return {
success: false,
error: `OAuth error: ${oauthError}`,
};
}
if (!code) {
return {
success: false,
error: "Authorization code not found",
};
}
if (!state) {
return {
success: false,
error: "State parameter not found",
};
}
if (!validateOAuthState(state)) {
return {
success: false,
error: "Invalid state parameter - possible CSRF attack",
};
}
// Clean up stored state
clearOAuthState();
// Extract provider from pathname (you might want to make this more robust)
const provider = this.extractProviderFromUrl();
// Handle OAuth callback
const response = await this.authClient.oauthLoginCallback(provider, {
code,
state,
});
// Extract tokens from response
const tokens = extractTokens(response);
// Extract user information from response
const user = {
id: response.id,
identifier: response.identifier,
};
return {
success: true,
tokens,
user,
};
}
catch (err) {
const errorMessage = err instanceof Error ? err.message : "OAuth authentication failed";
return {
success: false,
error: errorMessage,
};
}
}
/**
* Extracts the OAuth provider from the current URL
* Override this method for custom provider detection logic
*/
extractProviderFromUrl() {
const pathname = window.location.pathname;
if (pathname.includes("github"))
return "github";
if (pathname.includes("google"))
return "google";
if (pathname.includes("microsoft"))
return "microsoft";
// Default fallback - you might want to throw an error instead
return "github";
}
/**
* Cleans the OAuth parameters from the current URL
*/
cleanUrl() {
window.history.replaceState({}, document.title, window.location.origin);
}
}
/**
* OAuth2 Fragment Handler for processing tokens from URL fragments
* This handles the new OAuth2 flow where the backend redirects to frontend with tokens
*/
class OAuth2FragmentHandler {
/**
* Checks if the current URL contains OAuth2 fragment parameters
*/
static isOAuth2Fragment() {
const fragment = window.location.hash.substring(1);
const params = new URLSearchParams(fragment);
const hasFragment = params.has("access_token") || params.has("error");
console.log("[OAuth2FragmentHandler] isOAuth2Fragment:", hasFragment, fragment);
return hasFragment;
}
/**
* Resets the processing state (for testing or manual reset)
*/
static resetProcessingState() {
this.hasProcessed = false;
this.currentFragment = "";
}
/**
* Processes OAuth2 tokens from URL fragment
*/
static processFragment() {
const fragment = window.location.hash.substring(1);
// Prevent processing the same fragment multiple times
if (this.hasProcessed && this.currentFragment === fragment) {
console.log("[OAuth2FragmentHandler] Fragment already processed, skipping");
return {
success: false,
error: "already_processed",
errorDescription: "This OAuth2 fragment has already been processed",
};
}
this.currentFragment = fragment;
this.hasProcessed = true;
const params = new URLSearchParams(fragment);
console.log("[OAuth2FragmentHandler] processFragment: fragment =", fragment);
// Check for error first
if (params.has("error")) {
const error = params.get("error") || "Unknown OAuth error";
const errorDescription = params.get("error_description") || "No description provided";
console.warn("[OAuth2FragmentHandler] OAuth2 error detected:", error, errorDescription);
return {
success: false,
error,
errorDescription,
};
}
// Extract tokens
const accessToken = params.get("access_token");
const refreshToken = params.get("refresh_token");
const userId = params.get("user_id");
const tokenType = params.get("token_type"); // Usually "Bearer"
const expiresIn = params.get("expires_in"); // Token expiration in seconds
console.log("[OAuth2FragmentHandler] Extracted tokens:", {
accessToken,
refreshToken,
userId,
tokenType,
expiresIn,
});
if (!accessToken || !refreshToken || !userId) {
console.error("[OAuth2FragmentHandler] Missing required authentication parameters");
return {
success: false,
error: "incomplete_auth_data",
errorDescription: "Missing required authentication parameters",
};
}
return {
success: true,
tokens: {
access_token: accessToken,
refresh_token: refreshToken,
// Store additional metadata if needed
user_id: userId,
token_type: tokenType || "Bearer",
expires_in: expiresIn ? parseInt(expiresIn) : undefined,
},
};
}
/**
* Clears OAuth2 parameters from URL fragment for security
*/
static clearFragment() {
console.log("[OAuth2FragmentHandler] Clearing OAuth2 fragment from URL");
try {
// Remove the fragment from URL without triggering navigation
const newUrl = window.location.pathname + window.location.search;
window.history.replaceState(null, "", newUrl);
// Additional cleanup - ensure hash is completely removed
if (window.location.hash) {
window.location.hash = "";
}
// Reset processing state after clearing
this.resetProcessingState();
}
catch (error) {
console.error("[OAuth2FragmentHandler] Error clearing fragment:", error);
// Fallback: reload the page without the fragment
window.location.href = window.location.pathname + window.location.search;
}
}
/**
* Complete OAuth2 fragment processing - process and clean up
*/
static processAndClear() {
console.log("[OAuth2FragmentHandler] processAndClear called");
const result = this.processFragment();
this.clearFragment();
console.log("[OAuth2FragmentHandler] processAndClear result:", result);
return result;
}
}
OAuth2FragmentHandler.hasProcessed = false;
OAuth2FragmentHandler.currentFragment = "";
/**
* Utility function to check if current page is an OAuth2 callback
*/
function isOAuth2Callback() {
return OAuth2FragmentHandler.isOAuth2Fragment();
}
/**
* Utility function to extract OAuth2 tokens from URL fragment
*/
function extractOAuth2Tokens() {
return OAuth2FragmentHandler.processAndClear();
}
exports.AuthClient = AuthClient;
exports.OAuth2FragmentHandler = OAuth2FragmentHandler;
exports.OAuthCallbackHandler = OAuthCallbackHandler;
exports.cleanOAuthUrl = cleanOAuthUrl;
exports.clearOAuthState = clearOAuthState;
exports.clearStoredTokens = clearStoredTokens;
exports.extractOAuth2Tokens = extractOAuth2Tokens;
exports.extractOAuthParams = extractOAuthParams;
exports.extractTokens = extractTokens;
exports.formatTokensForStorage = formatTokensForStorage;
exports.generateOAuthState = generateOAuthState;
exports.getStoredOAuthState = getStoredOAuthState;
exports.getTokenExpiration = getTokenExpiration;
exports.getTokenPayload = getTokenPayload;
exports.isOAuth2Callback = isOAuth2Callback;
exports.isOAuthCallback = isOAuthCallback;
exports.isTokenExpired = isTokenExpired;
exports.isTokenExpiring = isTokenExpiring;
exports.parseTokensFromStorage = parseTokensFromStorage;
exports.retrieveTokens = retrieveTokens;
exports.storeOAuthState = storeOAuthState;
exports.storeTokens = storeTokens;
exports.validateOAuthState = validateOAuthState;
//# sourceMappingURL=index.cjs.js.map