Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/agents-hosting/src/app/agentApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export class AgentApplication<TState extends TurnState> {
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)
}
Expand Down Expand Up @@ -433,7 +433,7 @@ export class AgentApplication<TState extends TurnState> {
*
*/
public onSignInSuccess (handler: (context: TurnContext, state: TurnState, id?: string) => Promise<void>): this {
if (this.options.authorization) {
if (this.options.userAuthorization || this.options.authorization) {
this.authorization.onSignInSuccess(handler)
} else {
throw new Error(
Expand Down Expand Up @@ -463,7 +463,7 @@ export class AgentApplication<TState extends TurnState> {
*
*/
public onSignInFailure (handler: (context: TurnContext, state: TurnState, id?: string) => Promise<void>): this {
if (this.options.authorization) {
if (this.options.userAuthorization || this.options.authorization) {
this.authorization.onSignInFailure(handler)
} else {
throw new Error(
Expand Down
23 changes: 20 additions & 3 deletions packages/agents-hosting/src/app/agentApplicationBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand Down Expand Up @@ -55,12 +55,29 @@ export class AgentApplicationBuilder<TState extends TurnState = TurnState> {
// }

/**
* 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
}

Expand Down
26 changes: 25 additions & 1 deletion packages/agents-hosting/src/app/agentApplicationOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand Down Expand Up @@ -90,6 +90,8 @@ export interface AgentApplicationOptions<TState extends TurnState> {
fileDownloaders?: InputFileDownloader<TState>[];

/**
* @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.
Expand All @@ -98,6 +100,28 @@ export interface AgentApplicationOptions<TState extends TurnState> {
*/
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
Expand Down
65 changes: 56 additions & 9 deletions packages/agents-hosting/src/app/auth/authorizationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -46,6 +46,7 @@ export type GetHandlerIds = (activity: Activity) => string[] | Promise<string[]>
*/
export class AuthorizationManager {
private _handlers: Record<string, AuthorizationHandler> = {}
private _userAuthorizationOptions: UserAuthorizationOptions

/**
* Creates an instance of the AuthorizationManager.
Expand All @@ -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 {
Expand All @@ -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'),
Comment thread
sw-joelmut marked this conversation as resolved.
}
}

// 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('\', \'')}'.`)
}

Expand Down Expand Up @@ -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)
}
Comment thread
sw-joelmut marked this conversation as resolved.

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}`))
Expand Down
99 changes: 98 additions & 1 deletion packages/agents-hosting/src/app/auth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, AuthorizationHandlerConfig>

/**
* Function type for selecting whether to auto sign-in.
*/
export type AutoSignInSelector = (context: TurnContext) => Promise<boolean> | 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<string, AzureBotAuthorizationOptions | AgenticAuthorizationOptions>
export type AuthorizationOptions = Record<string, AuthorizationHandlerOptions>

/**
* Represents the status of a handler registration attempt.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down