Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Skills] add SkillHandler & SkillConversationIdFactoryBase classes #1482

Merged
merged 12 commits into from
Dec 9, 2019
Merged
26 changes: 20 additions & 6 deletions libraries/botbuilder-dialogs/src/prompts/oauthPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Activity, ActivityTypes, Attachment, CardFactory, InputHints, MessageFactory, OAuthLoginTimeoutKey, TokenResponse, TurnContext, IUserTokenProvider, } from 'botbuilder-core';
import { Activity, ActivityTypes, Attachment, CardFactory, InputHints, MessageFactory, OAuthLoginTimeoutKey, TokenResponse, TurnContext, IUserTokenProvider, OAuthCard, ActionTypes, } from 'botbuilder-core';
import { Dialog, DialogTurnResult } from '../dialog';
import { DialogContext } from '../dialogContext';
import { PromptOptions, PromptRecognizerResult, PromptValidator } from './prompt';
import { channels } from '../choices/channel';
import { isSkillClaim } from './skillsHelpers';

/**
* Settings used to configure an `OAuthPrompt` instance.
Expand Down Expand Up @@ -248,17 +249,31 @@ export class OAuthPrompt extends Dialog {
if (this.channelSupportsOAuthCard(context.activity.channelId)) {
const cards: Attachment[] = msg.attachments.filter((a: Attachment) => a.contentType === CardFactory.contentTypes.oauthCard);
if (cards.length === 0) {
let link: string = undefined;
let cardActionType = ActionTypes.Signin;
let link: string;
if (OAuthPrompt.isFromStreamingConnection(context.activity)) {
link = await (context.adapter as any).getSignInLink(context, this.settings.connectionName);
} else {
// Retrieve the ClaimsIdentity from a BotFrameworkAdapter. For more information see
// https://github.com/microsoft/botbuilder-js/commit/b7932e37bb6e421985d5ce53edd9e82af6240a63#diff-3e3af334c0c6adf4906ee5e2a23beaebR250
const identity = context.turnState.get((context.adapter as any).BotIdentityKey);
if (identity && isSkillClaim(identity.claims)) {
// Force magic code for Skills (to be addressed in R8)
link = await (context.adapter as any).getSignInLink(context, this.settings.connectionName);
cardActionType = ActionTypes.OpenUrl;
}
}
// Append oauth card
msg.attachments.push(CardFactory.oauthCard(
const card = CardFactory.oauthCard(
this.settings.connectionName,
this.settings.title,
this.settings.text,
link
));
);

// Set the appropriate ActionType for the button.
(card.content as OAuthCard).buttons[0].type = cardActionType;
msg.attachments.push(card);
}
} else {
const cards: Attachment[] = msg.attachments.filter((a: Attachment) => a.contentType === CardFactory.contentTypes.signinCard);
Expand All @@ -274,8 +289,7 @@ export class OAuthPrompt extends Dialog {
}

// Add the login timeout specified in OAuthPromptSettings to TurnState so it can be referenced if polling is needed
if (!context.turnState.get(OAuthLoginTimeoutKey) && this.settings.timeout)
{
if (!context.turnState.get(OAuthLoginTimeoutKey) && this.settings.timeout) {
context.turnState.set(OAuthLoginTimeoutKey, this.settings.timeout);
}

Expand Down
48 changes: 28 additions & 20 deletions libraries/botbuilder-dialogs/src/prompts/skillsHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export const AuthConstants = {
VersionClaim: 'ver'
};

export const GovConstants = {
ToBotFromChannelTokenIssuer: 'https://api.botframework.us'
}

/**
* @ignore
* Checks if the given object of claims represents a skill.
Expand All @@ -33,19 +37,20 @@ export const AuthConstants = {
* @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 {
export function isSkillClaim(claims: { [key: string]: any }[]): boolean {
if (!claims) {
throw new TypeError(`isSkillClaim(): missing claims.`);
}

const versionClaim = claims[AuthConstants.VersionClaim];
if (!versionClaim) {
const versionClaim = claims.find(c => c.type === AuthConstants.VersionClaim);
const versionValue = versionClaim && versionClaim.value;
if (!versionValue) {
// Must have a version claim.
return false;
}

const audClaim = claims[AuthConstants.AudienceClaim];
if (!audClaim || AuthConstants.ToBotFromChannelTokenIssuer === audClaim) {
const audClaim = claims.find(c => c.type === AuthConstants.AudienceClaim);
const audienceValue = audClaim && audClaim.value;
if (!audClaim || AuthConstants.ToBotFromChannelTokenIssuer === audienceValue || GovConstants.ToBotFromChannelTokenIssuer === audienceValue) {
// The audience is https://api.botframework.com and not an appId.
return false;
}
Expand All @@ -56,7 +61,7 @@ export function isSkillClaim(claims: { [key: string]: any }): boolean {
}

// Skill claims must contain and app ID and the AppID must be different than the audience.
return appId !== audClaim;
return appId !== audienceValue;
}

/**
Expand All @@ -71,23 +76,26 @@ export function isSkillClaim(claims: { [key: string]: any }): boolean {
* 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 {
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];
}
// Depending on Version, the AppId is either in the
// appid claim (Version 1) or the 'azp' claim (Version 2).
const versionClaim = claims.find(c => c.type === AuthConstants.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 = claims.find(c => c.type === AuthConstants.AppIdClaim);
appId = appIdClaim && appIdClaim.value;
} else if (versionValue === '2.0') {
// Version '2.0' puts the AppId in the 'azp' claim.
const azpClaim = claims.find(c => c.type === AuthConstants.AuthorizedParty);
appId = azpClaim && azpClaim.value;
}

return appId;
return appId;
}
37 changes: 23 additions & 14 deletions libraries/botbuilder-dialogs/tests/internalSkillHelpers.test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
const assert = require('assert');
const { AuthConstants, isSkillClaim, getAppIdFromClaims } = require('../lib/prompts/skillsHelpers');
const { AuthConstants, getAppIdFromClaims, GovConstants, isSkillClaim } = 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 versionClaim = {};
const audClaim = {};
const appIdClaim = {};
const claims = [versionClaim, audClaim, appIdClaim];
const audience = uuid();
const appId = uuid();

Expand All @@ -21,50 +24,56 @@ describe('Internal Skills-related methods', function() {
assert(!isSkillClaim(claims));

// No Audience claim
claims[AuthConstants.VersionClaim] = '1.0';
versionClaim.type = AuthConstants.VersionClaim;
versionClaim.value = '1.0';
assert(!isSkillClaim(claims));

// Emulator Audience claim
claims[AuthConstants.AudienceClaim] = AuthConstants.ToBotFromChannelTokenIssuer;
audClaim.type = AuthConstants.AudienceClaim;
audClaim.value = AuthConstants.ToBotFromChannelTokenIssuer;
assert(!isSkillClaim(claims));

// Government Audience claim
audClaim.value = GovConstants.ToBotFromChannelTokenIssuer;
assert(!isSkillClaim(claims));

// No AppId claim
claims[AuthConstants.AudienceClaim] = audience;
audClaim.value = audience;
assert(!isSkillClaim(claims));

// AppId != Audience
claims[AuthConstants.AppIdClaim] = audience;
// If not AppId != Audience, should return false
appIdClaim.type = AuthConstants.AppIdClaim;
appIdClaim.value = audience;
assert(!isSkillClaim(claims));

// All checks pass, should be good now
claims[AuthConstants.AudienceClaim] = appId;
audClaim.value = appId;
assert(isSkillClaim(claims));
});
});

describe('getAppIdFromClaims()', () => {
it('should get appId from claims', () => {
const appId = 'uuid.uuid4()';
const v1Claims = {};
const v2Claims = { [AuthConstants.VersionClaim]: '2.0' };
const v1Claims = [];
const v2Claims = [{ type: AuthConstants.VersionClaim, value : '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;
v1Claims[0] = { type: AuthConstants.AppIdClaim, value: appId };
assert.strictEqual(getAppIdFromClaims(v1Claims), appId);

// AppId exists with v1 version
v1Claims[AuthConstants.VersionClaim] = '1.0';
v1Claims[1] = { type: AuthConstants.VersionClaim, value: '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;
v2Claims[1] = { type: AuthConstants.AuthorizedParty, value: appId };
assert.strictEqual(getAppIdFromClaims(v2Claims), appId);
});

Expand Down
Loading