Skip to content

Commit

Permalink
Using PKCE for Azure AD
Browse files Browse the repository at this point in the history
  • Loading branch information
compulim committed Jun 16, 2019
1 parent 2a387b0 commit d4c5086
Show file tree
Hide file tree
Showing 8 changed files with 58 additions and 18 deletions.
17 changes: 4 additions & 13 deletions samples/19.a.single-sign-on-for-enterprise-apps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Although OAuth and [OpenID](https://openid.net/) are often related to each other

Instead of OpenID, most enterprise apps use OAuth plus a graph API to identify an individual user. In this demo, we will demonstrate how to use OAuth to obtain access to graph API and use the API to identifying the accessor.

This demo does not include any threat models and is designed for educational purpose only. When you design a production system, threat-modelling is an important task to make sure your system is secure and provide a way to quickly identify potential source of data breaches. IETF [RFC 6819](https://tools.ietf.org/html/rfc6819) is a good starting point for threat-modelling when using OAuth 2.0.
This demo does not include any threat models and is designed for educational purpose only. When you design a production system, threat-modelling is an important task to make sure your system is secure and provide a way to quickly identify potential source of data breaches. IETF [RFC 6819](https://tools.ietf.org/html/rfc6819) and [OAuth 2.0 for Browser-Based Apps](https://tools.ietf.org/html/draft-ietf-oauth-browser-based-apps-00#section-9.4) is a good starting point for threat-modelling when using OAuth 2.0.

# Test out the hosted sample

Expand Down Expand Up @@ -71,7 +71,8 @@ If you want to authenticate on Azure Active Directory, follow the steps below.
1. Click "New registration"
1. Give it a name
1. In "Redirect URI (optional)" section, add a new entry
1. Select "web" as type
1. Select "Public client (mobile & desktop)" as type
- Instead of client secret, we are using PKCE ([RFC 7636](https://tools.ietf.org/html/rfc7636)) to exchange for authorization token, thus, we need to set it to ["Public client" instead of "Web"](https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-protocols-oauth-code#use-the-authorization-code-to-request-an-access-token)
1. Enter `http://localhost:3000/api/aad/oauth/callback` as the redirect URI
- This must match `AAD_OAUTH_REDIRECT_URI` in `/rest-api/.env` we saved earlier
- Click "Register"
Expand All @@ -86,15 +87,6 @@ If you want to authenticate on Azure Active Directory, follow the steps below.
1. Select "Overview"
1. On the main pane, copy the content of "Application (client) ID" to `/rest-api/.env`
- `AAD_OAUTH_CLIENT_ID=12345678abcd-1234-5678-abcd-12345678abcd`
- Save the client secret
1. Select "Certificates & secrets"
1. In the "Client secrets" section, create a "New client secret"
1. Save the client secret to `/rest-api/.env` file
- `AAD_OAUTH_CLIENT_SECRET=<your app client secret>`
- Enable "Implicit Grant" flow
1. Select "Authentication"
1. In the "Advanced Settings" section, check "Access tokens"
1. Click "Save" button to save the changes

## Setup Azure Bot Services

Expand Down Expand Up @@ -196,7 +188,7 @@ This demo is coded in heterogeneous environment. Web Chat code is written in pur

## OAuth access token vs. refresh token

To make this demo simpler to understand, we are using the access token, a.k.a. "Implicit Grant" flow, in which the access token is considered as secure inside the browser.
To make this demo simpler to understand, we are using the access token via Authorization Code flow, in which the access token is considered as secure inside the browser.

In your production scenario, you may want to use the refresh token, a.k.a. "Authorization Code Grant" flow, instead of the access token. We did not use the Authorization Code Grant flow in this sample: it would increase the complexity of this demo because it requires server-to-server communications and secured persistent storage.

Expand All @@ -221,7 +213,6 @@ MICROSOFT_APP_PASSWORD=a1b2c3d4e5f6
AAD_OAUTH_ACCESS_TOKEN_URL=https://login.microsoftonline.com/12345678-1234-5678-abcd-12345678abcd/oauth2/v2.0/token
AAD_OAUTH_AUTHORIZE_URL=https://login.microsoftonline.com/12345678-1234-5678-abcd-12345678abcd/oauth2/v2.0/authorize
AAD_OAUTH_CLIENT_ID=12345678abcd-1234-5678-abcd-12345678abcd
AAD_OAUTH_CLIENT_SECRET=<your app client secret>
AAD_OAUTH_REDIRECT_URI=http://localhost:3000/api/aad/oauth/callback
DIRECT_LINE_SECRET=a1b2c3.d4e5f6g7h8i9j0
GITHUB_OAUTH_CLIENT_ID=a1b2c3d
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// For information about Base 64 URL encoding, please refer to https://en.wikipedia.org/wiki/Base64#URL_applications and https://tools.ietf.org/html/rfc4648#section-5
module.exports = buffer => buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=$/, '');
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
const encodeBase64URL = require('./encodeBase64URL');
const fetch = require('node-fetch');

// Exchanges an access token from OAuth provider.
module.exports = async function exchangeAccessToken(tokenURL, clientID, clientSecret, code, redirectURI, state) {
module.exports = async function exchangeAccessToken(tokenURL, clientID, clientSecret, code, redirectURI, state, codeVerifier) {
const params = new URLSearchParams({
client_id: clientID,
client_secret: clientSecret,
...clientSecret ? { client_secret: clientSecret } : {},
code,
code_verifier: codeVerifier,
grant_type: 'authorization_code',
redirect_uri: redirectURI,
state
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const random = require('math-random');

// Default environment variables
process.env = {
AAD_OAUTH_PKCE_SALT: random.toString(36).substr(2),
AAD_OAUTH_SCOPE: 'User.Read',
GITHUB_OAUTH_ACCESS_TOKEN_URL: 'https://github.com/login/oauth/access_token',
GITHUB_OAUTH_AUTHORIZE_URL: 'https://github.com/login/oauth/authorize',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
const { randomBytes } = require('crypto');
const createPKCECodeChallenge = require('./createPKCECodeChallenge');

const {
AAD_OAUTH_AUTHORIZE_URL,
AAD_OAUTH_CLIENT_ID,
Expand All @@ -8,11 +11,19 @@ const {
// GET /api/aad/oauth/authorize
// Redirects to https://login.microsoftonline.com/12345678-1234-5678-abcd-12345678abcd/oauth2/v2.0/authorize
module.exports = (_, res) => {
const seed = randomBytes(32);
const challenge = createPKCECodeChallenge(seed);
const params = new URLSearchParams({
client_id: AAD_OAUTH_CLIENT_ID,
code_challenge: challenge,
code_challenge_method: 'S256',
redirect_uri: AAD_OAUTH_REDIRECT_URI,
response_type: 'code',
scope: AAD_OAUTH_SCOPE
scope: AAD_OAUTH_SCOPE,

// https://tools.ietf.org/html/draft-ietf-oauth-browser-based-apps-00#section-9.4
// "using the "state" parameter to link client requests and responses to prevent CSRF
state: seed.toString('base64')
});

res.setHeader('location', `${ AAD_OAUTH_AUTHORIZE_URL }?${ params }`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const createHTMLWithPostMessage = require('../../../utils/createHTMLWithPostMessage');
const createPKCECodeVerifier = require('./createPKCECodeVerifier');
const exchangeAccessToken = require('../../../exchangeAccessToken');

const {
Expand All @@ -21,14 +22,18 @@ module.exports = async (req, res) => {
throw new Error(`OAuth: Failed to start authorization flow due to "${ req.query.error }"`);
}

const { code, seed } = req.query;
const { code, state } = req.query;
const seed = Buffer.from(state, 'base64');
const codeVerifier = createPKCECodeVerifier(seed);

const accessToken = await exchangeAccessToken(
AAD_OAUTH_ACCESS_TOKEN_URL,
AAD_OAUTH_CLIENT_ID,
AAD_OAUTH_CLIENT_SECRET,
code,
AAD_OAUTH_REDIRECT_URI,
seed
undefined,
codeVerifier
);

data = { access_token: accessToken };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { createHash } = require('crypto');
const encodeBase64URL = require('../../../encodeBase64URL');

const createPKCECodeVerifier = require('./createPKCECodeVerifier');

// Create a PKCE code challenge.
module.exports = seed => {
const verifier = createPKCECodeVerifier(seed);
const hash = createHash('sha256');

hash.update(verifier);

return encodeBase64URL(hash.digest());
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { createHash } = require('crypto');

const { AAD_OAUTH_PKCE_SALT } = process.env;

// Creating a PKCE code verifier based on salt and seed.
// We are using seed and salt to simplify the creation of PKCE code in a distributed manner.
module.exports = seed => {
const hash = createHash('sha512');

hash.update(seed);
hash.update(AAD_OAUTH_PKCE_SALT);

return hash.digest().toString('base64');
};

0 comments on commit d4c5086

Please sign in to comment.