UNPKG

@webqit/webflo-oauth2-client

Version:

Isomorphic OAuth2 Authorization Code Flow library for Webflo.

232 lines (184 loc) 8.64 kB
# Webflo OAuth2 Client <!-- BADGES/ --> <span class="badge-npmversion"><a href="https://npmjs.org/package/@webqit/webflo-oauth2-client" title="View this project on NPM"><img src="https://img.shields.io/npm/v/@webqit/webflo-oauth2-client.svg" alt="NPM version" /></a></span> <span class="badge-npmdownloads"><a href="https://npmjs.org/package/@webqit/webflo-oauth2-client" title="View this project on NPM"><img src="https://img.shields.io/npm/dm/@webqit/webflo-oauth2-client.svg" alt="NPM downloads" /></a></span> <!-- /BADGES --> Webflo OAuth2 library for Node.js. ## Installation ```shell npm i @webqit/webflo-oauth2-client ``` ## Usage ```js import { ConsentFlow } from '@webqit/webflo-oauth2-client'; ``` ### Initialization ```js const createConsentFlow = ( httpEvent, fetch ) => { // Client app config const client = { // The application's base URL. E.g. http://localhost:3000/ (local), https://example.com/ (production) baseUrl: process.env.OAUTH2_CALLBACK_HOST, // Application's auth-flow endpoints, relative to base URL callbacks: { // The route where you'll handle signed-in callback from provider screen. (See below: The "Signed-In" Callback Route) signedIn: '/auth', // The route where you'll handle signed-out callback from provider screen. (See below: A "Sign Out" Route) signedOut: '/', }, }; // Provider: auth0 const auth0 = { // Auth0-issued client ID clientId: process.env.AUTH0_CLIENT_ID, // Auth0-issued client secret clientSecret: process.env.AUTH0_CLIENT_SECRET, // OAuth2 server base URL. E.g. https://example.us.auth0.com/ baseUrl: process.env.AUTH0_BASE_URL, // Auth0-issued auth-flow endpoints, relative to base URL endpoints: { signIn: '/authorize', token: '/oauth/token', revoke: undefined, signOut: '/v2/logout', }, // Parameters for verifying Auth0-issued ID Tokens jwtokens: { jwksUrl: '.well-known/jwks.json', } }; // Provider: google const google = { // Google-issued client ID clientId: process.env.GOOGLE_CLIENT_ID, // Google-issued client secret clientSecret: process.env.GOOGLE_CLIENT_SECRET, // OAuth2 server base URL. Always https://oauth2.googleapis.com/ for Google baseUrl: process.env.GOOGLE_BASE_URL, // Google-issued auth-flow endpoints, relative to base URL endpoints: { signIn: 'https://accounts.google.com/o/oauth2/v2/auth', token: '/token', revoke: '/revoke', signOut: undefined, }, // Parameters for verifying Google-issued ID Tokens jwtokens: { jwksUrl: 'https://www.googleapis.com/oauth2/v3/certs', } }; // Auth instance return new ConsentFlow( httpEvent, { client, providers: { google/* as default provider */, auth0 }, }, fetch ); }; ``` ### Initialization Route At the application's root handler, you'd initialize oauth2 client into the `context` object that is passed around: ```js export default function( httpEvent, context, next, fetch ) { context.oauth2 = createConsentFlow( httpEvent, fetch ); if ( next.pathname ) return next( context ); return { titleBar: 'Home' }; } ``` ### A Protected "Sign In" Route To protect a route, you'd call the `signIn()` method with optional fields - `{ provider /* defaults to the name of the first provider in list */, scope, audience }`: ```js export default function( httpEvent, context, next, fetch ) { return context.oauth2.signIn( { /* optional */scope: 'openid' }, session => { // Authenticated... // otherwise this function is not called, and user is redirected to provider sign in screen // session is same as context.oauth2.session // Are we going to a protected child page? // session can always be accessed as context.oauth2.session down the hierarchy if ( next.pathname ) return next( context ); // See what's in auth session console.log( session ); // { access_token, token_type: 'Bearer', scope, provider, info, ...etc } // See what's in session.info console.log( session.info ); // { iss, sub: <user ID>, exp, ...etc } // (If "profile", "email" were in the value of the "scope" parameter (as an array or a space-delimitted string), other infos would be available: email, name, avatar_url, etc.) // Call an API endpoint at provider const provider = context.oauth2.client.provider( /* specify provider name, otherwise default provider is implied */ ); const endpoint = '/userinfo'; // Or if defined in provider settings object above: endpoint = provider.endpoints.userinfo const user = await provider.call( session.access_token, endpoint, { ...optionalBody } ); /* Or if you wish... const user = await fetch( 'https://oauth2.example.com/endpoint', { body: JSON.stringify( { ...optionalBody } ), headers: { 'Content-Type': 'application/json', Authorization: session.access_token } } ).then( res => res.json() ); */ // Show accounts page return { titleBar: 'My Account', email: user.email, } } ); } ``` <details> <summary>The <code>context.oauth2.signIn()</code> function could also be called without a callback...</summary> ```js export default function( httpEvent, context, next, fetch ) { let session, redirect, sessionOrRedirect = context.oauth2.signIn( { /* optional */scope: 'openid' } ); if ( sessionOrRedirect instanceof httpEvent.Response ) { // User is being redirected to provider sign in screen return ( redirect = sessionOrRedirect /* formality assignment */ ); } // Authenticated... session = sessionOrRedirect; // session is same as context.oauth2.session // Are we going to a protected child page? // session can always be accessed as context.oauth2.session down the hierarchy if ( next.pathname ) return next( context ); // See what's in auth session console.log( session ); // { access_token, token_type: 'Bearer', scope, provider, info, ...etc } // See what's in session.info console.log( session.info ); // { iss, sub: <user ID>, exp, ...etc } // (If "profile", "email" were in the value of the "scope" parameter (as an array or a space-delimitted string), other infos would be available: email, name, avatar_url, etc.) // Call an API endpoint at provider const provider = context.oauth2.client.provider( /* specify provider name, otherwise default provider is implied */ ); const endpoint = '/userinfo'; // Or if defined in provider settings object above: endpoint = provider.endpoints.userinfo const user = await provider.call( session.access_token, endpoint, { ...optionalBody } ); /* Or if you wish... const user = await fetch( 'https://oauth2.example.com/endpoint', { body: JSON.stringify( { ...optionalBody } ), headers: { 'Content-Type': 'application/json', Authorization: session.access_token } } ).then( res => res.json() ); */ // Show accounts page return { titleBar: 'My Account', email: user.email, } } ``` </details> ### The "Signed In" Callback Route To handle the "signed-in" redirect from *provider* screen - at the specified callback URL `client.callbacks.signedIn`: ```js export default function( httpEvent, context, next ) { if ( context.oauth2.isSigningIn() /* Detects if a sign-in session is ongoing */ ) { // Performs "authorization_code" grant and redirects user back to the original protected route - where signIn() was called return context.oauth2.handleSignInCallback(); } // Returns the context.oauth2.session object or false return context.oauth2.isSignedIn()?.info || {}; } ``` ### A "Sign Out" Route To perform "sign out" at any route: ```js export default function( httpEvent, context, next ) { if ( context.oauth2.isSignedIn() ) { // User is signed-out (and is redirected to provider's sign-out URL, where given), // then redirected back to "/" as specified in client.callbacks.signedOut return context.oauth2.signOut(); } return next(); } ``` ## Full Documentation Full documentation, including integrating other providers, coming soon. ## Issues To report bugs or request features, please submit an issue to this repository. ## License MIT.