UNPKG

@microsoft/agents-hosting

Version:

Microsoft 365 Agents SDK for JavaScript

379 lines 16.6 kB
"use strict"; /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Authorization = void 0; const logger_1 = require("@microsoft/agents-activity/logger"); const oauth_1 = require("../oauth"); const auth_1 = require("../auth"); const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); const logger = (0, logger_1.debug)('agents:authorization'); /** * Class responsible for managing authorization and OAuth flows. * Handles multiple OAuth providers and manages the complete authentication lifecycle. * * @remarks * The Authorization class provides a centralized way to handle OAuth authentication * flows within the agent application. It supports multiple authentication handlers, * token exchange, on-behalf-of flows, and provides event handlers for success/failure scenarios. * * Key features: * - Multiple OAuth provider support * - Token caching and exchange * - On-behalf-of (OBO) token flows * - Sign-in success/failure event handling * - Automatic configuration from environment variables * * Example usage: * ```typescript * const auth = new Authorization(storage, { * 'microsoft': { * name: 'Microsoft', * title: 'Sign in with Microsoft', * text: 'Please sign in' * } * }); * * auth.onSignInSuccess(async (context, state) => { * await context.sendActivity('Welcome! You are now signed in.'); * }); * ``` */ class Authorization { /** * Creates a new instance of Authorization. * * @param storage - The storage system to use for state management. * @param authHandlers - Configuration for OAuth providers. * @throws {Error} If storage is null/undefined or no auth handlers are provided. * * @remarks * The constructor initializes all configured auth handlers and sets up OAuth flows. * It automatically configures handler properties from environment variables if not provided: * - Connection name: {handlerId}_connectionName * - Connection title: {handlerId}_connectionTitle * - Connection text: {handlerId}_connectionText * * Example usage: * ```typescript * const auth = new Authorization(storage, { * 'microsoft': { * name: 'Microsoft', * title: 'Sign in with Microsoft' * }, * 'google': { * // Will use GOOGLE_connectionName from env vars * } * }); * ``` */ constructor(storage, authHandlers, userTokenClient) { var _a, _b, _c, _d; this.storage = storage; /** * Private handler for successful sign-in events. * @private */ this._signInSuccessHandler = null; /** * Private handler for failed sign-in events. * @private */ this._signInFailureHandler = null; if (storage === undefined || storage === null) { throw new Error('Storage is required for UserAuthorization'); } if (authHandlers === undefined || Object.keys(authHandlers).length === 0) { throw new Error('The authorization does not have any auth handlers'); } this.authHandlers = authHandlers; for (const ah in this.authHandlers) { if (this.authHandlers[ah].name === undefined && process.env[ah + '_connectionName'] === undefined) { throw new Error(`AuthHandler name ${ah}_connectionName not set in autorization and not found in env vars.`); } const currentAuthHandler = this.authHandlers[ah]; currentAuthHandler.name = (_a = currentAuthHandler.name) !== null && _a !== void 0 ? _a : process.env[ah + '_connectionName']; currentAuthHandler.title = (_b = currentAuthHandler.title) !== null && _b !== void 0 ? _b : process.env[ah + '_connectionTitle']; currentAuthHandler.text = (_c = currentAuthHandler.text) !== null && _c !== void 0 ? _c : process.env[ah + '_connectionText']; currentAuthHandler.cnxPrefix = (_d = currentAuthHandler.cnxPrefix) !== null && _d !== void 0 ? _d : process.env[ah + '_cnxPrefix']; currentAuthHandler.flow = new oauth_1.OAuthFlow(this.storage, currentAuthHandler.name, userTokenClient, currentAuthHandler.title, currentAuthHandler.text); } logger.info('Authorization handlers configured with', Object.keys(this.authHandlers).length, 'handlers'); } /** * Gets the token for a specific auth handler. * * @param context - The context object for the current turn. * @param authHandlerId - ID of the auth handler to use. * @returns A promise that resolves to the token response from the OAuth provider. * @throws {Error} If the auth handler is not configured. * @public * * @remarks * This method retrieves an existing token for the specified auth handler. * The token may be cached and will be retrieved from the OAuth provider if needed. * * Example usage: * ```typescript * const tokenResponse = await auth.getToken(context, 'microsoft'); * if (tokenResponse.token) { * console.log('User is authenticated'); * } * ``` */ async getToken(context, authHandlerId) { var _a; logger.info('getToken from user token service for authHandlerId:', authHandlerId); const authHandler = this.getAuthHandlerOrThrow(authHandlerId); return await ((_a = authHandler.flow) === null || _a === void 0 ? void 0 : _a.getUserToken(context)); } /** * Gets the auth handler by ID or throws an error if not found. * * @param authHandlerId - ID of the auth handler to retrieve. * @returns The auth handler instance. * @throws {Error} If the auth handler with the specified ID is not configured. * @private */ getAuthHandlerOrThrow(authHandlerId) { if (!Object.prototype.hasOwnProperty.call(this.authHandlers, authHandlerId)) { throw new Error(`AuthHandler with ID ${authHandlerId} not configured`); } return this.authHandlers[authHandlerId]; } /** * Exchanges a token for a new token with different scopes. * * @param context - The context object for the current turn. * @param scopes - Array of scopes to request for the new token. * @param authHandlerId - ID of the auth handler to use. * @returns A promise that resolves to the exchanged token response. * @throws {Error} If the auth handler is not configured. * @public * * @remarks * This method handles token exchange scenarios, particularly for on-behalf-of (OBO) flows. * It checks if the current token is exchangeable (e.g., has audience starting with 'api://') * and performs the appropriate token exchange using MSAL. * * Example usage: * ```typescript * const exchangedToken = await auth.exchangeToken( * context, * ['https://graph.microsoft.com/.default'], * 'microsoft' * ); * ``` */ async exchangeToken(context, scopes, authHandlerId) { var _a; logger.info('exchangeToken from user token service for authHandlerId:', authHandlerId); const authHandler = this.getAuthHandlerOrThrow(authHandlerId); const tokenResponse = await ((_a = authHandler.flow) === null || _a === void 0 ? void 0 : _a.getUserToken(context)); if (this.isExchangeable(tokenResponse.token)) { return await this.handleObo(context, tokenResponse.token, scopes, authHandler.cnxPrefix); } return tokenResponse; } /** * Checks if a token is exchangeable for an on-behalf-of flow. * * @param token - The token to check. * @returns True if the token is exchangeable, false otherwise. * @private */ isExchangeable(token) { var _a; if (!token || typeof token !== 'string') { return false; } const payload = jsonwebtoken_1.default.decode(token); return ((_a = payload === null || payload === void 0 ? void 0 : payload.aud) === null || _a === void 0 ? void 0 : _a.indexOf('api://')) === 0; } /** * Handles on-behalf-of token exchange using MSAL. * * @param context - The context object for the current turn. * @param token - The token to exchange. * @param scopes - Array of scopes to request for the new token. * @returns A promise that resolves to the exchanged token response. * @private */ async handleObo(context, token, scopes, cnxPrefix) { const msalTokenProvider = new auth_1.MsalTokenProvider(); let authConfig = context.adapter.authConfig; if (cnxPrefix) { authConfig = (0, auth_1.loadAuthConfigFromEnv)(cnxPrefix); } const newToken = await msalTokenProvider.acquireTokenOnBehalfOf(authConfig, scopes, token); return { token: newToken }; } /** * Begins or continues an OAuth flow. * * @param context - The context object for the current turn. * @param state - The state object for the current turn. * @param authHandlerId - ID of the auth handler to use. * @returns A promise that resolves to the token response from the OAuth provider. * @throws {Error} If the auth handler is not configured. * @public * * @remarks * This method manages the complete OAuth authentication flow: * - If no flow is active, it begins a new OAuth flow and shows the sign-in card * - If a flow is active, it continues the flow and processes the authentication response * - Handles success/failure callbacks and updates the sign-in state accordingly * * The method automatically manages the sign-in state and continuation activities, * allowing the conversation to resume after successful authentication. * * Example usage: * ```typescript * const tokenResponse = await auth.beginOrContinueFlow(context, state, 'microsoft'); * if (tokenResponse && tokenResponse.token) { * // User is now authenticated * await context.sendActivity('Authentication successful!'); * } * ``` */ async beginOrContinueFlow(context, state, authHandlerId, secRoute = true) { var _a, _b, _c, _d, _e; const authHandler = this.getAuthHandlerOrThrow(authHandlerId); logger.info('beginOrContinueFlow for authHandlerId:', authHandlerId); const signInState = state.getValue('user.__SIGNIN_STATE_') || { continuationActivity: undefined, handlerId: undefined, completed: false }; const flow = authHandler.flow; let tokenResponse; tokenResponse = await ((_a = authHandler.flow) === null || _a === void 0 ? void 0 : _a.getUserToken(context)); if ((tokenResponse === null || tokenResponse === void 0 ? void 0 : tokenResponse.token) && tokenResponse.token.length > 0) { (_b = authHandler.flow) === null || _b === void 0 ? true : delete _b.state.eTag; authHandler.flow.state.flowStarted = false; await ((_c = authHandler.flow) === null || _c === void 0 ? void 0 : _c.setFlowState(context, authHandler.flow.state)); if (secRoute) { return tokenResponse; } } if (flow.state === null || ((_d = flow.state) === null || _d === void 0 ? void 0 : _d.flowStarted) === false || ((_e = flow.state) === null || _e === void 0 ? void 0 : _e.flowStarted) === undefined) { tokenResponse = await flow.beginFlow(context); if (secRoute && (tokenResponse === null || tokenResponse === void 0 ? void 0 : tokenResponse.token) === undefined) { signInState.continuationActivity = context.activity; signInState.handlerId = authHandlerId; state.setValue('user.__SIGNIN_STATE_', signInState); } } else { tokenResponse = await flow.continueFlow(context); if (tokenResponse && tokenResponse.token) { if (this._signInSuccessHandler) { await this._signInSuccessHandler(context, state, authHandlerId); } if (secRoute) { state.deleteValue('user.__SIGNIN_STATE_'); } } else { logger.warn('Failed to complete OAuth flow, no token received'); if (this._signInFailureHandler) { await this._signInFailureHandler(context, state, authHandlerId, 'Failed to complete the OAuth flow'); } // signInState!.completed = false // state.setValue('user.__SIGNIN_STATE_', signInState) } } return tokenResponse; } /** * Signs out the current user. * * @param context - The context object for the current turn. * @param state - The state object for the current turn. * @param authHandlerId - Optional ID of the auth handler to use for sign out. If not provided, signs out from all handlers. * @returns A promise that resolves when sign out is complete. * @throws {Error} If the specified auth handler is not configured. * @public * * @remarks * This method clears the user's token and resets the authentication state. * If no specific authHandlerId is provided, it signs out from all configured handlers. * This ensures complete cleanup of authentication state across all providers. * * Example usage: * ```typescript * // Sign out from specific handler * await auth.signOut(context, state, 'microsoft'); * * // Sign out from all handlers * await auth.signOut(context, state); * ``` */ async signOut(context, state, authHandlerId) { var _a; logger.info('signOut for authHandlerId:', authHandlerId); if (authHandlerId === undefined) { // aw for (const ah in this.authHandlers) { const flow = this.authHandlers[ah].flow; await (flow === null || flow === void 0 ? void 0 : flow.signOut(context)); } } else { const authHandler = this.getAuthHandlerOrThrow(authHandlerId); await ((_a = authHandler.flow) === null || _a === void 0 ? void 0 : _a.signOut(context)); } } /** * Sets a handler to be called when sign-in is successfully completed. * * @param handler - The handler function to call on successful sign-in. * @public * * @remarks * This method allows you to register a callback that will be invoked whenever * a user successfully completes the authentication process. The handler receives * the turn context, state, and the ID of the auth handler that was used. * * Example usage: * ```typescript * auth.onSignInSuccess(async (context, state, authHandlerId) => { * await context.sendActivity(`Welcome! You signed in using ${authHandlerId}.`); * // Perform any post-authentication setup * }); * ``` */ onSignInSuccess(handler) { this._signInSuccessHandler = handler; } /** * Sets a handler to be called when sign-in fails. * * @param handler - The handler function to call on sign-in failure. * @public * * @remarks * This method allows you to register a callback that will be invoked whenever * a user's authentication attempt fails. The handler receives the turn context, * state, auth handler ID, and an optional error message describing the failure. * * Common failure scenarios include: * - User cancels the authentication process * - Invalid credentials or expired tokens * - Network connectivity issues * - OAuth provider errors * * Example usage: * ```typescript * auth.onSignInFailure(async (context, state, authHandlerId, errorMessage) => { * await context.sendActivity(`Sign-in failed: ${errorMessage || 'Unknown error'}`); * await context.sendActivity('Please try signing in again.'); * }); * ``` */ onSignInFailure(handler) { this._signInFailureHandler = handler; } } exports.Authorization = Authorization; //# sourceMappingURL=authorization.js.map