@financialforcedev/orizuru-auth

Salesforce authentication for Heroku Node applications.

Stats

StarsIssuesVersionUpdatedCreatedSize
@financialforcedev/orizuru-auth
187.3.02 years ago4 years agoMinified + gzip package size for @financialforcedev/orizuru-auth in KB

Readme

Orizuru Authentication

Build Status

Orizuru authentication is an Express-compatible authentication middleware for Node.js.

It is aimed at users of the Orizuru framework, but can also be used standalone. The authentication process has been tested with Salesforce and Google as Identity Providers, although it should be possible to use it with others.

In a Salesforce context, using the Token Validator and Grant Checker middleware in combination will establish the following.

  1. The caller has a valid Salesforce access token.
  2. The Salesforce user for the token is linked by a permission set to the ConnectedApp corresponding to the OpenID Connect Client ID (Consumer Key).

It is extremely important to treat your signing secret as sensitive material, please ensure that this is stored in a secure location.

Install

npm install @financialforcedev/orizuru-auth

Usage

OAuth 2.0 Web Server Authentication Flow

Orizuru Auth provides a function to initialise the OAuth 2.0 Web Server Authentication Flow. It uses the Salesforce OpenID Connect discovery endpoint supplied in the Environment, with the Connected app details supplied in the AuthUrlParameters, to create the URL. Optional configuration options for the URL are supplied in the AuthOptions.

For the examples, the initial configuration has been provided in the examples directory of this repository. This can be copied to another directory to be worked with.

Two configuration files should be provided: the default.json file which contains any insensitive data (this is included with the source); and the local.json configuration file which contains sensitive data. Create a local.json file, within the config directory with the following contents, where each of the values has been substituted for your Salesforce connected app details.

{
    "app": {
        "openid": {
            "salesforce": {
                "clientId": "<<<YOUR CONNECTED APP CLIENT ID - CONNECTED APP MUST HAVE THE OPENID SCOPE>>>",
                "clientSecret": "<<<YOUR CONNECTED APP CLIENT SECRET - CONNECTED APP MUST HAVE THE OPENID SCOPE>>>",
                "signingSecret": "<<<YOUR CONNECTED APP CERTIFICATE PRIVATE KEY - CONNECTED APP MUST HAVE THE OPENID SCOPE>>>"
            },
            "salesforceConnection": {
                "clientId": "<<<YOUR CONNECTED APP CLIENT ID - CONNECTED APP MUST HAVE THE API SCOPE>>>",
                "clientSecret": "<<<YOUR CONNECTED APP CLIENT SECRET - CONNECTED APP MUST HAVE THE API SCOPE>>>",
                "signingSecret": "<<<YOUR CONNECTED APP CERTIFICATE PRIVATE KEY - CONNECTED APP MUST HAVE THE API SCOPE>>>"
            }
        }
    }
}

Once completed, the server can be started via npm start or in VS Code via the launch configuration.

Changes to the imports are omitted from further examples; assuming VS Code is being used, the examples directory contains the default configuration for automatically optimising imports.

The first example illustrates how the authentication URL generator can be used with Orizuru and Orizuru Transport RabbitMQ. A HTTPS server is used with a generated self-signed certificate.

The route https://localhost:8080/api/v1.0/auth is added to the server. This route redirects the user to the Salesforce login page; it initialises the OAuth 2.0 Web Server Authentication Flow.

import config from 'config';
import https from 'https';
import pem, { CertificateCreationResult } from 'pem';

import { json, NextFunction, Request, Response, Server } from '@financialforcedev/orizuru';
import { flow } from '@financialforcedev/orizuru-auth';
import { Transport } from '@financialforcedev/orizuru-transport-rabbitmq';

// Define a function that creates a self-signed certificate
function createCertificate(): Promise<CertificateCreationResult> {
    return new Promise((resolve, reject) => {
        pem.createCertificate({ days: 1, selfSigned: true }, (err, result) => {
            if (err) {
                return reject(err);
            }
            process.stdout.write('Created certificate\n');
            return resolve(result);
        });
    });
}

// Define a simple error middleware
const errorMiddleware = (error: Error | undefined, req: Request, res: Response, next: NextFunction) => {
    if (error) {
        server.error(error);
        res.sendStatus(401);
    } else {
        next();
    }
};

// Create the server
const server = new Server({
    authProvider: {
        salesforce: config.get('app.authProvider.salesforce')
    },
    openid: {
        salesforce: config.get('app.openid.salesforce'),
        salesforceConnection: config.get('app.openid.salesforceConnection')
    },
    port: 8080,
    transport: new Transport({
        prefetch: 1,
        url: 'amqp://localhost'
    })
});

// Add listeners for the server error and info events
server.on(Server.ERROR, (message) => {
    process.stdout.write(`${message}\n`);
});

server.on(Server.INFO, (message) => {
    process.stdout.write(`${message}\n`);
});

// Add the route to generate the authorization URL (in this case we use 'test' as the state parameter)
server.addRoute({
    method: 'get',
    middleware: [
        json(),
        errorMiddleware
    ],
    responseWriter: () => async (err: Error | undefined, req: Request, res: Response) => {
        const url = await flow.webServer.authorizationUrlGenerator(server.options.authProvider.salesforce)(server.options.openid.salesforce, server.options.openid.salesforce);
        res.redirect(url);
    },
    schema: {
        fields: [],
        name: 'auth',
        namespace: 'api.v1_0',
        type: 'record'
    },
    synchronous: true
});

/**
 * All the code specified in the rest of the readme should be added here.
 */

// Create a self-signed certificate and then start the server listening to connections using HTTPS
createCertificate().then((certificate) => {

    const serverOptions: https.ServerOptions = {
        cert: certificate.certificate,
        key: certificate.clientKey
    };

    const httpsServer = https.createServer(serverOptions, server.serverImpl);
    httpsServer.listen(server.options.port);
    process.stdout.write('Started server\n');

});

Middleware

Multiple middleware functions are available.

Auth Callback

The auth callback middleware exchanges a verification code for an access token as part of the OAuth 2.0 Web Server Authentication Flow.

If a verification token is successfully exchanged for an access token then the Authorization HTTP header is set on the request.

The following example illustrates how the auth callback middleware can be used with Orizuru and Orizuru Transport RabbitMQ. It follows on from the example given in the OAuth 2.0 Web Server Authentication Flow section.

The route https://localhost:8080/api/auth/v1.0/callback is added to the server and is called once the user has authorized the connected app. If the request is successful, a Authorization Header Set message is printed to the console and the token is returned. This token can be used to test the subsequent routes.

// Add the listener
server.on(EVENT_AUTHORIZATION_HEADER_SET, (message) => {
    process.stdout.write(`${message}\n`);
});

// Add the route to the server
server.addRoute({
    method: 'get',
    middleware: [
        middleware.authCallback(server, 'salesforce', server.options.openid.salesforce, server.options.openid.salesforce),
        errorMiddleware
    ],
    responseWriter: (app) => async (error, req, res) => {
        res.json(req.headers.authorization);
    },
    schema: {
        fields: [],
        name: 'callback',
        namespace: 'api.auth.v1_0',
        type: 'record'
    },
    synchronous: true
});

Token Validator

The token validator middleware checks that a valid OpenID Connect Bearer token exists in an Authorization HTTP header. It does this by calling the specified Identity Provider's UserInfo endpoint with the Bearer token.

If the token is successfully validated then a user object is set on the request's orizuru object.

The following example illustrates how the token validator middleware can be used with Orizuru and Orizuru Transport RabbitMQ to validate tokens contained in the authorization request header. It follows on from the example given in the Auth Callback section.

The route https://localhost:8080/api/auth/v1.0/validateToken is added to the server as a GET request. If the request is successful, a token validated message is printed to the console and the orizuru object is returned. Otherwise, a denied message is printed to the console.

// Add the listeners
server.on(EVENT_DENIED, (message) => {
    process.stdout.write(`${message}\n`);
});

server.on(EVENT_TOKEN_VALIDATED, (message) => {
    process.stdout.write(`${message}\n`);
});

// Add a route to the server
server.addRoute({
    method: 'get',
    middleware: [
        middleware.tokenValidator(server, 'salesforce'),
        errorMiddleware
    ],
    responseWriter: (app) => async (error, req, res) => {
        res.json(req.orizuru);
    },
    schema: {
        fields: [],
        name: 'validateToken',
        namespace: 'api.auth.v1_0',
        type: 'record'
    },
    synchronous: true
});

Grant Checker

The grant checker is designed to be used in tandem with the token Validator. It uses the user object on the request's orizuru object and attempts to obtain an OpenID Connect access token using a JWT Bearer grant request. In order for this to work the Identity Provider must have a previously established authorisation for the user requested. With the Salesforce identity provider this is achieved by using a Connected App with an uploaded certificate.

If this completes successfully it sets the orizuru object grantChecked property to be true, otherwise the user will be refused access. The access token can also be directly set on the orizuru object by setting the setTokenOnContext option to true when creating the middleware. Ensure you have adequate security in place to prevent reading or interception of the token.

The following example illustrates how the grant checker can be used, with Orizuru and Orizuru Transport RabbitMQ, to validate tokens contained in the authorization request header. It follows on from the example given in the Token Validator section.

The route https://localhost:8080/api/auth/v1.0/checkGrant is added to the server as a GET request. If the request is successful, a grant checked message is printed to the console and the orizuru object is returned. Otherwise, a denied message is printed to the console.

// Add the listeners
server.on(EVENT_GRANT_CHECKED, (message) => {
    process.stdout.write(`${message}\n`);
});

// Add a route to the server
server.addRoute({
    method: 'get',
    middleware: [
        middleware.tokenValidator(server, 'salesforce'),
        middleware.grantChecker(server, 'salesforce', server.options.openid.salesforceConnection, {
            verifySignature: false
        }),
        errorMiddleware
    ],
    responseWriter: (app) => async (error, req, res) => {
        res.json(req.orizuru);
    },
    schema: {
        fields: [],
        name: 'checkGrant',
        namespace: 'api.auth.v1_0',
        type: 'record'
    }
});

Token Granter

This can be called at any time to obtain credentials to connect to Salesforce. It depends on the configuration of the Connected App in your Salesforce org, with pre-authorized users connected via a Permission Set to it and a certificate uploaded that corresponds to the signingSecret.

The credentials returned are in a form suitable to be used with JSforce.

The following example illustrates how the token granter can be used, with Orizuru and Orizuru Transport RabbitMQ, to retrieve the limits for a Salesforce organization. It follows on from the example given in the Grant Checker section.

The route https://localhost:8080/api/v1.0/limits is added to the server as a GET request. If the request is successful, a JSON response containing the limits is returned. Otherwise, a denied message is printed to the console. It makes use of an Orizuru response writer function, rather than a middleware, to retrieve the limits and send them to the user.

// Create a function to retrieve the organization limits
async function retrieveOrgLimits(err: Error | undefined, req: Request, res: Response) {

    if (!req.orizuru || !req.orizuru.user) {
        res.sendStatus(401);
        return;
    }

    // Configure the token grantor parameters and options
    const grantParams = Object.assign({}, server.options.openid.salesforceConnection, {
        user: req.orizuru.user
    });

    const grantOpts: GrantOptions = {
        verifySignature: false
    };

    // Obtain the credentials using the token grantor
    const credentials = await grant.getToken(server.options.authProvider.salesforce)(grantParams, grantOpts);

    // Create a new connection and query the limits
    const conn = await new Connection(credentials);
    const limits = await conn.limits();

    res.json(limits);

}

// Add a route to the server
server.addRoute({
    method: 'get',
    middleware: [
        middleware.tokenValidator(server, 'salesforce'),
        errorMiddleware
    ],
    responseWriter: (app) => retrieveOrgLimits,
    schema: {
        fields: [],
        name: 'limits',
        namespace: 'api.v1_0',
        type: 'record'
    },
    synchronous: true
});

Token Introspector

The token introspector middleware can be used to determine the active state of an OAuth 2.0 token and retrieve meta-information about this token. It can be used in tandem with either the Auth Callback or the Token Grantor middlewares.

The following example illustrates how the token introspector can be used, with Orizuru and Orizuru Transport RabbitMQ, to retrieve the token information. It follows on from the example given in the Token Granter section.

The route https://localhost:8080/api/auth/v1.0/introspectToken is added to the server as a GET request. If the request is successful, a token introspected message is printed to the console and the orizuru object is returned. Otherwise, a denied message is printed to the console.

// Add the listeners
server.on(EVENT_TOKEN_INTROSPECTED, (message) => {
    process.stdout.write(`${message}\n`);
});

// Add a route to the server
server.addRoute({
    method: 'get',
    middleware: [
        urlencoded({
            extended: true
        }),
        middleware.tokenIntrospection(server, 'salesforce'),
        errorMiddleware
    ],
    responseWriter: (app) => async (error, req, res) => {
        res.json(req.orizuru);
    },
    schema: {
        fields: [],
        name: 'introspectToken',
        namespace: 'api.auth.v1_0',
        type: 'record'
    },
    synchronous: true
});

API Docs

Click to view API documentation.

If you find any bugs or have a feature request, please open an issue on github!

The npm package download data comes from npm's download counts api and package details come from npms.io.