Webflo OAuth2 library for Node.js.
npm i @webqit/webflo-oauth2-client
import { ConsentFlow } from '@webqit/webflo-oauth2-client';
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 );
};
At the application's root handler, you'd initialize oauth2 client into the context
object that is passed around:
export default function( httpEvent, context, next, fetch ) {
context.oauth2 = createConsentFlow( httpEvent, fetch );
if ( next.pathname ) return next( context );
return { titleBar: 'Home' };
}
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 }
:
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,
}
} );
}
The context.oauth2.signIn()
function could also be called without a callback...
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,
}
}
To handle the "signed-in" redirect from provider screen - at the specified callback URL client.callbacks.signedIn
:
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 || {};
}
To perform "sign out" at any route:
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, including integrating other providers, coming soon.
To report bugs or request features, please submit an issue to this repository.
MIT.