Files
Prism-Notes-Main/auth-module/backend.ts
T
aewing 4c6058c656 Extract auth module: standalone PocketBase OAuth2 + Graph token management
- Create reusable auth-module/ with frontend and backend components
- Frontend: PocketBaseAuth class for OAuth2 login and token management
- Backend: GraphTokenManager and PocketBaseValidator for token operations
- Includes TypeScript types and comprehensive README
- No project-specific dependencies - ready to use in other projects
2026-01-16 03:49:52 +00:00

172 lines
4.5 KiB
TypeScript

import { ConfidentialClientApplication } from '@azure/msal-node';
import PocketBase from 'pocketbase';
import { GraphTokenCache, BackendAuthConfig } from './types';
/**
* Microsoft Graph Token Management (Backend)
* Handles token acquisition and caching for backend Graph API calls
*/
export class GraphTokenManager {
private cca: ConfidentialClientApplication;
private cache: GraphTokenCache | null = null;
private config: Required<BackendAuthConfig>;
constructor(config: BackendAuthConfig) {
this.config = {
clientId: config.clientId || process.env.CLIENT_ID || '',
tenantId: config.tenantId || process.env.TENANT_ID || '',
clientSecret: config.clientSecret || process.env.CLIENT_SECRET || '',
};
this.cca = new ConfidentialClientApplication({
auth: {
clientId: this.config.clientId,
authority: `https://login.microsoftonline.com/${this.config.tenantId}`,
clientSecret: this.config.clientSecret,
},
});
}
/**
* Get Graph token (from cache or acquire new)
*/
async getToken(): Promise<string> {
const now = Date.now();
// Check cache validity (with 60s buffer for expiration)
if (this.cache && this.cache.expiresOn - 60000 > now) {
return this.cache.token;
}
try {
const result = await this.cca.acquireTokenByClientCredential({
scopes: ['https://graph.microsoft.com/.default'],
});
if (!result?.accessToken) {
throw new Error('Failed to acquire Graph token');
}
const expiresOn = result.expiresOn
? new Date(result.expiresOn).getTime()
: now + 55 * 60 * 1000; // Default 55 minutes
this.cache = {
token: result.accessToken,
expiresOn,
};
return result.accessToken;
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
throw new Error(`Failed to acquire Graph token: ${message}`);
}
}
/**
* Get token with expiration info
*/
async getTokenWithExpiry(): Promise<{ token: string; expiresOnISO: string }> {
const token = await this.getToken();
const expiresOn = this.cache?.expiresOn || Date.now();
return {
token,
expiresOnISO: new Date(expiresOn).toISOString(),
};
}
/**
* Check if token is cached and valid
*/
isTokenValid(): boolean {
if (!this.cache) return false;
const now = Date.now();
return this.cache.expiresOn - 60000 > now;
}
/**
* Clear cache (force refresh on next call)
*/
clearCache(): void {
this.cache = null;
}
}
/**
* PocketBase Token Validation (Backend)
* Validates and uses user PocketBase tokens
*/
export class PocketBaseValidator {
private pb: PocketBase;
constructor(pbUrl?: string) {
this.pb = new PocketBase(pbUrl || process.env.PB_DB || 'http://127.0.0.1:8090');
}
/**
* Set user token and validate it
*/
async validateUserToken(token: string): Promise<boolean> {
try {
this.pb.authStore.save(token, null);
await this.pb.collection('Users').authRefresh();
return true;
} catch (error) {
console.error('Token validation failed:', error instanceof Error ? error.message : error);
return false;
}
}
/**
* Get user record from token
*/
async getUserRecord(token: string): Promise<any> {
try {
this.pb.authStore.save(token, null);
const record = this.pb.authStore.record || this.pb.authStore.model;
return record;
} catch (error) {
console.error('Failed to get user record:', error);
return null;
}
}
/**
* Get PocketBase instance
*/
getPocketBase(): PocketBase {
return this.pb;
}
}
/**
* Combined backend auth manager
*/
export class BackendAuth {
graphTokenManager: GraphTokenManager;
pbValidator: PocketBaseValidator;
constructor(config: BackendAuthConfig, pbUrl?: string) {
this.graphTokenManager = new GraphTokenManager(config);
this.pbValidator = new PocketBaseValidator(pbUrl);
}
/**
* Middleware to validate PocketBase token in requests
* Usage: app.use('/*', (c, next) => backendAuth.validateTokenMiddleware(c, next))
*/
async validateTokenMiddleware(c: any, next: any): Promise<any> {
const token = c.req.header('Authorization')?.replace('Bearer ', '') ||
(await c.req.json().catch(() => ({})))?.pbToken;
if (token) {
const isValid = await this.pbValidator.validateUserToken(token);
if (!isValid) {
return c.json({ error: 'Invalid token' }, 401);
}
}
return next();
}
}