UNPKG

@webqit/webflo-oauth2-client

Version:

Isomorphic OAuth2 Authorization Code Flow library for Webflo.

179 lines (170 loc) 7.55 kB
/** * @imports */ import JwksClient from 'jwks-rsa'; import Machine from "./Machine.js"; /** * Provider Class */ export default class Provider extends Machine { /** * Initializes a Provider instance * * @param String name - The name of the provider * @param String clientId - The "clientId" from provider * @param String clientSecret - The "clientSecret" from provider * @param String baseUrl - The base URL of the provider oauth2 server, from which "endpoint URLs" are resolved * @param String signIn - The "endpoint URL" for redirecting a user to sign in at provider screen * @param String token - The "endpoint URL" for performing token grants with provider * @param String revoke - (Optional) The "endpoint URL" for performing token revokation with provider * @param String signOut - The "endpoint URL" for redirecting a user to sign out at provider screen * @param Object rest - (Optional) Other URLs which may function as "endpoint URLs" * @param String publicKey - (Mutually exclusive with jwksUrl) The public key for verifying a JWT from provider * @param String jwksUrl - (Mutually exclusive with publicKey) The URL from which to fetch the jwks for verifying a JWT from provider * @param Object jwtokensRest - (Optional) Other attributes for verifying a JWT from provider * @param Function fetch - The function for making external requests. */ constructor( name, { clientId, clientSecret, baseUrl, endpoints: { signIn, token, revoke, signOut, ...rest }, jwtokens: { publicKey = null, jwksUrl = null, issuer = null, ...jwtokensRest } = {} }, fetch ) { super( { baseUrl, fetch } ); // --------- Object.defineProperty( this, 'name', { value: name, enumerable: true } ); Object.defineProperty( this, 'clientId', { value: clientId, enumerable: true } ); Object.defineProperty( this, 'clientSecret', { value: clientSecret, enumerable: true } ); Object.defineProperty( this, 'endpoints', { value: {}, enumerable: true } ); const endpoints = { signIn, token, revoke, signOut, ...rest }; Object.defineProperties( this.endpoints, Object.keys( endpoints ).reduce( ( prev, name ) => ( { [ name ]: { value: this.resolveUrl( endpoints[ name ] ), enumerable: true }, ...prev, } ), {} ) ); // --------- Object.defineProperty( this, 'jwtokens', { value: { publicKey, jwksUrl, issuer, ...jwtokensRest }, enumerable: true } ); // --------- } /** * Calls for a token grant. * * @param String grantType - "authorization_code"|"refresh_token"|"client_credentials" * @param String tokenOrAudience * @param Object options * * @return Object */ grant( grantType, tokenOrAudience, options = {} ) { if ( !this.endpoints.token ) throw new Error( 'No "token" endpoint defined.' ); const params = { client_id: this.clientId, client_secret: this.clientSecret, grant_type: grantType, ...options, }; // Properly set the token if ( grantType === 'refresh_token' ) { params.refresh_token = tokenOrAudience; } else if ( grantType === 'authorization_code' ) { params.code = tokenOrAudience; } else if ( grantType === 'client_credentials' ) { params.audience = tokenOrAudience; } else throw new Error( `Grant type "${ grantType }" unknown!` ); // Go... return this.fetch( this.endpoints.token, { method: 'POST', body: JSON.stringify( params ), headers: { 'Content-Type': 'application/json', }, } ).then( res => res.ok ? res.json() : Promise.reject( `${ res.status } - ${ res.statusText }` ) ); } /** * Calls for a token to be revoked. * * @param String token * * @return Object */ revoke( token ) { if ( !this.endpoints.revoke ) throw new Error( 'No "revoke" endpoint defined.' ); return this.fetch( this.endpoints.revoke, { method: 'POST', body: JSON.stringify( { token } ), headers: { 'Content-Type': 'application/json', }, } ).then( res => res.ok ? res.json() : Promise.reject( `${ res.status } - ${ res.statusText }` ) ); } /** * Calls an endpoint. * * @param String bearerToken * @param String endpoint * @param Object data * @param String method * * @return Object */ call( bearerToken, endpoint, data = null, method = 'post' ) { return this.fetch( this.resolveUrl( endpoint ), { method, // If data ...( data ? { body: JSON.stringify( data ) } : {} ), headers: { 'Authorization': `Bearer ${ bearerToken }`, // If data ...( data ? { 'Content-Type': 'application/json' } : {} ), }, } ).then( res => res.ok ? res.json() : Promise.reject( `${ res.status } - ${ res.statusText }` ) ); } /** * Decodes data from a signed JWT. * * @param String token * @param Object params * * @return Object|Undefined */ jwtVerify( token, params = {} ) { return new Promise( ( resolve, reject ) => { let publicKey = this.jwtokens.publicKey; if ( !publicKey && this.jwtokens.jwksUrl ) { publicKey = ( header, callback ) => { const jwksClient = JwksClient( { jwksUri: this.resolveUrl( this.jwtokens.jwksUrl ), } ); jwksClient.getSigningKey( header.kid, ( err, key ) => { if ( err ) return callback( err, null ); const signingKey = key.publicKey || key.rsaPublicKey; callback( null, signingKey ); } ); }; } if ( !publicKey ) throw new Error( `A provider must be initialized with a "config.jwtokens.publicKey" or "config.jwtokens.publicKeyUrl" parameter to verify ID Tokens.` ); this.jwt.verify( token, publicKey, { complete: true, // Verify audience claims - "token.payload.aud" - usually client_id audience: this.clientId, // Verify issuer claims - "token.payload.iss" - usually this.baseUrl (But must end as a path: "url/") issuer: this.baseUrl, // Verify signing algorithm - "token.header.alg" - HS256, RS256 algorithms: [ 'RS256', 'HS256' ], // Verify expiration - (auto) "token.payload.exp" - must be after the current date/time // Verify permissions (scopes) - "token.payload.scopes" - from the initiator request ...this.jwtokens, ...params, }, ( error, decoded ) => error ? reject( error ) : resolve( decoded )); } ); } /** * Decodes data from a signed JWT. * * @param String token * * @return Object|Undefined */ jwtDecode( token ) { return this.jwt.decode( token, { complete: true } ); } }