-
Notifications
You must be signed in to change notification settings - Fork 283
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Skills] Add SkillValidation class (#1461)
* add SkillValidation class * add SkillValidation.isSkillToken() tests * add more tests for SkillValidation * cleanup bf-connector, duplicate necessary auth code in bb-dialogs * add oAuthScope param to MicrosoftAppCredentials ctor & test * SkillValidation changes & tests * export validateIdentity for testing * validateIdentity is not documented as to be "internal" * change signature on isSkillClaim to take Claim[] * isSkillClaim returns false if audience value is GovernmentConstants.ToBotFromChannelTokenIssuer * JwtTokenValidation.getAppIdFromClaims() now takes Claim[]
- Loading branch information
Showing
11 changed files
with
731 additions
and
49 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/** | ||
* @module botbuilder-dialogs | ||
*/ | ||
/** | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. | ||
*/ | ||
|
||
// These internally exported constants and methods are duplicates of the AuthenticationConstants, JwtTokenValidation | ||
// and SkillValidation exports from the Node.js-reliant botframework-connector library. | ||
// The contents of this file should NOT be exported as this is a temporary patch for supporting Skills in | ||
// Node.js bots without making botbuilder-dialogs not browser-compatible. | ||
// isSkillClaim() is the only method directly called by the OAuthPrompt, but the other contents of this file are exported to facilitate the usage of the same tests as in botframework-connector. | ||
|
||
export const AuthConstants = { | ||
AppIdClaim: 'appid', | ||
AudienceClaim: 'aud', | ||
AuthorizedParty: 'azp', | ||
ToBotFromChannelTokenIssuer: 'https://api.botframework.com', | ||
VersionClaim: 'ver' | ||
}; | ||
|
||
/** | ||
* @ignore | ||
* Checks if the given object of claims represents a skill. | ||
* @remarks | ||
* A skill claim should contain: | ||
* An "AuthenticationConstants.VersionClaim" claim. | ||
* An "AuthenticationConstants.AudienceClaim" claim. | ||
* An "AuthenticationConstants.AppIdClaim" claim (v1) or an a "AuthenticationConstants.AuthorizedParty" claim (v2). | ||
* And the appId claim should be different than the audience claim. | ||
* The audience claim should be a guid, indicating that it is from another bot/skill. | ||
* @param claims An object of claims. | ||
* @returns {boolean} True if the object of claims is a skill claim, false if is not. | ||
*/ | ||
export function isSkillClaim(claims: { [key: string]: any }): boolean { | ||
if (!claims) { | ||
throw new TypeError(`isSkillClaim(): missing claims.`); | ||
} | ||
|
||
const versionClaim = claims[AuthConstants.VersionClaim]; | ||
if (!versionClaim) { | ||
// Must have a version claim. | ||
return false; | ||
} | ||
|
||
const audClaim = claims[AuthConstants.AudienceClaim]; | ||
if (!audClaim || AuthConstants.ToBotFromChannelTokenIssuer === audClaim) { | ||
// The audience is https://api.botframework.com and not an appId. | ||
return false; | ||
} | ||
|
||
const appId = getAppIdFromClaims(claims); | ||
if (!appId) { | ||
return false; | ||
} | ||
|
||
// Skill claims must contain and app ID and the AppID must be different than the audience. | ||
return appId !== audClaim; | ||
} | ||
|
||
/** | ||
* @ignore | ||
* Gets the AppId from a claims list. | ||
* @remarks | ||
* 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 claims An object containing claims types and their values. | ||
*/ | ||
export function getAppIdFromClaims(claims: { [key: string]: any }): string { | ||
if (!claims) { | ||
throw new TypeError(`getAppIdFromClaims(): missing claims.`); | ||
} | ||
let appId: string; | ||
|
||
// Depending on Version, the AppId is either in the | ||
// appid claim (Version 1) or the 'azp' claim (Version 2). | ||
const tokenClaim = claims[AuthConstants.VersionClaim]; | ||
if (!tokenClaim || tokenClaim === '1.0') { | ||
// No version or a version of '1.0' means we should look for | ||
// the claim in the 'appid' claim. | ||
appId = claims[AuthConstants.AppIdClaim]; | ||
} else if (tokenClaim === '2.0') { | ||
// Version '2.0' puts the AppId in the 'azp' claim. | ||
appId = claims[AuthConstants.AuthorizedParty]; | ||
} | ||
|
||
return appId; | ||
} |
97 changes: 97 additions & 0 deletions
97
libraries/botbuilder-dialogs/tests/internalSkillHelpers.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,97 @@ | ||
const assert = require('assert'); | ||
const { AuthConstants, isSkillClaim, getAppIdFromClaims } = require('../lib/prompts/skillsHelpers'); | ||
|
||
describe('Internal Skills-related methods', function() { | ||
this.timeout(5000); | ||
describe('isSkillClaim()', () => { | ||
it('should return false for invalid claims and true for valid claims', () => { | ||
const claims = {}; | ||
const audience = uuid(); | ||
const appId = uuid(); | ||
|
||
// No claims (falsey value) | ||
try { | ||
assert(!isSkillClaim()); | ||
throw new Error('isSkillClaim() should have failed with undefined parameter'); | ||
} catch (e) { | ||
assert.strictEqual(e.message, 'isSkillClaim(): missing claims.'); | ||
} | ||
|
||
// Empty list of claims | ||
assert(!isSkillClaim(claims)); | ||
|
||
// No Audience claim | ||
claims[AuthConstants.VersionClaim] = '1.0'; | ||
assert(!isSkillClaim(claims)); | ||
|
||
// Emulator Audience claim | ||
claims[AuthConstants.AudienceClaim] = AuthConstants.ToBotFromChannelTokenIssuer; | ||
assert(!isSkillClaim(claims)); | ||
|
||
// No AppId claim | ||
claims[AuthConstants.AudienceClaim] = audience; | ||
assert(!isSkillClaim(claims)); | ||
|
||
// AppId != Audience | ||
claims[AuthConstants.AppIdClaim] = audience; | ||
assert(!isSkillClaim(claims)); | ||
|
||
// All checks pass, should be good now | ||
claims[AuthConstants.AudienceClaim] = appId; | ||
assert(isSkillClaim(claims)); | ||
}); | ||
}); | ||
|
||
describe('getAppIdFromClaims()', () => { | ||
it('should get appId from claims', () => { | ||
const appId = 'uuid.uuid4()'; | ||
const v1Claims = {}; | ||
const v2Claims = { [AuthConstants.VersionClaim]: '2.0' }; | ||
|
||
// Empty array of Claims should yield undefined | ||
assert.strictEqual(getAppIdFromClaims(v1Claims), undefined); | ||
|
||
// AppId exists, but there is no version (assumes v1) | ||
v1Claims[AuthConstants.AppIdClaim] = appId; | ||
assert.strictEqual(getAppIdFromClaims(v1Claims), appId); | ||
|
||
// AppId exists with v1 version | ||
v1Claims[AuthConstants.VersionClaim] = '1.0'; | ||
assert.strictEqual(getAppIdFromClaims(v1Claims), appId); | ||
|
||
// v2 version should yield undefined with no "azp" claim | ||
v2Claims[AuthConstants.VersionClaim] = '2.0'; | ||
assert.strictEqual(getAppIdFromClaims(v2Claims), undefined); | ||
|
||
// v2 version with azp | ||
v2Claims[AuthConstants.AuthorizedParty] = appId; | ||
assert.strictEqual(getAppIdFromClaims(v2Claims), appId); | ||
}); | ||
|
||
it('should throw an error if claims is falsey', () => { | ||
try { | ||
getAppIdFromClaims(); | ||
} catch (e) { | ||
assert.strictEqual(e.message, 'getAppIdFromClaims(): missing claims.'); | ||
} | ||
}); | ||
}); | ||
|
||
describe('AuthConstants', () => { | ||
it('should have correct values', () => { | ||
// For reference see botframework-connector's AuthenticationConstants | ||
assert.strictEqual(AuthConstants.AppIdClaim, 'appid'); | ||
assert.strictEqual(AuthConstants.AudienceClaim, 'aud'); | ||
assert.strictEqual(AuthConstants.AuthorizedParty, 'azp'); | ||
assert.strictEqual(AuthConstants.ToBotFromChannelTokenIssuer, 'https://api.botframework.com'); | ||
assert.strictEqual(AuthConstants.VersionClaim, 'ver'); | ||
}); | ||
}); | ||
}); | ||
|
||
function uuid() { | ||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { | ||
const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); | ||
return v.toString(16); | ||
}); | ||
} |
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
Oops, something went wrong.