UNPKG

@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
'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