1.3.0 • Published 6 years ago

botbuilder-simple-authentication v1.3.0

Weekly downloads
5
License
ISC
Repository
github
Last release
6 years ago

botbuilder-simple-authentication

botbuilder-simple-authentication exposes middleware designed to make authentication implementation quick and easy for Bot Framework Adapters.

Table of Contents

  1. Basic Usage
  2. Samples
  3. Configuration Properties
  4. Usage with Express
  5. Using Environment Variables
  6. Custom Scopes
  7. Custom Button Text
  8. Custom Authentication Card
  9. Custom Magic Code HTML
  10. Custom Azure AD Tenant and Resource

Basic Usage

BotAuthenticationMiddleware assumes control of the conversation flow when a user is not authenticated and provides the user's access token and profile after successful login.

The middleware minimally requires 3 configuration properties: 1. A method that returns whether the user is authenticated or not. 1. A method that is triggered after the user has logged in successfully that receives the user's access token and profile. 1. At least one clientId/clientSecret for an application created with a supported provider.

Installation

npm install botbuilder-simple-authentication

Import the botbuilder-simple-authentication module

let simpleAuth = require('botbuilder-simple-authentication');

Create a BotAuthenticationConfiguration

The following example uses in memory storage, but any storage service can be used to store and manage authentication data. BotAuthenticationConfiguration methods can be synchronous or asynchronous.

let storage = new builder.MemoryStorage();
const conversationState = new builder.ConversationState(storage);
adapter.use(conversationState);

const authenticationConfig = {
	isUserAuthenticated: (context) => {
		//if this method returns false, the middleware will take over
		const state = conversationState.get(context);
		return state.authData;
	},
	onLoginSuccess: async (context, accessToken, profile, provider) => {
		//the middleware passes over the access token and profile retrieved for the user
		const state = conversationState.get(context);
		state.authData = { accessToken, profile, provider };
		await context.sendActivity(`Hi there ${profile.displayName}!`);
	},
	facebook: {
		clientId: 'FACEBOOK_CLIENT_ID',
		clientSecret: 'FACEBOOK_CLIENT_SECRET'
	},
	azureADv1: {
		clientId: 'AZURE_AD_V1_CLIENT_ID',
		clientSecret: 'AZURE_AD_V1_CLIENT_SECRET'
	},
	azureADv2: {
		clientId: 'AZURE_AD_V2_CLIENT_ID',
		clientSecret: 'AZURE_AD_V2_CLIENT_SECRET'
	},
	google: {
		clientId: 'GOOGLE_CLIENT_ID',
		clientSecret: 'GOOGLE_CLIENT_SECRET'
	},
	twitter: {
		consumerKey: 'TWITTER_CONSUMER_KEY',
		consumerSecret: 'TWITTER_CONSUMER_SECRET'
	},
	github: {
		clientId: 'GITHUB_CLIENT_ID',
		clientSecret: 'GITHUB_CLIENT_SECRET'
	}
};

Implement the BotAuthenticationMiddleware

Create a new instance of the middleware, passing in the BotAuthenticationConfiguration along with your server (compatible with Restify or Express).

adapter.use(new simpleAuth.BotAuthenticationMiddleware(server, authenticationConfig));

Create an Application with a Supported Provider

Navigate to a supported provider's developer site listed below and create a new application. Add the appropriate Redirect URL to your app's approved redirect urls, then copy the clientId and clientSecret used to create the BotAuthenticationConfiguration.

Supported ProvidersRedirect URLDeveloper Site
Facebook{BASE_URL}/auth/facebook/callbackhttps://developers.facebook.com/apps
AzureADv1{BASE_URL}/auth/azureAD/callbackhttps://apps.dev.microsoft.com
AzureADv2{BASE_URL}/auth/azureAD/callbackhttps://apps.dev.microsoft.com
Google{BASE_URL}/auth/google/callbackhttps://console.cloud.google.com/home
Twitter{BASE_URL}/auth/twitter/callbackhttps://apps.twitter.com
GitHub{BASE_URL}/auth/github/callbackhttps://github.com/settings/developers

Samples

The samples folder contains basic examples with minimal configuration and advanced examples that implement each of the optional configuration properties, for both JavaScript and TypeScript.

Configuration Properties

BotAuthenticationConfiguration

PropertyConstraintTypeDescription
isUserAuthenticatedRequired(context: TurnContext) => booleanRuns each converation turn. The middleware will prevent the bot logic from running when it returns false.
onLoginSuccessRequired(context: TurnContext, accessToken: string, profile: any, provider: string) => voidRuns when the user inputs the correct magic code. The middleware passes the user's access token and profile.
onLoginFailureOptional(context: TurnContext, provider: string) => voidRuns when the user inputs an incorrect magic code. The middleware will force another login attempt by default.
customAuthenticationCardGeneratorOptional(context: TurnContext, authorizationUris: {}[]) => Partial< Activity >Overrides the default Authentication Card. The middleware supplies the authorization uris necessary to build the card.
customMagicCodeRedirectEndpointOptionalstringOverrides the default magic code display page. The server endpoint provided will receive a redirect with the magic code in the query string.
noUserFoundMessageOptionalstringMessage sent on first conversation turn where the user is not authenticated, immediately prior to the Authentication Card.
facebookOptionalDefaultProviderConfigurationConfiguration object that enables Facebook authentication.
azureADv1OptionalAzureADConfigurationConfiguration object that enables AzureADv1 authentication.
azureADv2OptionalAzureADConfigurationConfiguration object that enables AzureADv2 authentication.
googleOptionalDefaultProviderConfigurationConfiguration object that enables Google authentication.
twitterOptionalTwitterConfigurationConfiguration object that enables Twitter authentication.
githubOptionalDefaultProviderConfigurationConfiguration object that enables GitHub authentication.

DefaultProviderConfiguration

PropertyConstraintTypeDescription
clientIdRequiredstringClient Id taken from the provider's authentication application.
clientSecretRequiredstringClient Secret taken from the provider's authentication application.
scopesOptionalstring[]Scopes that the user will be asked to consent to as part of the authentication flow.
buttonTextOptionalstringText displayed inside the button that triggers the provider's authentication flow.

AzureADConfiguration

PropertyConstraintTypeDescription
clientIdRequiredstringApplication Id taken from the Microsoft Application Registration Portal.
clientSecretRequiredstringApplication Secret taken from the Microsoft Application Registration Portal.
scopesOptionalstring[]Scopes that the user will be asked to consent to as part of the authentication flow.
buttonTextOptionalstringText displayed inside the button that triggers the provider's authentication flow.
tenantOptionalstringOrganizational tenant domain.
resourceOptionalstringIdentifier of the WebAPI that your client wants to access on behalf of the user

TwitterConfiguration

PropertyConstraintTypeDescription
consumerKeyRequiredstringConsumer Key taken from the Twitter Application Management page.
consumerSecretRequiredstringConsumer Secret taken from the Twitter Application Management page.
buttonTextOptionalstringText displayed inside the button that triggers the provider's authentication flow.

Usage With Express

IMPORTANT - For use with Express, the middleware must be instantiated before the adapter.processActivity() statement.

Express Application

Create an Express Application

let express = require('express');
let app = express();

Implement the BotAuthenticationMiddleware

adapter.use(new simpleAuth.BotAuthenticationMiddleware(app, authenticationConfig));

Express Router

Create an Express Application

let express = require('express');
let app = express();

Create a Router

let router = express.Router();
app.use('/', router);

Implement the BotAuthenticationMiddleware

adapter.use(new simpleAuth.BotAuthenticationMiddleware(router, authenticationConfig));

Using Environment Variables

Provider clientIds and clientSecrets can be set via environment variables and do not have to be set in provider configuration objects.

BotAuthenticationConfiguration PropertyEnvironment Variable
facebook.clientIdFACEBOOK_CLIENT_ID
facebook.clientSecretFACEBOOK_CLIENT_SECRET
azureADv1.clientIdAZURE_AD_V1_CLIENT_ID
azureADv1.clientSecretAZURE_AD_V1_CLIENT_SECRET
azureADv2.clientIdAZURE_AD_V2_CLIENT_ID
azureADv2.clientSecretAZURE_AD_V2_CLIENT_SECRET
google.clientIdGOOGLE_CLIENT_ID
google.clientSecretGOOGLE_CLIENT_SECRET
twitter.consumerKeyTWITTER_CONSUMER_KEY
twitter.consumerSecretTWITTER_CONSUMER_SECRET
github.clientIdGITHUB_CLIENT_ID
github.clientSecretGITHUB_CLIENT_SECRET

Example .env

FACEBOOK_CLIENT_ID = {VALUE}
FACEBOOK_CLIENT_SECRET = {VALUE}
AZURE_AD_V2_CLIENT_ID = {VALUE}
AZURE_AD_V2_CLIENT_SECRET = {VALUE}

Example BotAuthenticationConfiguration

let storage = new builder.MemoryStorage();
const conversationState = new builder.ConversationState(storage);
adapter.use(conversationState);

const authenticationConfig = {
	isUserAuthenticated: (context) => {
		//if this method returns false, the middleware will take over
		const state = conversationState.get(context);
		return state.authData;
	},
	onLoginSuccess: async (context, accessToken, profile, provider) => {
		//the middleware passes over the access token and profile retrieved for the user
		const state = conversationState.get(context);
		state.authData = { accessToken, profile, provider };
		await context.sendActivity(`Hi there ${profile.displayName}!`);
	},
};

Custom Scopes

Each provider declared in the BotAuthenticationConfiguration object except for Twitter has an optional scope property that accepts an array of strings (Twitter scopes are set in the Twitter Application Management page). If custom scopes aren't provided, the following scopes are used by default:

ProviderScopes
Facebookpublic_profile
AzureADv1User.Read
AzureADv2profile
Googlehttps://www.googleapis.com/auth/plus.login
GitHubuser

Default Scopes

facebook: {
	clientId: 'FACEBOOK_CLIENT_ID',
	clientSecret: 'FACEBOOK_CLIENT_SECRET'
}

Example Custom Scopes

facebook: {
	clientId: 'FACEBOOK_CLIENT_ID',
	clientSecret: 'FACEBOOK_CLIENT_SECRET'
	scopes: ['public_profile', 'email', 'user_likes']
}

Custom Button Text

Each provider declared in the BotAuthenticationConfiguration object has an optional buttonText property that accepts a string. If custom button text isn't provided, the following strings are used by default:

ProviderButton Text
FacebookLog in with Facebook
AzureADv1Log in with Microsoft
AzureADv2Log in with Microsoft
GoogleLog in with Google+
TwitterLog in with Twitter
GitHubLog in with GitHub

Default Button Text

facebook: {
	clientId: 'FACEBOOK_CLIENT_ID',
	clientSecret: 'FACEBOOK_CLIENT_SECRET'
}

Example Custom Button Text

facebook: {
	clientId: 'FACEBOOK_CLIENT_ID',
	clientSecret: 'FACEBOOK_CLIENT_SECRET'
	buttonText: 'Facebook'
}

Custom Authentication Card

The customAuthenticationCardGenerator property is used to override the default card. The method receives the authorization uris for each provider set in the BotAuthenticationConfiguration and is responsible for navigating the user to one of them.

Default Authentication Card

Example Custom Authentication Card

customAuthenticationCardGenerator: async (context, authorizationUris) => {
	let cardActions = [];
	let buttonTitle;
	authorizationUris.map((auth) => {
		if (auth.provider === 'azureADv1' || auth.provider === 'azureADv2') {
			buttonTitle = 'Microsoft';
		} else if (auth.provider === 'facebook') {
			buttonTitle = 'Facebook';
		} else if (auth.provider === 'google') {
			buttonTitle = 'Google';
		} else if (auth.provider === 'twitter') {
			buttonTitle = 'Twitter';
		} else if (auth.provider === 'github') {
			buttonTitle = 'GitHub';
		}
		cardActions.push({ type: 'openUrl', value: auth.authorizationUri, title: buttonTitle });
	});
	let card = builder.CardFactory.heroCard('', ['https://qualiscare.com/wp-content/uploads/2017/08/default-user.png'], cardActions);
	let authMessage = builder.MessageFactory.attachment(card);
	return authMessage;
}

Custom Magic Code HTML

The BotAuthenticationConfiguration object has an optional customMagicCodeRedirectEndpoint property used to override the default magic code HTML display and create a custom page.

In order to fully implement a custom page, the server passed to the middleware will need to expose an endpoint that is referenced by the customMagicCodeRedirectEndpoint property. BotAuthenticationMiddleware adds the queryParser middleware to the restify server and redirects to this endpoint with the magic code in the query string, so the code is accessible via req.query.magicCode. The server is responsible for serving an HTML page capable of displaying the magic code.

Default Magic Code HTML

Example Custom Magic Code HTML

In the example below, restify exposes an endpoint that serves up an html file expecting a magic code in the URL's hash. The customMagicCodeRedirectEndpoint property is set to another endpoint that parses the magic code and sends it in the hash to the html file.

/app.js

let storage = new builder.MemoryStorage();
const conversationState = new builder.ConversationState(storage);
adapter.use(conversationState);

const authenticationConfig = {
	isUserAuthenticated: (context) => {
		//if this method returns false, the middleware will take over
		const state = conversationState.get(context);
		return state.authData;
	},
	onLoginSuccess: async (context, accessToken, profile, provider) => {
		//the middleware passes over the access token and profile retrieved for the user
		const state = conversationState.get(context);
		state.authData = { accessToken, profile, provider };
		await context.sendActivity(`Hi there ${profile.displayName}!`);
	},
	facebook: {
		clientId: 'FACEBOOK_CLIENT_ID',
		clientSecret: 'FACEBOOK_CLIENT_SECRET'
	},
	customMagicCodeRedirectEndpoint: '/customCode'
};

server.get('/customCode', (req, res, next) => {
	//simple redirect where we set the code in the hash and pull it down on the webpage that restify will serve at this endpoint
	let magicCode = req.query.magicCode;
	let hashedUrl = `/renderCustomCode#${magicCode}`;
	res.redirect(302, hashedUrl, next);
});

server.get('/renderCustomCode', restify.plugins.serveStatic({
	//need a public folder in the same directory as this file that contains an index.html page expecting a hash
	'directory': path.join(__dirname, 'public'),
	'file': 'index.html'
}));

/public/index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Authentication Bot - Login Success</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
        crossorigin="anonymous">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp"
        crossorigin="anonymous">
    <style>
        body {
            -moz-user-select: none;
            -webkit-user-select: none;
            -ms-user-select: none;
            user-select: none;
        }
        .navbar {
            border-bottom: 3px solid #FFDB00;
            font-variant: small-caps
        }
        body {
            background-color: #EEEEEE;
        }
        .jumbotron {
            border: 3px solid #FFDB00;
            background-color: #333333;
            color: white;
        }
        h3 {
            font-variant: small-caps;
        }
        logo {
            margin-bottom: 1em;
        }
        #magic_code {
            font-size: 2em;
            font-family: monospace;
            font-weight: bold;
            -moz-user-select: all;
            -webkit-user-select: text;
            -ms-user-select: text;
            user-select: all;
        }
        .jumbotron {
            text-align: center;
        }
        .title {
            color: white !important;
        }
    </style>
</head>
<body>
    <nav class="navbar navbar-inverse navbar-static-top">
        <div class="container">
            <div class="navbar-header">
                <a class="navbar-brand logo">
                    <kbd><img src="http://icons.iconarchive.com/icons/paomedia/small-n-flat/256/key-icon.png" width="27" height="25" alt="">
                </a>
                <a class="navbar-brand title">Authentication Bot</a>
            </div>
        </div>
    </nav>
    <div class="container">
        <div class="jumbotron">
            <h3>Please type the magic code below into your conversation with the bot</h3>
        </div>
        <div class="jumbotron">
            <div id="magic_code"></div>
        </div>
    </div>
    <script type="text/javascript">
        document.getElementById("magic_code").innerText = (window.location.hash || '').replace('#', '');
    </script>
</body>
</html>

Custom Azure AD Tenant and Resource

The AzureADv1 and AzureADv2 providers declared in the BotAuthenticationConfiguration object have an optional tenant property that accepts a string. If a custom tenant isn't provided, the common endpoint is used by default:

Default Tenant

azureADv2: {
	clientId: 'AZURE_AD_V2_CLIENT_ID',
	clientSecret: 'AZURE_AD_V2_CLIENT_SECRET'
}

Example Custom Tenant (V1 or V2)

azureADv2: {
	clientId: 'AZURE_AD_V2_CLIENT_ID',
	clientSecret: 'AZURE_AD_V2_CLIENT_SECRET',
	tenant: 'microsoft.onmicrosoft.com'
}

The AzureADv1 provider declared in the BotAuthenticationConfiguration object has an optional resource property that accepts a string. If a custom resource isn't provided, the Microsoft Graph is used by default:

Default Resource

azureADv1: {
	clientId: 'AZURE_AD_V1_CLIENT_ID',
	clientSecret: 'AZURE_AD_V1_CLIENT_SECRET'
}

Example Custom Resource (V1 Only)

azureADv1: {
	clientId: 'AZURE_AD_V1_CLIENT_ID',
	clientSecret: 'AZURE_AD_V1_CLIENT_SECRET',
	//VSTS API resource
	resource: '499b84ac-1321-427f-aa17-267ca6975798'
}