205 lines
5.8 KiB
TypeScript
205 lines
5.8 KiB
TypeScript
// SDK Client for Auth-Yes Zero Trust Identity Provider
|
|
// Designed to be imported by subsidiary applications to validate stateful session tokens.
|
|
|
|
import { Redis } from "npm:ioredis";
|
|
import { createClient } from "npm:@connectrpc/connect@^1.4.0";
|
|
import { createConnectTransport } from "npm:@connectrpc/connect-node@^1.4.0";
|
|
import { AuthService } from "./gen/auth_connect.ts";
|
|
|
|
/**
|
|
* Configuration options for the Auth SDK.
|
|
*/
|
|
export interface AuthSdkConfig {
|
|
/**
|
|
* The internal network URL of the Auth API Gateway.
|
|
* e.g., 'http://auth-api.internal:8000'
|
|
*/
|
|
authApiUrl: string;
|
|
/**
|
|
* The Valkey URL for RESP3 Client-Side Caching (optional).
|
|
* e.g., 'redis://auth-valkey:6379'
|
|
*/
|
|
valkeyUrl?: string;
|
|
/**
|
|
* Optional custom transport if deploying in environments (like browser)
|
|
* where connect-node is unavailable.
|
|
*/
|
|
customTransport?: any;
|
|
/**
|
|
* The mTLS certificate.
|
|
*/
|
|
tlsCert?: string;
|
|
/**
|
|
* The mTLS private key.
|
|
*/
|
|
tlsKey?: string;
|
|
/**
|
|
* The mTLS CA certificate.
|
|
*/
|
|
tlsCa?: string;
|
|
}
|
|
|
|
/**
|
|
* The validated session data returned by the Auth API.
|
|
*/
|
|
export interface SessionData {
|
|
valid: boolean;
|
|
uuid?: string;
|
|
scopes?: string[];
|
|
error?: string;
|
|
}
|
|
|
|
export class AuthSdk {
|
|
private config: AuthSdkConfig;
|
|
private l1Cache: Map<string, SessionData>;
|
|
private valkeyClient: Redis | null = null;
|
|
private grpcClient: any;
|
|
|
|
constructor(config: AuthSdkConfig) {
|
|
this.config = config;
|
|
this.l1Cache = new Map();
|
|
|
|
if (this.config.valkeyUrl) {
|
|
this.initValkeyClient();
|
|
}
|
|
|
|
const nodeOptions: Record<string, any> = { rejectUnauthorized: false };
|
|
if (this.config.tlsCert && this.config.tlsKey && this.config.tlsCa) {
|
|
nodeOptions.rejectUnauthorized = true;
|
|
nodeOptions.cert = this.config.tlsCert;
|
|
nodeOptions.key = this.config.tlsKey;
|
|
nodeOptions.ca = this.config.tlsCa;
|
|
}
|
|
|
|
const transport = this.config.customTransport || createConnectTransport({
|
|
baseUrl: this.config.authApiUrl,
|
|
httpVersion: "2",
|
|
nodeOptions: nodeOptions,
|
|
});
|
|
|
|
this.grpcClient = createClient(AuthService, transport);
|
|
}
|
|
|
|
private initValkeyClient() {
|
|
this.valkeyClient = new Redis(this.config.valkeyUrl!, {
|
|
enableOfflineQueue: false,
|
|
});
|
|
|
|
this.valkeyClient.on("ready", async () => {
|
|
// Negotiate RESP3 and enable client tracking
|
|
try {
|
|
await this.valkeyClient!.hello(3);
|
|
// Enable tracking in BCAST (broadcast) mode because this client
|
|
// never actually issues GET commands to trigger standard tracking
|
|
await this.valkeyClient!.client("TRACKING", "ON", "BCAST");
|
|
} catch (e) {
|
|
console.error("Failed to enable RESP3 client tracking:", e);
|
|
}
|
|
});
|
|
|
|
// Listen for RESP3 push invalidation messages
|
|
this.valkeyClient.on("push", (msg: unknown) => {
|
|
if (
|
|
Array.isArray(msg) && msg.length >= 2 && msg[0] === "invalidate"
|
|
) {
|
|
const keysToInvalidate = msg[1];
|
|
if (Array.isArray(keysToInvalidate)) {
|
|
for (const key of keysToInvalidate) {
|
|
// SIDE EFFECT: Delete the invalidated key from the local Map
|
|
this.l1Cache.delete(key);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
this.valkeyClient.on("error", (err: unknown) => {
|
|
console.error("Valkey SDK Client error:", err);
|
|
console.warn(
|
|
"[AuthSdk] Valkey connection lost. Clearing L1 cache to prevent stale sessions.",
|
|
);
|
|
this.l1Cache.clear();
|
|
});
|
|
|
|
this.valkeyClient.on("close", () => {
|
|
console.warn(
|
|
"[AuthSdk] Valkey connection lost. Clearing L1 cache to prevent stale sessions.",
|
|
);
|
|
this.l1Cache.clear();
|
|
});
|
|
|
|
this.valkeyClient.on("end", () => {
|
|
console.warn(
|
|
"[AuthSdk] Valkey connection lost. Clearing L1 cache to prevent stale sessions.",
|
|
);
|
|
this.l1Cache.clear();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Validates an opaque session token against the Auth API.
|
|
* This is a fast-path operation that leverages the central Valkey cache.
|
|
*
|
|
* @param token The opaque session token (e.g., extracted from a cookie).
|
|
* @returns The validated session data containing the UUID and scopes.
|
|
*/
|
|
async validateSession(token: string): Promise<SessionData> {
|
|
// Check L1 cache first
|
|
const cachedSession = this.l1Cache.get(token);
|
|
if (cachedSession) {
|
|
return cachedSession;
|
|
}
|
|
|
|
try {
|
|
const response = await this.grpcClient.validateSession({ token });
|
|
|
|
const sessionData: SessionData = {
|
|
valid: response.valid,
|
|
uuid: response.uuid,
|
|
scopes: response.scopes,
|
|
error: response.error,
|
|
};
|
|
|
|
// Only populate L1 cache if Valkey integration is enabled for invalidations
|
|
if (sessionData.valid && this.config.valkeyUrl) {
|
|
this.l1Cache.set(token, sessionData);
|
|
}
|
|
|
|
return sessionData;
|
|
} catch (error) {
|
|
// Typically network errors or internal DNS resolution failures
|
|
return {
|
|
valid: false,
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Middleware for web frameworks (e.g., Oak, Hono) to intercept and validate requests.
|
|
* Developers should wrap this around protected routes.
|
|
*
|
|
* @param token The extracted session token.
|
|
* @throws Error if the token is invalid or missing.
|
|
* @returns The user's UUID.
|
|
*/
|
|
async requireAuth(token: string | null | undefined): Promise<string> {
|
|
if (!token) {
|
|
throw new Error("Unauthorized: Missing session token.");
|
|
}
|
|
|
|
const session = await this.validateSession(token);
|
|
if (!session.valid || !session.uuid) {
|
|
throw new Error(`Unauthorized: ${session.error || "Invalid session."}`);
|
|
}
|
|
|
|
return session.uuid;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new instance of the Auth SDK.
|
|
*/
|
|
export function createAuthSdk(config: AuthSdkConfig): AuthSdk {
|
|
return new AuthSdk(config);
|
|
}
|