Created
May 11, 2018 10:01
-
-
Save Qwerios/6d94b4b2f821981dbd57a459cc32db16 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createHash, randomBytes } from 'crypto'; | |
import * as request from 'request'; | |
/** | |
* The configuration object | |
* | |
* @export | |
* @interface IAuthServiceConfig | |
*/ | |
export interface IAuthServiceConfig { | |
authorizeEndpoint: string; | |
clientId: string; | |
audience: string; | |
scope: string; | |
redirectUri: string; | |
tokenEndpoint: string; | |
} | |
/** | |
* Challenge pair generation payload | |
* | |
* @export | |
* @interface IChallengePair | |
*/ | |
export interface IChallengePair { | |
verifier: string; | |
challenge: string; | |
} | |
/** | |
* Authentication response payload | |
* | |
* @export | |
* @interface IAuthResponse | |
*/ | |
export interface IAuthResponse { | |
response: request.Response; | |
body: any; | |
} | |
// This service implements an Authorization Code Grant Flow with PKCE | |
// | |
// Based on: https://gist.github.com/adeperio/73ce6680d4b80b45e624ab62bacfbdca | |
// | |
// Auth0 docs: https://auth0.com/docs/api-auth/tutorials/authorization-code-grant-pkce | |
// | |
export class Auth0PKCEService { | |
/** | |
* The PKCE Challenge pair | |
* | |
* @private | |
* @type {IChallengePair} | |
* @memberof Auth0PKCEService | |
*/ | |
private challengePair: IChallengePair; | |
/** | |
* Generate a challenge paier | |
* | |
* @static | |
* @returns {IChallengePair} | |
* @memberof Auth0PKCEService | |
*/ | |
public static getPKCEChallengePair(): IChallengePair { | |
const verifier = Auth0PKCEService.base64URLEncode( randomBytes( 32 ) ); | |
const challenge = Auth0PKCEService.base64URLEncode( Auth0PKCEService.sha256( verifier ) ); | |
return { verifier, challenge }; | |
} | |
/** | |
* Generate a SHA256 hash | |
* | |
* @static | |
* @param {string} buffer Input buffer | |
* @returns {Buffer} The hash | |
* @memberof Auth0PKCEService | |
*/ | |
public static sha256( buffer: string ): Buffer { | |
return createHash( 'sha256' ).update( buffer ).digest(); | |
} | |
/** | |
* Base64 encode a buffer for use in a URL | |
* | |
* @static | |
* @param {Buffer} buffer Input buffer | |
* @returns {string} The url encoded | |
* @memberof Auth0PKCEService | |
*/ | |
public static base64URLEncode( buffer: Buffer ): string { | |
return buffer.toString('base64') | |
.replace(/\+/g, '-') | |
.replace(/\//g, '_') | |
.replace(/=/g, ''); | |
} | |
/** | |
* Utility method to retrieve a URL paramter by name | |
* | |
* @static | |
* @param {string} name The name of the parameter | |
* @param {string} url The url to extract the parameter from | |
* @returns {string|null} The found value of the parameter | |
* @memberof Auth0PKCEService | |
*/ | |
public static getParameterByName( name: string, url: string ): string|null { | |
name = name.replace( /[\[\]]/g, '\\$&' ); | |
const regex = new RegExp( '[?&]' + name + '(=([^&#]*)|&|#|$)' ); | |
const results = regex.exec( url ); | |
if ( !results ) { return null; } | |
if ( !results[2] ) { return ''; } | |
return decodeURIComponent( results[ 2 ].replace( /\+/g, ' ' ) ); | |
} | |
/** | |
* Creates an instance of Auth0PKCEService | |
* | |
* @param {IAuthServiceConfig} config Auth0 configuration details | |
* @memberof Auth0PKCEService | |
*/ | |
constructor( private config: IAuthServiceConfig ) { | |
} | |
/** | |
* Create a request URL with current challenge pair | |
* | |
* @returns {string} | |
* @memberof Auth0PKCEService | |
*/ | |
public requestAuthCode(): string { | |
this.challengePair = Auth0PKCEService.getPKCEChallengePair(); | |
return this.getAuthoriseUrl( this.challengePair ); | |
} | |
/** | |
* Build an authorisation request URL | |
* | |
* @param {IChallengePair} challengePair The PKCE challenge pair | |
* @returns {string} The request URL | |
* @memberof Auth0PKCEService | |
*/ | |
getAuthoriseUrl( challengePair: IChallengePair ): string { | |
return `${this.config.authorizeEndpoint}?audience=${this.config.audience}` | |
+ `&scope=${this.config.scope}` | |
+ `&response_type=code` | |
+ `&client_id=${this.config.clientId}` | |
+ `&code_challenge=${challengePair.challenge}` | |
+ `&code_challenge_method=S256` | |
+ `&redirect_uri=${this.config.redirectUri}`; | |
} | |
/** | |
* Build the token post request body | |
* | |
* @param {string} authCode The auth0 authenticaton code | |
* @param {string} verifier The auth0 verifier code | |
* @returns | |
* @memberof Auth0PKCEService | |
*/ | |
buildTokenPostRequest( authCode: string, verifier: string ): request.Options { | |
return { | |
method: 'POST', | |
url: this.config.tokenEndpoint, | |
headers: { 'content-type': 'application/json' }, | |
json: true, | |
body: { | |
grant_type: 'authorization_code', | |
client_id: this.config.clientId, | |
code_verifier: verifier, | |
code: authCode, | |
redirect_uri: this.config.redirectUri, | |
} | |
}; | |
} | |
/** | |
* Checks if the callback url is valid | |
* | |
* @param {string} callbackUrl The callback url to check | |
* @returns {boolean} Validity of the url | |
* @memberof Auth0PKCEService | |
*/ | |
isValidAccessCodeCallBackUrl( callbackUrl: string ): boolean { | |
return callbackUrl.indexOf( this.config.redirectUri ) > -1; | |
} | |
/** | |
* Requests an access code from Auth0 | |
* | |
* @param {string} callbackUrl The callback URL for the authentication request | |
* @returns {Promise<any>} | |
* @memberof Auth0PKCEService | |
*/ | |
requestAccessCode( callbackUrl: string ): Promise<IAuthResponse> { | |
return new Promise( ( resolve, reject ) => { | |
// Check the callback URL provided is valid | |
// | |
if ( this.isValidAccessCodeCallBackUrl( callbackUrl ) ) { | |
// Extract the auth code from the callback URL | |
// | |
const authCode = Auth0PKCEService.getParameterByName( 'code', callbackUrl ); | |
if ( authCode != null ) { | |
// Build the post request for Auth0 | |
// | |
const verifier = this.challengePair.verifier; | |
const options = this.buildTokenPostRequest( authCode, verifier ); | |
// Perform the actual call | |
// | |
request( options, ( error, response, body ) => { | |
if ( error ) { | |
reject( error ); | |
} else { | |
if ( response.statusCode >= 200 && response.statusCode < 300 ) { | |
console.log( '[Auth0-PKCE] Request success', body ); | |
resolve( { | |
response: response, | |
body: body, | |
} ); | |
} else { | |
console.error( '[Auth0-PKCE] Request failed', body ); | |
reject( body ); | |
} | |
} | |
} ); | |
} else { | |
reject( 'Could not find the authorization code in the callback url' ); | |
} | |
} else { | |
reject( 'Callback url is invalid' ); | |
} | |
} ); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment