shogun-core
Version:
SHOGUN CORE - Core library for Shogun Ecosystem
389 lines (388 loc) • 16.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.OAuthPlugin = void 0;
const base_1 = require("../base");
const oauthConnector_1 = require("./oauthConnector");
const errorHandler_1 = require("../../utils/errorHandler");
const storage_1 = require("../../storage/storage");
/**
* OAuth Plugin for ShogunCore
* Provides authentication with external OAuth providers
*/
class OAuthPlugin extends base_1.BasePlugin {
name = "oauth";
version = "1.0.0";
description = "Provides OAuth authentication with external providers for ShogunCore";
oauthConnector = null;
config = {};
storage = null;
/**
* Constructor for OAuthPlugin
* @param config - Initial configuration for OAuth
*/
constructor(config) {
super();
if (config) {
this.config = config;
}
}
/**
* @inheritdoc
*/
initialize(core) {
this.core = core;
this.storage = new storage_1.ShogunStorage();
// Inizializziamo il connector OAuth con la configurazione già presente
this.oauthConnector = new oauthConnector_1.OAuthConnector(this.config);
// Valida la configurazione di sicurezza dopo l'inizializzazione
this.validateOAuthSecurity();
}
/**
* Valida la configurazione di sicurezza OAuth
*/
validateOAuthSecurity() {
if (!this.oauthConnector)
return;
const providers = this.oauthConnector.getAvailableProviders();
for (const provider of providers) {
const providerConfig = this.config.providers?.[provider];
if (!providerConfig)
continue;
// Verifica che PKCE sia abilitato per tutti i provider
if (!providerConfig.usePKCE && typeof window !== "undefined") {
console.warn(`[oauthPlugin] Provider ${provider} non ha PKCE abilitato - non sicuro per browser`);
}
// Verifica che non ci sia client_secret nel browser (eccetto Google con PKCE)
if (providerConfig.clientSecret && typeof window !== "undefined") {
if (provider === "google" && providerConfig.usePKCE) {
// Non lanciare errore per Google con PKCE
continue;
}
else {
console.error(`[oauthPlugin] Provider ${provider} ha client_secret configurato nel browser - RIMUOVERE`);
throw new Error(`Client secret non può essere usato nel browser per ${provider}`);
}
}
}
}
/**
* Configure the OAuth plugin with provider settings
* @param config - Configuration options for OAuth
*/
configure(config) {
this.config = { ...this.config, ...config };
// Inizializza il connector se non è già stato fatto
if (!this.oauthConnector) {
this.oauthConnector = new oauthConnector_1.OAuthConnector(this.config);
}
else {
// Update connector configuration se già inizializzato
this.oauthConnector.updateConfig(this.config);
}
// Validate security settings
this.validateOAuthSecurity();
}
/**
* @inheritdoc
*/
destroy() {
if (this.oauthConnector) {
this.oauthConnector.cleanup();
}
this.oauthConnector = null;
this.storage = null;
super.destroy();
}
/**
* Ensure that the OAuth connector is initialized
* @private
*/
assertOAuthConnector() {
this.assertInitialized();
if (!this.oauthConnector) {
throw new Error("OAuth connector not initialized");
}
return this.oauthConnector;
}
/**
* @inheritdoc
*/
isSupported() {
return this.assertOAuthConnector().isSupported();
}
/**
* @inheritdoc
*/
getAvailableProviders() {
return this.assertOAuthConnector().getAvailableProviders();
}
/**
* @inheritdoc
*/
async initiateOAuth(provider) {
return this.assertOAuthConnector().initiateOAuth(provider);
}
/**
* @inheritdoc
*/
async completeOAuth(provider, authCode, state) {
return this.assertOAuthConnector().completeOAuth(provider, authCode, state);
}
/**
* @inheritdoc
*/
async generateCredentials(userInfo, provider) {
return this.assertOAuthConnector().generateCredentials(userInfo, provider);
}
/**
* Login with OAuth
* @param provider - OAuth provider to use
* @returns {Promise<AuthResult>} Authentication result
* @description Authenticates user using OAuth with external providers
* NOTE: This method only initiates the OAuth flow. The actual authentication
* happens in handleOAuthCallback when the provider redirects back.
*/
async login(provider) {
try {
const core = this.assertInitialized();
if (!provider) {
throw (0, errorHandler_1.createError)(errorHandler_1.ErrorType.VALIDATION, "PROVIDER_REQUIRED", "OAuth provider required for OAuth login");
}
if (!this.isSupported()) {
throw (0, errorHandler_1.createError)(errorHandler_1.ErrorType.ENVIRONMENT, "OAUTH_UNAVAILABLE", "OAuth is not supported in this environment");
}
// Check if provider is available
const availableProviders = this.getAvailableProviders();
if (!availableProviders.includes(provider)) {
throw (0, errorHandler_1.createError)(errorHandler_1.ErrorType.VALIDATION, "PROVIDER_NOT_CONFIGURED", `Provider ${provider} is not configured or available`);
}
// Initiate OAuth flow with the provider
const oauthResult = await this.initiateOAuth(provider);
if (!oauthResult.success) {
throw (0, errorHandler_1.createError)(errorHandler_1.ErrorType.AUTHENTICATION, "OAUTH_INITIATION_FAILED", oauthResult.error || "Failed to initiate OAuth flow");
}
// In a browser environment, this would redirect to the OAuth provider
// The frontend should handle the redirect and then call handleOAuthCallback
// with the received code and state when the provider redirects back
// Return early with the auth URL that the frontend should use for redirection
return {
success: true,
redirectUrl: oauthResult.authUrl,
pendingAuth: true,
message: "Redirect to OAuth provider required to complete authentication",
provider,
authMethod: "oauth",
};
}
catch (error) {
// Handle both ShogunError and generic errors
const errorType = error?.type || errorHandler_1.ErrorType.AUTHENTICATION;
const errorCode = error?.code || "OAUTH_LOGIN_ERROR";
const errorMessage = error?.message || "Unknown error during OAuth login";
const handledError = errorHandler_1.ErrorHandler.handle(errorType, errorCode, errorMessage, error);
return {
success: false,
error: handledError.message,
};
}
}
/**
* Register new user with OAuth provider
* @param provider - OAuth provider
* @returns {Promise<SignUpResult>} Registration result
*/
async signUp(provider) {
try {
const core = this.assertInitialized();
if (!provider) {
throw (0, errorHandler_1.createError)(errorHandler_1.ErrorType.VALIDATION, "PROVIDER_REQUIRED", "OAuth provider required for OAuth signup");
}
if (!this.isSupported()) {
throw (0, errorHandler_1.createError)(errorHandler_1.ErrorType.ENVIRONMENT, "OAUTH_UNAVAILABLE", "OAuth is not supported in this environment");
}
// Check if provider is available
const availableProviders = this.getAvailableProviders();
if (!availableProviders.includes(provider)) {
throw (0, errorHandler_1.createError)(errorHandler_1.ErrorType.VALIDATION, "PROVIDER_NOT_CONFIGURED", `Provider ${provider} is not configured or available`);
}
// Initiate OAuth flow with the provider
const oauthResult = await this.initiateOAuth(provider);
if (!oauthResult.success) {
throw (0, errorHandler_1.createError)(errorHandler_1.ErrorType.AUTHENTICATION, "OAUTH_INITIATION_FAILED", oauthResult.error || "Failed to initiate OAuth flow");
}
// In a browser environment, this would redirect to the OAuth provider
// The frontend should handle the redirect and then call handleOAuthCallback
// with the received code and state when the provider redirects back
// Return early with the auth URL that the frontend should use for redirection
return {
success: true,
redirectUrl: oauthResult.authUrl,
pendingAuth: true,
message: "Redirect to OAuth provider required to complete registration",
provider,
authMethod: "oauth",
};
}
catch (error) {
// Handle both ShogunError and generic errors
const errorType = error?.type || errorHandler_1.ErrorType.AUTHENTICATION;
const errorCode = error?.code || "OAUTH_SIGNUP_ERROR";
const errorMessage = error?.message || "Unknown error during OAuth signup";
const handledError = errorHandler_1.ErrorHandler.handle(errorType, errorCode, errorMessage, error);
return {
success: false,
error: handledError.message,
};
}
}
/**
* Handle OAuth callback (for frontend integration)
* This method would be called when the OAuth provider redirects back
*/
async handleOAuthCallback(provider, authCode, state) {
try {
const core = this.assertInitialized();
// Validazione di sicurezza pre-callback
if (!authCode || !state) {
throw new Error("Authorization code and state parameter are required");
}
// Complete the OAuth flow
const result = await this.completeOAuth(provider, authCode, state);
if (!result.success || !result.userInfo) {
throw new Error(result.error || "Failed to complete OAuth flow");
}
// Genera credenziali da user info
const credentials = await this.generateCredentials(result.userInfo, provider);
// Set authentication method
core.setAuthMethod("oauth");
// Login o signup usando la chiave derivata
const authResult = await this._loginOrSignUp(credentials.username, credentials.key);
if (authResult.success) {
// Store user info in user metadata
if (core.user) {
await core.user.put({
oauth: {
provider,
id: result.userInfo.id,
email: result.userInfo.email,
name: result.userInfo.name,
picture: result.userInfo.picture,
lastLogin: Date.now(),
},
});
}
// Emit appropriate event
const eventType = authResult.isNewUser ? "auth:signup" : "auth:login";
core.emit(eventType, {
userPub: authResult.userPub || "",
username: credentials.username,
method: "oauth",
provider,
});
// Pulisci i dati OAuth scaduti dopo un login riuscito
this.cleanupExpiredOAuthData();
// Return auth result with OAuth user data included
return {
...authResult,
sea: authResult.sea, // Include SEA pair from core
user: {
userPub: authResult.userPub,
username: credentials.username,
email: result.userInfo.email,
name: result.userInfo.name ||
result.userInfo.email ||
`OAuth User (${provider})`,
picture: result.userInfo.picture,
oauth: {
provider,
id: result.userInfo.id,
email: result.userInfo.email,
name: result.userInfo.name,
picture: result.userInfo.picture,
lastLogin: Date.now(),
},
},
};
}
return authResult;
}
catch (error) {
// Pulisci i dati OAuth anche in caso di errore
this.cleanupExpiredOAuthData();
return {
success: false,
error: error.message || "Failed to handle OAuth callback",
};
}
}
/**
* Pulisce i dati OAuth scaduti
*/
cleanupExpiredOAuthData() {
if (this.oauthConnector) {
// Il metodo cleanupExpiredOAuthData è privato nel connector
// quindi usiamo il metodo pubblico clearUserCache
this.oauthConnector.clearUserCache();
}
}
/**
* Private helper to login or sign up a user
*/
async _loginOrSignUp(username, k) {
if (!this.core) {
return { success: false, error: "Shogun core not available" };
}
// Try login first
const loginResult = await this.core.login(username, "", k);
if (loginResult.success) {
// Session is automatically saved by the login method
loginResult.isNewUser = false;
// Include SEA pair from core
if (this.core.user && this.core.user._?.sea) {
loginResult.sea = this.core.user._.sea;
}
return loginResult;
}
// If login fails, try signup
const signupResult = await this.core.signUp(username, "", "", k);
if (signupResult.success) {
// Immediately login after signup
const postSignupLogin = await this.core.login(username, "", k);
if (postSignupLogin.success) {
// Session is automatically saved by the login method
postSignupLogin.isNewUser = true;
// Include SEA pair from core
if (this.core.user && this.core.user._?.sea) {
postSignupLogin.sea = this.core.user._.sea;
}
return postSignupLogin;
}
return {
success: false,
error: postSignupLogin.error || "Login failed after successful signup.",
};
}
// Return the original signup error for other failures
return signupResult;
}
/**
* Alias for handleOAuthCallback for backward compatibility
* @deprecated Use handleOAuthCallback instead
*/
async handleSimpleOAuth(provider, authCode, state) {
return this.handleOAuthCallback(provider, authCode, state);
}
/**
* Get cached user info for a user
*/
getCachedUserInfo(userId, provider) {
return this.assertOAuthConnector().getCachedUserInfo(userId, provider);
}
/**
* Clear user info cache
*/
clearUserCache(userId, provider) {
this.assertOAuthConnector().clearUserCache(userId, provider);
}
}
exports.OAuthPlugin = OAuthPlugin;