UNPKG

botframework-connector

Version:

Bot Connector is autorest generated connector client.

322 lines (286 loc) 12.1 kB
/** * @module botframework-connector */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { Activity, Channels, RoleTypes, StatusCodes } from 'botframework-schema'; import { AuthenticationError } from './authenticationError'; import { AuthenticationConfiguration } from './authenticationConfiguration'; import { AuthenticationConstants } from './authenticationConstants'; import { ChannelValidation } from './channelValidation'; import { Claim, ClaimsIdentity } from './claimsIdentity'; import { ICredentialProvider } from './credentialProvider'; import { EmulatorValidation } from './emulatorValidation'; import { EnterpriseChannelValidation } from './enterpriseChannelValidation'; import { GovernmentChannelValidation } from './governmentChannelValidation'; import { GovernmentConstants } from './governmentConstants'; import { SkillValidation } from './skillValidation'; import { AseChannelValidation } from './aseChannelValidation'; /** * @deprecated Use `ConfigurationBotFrameworkAuthentication` instead to perform JWT token validation. */ // eslint-disable-next-line @typescript-eslint/no-namespace export namespace JwtTokenValidation { /** * Authenticates the request and sets the service url in the set of trusted urls. * * @param {Partial<Activity>} activity The incoming Activity from the Bot Framework or the Emulator * @param {string} authHeader The Bearer token included as part of the request * @param {ICredentialProvider} credentials The set of valid credentials, such as the Bot Application ID * @param {string} channelService The channel service * @param {AuthenticationConfiguration} authConfig Optional, the auth config * @returns {Promise<ClaimsIdentity>} Promise with ClaimsIdentity for the request. */ export async function authenticateRequest( activity: Partial<Activity>, authHeader: string, credentials: ICredentialProvider, channelService: string, authConfig?: AuthenticationConfiguration, ): Promise<ClaimsIdentity> { if (!authConfig) { authConfig = new AuthenticationConfiguration(); } const isAuthDisabled = await credentials.isAuthenticationDisabled(); if (isAuthDisabled) { // Check if the activity is for a skill call and is coming from the Emulator. if ( activity.channelId === Channels.Emulator && activity.recipient && activity.recipient.role === RoleTypes.Skill ) { return SkillValidation.createAnonymousSkillClaim(); } // In the scenario where Auth is disabled, we still want to have the // IsAuthenticated flag set in the ClaimsIdentity. To do this requires // adding in an empty claim. return new ClaimsIdentity([], AuthenticationConstants.AnonymousAuthType); } else { if (!authHeader.trim()) { throw new AuthenticationError( 'Unauthorized Access. Request is not authorized', StatusCodes.UNAUTHORIZED, ); } const claimsIdentity: ClaimsIdentity = await validateAuthHeader( authHeader, credentials, channelService, activity.channelId, activity.serviceUrl, authConfig, ); return claimsIdentity; } } /** * Validate an auth header. * * @param {string} authHeader the auth header * @param {ICredentialProvider} credentials the credentials * @param {string} channelService the channel service * @param {string} channelId the channel ID * @param {string} serviceUrl the service URL * @param {AuthenticationConfiguration} authConfig the auth config * @returns {Promise<ClaimsIdentity>} a promise that resolves to the validated claims, or rejects if validation fails */ export async function validateAuthHeader( authHeader: string, credentials: ICredentialProvider, channelService: string, channelId: string, serviceUrl = '', authConfig: AuthenticationConfiguration = new AuthenticationConfiguration(), ): Promise<ClaimsIdentity> { if (!authHeader.trim()) { throw new AuthenticationError("'authHeader' required.", StatusCodes.BAD_REQUEST); } const identity = await authenticateToken( authHeader, credentials, channelService, channelId, authConfig, serviceUrl, ); await validateClaims(authConfig, identity.claims); return identity; } async function authenticateToken( authHeader: string, credentials: ICredentialProvider, channelService: string, channelId: string, authConfig: AuthenticationConfiguration, serviceUrl: string, ): Promise<ClaimsIdentity> { if (AseChannelValidation.isTokenFromAseChannel(channelId)) { return AseChannelValidation.authenticateAseChannelToken(authHeader); } if (SkillValidation.isSkillToken(authHeader)) { return await SkillValidation.authenticateChannelToken( authHeader, credentials, channelService, channelId, authConfig, ); } if (EmulatorValidation.isTokenFromEmulator(authHeader)) { return await EmulatorValidation.authenticateEmulatorToken( authHeader, credentials, channelService, channelId, ); } if (isPublicAzure(channelService)) { if (isValidServiceURL(serviceUrl)) { return await ChannelValidation.authenticateChannelTokenWithServiceUrl( authHeader, credentials, serviceUrl, channelId, ); } return await ChannelValidation.authenticateChannelToken(authHeader, credentials, channelId); } if (isGovernment(channelService)) { if (isValidServiceURL(serviceUrl)) { return await GovernmentChannelValidation.authenticateChannelTokenWithServiceUrl( authHeader, credentials, serviceUrl, channelId, ); } return await GovernmentChannelValidation.authenticateChannelToken(authHeader, credentials, channelId); } // Otherwise use Enterprise Channel Validation if (isValidServiceURL(serviceUrl)) { return await EnterpriseChannelValidation.authenticateChannelTokenWithServiceUrl( authHeader, credentials, serviceUrl, channelId, channelService, ); } return await EnterpriseChannelValidation.authenticateChannelToken( authHeader, credentials, channelId, channelService, ); } /** * Validates the identity claims against the ClaimsValidator in AuthenticationConfiguration if present. * * @param authConfig The authentication configuration. * @param claims The list of claims to validate. */ async function validateClaims(authConfig: AuthenticationConfiguration, claims: Claim[] = []): Promise<void> { if (authConfig.validateClaims) { // Call the validation method if defined (it should throw an exception if the validation fails) await authConfig.validateClaims(claims); } else if (SkillValidation.isSkillClaim(claims)) { // Skill claims must be validated using AuthenticationConfiguration validateClaims throw new AuthenticationError( 'Unauthorized Access. Request is not authorized. Skill Claims require validation.', StatusCodes.UNAUTHORIZED, ); } } /** * Gets the AppId from a claims list. * * @summary * In v1 tokens the AppId is in the "ver" AuthenticationConstants.AppIdClaim claim. * In v2 tokens the AppId is in the "azp" AuthenticationConstants.AuthorizedParty claim. * If the AuthenticationConstants.VersionClaim is not present, this method will attempt to * obtain the attribute from the AuthenticationConstants.AppIdClaim or if present. * * Throws a TypeError if claims is falsy. * * @param {Claim[]} claims An object containing claims types and their values. * @returns {string} the app ID */ export function getAppIdFromClaims(claims: Claim[]): string { if (!claims) { throw new TypeError('JwtTokenValidation.getAppIdFromClaims(): missing claims.'); } let appId: string; // Group claims by type for fast lookup const claimsByType = claims.reduce((acc, claim) => ({ ...acc, [claim.type]: claim }), {}); // Depending on Version, the AppId is either in the // appid claim (Version 1) or the 'azp' claim (Version 2). const versionClaim = claimsByType[AuthenticationConstants.VersionClaim]; const versionValue = versionClaim && versionClaim.value; if (!versionValue || versionValue === '1.0') { // No version or a version of '1.0' means we should look for // the claim in the 'appid' claim. const appIdClaim = claimsByType[AuthenticationConstants.AppIdClaim]; if (appIdClaim && appIdClaim.value) { appId = appIdClaim.value; } } else if (versionValue === '2.0') { // Version '2.0' puts the AppId in the 'azp' claim. const azpClaim = claimsByType[AuthenticationConstants.AuthorizedParty]; if (azpClaim && azpClaim.value) { appId = azpClaim.value; } } return appId; } function isPublicAzure(channelService: string): boolean { return !channelService || channelService.length === 0; } function isValidServiceURL(serviceUrl: string): boolean { const trimmedUrl = serviceUrl.trim(); const absoluteUrl = trimmedUrl.startsWith('http') || trimmedUrl.startsWith('wss') ? trimmedUrl : `https://${trimmedUrl}`; try { const newUri = new URL(absoluteUrl); return !!newUri; } catch { return false; } } /** * Determine whether or not a channel service is government * * @param {string} channelService the channel service * @returns {boolean} true if this is a government channel service */ export function isGovernment(channelService: string): boolean { return channelService && channelService.toLowerCase() === GovernmentConstants.ChannelService; } /** * Internal helper to check if the token has the shape we expect "Bearer [big long string]". * * @param {string} authHeader A string containing the token header. * @returns {boolean} True if the token is valid, false if not. */ export function isValidTokenFormat(authHeader: string): boolean { if (!authHeader) { // No token, not valid. return false; } const parts: string[] = authHeader.trim().split(' '); if (parts.length !== 2) { // Tokens MUST have exactly 2 parts. If we don't have 2 parts, it's not a valid token return false; } // We now have an array that should be: // [0] = "Bearer" // [1] = "[Big Long String]" const authScheme: string = parts[0]; if (authScheme !== 'Bearer') { // The scheme MUST be "Bearer" return false; } return true; } }