Skip to content

Commit

Permalink
Support for OAuthCards (real and mocked) (#433)
Browse files Browse the repository at this point in the history
* Emulator OAuthCard support

* PKCE support to login without magic code

* Extra button for OAuthCards for code verification path

* Merge in latest DL and WebChat and support setting

* Update webchat package
  • Loading branch information
Jeffders committed May 1, 2018
1 parent e3ada04 commit aa6c640
Show file tree
Hide file tree
Showing 25 changed files with 777 additions and 33 deletions.
6 changes: 4 additions & 2 deletions gulpfile.js
@@ -1,4 +1,5 @@
var gulp = require('gulp'); var gulp = require('gulp');
var typescript = require('gulp-tsc');


gulp.task('clean', function () { gulp.task('clean', function () {
var clean = require('gulp-clean'); var clean = require('gulp-clean');
Expand All @@ -10,14 +11,15 @@ gulp.task('build-app', function () {
var tsc = require('gulp-tsc'); var tsc = require('gulp-tsc');
var tsconfig = require('./tsconfig.json'); var tsconfig = require('./tsconfig.json');
return gulp.src(['src/**/*.ts', 'src/**/*.tsx']) return gulp.src(['src/**/*.ts', 'src/**/*.tsx'])
.pipe(tsc(tsconfig.compilerOptions)) .pipe(typescript(tsconfig.compilerOptions))
.pipe(gulp.dest('app/')); .pipe(gulp.dest('app/'));
}); });


gulp.task('build-site', function () { gulp.task('build-site', function () {
return gulp.src([ return gulp.src([
'./src/**/*.html', './src/**/*.html',
'./src/**/*.css']) './src/**/*.css',
'./src/**/adaptivecards-hostconfig.json'])
.pipe(gulp.dest('app/')); .pipe(gulp.dest('app/'));
}); });


Expand Down
8 changes: 6 additions & 2 deletions package.json
Expand Up @@ -66,15 +66,17 @@
"object-assign": "4.1.0", "object-assign": "4.1.0",
"restify": "5.1.0", "restify": "5.1.0",
"tslib": "1.5.0", "tslib": "1.5.0",
"typescript": "2.3.4", "typescript": "^2.4.0",
"vinyl": "2.0.1", "vinyl": "2.0.1",
"vinyl-fs": "2.4.4" "vinyl-fs": "2.4.4"
}, },
"dependencies": { "dependencies": {
"async": "2.1.4", "async": "2.1.4",
"base64url": "2.0.0", "base64url": "2.0.0",
"botframework-webchat": "0.11.2", "botframework-webchat": "^0.13.1",
"btoa": "^1.2.1",
"command-line-args": "4.0.7", "command-line-args": "4.0.7",
"crypto-js": "^3.1.9-1",
"electron-debug": "1.1.0", "electron-debug": "1.1.0",
"electron-localshortcut": "2.0.2", "electron-localshortcut": "2.0.2",
"es6-shim": "0.35.2", "es6-shim": "0.35.2",
Expand All @@ -94,6 +96,8 @@
"restify": "4.3.0", "restify": "4.3.0",
"rsa-pem-from-mod-exp": "0.8.4", "rsa-pem-from-mod-exp": "0.8.4",
"rxjs": "5.4.2", "rxjs": "5.4.2",
"sha.js": "^2.4.11",
"utf8": "^3.0.0",
"xmldom": "0.1.27", "xmldom": "0.1.27",
"xtend": "4.0.1" "xtend": "4.0.1"
}, },
Expand Down
121 changes: 121 additions & 0 deletions src/client/adaptivecards-hostconfig.json
@@ -0,0 +1,121 @@
{
"supportsInteractivity": true,
"spacing": {
"small": 4,
"default": 8,
"medium": 16,
"large": 24,
"extraLarge": 32,
"padding": 8
},
"separator": {
"lineThickness": 1,
"lineColor": "#cccccc"
},
"fontFamily": "\"Segoe UI\", sans-serif",
"fontSizes": {
"small": 12,
"default": 13,
"medium": 15,
"large": 17,
"extraLarge": 19
},
"fontWeights": {
"lighter": 200,
"default": 400,
"bolder": 700
},
"containerStyles": {
"default": {
"backgroundColor": "#00000000",
"foregroundColors": {
"default": {
"default": "#000000",
"subtle": "#808c95"
},
"accent": {
"default": "#2e89fc",
"subtle": "#802E8901"
},
"attention": {
"default": "#ffd800",
"subtle": "#CCFFD800"
},
"good": {
"default": "#00ff00",
"subtle": "#CC00FF00"
},
"warning": {
"default": "#ff0000",
"subtle": "#CCFF0000"
}
}
},
"emphasis": {
"backgroundColor": "#08000000",
"foregroundColors": {
"default": {
"default": "#333333",
"subtle": "#EE333333"
},
"accent": {
"default": "#2e89fc",
"subtle": "#882E89FC"
},
"attention": {
"default": "#cc3300",
"subtle": "#DDCC3300"
},
"good": {
"default": "#54a254",
"subtle": "#DD54A254"
},
"warning": {
"default": "#e69500",
"subtle": "#DDE69500"
}
}
}
},
"imageSizes": {
"small": 40,
"medium": 80,
"large": 160
},
"actions": {
"maxActions": 100,
"spacing": "default",
"buttonSpacing": 8,
"showCard": {
"actionMode": "inline",
"inlineTopMargin": 8
},
"actionsOrientation": "vertical",
"actionAlignment": "stretch"
},
"adaptiveCard": {
"allowCustomStyle": false
},
"imageSet": {
"imageSize": "medium",
"maxImageHeight": 100
},
"factSet": {
"title": {
"color": "default",
"size": "default",
"isSubtle": false,
"weight": "bolder",
"wrap": true,
"maxWidth": 150
},
"value": {
"color": "default",
"size": "default",
"isSubtle": false,
"weight": "default",
"wrap": true
},
"spacing": 8
}
}
15 changes: 14 additions & 1 deletion src/client/dialogs/appSettingsDialog.tsx
Expand Up @@ -53,6 +53,7 @@ export class AppSettingsDialog extends React.Component<{}, AppSettingsDialogStat
stateSizeLimitInputRef: any; stateSizeLimitInputRef: any;
bypassNgrokLocalhostInputRef: any; bypassNgrokLocalhostInputRef: any;
use10TokensInputRef: any; use10TokensInputRef: any;
useCodeValidationInputRef: any;
showing: boolean; showing: boolean;
lastFocusRef: any; lastFocusRef: any;


Expand All @@ -78,7 +79,8 @@ export class AppSettingsDialog extends React.Component<{}, AppSettingsDialogStat
ngrokPath: this.ngrokPathInputRef.value, ngrokPath: this.ngrokPathInputRef.value,
bypassNgrokLocalhost: this.bypassNgrokLocalhostInputRef.checked, bypassNgrokLocalhost: this.bypassNgrokLocalhostInputRef.checked,
stateSizeLimit: this.stateSizeLimitInputRef.value, stateSizeLimit: this.stateSizeLimitInputRef.value,
use10Tokens: this.use10TokensInputRef.checked use10Tokens: this.use10TokensInputRef.checked,
useCodeValidation: this.useCodeValidationInputRef.checked
}); });
AddressBarActions.hideAppSettings(); AddressBarActions.hideAppSettings();
} }
Expand Down Expand Up @@ -223,6 +225,17 @@ export class AppSettingsDialog extends React.Component<{}, AppSettingsDialogStat
Use version 1.0 authentication tokens Use version 1.0 authentication tokens
</label> </label>
</div> </div>
<div className="input-group appsettings-checkbox-group">
<label className="form-label clickable">
<input
type="checkbox"
name="useCodeValidation"
ref={ref => this.useCodeValidationInputRef = ref}
className="form-input"
defaultChecked={ serverSettings.framework.useCodeValidation } />
Use a sign-in verification code for OAuthCards
</label>
</div>
</div>) )} </div>) )}
{this.renderNavTab("state", (<div> {this.renderNavTab("state", (<div>
<div className='emu-dialog-text'> <div className='emu-dialog-text'>
Expand Down
13 changes: 13 additions & 0 deletions src/client/emulator.ts
Expand Up @@ -169,6 +169,19 @@ export class Emulator {
request(options, responseCallback); request(options, responseCallback);
} }


public static sendTokenResponse(connectionName: string, token: string, cb: (result: boolean) => void) {
const settings = getSettings();
let options: request.OptionsWithUrl = {
url: `${this.serviceUrl}/emulator/${settings.conversation.conversationId}/invoke/sendTokenResponse`,
method: "POST",
json: [{token: token, connectionName: connectionName }],
};
let responseCallback = (err, resp: http.IncomingMessage, body) => {
cb(true);
};
request(options, responseCallback);
}

public static zoomIn() { public static zoomIn() {
let options: request.OptionsWithUrl = { let options: request.OptionsWithUrl = {
url: `${this.serviceUrl}/emulator/window/zoomIn`, url: `${this.serviceUrl}/emulator/window/zoomIn`,
Expand Down
22 changes: 21 additions & 1 deletion src/client/hyperlinkHandler.ts
Expand Up @@ -39,9 +39,11 @@ import { getSettings, selectedActivity$ } from './settings';
import { Settings as ServerSettings } from '../types/serverSettingsTypes'; import { Settings as ServerSettings } from '../types/serverSettingsTypes';
import { Emulator } from './emulator'; import { Emulator } from './emulator';
import { PaymentEncoder } from '../shared/paymentEncoder'; import { PaymentEncoder } from '../shared/paymentEncoder';
import { OAuthClientEncoder } from '../shared/oauthClientEncoder';
import { OAuthLinkEncoder } from '../shared/oauthLinkEncoder';
import * as log from './log'; import * as log from './log';
import * as Electron from 'electron'; import * as Electron from 'electron';

import { uniqueId } from '../shared/utils';


export function navigate(url: string) { export function navigate(url: string) {
try { try {
Expand All @@ -59,6 +61,10 @@ export function navigate(url: string) {
} }
} else if (parsed.protocol.startsWith(PaymentEncoder.PaymentEmulatorUrlProtocol)) { } else if (parsed.protocol.startsWith(PaymentEncoder.PaymentEmulatorUrlProtocol)) {
navigatePaymentUrl(parsed.path); navigatePaymentUrl(parsed.path);
} else if (parsed.protocol.startsWith(OAuthClientEncoder.OAuthEmulatorUrlProtocol)) {
navigateEmulatedOAuthUrl(url.substring(8));
} else if (parsed.protocol.startsWith(OAuthLinkEncoder.OAuthUrlProtocol)) {
navigateOAuthUrl(url.substring(12));
} else if (parsed.protocol.startsWith('file:')) { } else if (parsed.protocol.startsWith('file:')) {
// ignore // ignore
} else if (parsed.protocol.startsWith('javascript:')) { } else if (parsed.protocol.startsWith('javascript:')) {
Expand Down Expand Up @@ -141,3 +147,17 @@ function navigatePaymentUrl(payload: string) {
serviceUrl: Emulator.serviceUrl serviceUrl: Emulator.serviceUrl
}); });
} }

function navigateEmulatedOAuthUrl(connectionName: string) {
Emulator.sendTokenResponse(connectionName, 'emulatedToken_' + uniqueId(),
(result: boolean) => {});
}

function navigateOAuthUrl(url: string) {
const settings = getSettings();
Electron.ipcRenderer.send("createOAuthWindow", {
url: url,
settings: settings,
serviceUrl: Emulator.serviceUrl
});
}
3 changes: 3 additions & 0 deletions src/client/mainView.tsx
Expand Up @@ -52,6 +52,8 @@ import { ISpeechTokenInfo } from '../types/speechTypes';
const CognitiveServices = require('../../node_modules/botframework-webchat/CognitiveServices'); const CognitiveServices = require('../../node_modules/botframework-webchat/CognitiveServices');
const remote = require('electron').remote; const remote = require('electron').remote;
const ipcRenderer = require('electron').ipcRenderer; const ipcRenderer = require('electron').ipcRenderer;
const AdaptiveCardsHostConfig = require('./adaptivecards-hostconfig.json');



export class MainView extends React.Component<{}, {}> { export class MainView extends React.Component<{}, {}> {
settingsUnsubscribe: any; settingsUnsubscribe: any;
Expand Down Expand Up @@ -241,6 +243,7 @@ export class MainView extends React.Component<{}, {}> {
const srvSettings = new ServerSettings(settings.serverSettings); const srvSettings = new ServerSettings(settings.serverSettings);
const activeBot = srvSettings.getActiveBot(); const activeBot = srvSettings.getActiveBot();
const props: BotChat.ChatProps = { const props: BotChat.ChatProps = {
adaptiveCardsHostConfig: AdaptiveCardsHostConfig,
botConnection: this.directline, botConnection: this.directline,
locale: activeBot.locale || remote.app.getLocale(), locale: activeBot.locale || remote.app.getLocale(),
formatOptions: { formatOptions: {
Expand Down
1 change: 1 addition & 0 deletions src/client/reducers.ts
Expand Up @@ -404,6 +404,7 @@ export class ServerSettingsActions {
ngrokPath: string, ngrokPath: string,
bypassNgrokLocalhost: boolean, bypassNgrokLocalhost: boolean,
use10Tokens: boolean, use10Tokens: boolean,
useCodeValidation: boolean,
stateSizeLimit: number stateSizeLimit: number
}) { }) {
serverChangeSetting('Framework_Set', state); serverChangeSetting('Framework_Set', state);
Expand Down
1 change: 0 additions & 1 deletion src/server/OpenIdMetadata.ts
Expand Up @@ -32,7 +32,6 @@
// //


import * as got from 'got'; import * as got from 'got';
import { emulator } from './emulator';
let getPem = require('rsa-pem-from-mod-exp'); let getPem = require('rsa-pem-from-mod-exp');
let base64url = require('base64url'); let base64url = require('base64url');


Expand Down
8 changes: 8 additions & 0 deletions src/server/botFrameworkService.ts
Expand Up @@ -36,6 +36,8 @@ import { ConversationsController } from './controllers/connector/conversationsCo
import { AttachmentsController } from './controllers/connector/attachmentsController'; import { AttachmentsController } from './controllers/connector/attachmentsController';
import { BotStateController } from './controllers/connector/botStateController'; import { BotStateController } from './controllers/connector/botStateController';
import { ConversationsControllerV3 as DirectLineConversationsController } from './controllers/directLine/conversationsControllerV3'; import { ConversationsControllerV3 as DirectLineConversationsController } from './controllers/directLine/conversationsControllerV3';
import { SessionController } from './controllers/directLine/sessionController';
import { UserTokenController } from './controllers/connector/userTokenController';
import { EmulatorController } from './controllers/emulator/emulatorController'; import { EmulatorController } from './controllers/emulator/emulatorController';
import { RestServer } from './restServer'; import { RestServer } from './restServer';
import { getStore, getSettings, addSettingsListener } from './settings'; import { getStore, getSettings, addSettingsListener } from './settings';
Expand Down Expand Up @@ -66,6 +68,10 @@ export class BotFrameworkService extends RestServer {
} }
} }


public getNgrokServiceUrl() : string {
return this.ngrokServiceUrl;
}

authentication = new BotFrameworkAuthentication(); authentication = new BotFrameworkAuthentication();


constructor() { constructor() {
Expand All @@ -74,6 +80,8 @@ export class BotFrameworkService extends RestServer {
AttachmentsController.registerRoutes(this); AttachmentsController.registerRoutes(this);
BotStateController.registerRoutes(this, this.authentication); BotStateController.registerRoutes(this, this.authentication);
DirectLineConversationsController.registerRoutes(this); DirectLineConversationsController.registerRoutes(this);
SessionController.registerRoutes(this);
UserTokenController.registerRoutes(this, this.authentication);
EmulatorController.registerRoutes(this); EmulatorController.registerRoutes(this);
addSettingsListener((settings: Settings) => { addSettingsListener((settings: Settings) => {
this.configure(settings); this.configure(settings);
Expand Down
41 changes: 26 additions & 15 deletions src/server/controllers/connector/conversationsController.ts
Expand Up @@ -47,7 +47,7 @@ import { BotFrameworkAuthentication } from '../../botFrameworkAuthentication';
import { error } from '../../log'; import { error } from '../../log';
import { jsonBodyParser } from '../../jsonBodyParser'; import { jsonBodyParser } from '../../jsonBodyParser';
import { VersionManager } from '../../versionManager'; import { VersionManager } from '../../versionManager';

import { OAuthLinkEncoder } from '../../../shared/oauthLinkEncoder';


interface IConversationAPIPathParameters { interface IConversationAPIPathParameters {
conversationId: string; conversationId: string;
Expand Down Expand Up @@ -184,20 +184,31 @@ export class ConversationsController {
activity.id = null; activity.id = null;
activity.replyToId = req.params.activityId; activity.replyToId = req.params.activityId;


// look up conversation let continuation = function(): void {
const conversation = emulator.conversations.conversationById(activeBot.botId, parms.conversationId); // look up conversation
if (!conversation) const conversation = emulator.conversations.conversationById(activeBot.botId, parms.conversationId);
throw ResponseTypes.createAPIException(HttpStatus.NOT_FOUND, ErrorCodes.BadArgument, "conversation not found"); if (!conversation)

throw ResponseTypes.createAPIException(HttpStatus.NOT_FOUND, ErrorCodes.BadArgument, "conversation not found");
// if we found the activity to reply to
//if (!conversation.activities.find((existingActivity, index, obj) => existingActivity.id == activity.replyToId)) // if we found the activity to reply to
// throw ResponseTypes.createAPIException(HttpStatus.NOT_FOUND, ErrorCodes.BadArgument, "replyToId is not a known activity id"); //if (!conversation.activities.find((existingActivity, index, obj) => existingActivity.id == activity.replyToId))

// throw ResponseTypes.createAPIException(HttpStatus.NOT_FOUND, ErrorCodes.BadArgument, "replyToId is not a known activity id");
// post activity
let response: IResourceResponse = conversation.postActivityToUser(activity); // post activity
res.send(HttpStatus.OK, response); let response: IResourceResponse = conversation.postActivityToUser(activity);
res.end(); res.send(HttpStatus.OK, response);
log.api(`Reply[${activity.type}]`, req, res, activity, response, getActivityText(activity)); res.end();
log.api(`Reply[${activity.type}]`, req, res, activity, response, getActivityText(activity));
};

let visitor = new OAuthLinkEncoder(emulator.framework.getNgrokServiceUrl(), req.headers['authorization'], activity);
visitor.resolveOAuthCards(activity).then((value?: any) =>
{
continuation();
}, (reason: any) => {
log.api(`Failed resolveOAuthCards`, req, res, activity);
continuation();
});
} catch (err) { } catch (err) {
let error = ResponseTypes.sendErrorResponse(req, res, next, err); let error = ResponseTypes.sendErrorResponse(req, res, next, err);
log.api(`Reply[${activity.type}]`, req, res, activity, error, getActivityText(activity)); log.api(`Reply[${activity.type}]`, req, res, activity, error, getActivityText(activity));
Expand Down

0 comments on commit aa6c640

Please sign in to comment.