Skip to content

Instantly share code, notes, and snippets.

@Qwerios
Created May 11, 2018 10:01
Show Gist options
  • Save Qwerios/6d94b4b2f821981dbd57a459cc32db16 to your computer and use it in GitHub Desktop.
Save Qwerios/6d94b4b2f821981dbd57a459cc32db16 to your computer and use it in GitHub Desktop.
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