Skip to content
This repository has been archived by the owner on Sep 21, 2021. It is now read-only.

added pkce and fixed some bugs with authorization code grant #137

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@
"@types/office-js": "^0.0.51",
"core-js": "^2.5.3",
"lodash-es": "^4.17.5",
"rxjs": "^5.5.6"
"rxjs": "^5.5.6",
"crypto-js": "^3.1.9-1"
},
"devDependencies": {
"@types/jest": "22.1.3",
"@types/lodash": "4.14.104",
"@types/node": "9.4.6",
"@types/webpack": "3.8.8",
"@types/crypto-js": "^3.1.43",
"awesome-typescript-loader": "3.4.1",
"babel-core": "6.26.0",
"babel-jest": "22.4.1",
Expand Down
14 changes: 7 additions & 7 deletions src/authentication/authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,14 @@ export class Authenticator {
}

// Set the authentication state to redirect and begin the auth flow.
let { state, url } = EndpointStorage.getLoginParams(endpoint);
let { state, url, codeVerifier } = EndpointStorage.getLoginParams(endpoint);

// Launch the dialog and perform the OAuth flow. We launch the dialog at the redirect
// url where we expect the call to isAuthDialog to be available.
let redirectUrl = await new Dialog<string>(url, 1024, 768, useMicrosoftTeams).result;

// Try and extract the result and pass it along.
return this._handleTokenResult(redirectUrl, endpoint, state);
return this._handleTokenResult(redirectUrl, endpoint, state, codeVerifier);
}

/**
Expand All @@ -166,7 +166,7 @@ export class Authenticator {
* @param {object} headers Headers to be sent to the tokenUrl.
* @return {Promise<IToken>} Returns a promise of the token or error.
*/
private _exchangeCodeForToken(endpoint: IEndpointConfiguration, data: any, headers?: any): Promise<IToken> {
private _exchangeCodeForToken(endpoint: IEndpointConfiguration, data: any, headers?: any, codeVerifier?: string): Promise<IToken> {
return new Promise((resolve, reject) => {
if (endpoint.tokenUrl == null) {
console.warn('We couldn\'t exchange the received code for an access_token. The value returned is not an access_token. Please set the tokenUrl property or refer to our docs.');
Expand All @@ -177,7 +177,7 @@ export class Authenticator {
xhr.open('POST', endpoint.tokenUrl);

xhr.setRequestHeader('Accept', 'application/json');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

for (let header in headers) {
if (header === 'Accept' || header === 'Content-Type') {
Expand Down Expand Up @@ -213,11 +213,11 @@ export class Authenticator {
}
};

xhr.send(JSON.stringify(data));
xhr.send(EndpointStorage.getTokenExchangeParams(endpoint, data, codeVerifier));
});
}

private _handleTokenResult(redirectUrl: string, endpoint: IEndpointConfiguration, state: number) {
private _handleTokenResult(redirectUrl: string, endpoint: IEndpointConfiguration, state: number, codeVerifier?: string) {
let result = Authenticator.getUrlParams(redirectUrl, endpoint.redirectUrl);
if (result == null) {
throw new AuthError('No access_token or code could be parsed.');
Expand All @@ -226,7 +226,7 @@ export class Authenticator {
throw new AuthError('State couldn\'t be verified');
}
else if ('code' in result) {
return this._exchangeCodeForToken(endpoint, result as ICode);
return this._exchangeCodeForToken(endpoint, result as ICode, [], codeVerifier);
}
else if ('access_token' in result) {
return this.tokens.add(endpoint.provider, result as IToken);
Expand Down
47 changes: 45 additions & 2 deletions src/authentication/endpoint.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ export interface IEndpointConfiguration {
// OAuth responseType.
responseType?: string;

// Enable PKCE if responseType is code
pkce?: boolean;

// PKCE Code Challenge, defaults to S256
pkceMethod?: string;

// Additional object for query parameters.
// Will be appending them after encoding the values.
extraQueryParameters?: { [index: string]: string };
Expand Down Expand Up @@ -206,12 +212,14 @@ export class EndpointStorage extends Storage<IEndpointConfiguration> {
*/
static getLoginParams(endpointConfig: IEndpointConfiguration): {
url: string,
state: number
state: number,
codeVerifier?: string
} {
let scope = (endpointConfig.scope) ? encodeURIComponent(endpointConfig.scope) : null;
let resource = (endpointConfig.resource) ? encodeURIComponent(endpointConfig.resource) : null;
let state = endpointConfig.state && Utilities.generateCryptoSafeRandom();
let nonce = endpointConfig.nonce && Utilities.generateCryptoSafeRandom();
let codeVerifier = endpointConfig.pkce ? this._generateRandomString(43) : null;

let urlSegments = [
`response_type=${endpointConfig.responseType}`,
Expand All @@ -231,6 +239,16 @@ export class EndpointStorage extends Storage<IEndpointConfiguration> {
if (nonce) {
urlSegments.push(`nonce=${nonce}`);
}
if (codeVerifier) {
if (endpointConfig.pkceMethod === 'plain') {
urlSegments.push(`code_challenge=${codeVerifier}`);
urlSegments.push(`code_challenge_method=plain`);
}
else {
urlSegments.push(`code_challenge=${Utilities.codeChallenge(codeVerifier)}`);
urlSegments.push(`code_challenge_method=S256`);
}
}
if (endpointConfig.extraQueryParameters) {
for (let param of Object.keys(endpointConfig.extraQueryParameters)) {
urlSegments.push(`${param}=${encodeURIComponent(endpointConfig.extraQueryParameters[param])}`);
Expand All @@ -239,7 +257,32 @@ export class EndpointStorage extends Storage<IEndpointConfiguration> {

return {
url: `${endpointConfig.baseUrl}${endpointConfig.authorizeUrl}?${urlSegments.join('&')}`,
state: state
state: state,
codeVerifier: codeVerifier
};
}

static getTokenExchangeParams(endpointConfig: IEndpointConfiguration, data: any, codeVerifier?: string): string {
let segments = [
`grant_type=authorization_code`,
`code=${data.code}`,
`redirect_uri=${endpointConfig.redirectUrl}`,
`client_id=${endpointConfig.clientId}`
];

if (codeVerifier) {
segments.push(`code_verifier=${codeVerifier}`);
}

return segments.join('&');
}

private static _generateRandomString(length) {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < length; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
}
9 changes: 9 additions & 0 deletions src/helpers/utilities.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. */
import { CustomError } from '../errors/custom.error';
import * as CryptoJS from 'crypto-js';

interface IContext {
host: string;
Expand Down Expand Up @@ -191,6 +192,14 @@ export class Utilities {
return /Edge\/|Trident\//gi.test(window.navigator.userAgent);
}

static base64URL(string) {
return CryptoJS.enc.Base64.stringify(string).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}

static codeChallenge(codeVerifier) {
return this.base64URL(CryptoJS.SHA256(codeVerifier));
}

/**
* Utility to generate crypto safe random numbers
*/
Expand Down
Loading