Skip to content

webqit/webflo-oauth2-client

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Webflo OAuth2 Client

NPM version NPM downloads

Webflo OAuth2 library for Node.js.

Installation

npm i @webqit/webflo-oauth2-client

Usage

import { ConsentFlow } from '@webqit/webflo-oauth2-client';

Initialization

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:

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 }:

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,
    }
}

The "Signed In" Callback Route

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 || {};
}

A "Sign Out" Route

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

Full documentation, including integrating other providers, coming soon.

Issues

To report bugs or request features, please submit an issue to this repository.

License

MIT.