diff --git a/packages/agents-hosting/src/app/agentApplication.ts b/packages/agents-hosting/src/app/agentApplication.ts index ed8230389..7cd663fb7 100644 --- a/packages/agents-hosting/src/app/agentApplication.ts +++ b/packages/agents-hosting/src/app/agentApplication.ts @@ -125,7 +125,7 @@ export class AgentApplication { this._adapter = new CloudAdapter() } - if (this._options.authorization) { + if (this._options.userAuthorization || this._options.authorization) { this._authorizationManager = new AuthorizationManager(this, this._adapter.connectionManager) this._authorization = new UserAuthorization(this._authorizationManager) } @@ -433,7 +433,7 @@ export class AgentApplication { * */ public onSignInSuccess (handler: (context: TurnContext, state: TurnState, id?: string) => Promise): this { - if (this.options.authorization) { + if (this.options.userAuthorization || this.options.authorization) { this.authorization.onSignInSuccess(handler) } else { throw new Error( @@ -463,7 +463,7 @@ export class AgentApplication { * */ public onSignInFailure (handler: (context: TurnContext, state: TurnState, id?: string) => Promise): this { - if (this.options.authorization) { + if (this.options.userAuthorization || this.options.authorization) { this.authorization.onSignInFailure(handler) } else { throw new Error( diff --git a/packages/agents-hosting/src/app/agentApplicationBuilder.ts b/packages/agents-hosting/src/app/agentApplicationBuilder.ts index 24f9b0ab7..dcaff8840 100644 --- a/packages/agents-hosting/src/app/agentApplicationBuilder.ts +++ b/packages/agents-hosting/src/app/agentApplicationBuilder.ts @@ -6,7 +6,7 @@ import { Storage } from '../storage' import { AgentApplication } from './agentApplication' import { AgentApplicationOptions } from './agentApplicationOptions' -import { AuthorizationOptions } from './auth/types' +import { AuthorizationOptions, UserAuthorizationOptions } from './auth/types' import { TurnState } from './turnState' /** @@ -55,12 +55,29 @@ export class AgentApplicationBuilder { // } /** + * Sets user authorization options for the AgentApplication. + * @param options The user authorization configuration + * @returns This builder instance for chaining + */ + public withAuthorization (options: UserAuthorizationOptions): this + /** + * @deprecated Use `withAuthorization(UserAuthorizationOptions)` instead. + * * Sets authentication options for the AgentApplication. * @param authHandlers The user identity authentication options * @returns This builder instance for chaining */ - public withAuthorization (authHandlers: AuthorizationOptions): this { - this._options.authorization = authHandlers + public withAuthorization (authHandlers: AuthorizationOptions): this + public withAuthorization (options: UserAuthorizationOptions | AuthorizationOptions): this { + if ( + 'handlers' in options && + typeof options.handlers === 'object' && + Object.values(options.handlers).some(handler => typeof handler === 'object' && 'settings' in handler) + ) { + this._options.userAuthorization = options as UserAuthorizationOptions + } else { + this._options.authorization = options as AuthorizationOptions + } return this } diff --git a/packages/agents-hosting/src/app/agentApplicationOptions.ts b/packages/agents-hosting/src/app/agentApplicationOptions.ts index 660cea692..7829993e1 100644 --- a/packages/agents-hosting/src/app/agentApplicationOptions.ts +++ b/packages/agents-hosting/src/app/agentApplicationOptions.ts @@ -10,7 +10,7 @@ import { AdaptiveCardsOptions } from './adaptiveCards' import { InputFileDownloader } from './inputFileDownloader' import { TurnState } from './turnState' import { HeaderPropagationDefinition } from '../headerPropagation' -import { AuthorizationOptions } from './auth/types' +import { AuthorizationOptions, UserAuthorizationOptions } from './auth/types' import { Connections } from '../auth/connections' /** @@ -90,6 +90,8 @@ export interface AgentApplicationOptions { fileDownloaders?: InputFileDownloader[]; /** + * @deprecated Use `userAuthorization` instead. + * * Handlers for managing user authentication and authorization within the agent. * This includes OAuth flows, token management, and permission validation. * Use this to implement secure access to protected resources or user-specific data. @@ -98,6 +100,28 @@ export interface AgentApplicationOptions { */ authorization?: AuthorizationOptions; + /** + * Configuration for user authentication and authorization within the agent. + * This includes OAuth flows, token management, and permission validation. + * Use this to implement secure access to protected resources or user-specific data. + * + * @example + * ```typescript + * userAuthorization: { + * defaultHandlerName: 'graph', + * autoSignIn: () => true, + * handlers: { + * graph: { + * settings: { text: 'Sign in', title: 'Graph Sign In' } + * }, + * } + * } + * ``` + * + * @default undefined (no authorization required) + */ + userAuthorization?: UserAuthorizationOptions; + /** * Configuration options for handling Adaptive Card actions and interactions. * This controls how the agent processes card submissions, button clicks, and other diff --git a/packages/agents-hosting/src/app/auth/authorizationManager.ts b/packages/agents-hosting/src/app/auth/authorizationManager.ts index ff264017f..af1cd3892 100644 --- a/packages/agents-hosting/src/app/auth/authorizationManager.ts +++ b/packages/agents-hosting/src/app/auth/authorizationManager.ts @@ -8,7 +8,7 @@ import { AgentApplication } from '../agentApplication' import { AgenticAuthorization, AzureBotAuthorization } from './handlers' import { TurnContext } from '../../turnContext' import { HandlerStorage } from './handlerStorage' -import { ActiveAuthorizationHandler, AuthorizationHandlerStatus, AuthorizationHandler, AuthorizationHandlerSettings, AuthorizationOptions } from './types' +import { ActiveAuthorizationHandler, AuthorizationHandlerStatus, AuthorizationHandler, AuthorizationHandlerSettings, UserAuthorizationOptions, AuthorizationHandlerOptions } from './types' import { Connections } from '../../auth/connections' const logger = debug('agents:authorization:manager') @@ -46,6 +46,7 @@ export type GetHandlerIds = (activity: Activity) => string[] | Promise */ export class AuthorizationManager { private _handlers: Record = {} + private _userAuthorizationOptions: UserAuthorizationOptions /** * Creates an instance of the AuthorizationManager. @@ -56,13 +57,15 @@ export class AuthorizationManager { throw new Error('Storage is required for Authorization. Ensure that a storage provider is configured in the AgentApplication options.') } - if (app.options.authorization === undefined || Object.keys(app.options.authorization).length === 0) { - throw new Error('The AgentApplication.authorization does not have any auth handlers') + this._userAuthorizationOptions = this.normalizeOptions() + + if (Object.keys(this._userAuthorizationOptions.handlers).length === 0) { + throw new Error('No authorization handlers configured. Provide handlers via \'AgentApplication.userAuthorization.handlers\' property.') } const settings: AuthorizationHandlerSettings = { storage: app.options.storage, connections } - for (const [id, handler] of Object.entries(app.options.authorization)) { - const options = this.loadOptions(id, handler) + for (const [id, handlerConfig] of Object.entries(this._userAuthorizationOptions.handlers ?? {})) { + const options = this.loadOptions(id, handlerConfig.settings) if (options.type === 'agentic') { this._handlers[id] = new AgenticAuthorization(id, options, settings) } else { @@ -71,18 +74,51 @@ export class AuthorizationManager { } } + /** + * Normalizes authorization options from either legacy or new format. + * Shows a deprecation warning if legacy format is used. + */ + private normalizeOptions (): UserAuthorizationOptions { + const { authorization, userAuthorization } = this.app.options + + // Prefer new format if provided + if (userAuthorization) { + return { + handlers: userAuthorization.handlers, + defaultHandlerName: userAuthorization.defaultHandlerName ?? process.env['AGENTAPPLICATION__USERAUTHORIZATION__DEFAULTHANDLERNAME'], + autoSignIn: userAuthorization.autoSignIn ?? (() => process.env['AGENTAPPLICATION__USERAUTHORIZATION__AUTOSIGNIN'] !== 'false'), + } + } + + // Fall back to legacy format with deprecation warning + if (authorization) { + logger.warn('The \'AgentApplication.authorization\' option is deprecated. Please use \'AgentApplication.userAuthorization\' with the new format instead.') + + // Convert legacy format to new format + const handlers: UserAuthorizationOptions['handlers'] = {} + for (const [id, options] of Object.entries(authorization)) { + handlers[id] = { settings: options } + } + + return { handlers } + } + + // No authorization configured + return { handlers: {} } + } + /** * Loads and validates the authorization handler options. */ - private loadOptions (id: string, options: AuthorizationOptions[string]) { - const result: AuthorizationOptions[string] = { + private loadOptions (id: string, options: AuthorizationHandlerOptions) { + const result: AuthorizationHandlerOptions = { ...options, type: (options.type ?? process.env[`${id}_type`])?.toLowerCase() as typeof options.type, } - // Validate supported types, agentic, and default (Azure Bot - undefined) + // Validate supported types: agentic, and default (Azure Bot - undefined) const supportedTypes = ['agentic', undefined] - if (!supportedTypes.includes(result.type)) { + if (result.type && !supportedTypes.includes(result.type)) { throw new Error(`Unsupported authorization handler type: '${result.type}' for auth handler: '${id}'. Supported types are: '${supportedTypes.filter(Boolean).join('\', \'')}'.`) } @@ -110,6 +146,17 @@ export class AuthorizationManager { const handlers = active?.handlers ?? this.mapHandlers(await getHandlerIds(context.activity) ?? []) ?? [] + // AutoSignIn feature: performs automatic sign-in using the provided default or first available handler. + const shouldAutoSignIn = await this._userAuthorizationOptions.autoSignIn?.(context) + if (shouldAutoSignIn && handlers.length === 0) { + const firstHandler = Object.values(this._handlers)[0] + const defaultHandler = this._handlers[this._userAuthorizationOptions.defaultHandlerName ?? ''] + if (!defaultHandler && this._userAuthorizationOptions.defaultHandlerName) { + logger.warn(`AutoSignIn is enabled but default handler '${this._userAuthorizationOptions.defaultHandlerName}' is not found. Falling back to the first available handler (${firstHandler?.id}).`) + } + handlers.push(defaultHandler ?? firstHandler) + } + for (const handler of handlers) { const status = await this.signin(storage, handler, context, active?.data) logger.debug(this.prefix(handler.id, `Sign-in status: ${status}`)) diff --git a/packages/agents-hosting/src/app/auth/types.ts b/packages/agents-hosting/src/app/auth/types.ts index d1dc17c08..673bab1a0 100644 --- a/packages/agents-hosting/src/app/auth/types.ts +++ b/packages/agents-hosting/src/app/auth/types.ts @@ -10,10 +10,107 @@ import { AgenticAuthorizationOptions, AzureBotAuthorizationOptions } from './han import { TokenResponse } from '../../oauth' import { Connections } from '../../auth/connections' +/** + * Handler options type for authorization handlers. + */ +export type AuthorizationHandlerOptions = AzureBotAuthorizationOptions | AgenticAuthorizationOptions + +/** + * Configuration for an individual authorization handler. + */ +export interface AuthorizationHandlerConfig { + /** + * The settings for the authorization handler. + */ + settings: AuthorizationHandlerOptions +} + +/** + * A record of authorization handler configurations keyed by their unique identifiers. + */ +export type AuthorizationHandlers = Record + +/** + * Function type for selecting whether to auto sign-in. + */ +export type AutoSignInSelector = (context: TurnContext) => Promise | boolean + +/** + * User authorization configuration options. + * + * @remarks + * Properties can be configured via environment variables. + * Use the format: + * `AgentApplication__UserAuthorization__{propertyName}` + * + * @example + * ```env + * # For all handlers + * + * AGENTAPPLICATION__USERAUTHORIZATION__DEFAULTHANDLERNAME=graph + * AGENTAPPLICATION__USERAUTHORIZATION__AUTOSIGNIN=true + * ``` + * + * @example + * ```typescript + * userAuthorization: { + * defaultHandlerName: 'graph', + * autoSignIn: () => true, + * handlers: { + * graph: { + * settings: { text: 'Sign in with Microsoft Graph', title: 'Graph Sign In' } + * }, + * github: { + * settings: { text: 'Sign in with GitHub', title: 'GitHub Sign In' } + * }, + * } + * } + * ``` + */ +export interface UserAuthorizationOptions { + /** + * The name of the default authorization handler to use when no specific handler is specified. + * @remarks If not provided, the first handler in the list will be used as the default. + */ + defaultHandlerName?: string + /** + * Indicates whether to automatically sign in users when they interact with the application. + * @remarks Auto sign-in is enabled by default and remains enabled unless explicitly disabled (for example, by setting the corresponding configuration or environment variable to 'false'). + */ + autoSignIn?: AutoSignInSelector + /** + * A record of authorization handlers keyed by their unique identifiers. + */ + handlers: AuthorizationHandlers +} + /** * Authorization configuration options. + * @deprecated Use {@link UserAuthorizationOptions} with the `userAuthorization` property instead. + * This flat structure will be removed in a future version. + * + * @example + * ```typescript + * // Deprecated: + * authorization: { + * graph: { text: '...', title: '...' }, + * github: { text: '...', title: '...' }, + * } + * + * // Use instead: + * userAuthorization: { + * handlers: { + * graph: { + * settings: { text: '...', title: '...' } + * }, + * github: { + * settings: { text: '...', title: '...' } + * }, + * } + * } + * ``` */ -export type AuthorizationOptions = Record +export type AuthorizationOptions = Record /** * Represents the status of a handler registration attempt. diff --git a/packages/agents-hosting/test/hosting/app/authorization.test.ts b/packages/agents-hosting/test/hosting/app/authorization.test.ts index dbad865b8..b0ded0425 100644 --- a/packages/agents-hosting/test/hosting/app/authorization.test.ts +++ b/packages/agents-hosting/test/hosting/app/authorization.test.ts @@ -27,7 +27,7 @@ describe('AgentApplication', () => { authorization: {} }) assert.equal(app.options.authorization, undefined) - }, { message: 'The AgentApplication.authorization does not have any auth handlers' }) + }, { message: 'No authorization handlers configured. Provide handlers via \'AgentApplication.userAuthorization.handlers\' property.' }) }) it('should initialize successfully with valid auth configuration', () => {