-
Notifications
You must be signed in to change notification settings - Fork 280
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add ASE channel validation. (#4589)
* aseChannelValidation * fix usgov single tenant * fix js lint * fix js lint
- Loading branch information
1 parent
aa83fbe
commit 21e7caa
Showing
7 changed files
with
267 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
164 changes: 164 additions & 0 deletions
164
libraries/botframework-connector/src/auth/aseChannelValidation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
/** | ||
* @module botframework-connector | ||
*/ | ||
/** | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. | ||
*/ | ||
|
||
/* eslint-disable @typescript-eslint/no-namespace */ | ||
|
||
import { ClaimsIdentity } from './claimsIdentity'; | ||
import { AuthenticationConstants } from './authenticationConstants'; | ||
import { AuthenticationConfiguration } from './authenticationConfiguration'; | ||
import { GovernmentConstants } from './governmentConstants'; | ||
import { ICredentialProvider } from './credentialProvider'; | ||
import { JwtTokenExtractor } from './jwtTokenExtractor'; | ||
import { JwtTokenValidation } from './jwtTokenValidation'; | ||
import { AuthenticationError } from './authenticationError'; | ||
import { SimpleCredentialProvider } from './credentialProvider'; | ||
import { StatusCodes } from 'botframework-schema'; | ||
import { BetweenBotAndAseChannelTokenValidationParameters } from './tokenValidationParameters'; | ||
|
||
/** | ||
* @deprecated Use `ConfigurationBotFrameworkAuthentication` instead to perform AseChannel validation. | ||
* Validates and Examines JWT tokens from the Bot Framework AseChannel | ||
*/ | ||
export namespace AseChannelValidation { | ||
const ChannelId = 'AseChannel'; | ||
let _creadentialProvider: ICredentialProvider; | ||
let _channelService: string; | ||
export let MetadataUrl: string; | ||
|
||
/** | ||
* init authentication from user .env configuration. | ||
* | ||
* @param configuration The user .env configuration. | ||
*/ | ||
export function init(configuration: any) { | ||
const appId = configuration.MicrosoftAppId; | ||
const tenantId = configuration.MicrosoftAppTenantId; | ||
_channelService = configuration.ChannelService; | ||
MetadataUrl = | ||
_channelService !== undefined && JwtTokenValidation.isGovernment(_channelService) | ||
? GovernmentConstants.ToBotFromEmulatorOpenIdMetadataUrl | ||
: AuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl; | ||
|
||
_creadentialProvider = new SimpleCredentialProvider(appId, ''); | ||
|
||
const tenantIds: string[] = [ | ||
tenantId, | ||
'f8cdef31-a31e-4b4a-93e4-5f571e91255a', // US Gov MicrosoftServices.onmicrosoft.us | ||
'd6d49420-f39b-4df7-a1dc-d59a935871db', // Public botframework.com | ||
]; | ||
const validIssuers: string[] = []; | ||
tenantIds.forEach((tmpId: string) => { | ||
validIssuers.push(`https://sts.windows.net/${tmpId}/`); // Auth Public/US Gov, 1.0 token | ||
validIssuers.push(`https://login.microsoftonline.com/${tmpId}/v2.0`); // Auth Public, 2.0 token | ||
validIssuers.push(`https://login.microsoftonline.us/${tmpId}/v2.0`); // Auth for US Gov, 2.0 token | ||
}); | ||
BetweenBotAndAseChannelTokenValidationParameters.issuer = validIssuers; | ||
} | ||
|
||
/** | ||
* Determines if a given Auth header is from the Bot Framework AseChannel | ||
* | ||
* @param {string} channelId The channelId. | ||
* @returns {boolean} True, if the token was issued by the AseChannel. Otherwise, false. | ||
*/ | ||
export function isTokenFromAseChannel(channelId: string): boolean { | ||
return channelId === ChannelId; | ||
} | ||
|
||
/** | ||
* Validate the incoming Auth Header as a token sent from the Bot Framework AseChannel. | ||
* A token issued by the Bot Framework will FAIL this check. Only AseChannel tokens will pass. | ||
* | ||
* @param {string} authHeader The raw HTTP header in the format: 'Bearer [longString]' | ||
* @param {AuthenticationConfiguration} authConfig The authentication configuration. | ||
* @returns {Promise<ClaimsIdentity>} A valid ClaimsIdentity. | ||
*/ | ||
export async function authenticateAseChannelToken( | ||
authHeader: string, | ||
authConfig: AuthenticationConfiguration = new AuthenticationConfiguration() | ||
): Promise<ClaimsIdentity> { | ||
const tokenExtractor: JwtTokenExtractor = new JwtTokenExtractor( | ||
BetweenBotAndAseChannelTokenValidationParameters, | ||
MetadataUrl, | ||
AuthenticationConstants.AllowedSigningAlgorithms | ||
); | ||
|
||
const identity: ClaimsIdentity = await tokenExtractor.getIdentityFromAuthHeader( | ||
authHeader, | ||
ChannelId, | ||
authConfig.requiredEndorsements | ||
); | ||
if (!identity) { | ||
// No valid identity. Not Authorized. | ||
throw new AuthenticationError('Unauthorized. No valid identity.', StatusCodes.UNAUTHORIZED); | ||
} | ||
|
||
if (!identity.isAuthenticated) { | ||
// The token is in some way invalid. Not Authorized. | ||
throw new AuthenticationError('Unauthorized. Is not authenticated', StatusCodes.UNAUTHORIZED); | ||
} | ||
|
||
// Now check that the AppID in the claimset matches | ||
// what we're looking for. Note that in a multi-tenant bot, this value | ||
// comes from developer code that may be reaching out to a service, hence the | ||
// Async validation. | ||
const versionClaim: string = identity.getClaimValue(AuthenticationConstants.VersionClaim); | ||
if (versionClaim === null) { | ||
throw new AuthenticationError( | ||
'Unauthorized. "ver" claim is required on Emulator Tokens.', | ||
StatusCodes.UNAUTHORIZED | ||
); | ||
} | ||
|
||
let appId = ''; | ||
|
||
// The Emulator, depending on Version, sends the AppId via either the | ||
// appid claim (Version 1) or the Authorized Party claim (Version 2). | ||
if (!versionClaim || versionClaim === '1.0') { | ||
// either no Version or a version of "1.0" means we should look for | ||
// the claim in the "appid" claim. | ||
const appIdClaim: string = identity.getClaimValue(AuthenticationConstants.AppIdClaim); | ||
if (!appIdClaim) { | ||
// No claim around AppID. Not Authorized. | ||
throw new AuthenticationError( | ||
'Unauthorized. "appid" claim is required on Emulator Token version "1.0".', | ||
StatusCodes.UNAUTHORIZED | ||
); | ||
} | ||
|
||
appId = appIdClaim; | ||
} else if (versionClaim === '2.0') { | ||
// Emulator, "2.0" puts the AppId in the "azp" claim. | ||
const appZClaim: string = identity.getClaimValue(AuthenticationConstants.AuthorizedParty); | ||
if (!appZClaim) { | ||
// No claim around AppID. Not Authorized. | ||
throw new AuthenticationError( | ||
'Unauthorized. "azp" claim is required on Emulator Token version "2.0".', | ||
StatusCodes.UNAUTHORIZED | ||
); | ||
} | ||
|
||
appId = appZClaim; | ||
} else { | ||
// Unknown Version. Not Authorized. | ||
throw new AuthenticationError( | ||
`Unauthorized. Unknown Emulator Token version "${versionClaim}".`, | ||
StatusCodes.UNAUTHORIZED | ||
); | ||
} | ||
|
||
if (!(await _creadentialProvider.isValidAppId(appId))) { | ||
throw new AuthenticationError( | ||
`Unauthorized. Invalid AppId passed on token: ${appId}`, | ||
StatusCodes.UNAUTHORIZED | ||
); | ||
} | ||
|
||
return identity; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
libraries/botframework-connector/tests/auth/aseChannelValidation.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
const { | ||
AseChannelValidation, | ||
GovernmentConstants, | ||
AuthenticationConstants, | ||
BetweenBotAndAseChannelTokenValidationParameters, | ||
} = require('../..'); | ||
const assert = require('assert'); | ||
|
||
describe('AseChannelTestSuite', function () { | ||
describe('AseChannelTestCase', function () { | ||
it('ValidationMetadataUrlTest_AseChannel_USGov', function () { | ||
const config = { | ||
ChannelService: GovernmentConstants.ChannelService, | ||
}; | ||
AseChannelValidation.init(config); | ||
assert.strictEqual( | ||
AseChannelValidation.MetadataUrl, | ||
GovernmentConstants.ToBotFromEmulatorOpenIdMetadataUrl | ||
); | ||
}); | ||
|
||
it('ValidationMetadataUrlTest_AseChannel_Public', function () { | ||
const config = {}; | ||
AseChannelValidation.init(config); | ||
assert.strictEqual( | ||
AseChannelValidation.MetadataUrl, | ||
AuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl | ||
); | ||
}); | ||
|
||
it('ValidationIssueUrlTest_AseChannel', function () { | ||
const config = { | ||
MicrosoftAppTenantId: 'testTenantId', | ||
}; | ||
AseChannelValidation.init(config); | ||
const tenantIds = [ | ||
'testTenantId', | ||
'f8cdef31-a31e-4b4a-93e4-5f571e91255a', // US Gov MicrosoftServices.onmicrosoft.us | ||
'd6d49420-f39b-4df7-a1dc-d59a935871db', // Public botframework.com | ||
]; | ||
tenantIds.forEach(function (tmpId) { | ||
assert.strictEqual( | ||
true, | ||
BetweenBotAndAseChannelTokenValidationParameters.issuer.includes( | ||
`https://sts.windows.net/${tmpId}/` | ||
) | ||
); | ||
assert.strictEqual( | ||
true, | ||
BetweenBotAndAseChannelTokenValidationParameters.issuer.includes( | ||
`https://login.microsoftonline.com/${tmpId}/v2.0` | ||
) | ||
); | ||
assert.strictEqual( | ||
true, | ||
BetweenBotAndAseChannelTokenValidationParameters.issuer.includes( | ||
`https://login.microsoftonline.us/${tmpId}/v2.0` | ||
) | ||
); | ||
}); | ||
}); | ||
|
||
it('ValidationChannelIdTest_AseChannel', function () { | ||
assert.strictEqual(true, AseChannelValidation.isTokenFromAseChannel('AseChannel')); | ||
}); | ||
}); | ||
}); |