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

Commit 3e34857

Browse files
committed
added pkce and fixed some bugs with authorization code grant
office-js had a major flaw for authorization code grant types, it didn't send the correct payload for a token exchange currently this fixes it by sending a correct application/x-www-form-urlencoded request and also made it possible to configure PKCE by utilizing CryptoJS
1 parent 9bc0682 commit 3e34857

File tree

5 files changed

+833
-12
lines changed

5 files changed

+833
-12
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@
3131
"@types/office-js": "^0.0.51",
3232
"core-js": "^2.5.3",
3333
"lodash-es": "^4.17.5",
34-
"rxjs": "^5.5.6"
34+
"rxjs": "^5.5.6",
35+
"crypto-js": "^3.1.9-1"
3536
},
3637
"devDependencies": {
3738
"@types/jest": "22.1.3",
3839
"@types/lodash": "4.14.104",
3940
"@types/node": "9.4.6",
4041
"@types/webpack": "3.8.8",
42+
"@types/crypto-js": "^3.1.43",
4143
"awesome-typescript-loader": "3.4.1",
4244
"babel-core": "6.26.0",
4345
"babel-jest": "22.4.1",

src/authentication/authenticator.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -144,14 +144,14 @@ export class Authenticator {
144144
}
145145

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

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

153153
// Try and extract the result and pass it along.
154-
return this._handleTokenResult(redirectUrl, endpoint, state);
154+
return this._handleTokenResult(redirectUrl, endpoint, state, codeVerifier);
155155
}
156156

157157
/**
@@ -166,7 +166,7 @@ export class Authenticator {
166166
* @param {object} headers Headers to be sent to the tokenUrl.
167167
* @return {Promise<IToken>} Returns a promise of the token or error.
168168
*/
169-
private _exchangeCodeForToken(endpoint: IEndpointConfiguration, data: any, headers?: any): Promise<IToken> {
169+
private _exchangeCodeForToken(endpoint: IEndpointConfiguration, data: any, headers?: any, codeVerifier?: string): Promise<IToken> {
170170
return new Promise((resolve, reject) => {
171171
if (endpoint.tokenUrl == null) {
172172
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.');
@@ -177,7 +177,7 @@ export class Authenticator {
177177
xhr.open('POST', endpoint.tokenUrl);
178178

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

182182
for (let header in headers) {
183183
if (header === 'Accept' || header === 'Content-Type') {
@@ -213,11 +213,11 @@ export class Authenticator {
213213
}
214214
};
215215

216-
xhr.send(JSON.stringify(data));
216+
xhr.send(EndpointStorage.getTokenExchangeParams(endpoint, data, codeVerifier));
217217
});
218218
}
219219

220-
private _handleTokenResult(redirectUrl: string, endpoint: IEndpointConfiguration, state: number) {
220+
private _handleTokenResult(redirectUrl: string, endpoint: IEndpointConfiguration, state: number, codeVerifier?: string) {
221221
let result = Authenticator.getUrlParams(redirectUrl, endpoint.redirectUrl);
222222
if (result == null) {
223223
throw new AuthError('No access_token or code could be parsed.');
@@ -226,7 +226,7 @@ export class Authenticator {
226226
throw new AuthError('State couldn\'t be verified');
227227
}
228228
else if ('code' in result) {
229-
return this._exchangeCodeForToken(endpoint, result as ICode);
229+
return this._exchangeCodeForToken(endpoint, result as ICode, [], codeVerifier);
230230
}
231231
else if ('access_token' in result) {
232232
return this.tokens.add(endpoint.provider, result as IToken);

src/authentication/endpoint.manager.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ export interface IEndpointConfiguration {
4747
// OAuth responseType.
4848
responseType?: string;
4949

50+
// Enable PKCE if responseType is code
51+
pkce?: boolean;
52+
53+
// PKCE Code Challenge, defaults to S256
54+
pkceMethod?: string;
55+
5056
// Additional object for query parameters.
5157
// Will be appending them after encoding the values.
5258
extraQueryParameters?: { [index: string]: string };
@@ -206,12 +212,14 @@ export class EndpointStorage extends Storage<IEndpointConfiguration> {
206212
*/
207213
static getLoginParams(endpointConfig: IEndpointConfiguration): {
208214
url: string,
209-
state: number
215+
state: number,
216+
codeVerifier?: string
210217
} {
211218
let scope = (endpointConfig.scope) ? encodeURIComponent(endpointConfig.scope) : null;
212219
let resource = (endpointConfig.resource) ? encodeURIComponent(endpointConfig.resource) : null;
213220
let state = endpointConfig.state && Utilities.generateCryptoSafeRandom();
214221
let nonce = endpointConfig.nonce && Utilities.generateCryptoSafeRandom();
222+
let codeVerifier = endpointConfig.pkce ? this._generateRandomString(43) : null;
215223

216224
let urlSegments = [
217225
`response_type=${endpointConfig.responseType}`,
@@ -231,6 +239,16 @@ export class EndpointStorage extends Storage<IEndpointConfiguration> {
231239
if (nonce) {
232240
urlSegments.push(`nonce=${nonce}`);
233241
}
242+
if (codeVerifier) {
243+
if (endpointConfig.pkceMethod === 'plain') {
244+
urlSegments.push(`code_challenge=${codeVerifier}`);
245+
urlSegments.push(`code_challenge_method=plain`);
246+
}
247+
else {
248+
urlSegments.push(`code_challenge=${Utilities.codeChallenge(codeVerifier)}`);
249+
urlSegments.push(`code_challenge_method=S256`);
250+
}
251+
}
234252
if (endpointConfig.extraQueryParameters) {
235253
for (let param of Object.keys(endpointConfig.extraQueryParameters)) {
236254
urlSegments.push(`${param}=${encodeURIComponent(endpointConfig.extraQueryParameters[param])}`);
@@ -239,7 +257,32 @@ export class EndpointStorage extends Storage<IEndpointConfiguration> {
239257

240258
return {
241259
url: `${endpointConfig.baseUrl}${endpointConfig.authorizeUrl}?${urlSegments.join('&')}`,
242-
state: state
260+
state: state,
261+
codeVerifier: codeVerifier
243262
};
244263
}
264+
265+
static getTokenExchangeParams(endpointConfig: IEndpointConfiguration, data: any, codeVerifier?: string): string {
266+
let segments = [
267+
`grant_type=authorization_code`,
268+
`code=${data.code}`,
269+
`redirect_uri=${endpointConfig.redirectUrl}`,
270+
`client_id=${endpointConfig.clientId}`
271+
];
272+
273+
if (codeVerifier) {
274+
segments.push(`code_verifier=${codeVerifier}`);
275+
}
276+
277+
return segments.join('&');
278+
}
279+
280+
private static _generateRandomString(length) {
281+
let text = '';
282+
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
283+
for (let i = 0; i < length; i++) {
284+
text += possible.charAt(Math.floor(Math.random() * possible.length));
285+
}
286+
return text;
287+
}
245288
}

src/helpers/utilities.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. */
22
import { CustomError } from '../errors/custom.error';
3+
import * as CryptoJS from 'crypto-js';
34

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

195+
static base64URL(string) {
196+
return CryptoJS.enc.Base64.stringify(string).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
197+
}
198+
199+
static codeChallenge(codeVerifier) {
200+
return this.base64URL(CryptoJS.SHA256(codeVerifier));
201+
}
202+
194203
/**
195204
* Utility to generate crypto safe random numbers
196205
*/

0 commit comments

Comments
 (0)