Skip to content

Commit e9ba900

Browse files
JeffdersStevenic
authored andcommitted
Final Node support for OAuthCards (#4553)
* Final support for OAuthCards * Typo!
1 parent 4d66269 commit e9ba900

File tree

6 files changed

+210
-13
lines changed

6 files changed

+210
-13
lines changed

Node/core/lib/botbuilder.d.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,6 +1372,9 @@ export interface IChatConnectorEndpoint {
13721372

13731373
/** Default value is https://state.botframework.com. Configurable via IChatConnectorSettings.stateEndpoint. */
13741374
stateEndpoint: string;
1375+
1376+
/** Default value is https://api.botframework.com. Configurable via IChatConnectorSettings.oAuthEndpoint. */
1377+
oAuthEndpoint: string;
13751378
}
13761379

13771380
/** Options used to initialize a UniversalBot instance. */
@@ -2701,16 +2704,19 @@ export class OAuthCard implements IIsAttachment {
27012704
constructor(session?: Session);
27022705

27032706
/** The name of the OAuth connection to use. */
2704-
connectionName(name: string): SigninCard;
2707+
connectionName(name: string): OAuthCard;
27052708

27062709
/** Title of the Card. */
2707-
text(prompts: TextType, ...args: any[]): SigninCard;
2710+
text(prompts: TextType, ...args: any[]): OAuthCard;
27082711

27092712
/** Signin button label. */
2710-
button(title: TextType): SigninCard;
2713+
button(title: TextType): OAuthCard;
27112714

27122715
/** Returns the JSON for the card, */
27132716
toAttachment(): IAttachment;
2717+
2718+
/** Factory method for returning a message with the proper signin attachment */
2719+
static create(connector: ChatConnector, session: Session, connectionName: string, text: string, buttonTitle: string, done: (err: Error, message: Message) => void): void;
27142720
}
27152721

27162722
/** Card builder class that simplifies building receipt cards. */
@@ -4292,7 +4298,7 @@ export class ChatConnector implements IConnector, IBotStorage {
42924298
* @param connectionName Name of the auth connection to use.
42934299
* @param done Callback to retrieve the users token.
42944300
*/
4295-
signOutUser(address: IChatConnectorAddress, connectionName: string, magicCode: string|undefined, done: (err: Error, results: ITokenResponse) => void): void;
4301+
signOutUser(address: IChatConnectorAddress, connectionName: string, done: (err: Error, results: ITokenResponse) => void): void;
42964302

42974303
/**
42984304
* Gets a signin link from the token server that can be sent as part of a SigninCard.

Node/core/lib/bots/ChatConnector.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ var ChatConnector = (function () {
3131
emulatorAuthV31IssuerV2: 'https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0',
3232
emulatorAuthV32IssuerV1: 'https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/',
3333
emulatorAuthV32IssuerV2: 'https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0',
34-
stateEndpoint: this.settings.stateEndpoint || 'https://state.botframework.com'
34+
stateEndpoint: this.settings.stateEndpoint || 'https://state.botframework.com',
35+
oAuthEndpoint: this.settings.oAuthEndpoint || 'https://api.botframework.com',
3536
};
3637
}
3738
this.botConnectorOpenIdMetadata = new OpenIdMetadata_1.OpenIdMetadata(this.settings.endpoint.botConnectorOpenIdMetadata);
@@ -323,7 +324,7 @@ var ChatConnector = (function () {
323324
}
324325
var options = {
325326
method: 'GET',
326-
url: urlJoin(address.serviceUrl, path),
327+
url: urlJoin(this.getOAuthPath(address), path),
327328
json: true
328329
};
329330
this.authenticatedRequest(options, function (err, response, body) { return done(err, body); });
@@ -333,7 +334,7 @@ var ChatConnector = (function () {
333334
path += '&connectionName=' + encodeURIComponent(connectionName);
334335
var options = {
335336
method: 'DELETE',
336-
url: urlJoin(address.serviceUrl, path),
337+
url: urlJoin(this.getOAuthPath(address), path),
337338
json: true
338339
};
339340
this.authenticatedRequest(options, function (err, response, body) { return done(err); });
@@ -355,7 +356,7 @@ var ChatConnector = (function () {
355356
var path = 'api/botsignin/getsigninurl?state=' + encodeURIComponent(finalState);
356357
var options = {
357358
method: 'GET',
358-
url: urlJoin(address.serviceUrl, path)
359+
url: urlJoin(this.getOAuthPath(address), path)
359360
};
360361
this.authenticatedRequest(options, function (err, response, body) { return done(err, body); });
361362
};
@@ -768,6 +769,9 @@ var ChatConnector = (function () {
768769
}
769770
return path + '/v3/botstate/' + encodeURIComponent(address.channelId);
770771
};
772+
ChatConnector.prototype.getOAuthPath = function (address) {
773+
return this.settings.endpoint.oAuthEndpoint;
774+
};
771775
ChatConnector.prototype.prepIncomingMessage = function (msg) {
772776
utils.moveFieldsTo(msg, msg, {
773777
'locale': 'textLocale',

Node/core/lib/cards/OAuthCard.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use strict";
22
Object.defineProperty(exports, "__esModule", { value: true });
33
var Message_1 = require("../Message");
4+
var SigninCard_1 = require("./SigninCard");
45
var OAuthCard = (function () {
56
function OAuthCard(session) {
67
this.session = session;
@@ -38,6 +39,42 @@ var OAuthCard = (function () {
3839
OAuthCard.prototype.toAttachment = function () {
3940
return this.data;
4041
};
42+
OAuthCard.create = function (connector, session, connectionName, text, buttonTitle, done) {
43+
var msg = new Message_1.Message(session);
44+
var asSignInCard = false;
45+
switch (session.message.address.channelId) {
46+
case 'msteams':
47+
case 'cortana':
48+
case 'skype':
49+
case 'skypeforbusiness':
50+
asSignInCard = true;
51+
break;
52+
}
53+
if (asSignInCard) {
54+
connector.getSignInLink(session.message.address, connectionName, function (getSignInLinkErr, link) {
55+
if (getSignInLinkErr) {
56+
done(getSignInLinkErr, undefined);
57+
}
58+
else {
59+
msg.attachments([
60+
new SigninCard_1.SigninCard(session)
61+
.text(text)
62+
.button(buttonTitle, link)
63+
]);
64+
done(undefined, msg);
65+
}
66+
});
67+
}
68+
else {
69+
msg.attachments([
70+
new OAuthCard(session)
71+
.text(text)
72+
.connectionName(connectionName)
73+
.button(buttonTitle)
74+
]);
75+
done(undefined, msg);
76+
}
77+
};
4178
return OAuthCard;
4279
}());
4380
exports.OAuthCard = OAuthCard;

Node/core/src/bots/ChatConnector.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export interface IChatConnectorSettings {
6161
gzipData?: boolean;
6262
endpoint?: IChatConnectorEndpoint;
6363
stateEndpoint?: string;
64+
oAuthEndpoint?: string;
6465
openIdMetadata?: string;
6566
}
6667

@@ -77,6 +78,7 @@ export interface IChatConnectorEndpoint {
7778
emulatorAuthV32IssuerV2: string;
7879
emulatorAudience: string;
7980
stateEndpoint: string;
81+
oAuthEndpoint: string;
8082
}
8183

8284
export interface IChatConnectorAddress extends IAddress {
@@ -115,7 +117,8 @@ export class ChatConnector implements IConnector, IBotStorage {
115117
emulatorAuthV31IssuerV2: 'https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0',
116118
emulatorAuthV32IssuerV1: 'https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/',
117119
emulatorAuthV32IssuerV2: 'https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0',
118-
stateEndpoint: this.settings.stateEndpoint || 'https://state.botframework.com'
120+
stateEndpoint: this.settings.stateEndpoint || 'https://state.botframework.com',
121+
oAuthEndpoint: this.settings.oAuthEndpoint || 'https://api.botframework.com',
119122
}
120123
}
121124

@@ -443,7 +446,7 @@ export class ChatConnector implements IConnector, IBotStorage {
443446
// We use urlJoin to concatenate urls. url.resolve should not be used here,
444447
// since it resolves urls as hrefs are resolved, which could result in losing
445448
// the last fragment of the serviceUrl
446-
url: urlJoin(address.serviceUrl, path),
449+
url: urlJoin(this.getOAuthPath(address), path),
447450
json: true
448451
};
449452
this.authenticatedRequest(options, (err, response, body) => done(err, body));
@@ -460,7 +463,7 @@ export class ChatConnector implements IConnector, IBotStorage {
460463
// We use urlJoin to concatenate urls. url.resolve should not be used here,
461464
// since it resolves urls as hrefs are resolved, which could result in losing
462465
// the last fragment of the serviceUrl
463-
url: urlJoin(address.serviceUrl, path),
466+
url: urlJoin(this.getOAuthPath(address), path),
464467
json: true
465468
};
466469
this.authenticatedRequest(options, (err, response, body) => done(err));
@@ -491,7 +494,7 @@ export class ChatConnector implements IConnector, IBotStorage {
491494
// We use urlJoin to concatenate urls. url.resolve should not be used here,
492495
// since it resolves urls as hrefs are resolved, which could result in losing
493496
// the last fragment of the serviceUrl
494-
url: urlJoin(address.serviceUrl, path)
497+
url: urlJoin(this.getOAuthPath(address), path)
495498
};
496499
this.authenticatedRequest(options, (err, response, body) => done(err, body));
497500
}
@@ -933,6 +936,10 @@ export class ChatConnector implements IConnector, IBotStorage {
933936
return path + '/v3/botstate/' + encodeURIComponent(address.channelId);
934937
}
935938

939+
private getOAuthPath(address: IChatConnectorAddress): string {
940+
return this.settings.endpoint.oAuthEndpoint;
941+
}
942+
936943
private prepIncomingMessage(msg: IMessage): void {
937944
// Patch locale and channelData
938945
utils.moveFieldsTo(msg, msg, {

Node/core/src/cards/OAuthCard.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
//
3333

3434
import { Session } from '../Session';
35-
import { fmtText } from '../Message';
35+
import { ChatConnector } from '../bots/ChatConnector';
36+
import { fmtText, Message } from '../Message';
37+
import { SigninCard } from './SigninCard';
3638

3739
export class OAuthCard implements IIsAttachment {
3840
private data = {
@@ -71,4 +73,41 @@ export class OAuthCard implements IIsAttachment {
7173
public toAttachment(): IAttachment {
7274
return this.data;
7375
}
76+
77+
public static create(connector: ChatConnector, session: Session, connectionName: string, text: string, buttonTitle: string, done: (err: Error, message: Message) => void): void {
78+
var msg = new Message(session);
79+
80+
var asSignInCard: boolean = false;
81+
switch (session.message.address.channelId) {
82+
case 'msteams':
83+
case 'cortana':
84+
case 'skype':
85+
case 'skypeforbusiness':
86+
asSignInCard = true;
87+
break;
88+
}
89+
90+
if (asSignInCard) {
91+
connector.getSignInLink(session.message.address, connectionName, (getSignInLinkErr: Error, link: string) => {
92+
if (getSignInLinkErr) {
93+
done(getSignInLinkErr, undefined);
94+
} else {
95+
msg.attachments([
96+
new SigninCard(session)
97+
.text(text)
98+
.button(buttonTitle, link)
99+
]);
100+
done(undefined, msg);
101+
}
102+
});
103+
} else {
104+
msg.attachments([
105+
new OAuthCard(session)
106+
.text(text)
107+
.connectionName(connectionName)
108+
.button(buttonTitle)
109+
]);
110+
done(undefined, msg);
111+
}
112+
}
74113
}

Node/examples/basics-oauth/app.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*-----------------------------------------------------------------------------
2+
A simple OAuthCard bot for the Microsoft Bot Framework.
3+
-----------------------------------------------------------------------------*/
4+
5+
var restify = require('restify');
6+
var builder = require('../../core/');
7+
8+
// Setup Restify Server
9+
var server = restify.createServer();
10+
server.listen(process.env.port || process.env.PORT || 4000, function () {
11+
console.log('%s listening to %s', server.name, server.url);
12+
});
13+
14+
// Bot Storage: Here we register the state storage for your bot.
15+
// Default store: volatile in-memory store - Only for prototyping!
16+
// We provide adapters for Azure Table, CosmosDb, SQL Azure, or you can implement your own!
17+
// For samples and documentation, see: https://github.com/Microsoft/BotBuilder-Azure
18+
var inMemoryStorage = new builder.MemoryBotStorage();
19+
20+
// Create chat connector for communicating with the Bot Framework Service
21+
var connector = new builder.ChatConnector({
22+
appId: process.env.MICROSOFT_APP_ID,
23+
appPassword: process.env.MICROSOFT_APP_PASSWORD
24+
});
25+
26+
var connectionName = process.env.CONNECTION_NAME;
27+
28+
// Listen for messages from users
29+
server.post('/api/messages', connector.listen());
30+
31+
// Create your bot with a function to receive messages from the user
32+
var bot = new builder.UniversalBot(connector, function (session) {
33+
if (session.message.text == 'signout') {
34+
// It is important to have a SignOut intent
35+
connector.signOutUser(session.message.address, connectionName, (err, result) => {
36+
if (!err) {
37+
session.send('You are signed out.');
38+
} else {
39+
session.send('There was a problem signing you out.');
40+
}
41+
});
42+
} else {
43+
// First check whether the Azure Bot Service already has a token for this user
44+
connector.getUserToken(session.message.address, connectionName, undefined, (err, result) => {
45+
if (result) {
46+
// If there is already a token, the bot can use it directly
47+
session.send('You are already signed in with token: ' + result.token);
48+
} else {
49+
// If there not is already a token, the bot can send an OAuthCard to have the user log in
50+
if (!session.userData.activeSignIn) {
51+
session.send("Hello! Let's get you signed in!");
52+
builder.OAuthCard.create(connector, session, connectionName, "Please sign in", "Sign in", (createSignInErr, signInMessage) =>
53+
{
54+
if (signInMessage) {
55+
session.send(signInMessage);
56+
session.userData.activeSignIn = true;
57+
} else {
58+
session.send("Something went wrong trying to sign you in.");
59+
}
60+
});
61+
} else {
62+
// Some clients require a 6 digit code validation so we can check that here
63+
session.send("Let's see if that code works...");
64+
connector.getUserToken(session.message.address, connectionName, session.message.text, (err2, tokenResponse) => {
65+
if (tokenResponse) {
66+
session.send('It worked! You are now signed in with token: ' + tokenResponse.token);
67+
session.userData.activeSignIn = false;
68+
} else {
69+
session.send("Hmm, that code wasn't right");
70+
}
71+
});
72+
}
73+
}
74+
});
75+
}
76+
})
77+
.set('storage', inMemoryStorage) // Register in memory storage
78+
.on("event", (event) => { // Handle 'event' activities
79+
if (event.name == 'tokens/response') {
80+
// received a TokenResponse, which is how the Azure Bot Service responds with the user token after an OAuthCard
81+
bot.loadSession(event.address, (err, session) => {
82+
let tokenResponse = event.value;
83+
session.send('You are now signed in with token: ' + tokenResponse.token);
84+
session.userData.activeSignIn = false;
85+
});
86+
}
87+
});
88+
89+
connector.onInvoke((event, cb) => {
90+
if (event.name == 'signin/verifyState') {
91+
// received a MS Team's code verification Invoke Activity
92+
bot.loadSession(event.address, (err, session) => {
93+
let verificationCode = event.value.state;
94+
// Get the user token using the verification code sent by MS Teams
95+
connector.getUserToken(session.message.address, connectionName, verificationCode, (err, result) => {
96+
session.send('You are now signed in with token: ' + result.token);
97+
session.userData.activeSignIn = false;
98+
cb(undefined, {}, 200);
99+
});
100+
});
101+
} else {
102+
cb(undefined, {}, 200);
103+
}
104+
});

0 commit comments

Comments
 (0)