UNPKG

@webqit/webflo-oauth2-client

Version:

Isomorphic OAuth2 Authorization Code Flow library for Webflo.

186 lines (176 loc) 7.01 kB
/** * @imports */ import { scopeSplit } from './util.js'; import Machine from "./Machine.js"; import Provider from './Provider.js'; /** * Client Class */ export default class Client extends Machine { /** * Initializes a Client instance * * @param String baseUrl - The base URL of the app, from which "callback URLs" are resolved * @param String signedIn - The "callback URL" for when a user has been signed in at provider screen * @param String signedOut - The "callback URL" for when a user has been signed out at provider screen * @param Object rest - (Optional) Other URLs on the app which may function as "callback URLs" * @param String privateKey - (Optional, but required where jwtSign() is called) The private for signing a JWT to an external service * @param Object jwtokensRest - (Optional) Other attributes for signing a JWT to an external service * @param Object providers - A hash of oauth2 providers. (See Provider constructor for details.) * @param Function fetch - The function for making external requests. */ constructor( { baseUrl, callbacks: { signedIn, signedOut, ...rest }, jwtokens: { privateKey, ...jwtokensRest } = {} }, providers, fetch ) { super( { baseUrl, fetch } ); // --------- Object.defineProperty( this, 'callbacks', { value: {}, enumerable: true } ); const callbacks = { signedIn, signedOut, ...rest }; Object.defineProperties( this.callbacks, Object.keys( callbacks ).reduce( ( prev, name ) => ( { [ name ]: { value: this.resolveUrl( callbacks[ name ] ), enumerable: true }, ...prev, } ), {} ) ); // --------- Object.defineProperty( this, 'jwtokens', { value: { privateKey, ...jwtokensRest }, enumerable: true } ); // --------- this.providersConfig = providers; this.defaultProvider = Object.keys( providers )[ 0 ]; this.providers = new Map; } /** * Returns a Provider instance. * * @param String name * * @returns String */ provider( name = this.defaultProvider ) { if ( !this.providers.has( name ) ) { if ( !this.providersConfig[ name ] ) throw new Error( `The implied provider "${ name }" is not configured!` ); this.providers.set( name, new Provider( name, this.providersConfig[ name ], this.fetch ) ); } return this.providers.get( name ); } /** * Calls provider to authenticate self - specifying an audience. * * @param String provider * @param String audience * * @returns Object */ async signInWithClientCredentials( { provider, audience } = {} ) { const providr = this.provider( provider ); const oauth = await providr.grant( 'client_credentials', audience ); return oauth; } /** * Calls provider to exchange a code for token. * * @param String provider * @param String code * @param String pkceVerifier * * @returns Object */ async signInWithAuthorizationCode( { provider, code, pkceVerifier = null } = {} ) { const providr = this.provider( provider ); const response = await providr.grant( 'authorization_code', code, { redirect_uri: this.callbacks.signedIn, ...( pkceVerifier ? { code_verifier : pkceVerifier } : { }), } ); const oauth = { ...response, provider: providr.name }; if ( oauth.id_token ) { let id_token = oauth.id_token; delete oauth.id_token; try { id_token = await providr.jwtVerify( id_token ); } catch ( e ) { throw e; id_token = await providr.jwtDecode( id_token ); } oauth.header = id_token.header; oauth.signature = id_token.signature; oauth.info = id_token.payload; } else { oauth.info = {}; } if ( oauth.refresh_token ) { // TODO: Store refresh_token and monitore access_token expiration so as to auto-refresh it } return oauth; } /** * Dynamically creates the URL for obtaining "authorization_code". * * @param String state * @param String provider * @param Array scope * @param String audience * @param Boolean offline * @param String pkceChallenge * * @returns String */ generateAuthorizationSignInUrl( { state, provider, scope = [], audience = null, offline = false, pkceChallenge = null } = {} ) { const providr = this.provider( provider ); const givenScopes = scopeSplit( scope ); const incrementalAuth = givenScopes[ 0 ] === '+' ? givenScopes.shift() : false; const url = providr.endpoints.signIn + '?response_type=code' + '&client_id=' + providr.clientId + '&redirect_uri=' + this.callbacks.signedIn + ( givenScopes.length ? '&scope=' + givenScopes.join( '%20' ) : '' ) // "openid" to include id_token, "offline_access" - to include refresh_token + ( audience ? '&audience=' + audience : '' ) + ( offline ? '&access_type=offline' : '' ) + ( pkceChallenge ? '&code_challenge=' + pkceChallenge + '&code_challenge_method=S256' : '' ) + ( incrementalAuth ? '&include_granted_scopes=true' : '' ) + ( state ? '&state=' + state : '' ); return url; } /** * Dynamically creates the URL for obtaining "authorization_code". * * @param String provider * * @returns String */ generateAuthorizationSignOutUrl( { provider } = {} ) { const providr = this.provider( provider ); if ( !providr.endpoints.signOut ) return this.callbacks.signedOut; const url = providr.endpoints.signOut + '?client_id=' + providr.clientId + '&returnTo=' + this.callbacks.signedOut; return url; } /** * Encodes data as a signed JWT. * * @param String|Object data * @param Object params * * @return String */ jwtSign( data, params = {} ) { return new Promise(( resolve, reject ) => { this.jwt.sign( data, this.jwtokens.privateKey, { // Add issuer claims - usually this.baseUrl (Note: ending as a path: "url/", to be consistent with the same field in RemoteProvider.jwtVerify()) issuer: this.baseUrl, // The default signing algorithm algorithm: 'RS256', // Add audience, etc claims ...params }, ( error, decoded ) => error ? reject( error ) : resolve( decoded ) ); } ); } }