sessionize-auth
Version:
A flexible session management library for React, Next.js, Angular, and React Native
241 lines (210 loc) • 6.75 kB
text/typescript
import {
SSOConfig,
SSOProvider,
SSOUser,
SSOAuthResult,
SSOAuthState,
SSOError
} from "./types";
import { BaseSSOProvider, SSOErrorClass } from "./providers/base";
import { GoogleSSOProvider } from "./providers/google";
import { MicrosoftSSOProvider } from "./providers/microsoft";
import { GitHubSSOProvider } from "./providers/github";
/**
* SSO Manager for handling multiple OAuth providers
*/
export class SSOManager {
private config: SSOConfig;
private providers: Map<string, BaseSSOProvider> = new Map();
private stateStorage: Map<string, SSOAuthState> = new Map();
constructor(config: SSOConfig) {
this.config = config;
this.initializeProviders();
}
/**
* Initialize all configured providers
*/
private initializeProviders(): void {
this.config.providers.forEach(providerConfig => {
let provider: BaseSSOProvider;
switch (providerConfig.id) {
case 'google':
provider = new GoogleSSOProvider(providerConfig.id, {
clientId: providerConfig.clientId,
redirectUri: providerConfig.redirectUri,
scopes: providerConfig.scopes
});
break;
case 'microsoft':
provider = new MicrosoftSSOProvider(providerConfig.id, {
clientId: providerConfig.clientId,
redirectUri: providerConfig.redirectUri,
scopes: providerConfig.scopes
});
break;
case 'github':
provider = new GitHubSSOProvider(providerConfig.id, {
clientId: providerConfig.clientId,
redirectUri: providerConfig.redirectUri,
scopes: providerConfig.scopes
});
break;
default:
throw new SSOErrorClass({
code: 'UNSUPPORTED_PROVIDER',
message: `Unsupported SSO provider: ${providerConfig.id}`
});
}
this.providers.set(providerConfig.id, provider);
});
}
/**
* Get all available providers
*/
getProviders(): SSOProvider[] {
return Array.from(this.providers.values()).map(provider => provider.getProvider());
}
/**
* Get a specific provider
*/
getProvider(providerId: string): SSOProvider | null {
const provider = this.providers.get(providerId);
return provider ? provider.getProvider() : null;
}
/**
* Generate authorization URL for a provider
*/
generateAuthUrl(providerId: string, returnTo?: string): string {
const provider = this.providers.get(providerId);
if (!provider) {
throw new SSOErrorClass({
code: 'PROVIDER_NOT_FOUND',
message: `Provider not found: ${providerId}`
});
}
const state = this.generateState();
const authState: SSOAuthState = {
provider: providerId,
state: state,
timestamp: Date.now(),
returnTo: returnTo
};
this.stateStorage.set(state, authState);
return provider.generateAuthUrl(state, returnTo);
}
/**
* Handle OAuth callback
*/
async handleCallback(
providerId: string,
code: string,
state: string
): Promise<SSOAuthResult> {
try {
// Verify state
const authState = this.stateStorage.get(state);
if (!authState) {
throw new SSOErrorClass({
code: 'INVALID_STATE',
message: 'Invalid or expired state parameter'
});
}
if (authState.provider !== providerId) {
throw new SSOErrorClass({
code: 'PROVIDER_MISMATCH',
message: 'Provider mismatch in state'
});
}
// Check state expiration (5 minutes)
if (Date.now() - authState.timestamp > 5 * 60 * 1000) {
this.stateStorage.delete(state);
throw new SSOErrorClass({
code: 'STATE_EXPIRED',
message: 'State parameter has expired'
});
}
const provider = this.providers.get(providerId);
if (!provider) {
throw new SSOErrorClass({
code: 'PROVIDER_NOT_FOUND',
message: `Provider not found: ${providerId}`
});
}
// Exchange code for token
const tokenData = await provider.exchangeCodeForToken(code, state);
// Get user info
const user = await provider.getUserInfo(tokenData.accessToken);
// Add token information
user.accessToken = tokenData.accessToken;
user.refreshToken = tokenData.refreshToken;
user.expiresAt = tokenData.expiresIn ? Date.now() + (tokenData.expiresIn * 1000) : undefined;
// Clean up state
this.stateStorage.delete(state);
return {
success: true,
user: user,
provider: providerId
};
} catch (error) {
return {
success: false,
error: error instanceof SSOErrorClass ? error.message : 'Unknown error occurred',
provider: providerId
};
}
}
/**
* Refresh access token
*/
async refreshToken(providerId: string, refreshToken: string): Promise<{ accessToken: string; expiresIn?: number }> {
const provider = this.providers.get(providerId);
if (!provider) {
throw new SSOErrorClass({
code: 'PROVIDER_NOT_FOUND',
message: `Provider not found: ${providerId}`
});
}
return provider.refreshToken(refreshToken);
}
/**
* Get user info with current access token
*/
async getUserInfo(providerId: string, accessToken: string): Promise<SSOUser> {
const provider = this.providers.get(providerId);
if (!provider) {
throw new SSOErrorClass({
code: 'PROVIDER_NOT_FOUND',
message: `Provider not found: ${providerId}`
});
}
return provider.getUserInfo(accessToken);
}
/**
* Generate a random state parameter
*/
private generateState(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
/**
* Clean up expired states
*/
cleanupExpiredStates(): void {
const now = Date.now();
const expiredStates: string[] = [];
this.stateStorage.forEach((state, key) => {
if (now - state.timestamp > 5 * 60 * 1000) { // 5 minutes
expiredStates.push(key);
}
});
expiredStates.forEach(state => this.stateStorage.delete(state));
}
/**
* Update configuration
*/
updateConfig(newConfig: Partial<SSOConfig>): void {
this.config = { ...this.config, ...newConfig };
this.initializeProviders();
}
}