This sample shows how to implement a multi-tenant scenario, where:
- A single instance of a jQuery SPA + a Node.js API serves multiple customers (tenants).
- Each tenant has its own Auth0 account, so they can have access to the Auth0 Dashboard.
- Each tenant configures its own Auth0 connections for authenticating its users, and for calling third party APIs.
- Each customer accesses the App using a different domain. The tenant name is extracted from the domain.
You can read more about multi-tenant scenarios here: https://auth0.com/docs/saas-apps.
The API is based on this sample: https://github.com/auth0/multitenant-jwt-auth.
The SPA is able to:
- Extract tenant name from domain
- Provide Login functionality using the tenant specific Auth0 settings
- Call the API using the JWT obtained after login
The API is able to:
- Authenticate users using JWTs
- Validate JWTs signed with symmetric keys (HS256 protocol)
- Validate JWTs signed with asymmetric keys (RS256 protocol)
- Support multiple tenants, identified by the JWT's issuer claim.
The client is a node.js express app. The only code executing server side is the one for:
- Obtaining the tenant name from the domain.
- Obtaining the auth0 domain and clientId from the tenant storage using the tenant name.
- Serving the index view (using Jade template engine) along with the
loginConfig
that will be used for authenticating the users.
function getTenantFromDomain(host){
var re = new RegExp(/(\w+)-yourcompany.com$/);
var matches = host.match(re);
if (matches && matches.length > 1){
return matches[1];
}
return null;
}
app.get('/', function(req, res, next) {
var tenantName = getTenantFromDomain(req.hostname);
if (!tenantName) return next('Invalid domain: ' + req.hostname);
var tenantConfig = _.find(tenants,'name',tenantName);
if (!tenantConfig) return next('Invalid Tenant ' + tenantName);
var loginConfig = {
auth0Domain:tenantConfig.auth0Domain,
auth0ClientId:tenantConfig.auth0ClientId
};
res.render('index', loginConfig);
});
Once the initial page is rendered, the app continues as a JQuery Single Page App. It uses the auth0Domain
and auth0ClientId
obtained from server side to configure auth0.js, which will be used to perform the Login.
The access_token obtained during login is saved to localStorage and will be used later to invoke the API. It is also used to fetch the user's profile.
The custom API uses express-jwt for validating JWTs.
var expressJwt = require('express-jwt');
var express = require('express');
var app = express();
app.use('/api', expressJwt({
secret: secretCallback,
algorithms: [ 'HS256','RS256'] //the 2 algorithms supported by Auth0
}));
Because of the multi-tenant support, the secret used to sign tokens is not static and hence a secretCallback
is used. The secretCallback supports verifying tokens signed with HS256 algorithm (clientId) and RS256 (asymmetric keys). It uses an LRU cache to store the secrets.
function secretCallback (req, header, payload, cb){
var cacheKey = payload.iss + '|' + payload.aud ;
var cachedSecret = secretsCache.get(cacheKey);
if (cachedSecret) {
return cb(null, cachedSecret);
}
var tenant = data.getTenantByIssuer(payload.iss);
if (!tenant) {
return cb(new Error('Invalid issuer '+payload.iss));
}
switch (header.alg) {
case 'HS256': //client secret
var secret = new Buffer(tenant.secret, 'base64');
secretsCache.set(cacheKey, secret);
return cb(null, secret);
case 'RS256': // asymmetric keys
var url = payload.iss + '.well-known/jwks.json';
request.get(url, { json: true, strictSSL: false }, function (err, resp, jwks) {
if (err) {
return cb(err);
}
if (resp.statusCode !== 200) {
return cb(new Error('Failed to obtain JWKS from ' + payload.iss));
}
var key = _.find(jwks.keys, function(key) {
return key.kid == header.kid;
});
if (!key) {
return cb(new Error('Failed to obtain signing key used by ' + payload.iss));
}
var publicKey = certToPEM(key.x5c[0]);
secretsCache.set(cacheKey, publicKey);
return cb(null, publicKey);
});
break;
default:
return cb(new Error('Unsupported JWT algorithm: ' + header.alg));
}
}
The API provides a sample secured endpoint for getting a list of users specific for the tenant:
app.get('/api/users',
tenantMiddleware(), //adds tenant's name to req from JWT's issuer claim
function (req, res, next) {
data.getUsersByTenantIdentifier(req.tenantName, function(err, users){
if (err) return next(err);
res.json(users);
});
});
The tenant middleware adds the tenantName to the request. It obtains the tenant from the tenant store, using the JWT's issuer as a key:
var data =require('./data');
module.exports = function(app){
return function addTenant(req, res, next){
var tenant = data.getTenantByIssuer(req.user.iss);
req.tenantName = tenant.name;
next();
};
};
In order to emulate a multi-tenant environment, add the following entries to your machine's hosts file:
127.0.0.1 tenant1-yourcompany.com
127.0.0.1 tenant2-yourcompany.com
Each tenant needs its own Auth0 account. If the tenant already has an account it can use the existing one. Otherwise you can provision an account for him. You can create new accounts from the Auth0 master account, under the user's menu > New Account option.
You can name each tenant as tenantname-yourcompany.auth0.com
.
For each of the Auth0 accounts:
-
Make sure there is at least one Auth0 Application and take note of its clientId.
-
Make sure there is at least one Idenity Provider enabled for users to login.
-
Take note of the Applications ClientSecret. It will be used by the API to validate the tokens.
Alternatively, if the client doesn't want to share the secret, the API can validate tokens using the public key. In this case make sure they selected RS256 under Show Advanced Settings > JsonWebToken Token Signature Algorithm
-
Add the SPA domain to the Allowed Callback URLs. i.e:
http://tenant1-yourcompany.com:3000
Create a tenants.js file using tenants-sample.js as a template. For each tenant provide:
- The Auth0 domain
- The Auth0 ClientId
- The Auth0 ClientSecret (optional - don't needed if tokens are signed using an asymmetric key)
Example with tenant1 using its own auth0 domain and using asymetric keys and tenant2 having an auth0 account provisioned by your company and using the client secret:
module.exports = [
{
name: 'tenant1',
auth0Domain: 'tenant1-custom-domain.auth0.com',
auth0ClientId:'6Y97vVD...C0sxSebDnw7R'
},
{
name: 'tenant2',
auth0Domain: 'tenant2-yourcompany.auth0.com',
auth0ClientId:'VIRUMI46...lU8f7d62s4',
secret: 'TUtF1F28Tage...SIMPhdQZuSy'
}
];
- Open a terminal and go to the SPA folder
- Install dependencies with
npm install
- Run the app with
npm start
- Open another terminal and go to the API folder
- Install dependencies with
npm install
- Run the app with
npm start
- Navigate to
http://tenant1-yourcompany.com:3000
, login with tenant1's crentials and call the API. You should see the data associated with tenant1. - Navitage to
http://tenant2-yourcompany.com:3000
and do the same for tenant2.
Auth0 helps you to:
- Add authentication with multiple authentication sources, either social like Google, Facebook, Microsoft Account, LinkedIn, GitHub, Twitter, Box, Salesforce, amont others, or enterprise identity systems like Windows Azure AD, Google Apps, Active Directory, ADFS or any SAML Identity Provider.
- Add authentication through more traditional username/password databases.
- Add support for linking different user accounts with the same user.
- Support for generating signed Json Web Tokens to call your APIs and flow the user identity securely.
- Analytics of how, when and where users are logging in.
- Pull data from other sources and add it to the user profile, through JavaScript rules.
- Go to Auth0 and click Sign Up.
- Use Google, GitHub or Microsoft Account to login.
If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The Responsible Disclosure Program details the procedure for disclosing security issues.
This project is licensed under the MIT license. See the LICENSE file for more info.