Cloud-based authentication and authorization platforms—sometimes known as IDaaS, or identity as a service — are an expanding area of cloud tooling, and it’s easy to see why. App security is difficult and error-prone, and virtually every project requires it. The ability to offload much of the work to a dedicated and proven service is enticing.
Auth0 is an up-and-coming provider of authentication and authorization services (and open source software). In this article, you will see how to incorporate Auth0 log-in capabilities into a app with a Node.js/Express back end, serving a straight JS front end, and then use the authenticated user info (via JWT) to show/hide UI information and secure RESTful endpoints.
Create a simple Node.js/Express app
First you will set up a simple Express app. Begin by typing npm init
from the command line. You can provide whatever values you like for the project name and so on. The final code from the project is available here. Note that this sample app is intended to highlight the security elements in a simple and condensed manner, so many production-necessary features like error handling and configuration files have been left out.
Next, install Express from the same directory where you ran init by running npm install express
.
In your code editor of choice, add a server.js file in the root directory and put the contents of Listing 1 in it.
Listing 1. Basic server.js file
const express = require('express');
const app = express();
app.get('/api/open', function(req, res) {
console.log("/api/open");
res.json({
message: "Open Endpoint"
});
});
app.get('/api/members-only', function(req, res){
console.log("/api/members-only")
res.json({
message: 'Members Only Endpoint'
});
})
app.get('/api/protected', function(req, res) {
console.log("/api/protected")
res.json({
message: 'Protected Endpoint'
});
});
app.listen(3000);
console.log('Listening on http://localhost:3000');
Listing 1 sketches out what we’re shooting for: three API endpoints, one open, one requiring an active log-in, and one requiring log-in and a specific permission.
Now add a dev script to the script section of the package.json file:
"dev": "nodemon server.js"
You’ll need to install the nodemon tool:
npm install -g nodemon
You can now run the simple server with npm run dev
, and view the API responding at localhost:3000/api/open.
Serve static files
We’ll use express.static to serve the client from /public/index.html. This file is going to contain all the HTML and JS in one place to make it easy to comprehend everything. This will be our client — what the Auth0 docs refer to as a single-page app (SPA). Our client will make calls to the back-end API we defined above.
Just before the app.listen
line in server.js, add the following line:
app.use(express.static(join(__dirname, "public")));
This instructs Node.js to serve the files in /public. Now create the file /public/index.html and put the contents of Listing 2 in it.
Listing 2. Beginning index.html
<html>
<head>
<style>
.hidden {
display: none;
}
label {
margin-bottom: 10px;
display: block;
}
</style>
</head>
<body>
<h1>Infoworld: Intro to Auth0</h1>
<button id="btn-login" disabled="true" onclick="login()">Log in</button>
<button id="btn-logout" disabled="true" onclick="logout()">Log out</button>
<h2>Fetch Open API</h2>
<h3 id="openMessage"></h3>
<button onclick="fetchOpenApi()">Open API</button>
<h2>Fetch Members Only API</h2>
<h3 id="moMessage"></h3>
<button onclick="fetchMembersOnlyApi()">Members Only API</button>
<h2>Fetch Protected API</h2>
<h3 id="protectedMessage"></h3>
<button onclick="fetchProtectedApi()">Protected API</button>
<hr>
<div class="hidden" id="gated-content">
<p>
This content is hidden until user is logged in.
</p>
<label>
Access token:
<pre id="ipt-access-token"></pre>
</label>
<label>
User profile:
<pre id="ipt-user-profile"></pre>
</label>
</div>
</body>
</html>
<script>
async function fetchOpenApi(){
let result = await fetch("/api/open");
let json = await result.json();
document.getElementById("openMessage").innerHTML = JSON.stringify(json.message);
}
async function fetchMembersOnlyApi(){
const token = await auth0.getTokenSilently();
let result = await fetch("/api/members-only");
let json = await result.json();
document.getElementById("moMessage").innerHTML = JSON.stringify(json.message);
}
async function fetchProtectedApi(){
const token = await auth0.getTokenSilently();
let result = await fetch("/api/protected");
let json = await result.json();
document.getElementById("protectedMessage").innerHTML = JSON.stringify(json.message);
}
</script>
With Listing 2, you can now go to localhost:3000/ and you will find a basic HTML page with the three buttons that hit the three endpoints. At this stage, if you click the buttons, all three will return their results because the secure endpoints are not secure yet. Further, the log-in and log-out buttons don’t do anything yet, and the content at the bottom of the page remains hidden.
Secure the endpoints
Now you are to the point where you need an Auth0 account, which is free for basic usage. You can sign up here. When you sign up, Auth0 will create a default “System API” for you. This is a special API that is one-per-tenant, and gives you access to the Auth0 platform. The public keys (in this case jwks for RS256) are exposed via this API.
Next we will create an API in the Auth0 system. This is a representation of your real-world API (the endpoints we want to secure) that allows you to apply Auth0 capabilities. Click the “Create API” button, which will open the screen you see in Figure 1.
Figure 1. Create an Auth0 API
For the name
field, you can use anything that is memorable. For identifier
, you should use a URL, but you don’t have to expose the URL or even own it — it is just an identifier to which you will refer in your code. Of course, in a real-world project, you would use your actual domain name or other owned resource. For the last field on this form, you can leave the algorithm as RS256.
Use the Auth0 API
The public key for the RS256 pair is now hosted for you at the URL with the format https://[your_domain].auth0.com/.well-known/jwks.json. You can find the detail for your new API by clicking the "settings" link by it. Notice the identifier you provided now has the form https://[your_domain].us.auth0.com/api/v2/. You will see both of these URLs in action shortly.
The next bit of housekeeping you have to do is to define permissions. In this case, you want a permission that will be required for accessing the protected endpoint we created earlier. From the settings page, select the “Permissions” tab. Create a read:protected
permission and hit the “Add” button.
In a moment you will apply this permission to the protected endpoint.
Use the Express auth middleware
You are going to use Express middleware to enforce the permission policy. Go ahead and install the dependencies in Listing 3, which includes an Express JWT (JSON web token), a JSON web key, and Express JWT authorization extensions, respectively. Remember that JWT is an encrypted token that carries the auth information. It will be used to communicate between the front end, the back end, and the Auth0 platform.
Listing 3. Add auth dependencies
npm install --save express-jwt jwks-rsa express-jwt-authz
Add checkJwt
to server.js, along with the necessary imports, as seen in Listing 4. Notice that there are elements (in square brackets) that you will fill in with your details.
Listing 4. Securing the endpoint
//...
const jwt = require('express-jwt');
const jwtAuthz = require('express-jwt-authz');
const jwksRsa = require('jwks-rsa');
//...
const checkJwt = jwt({
secret: jwksRsa.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://[YOUR SYSTEM API DOMAIN].us.auth0.com/.well-known/jwks.json`
}),
audience: '[THE IDENTIFIER FROM YOUR API]',
issuer: [`https://[YOUR SYSTEM API DOMAIN].us.auth0.com/`],
algorithms: ['RS256']
});
var options = { customScopeKey: 'permissions'}; // This is necessary to support the direct-user permissions
const checkScopes = jwtAuthz([ 'read:protected' ]);
//...
app.get('/api/members-only', checkJwt, function(req, res){
console.log("/api/members-only")
res.json({
message: 'Members Only Endpoint'
});
})
app.get('/api/protected', checkJwt, checkScopes, function(req, res) {
console.log("/api/protected")
res.json({
message: 'Protected Endpoint'
});
});
In broad strokes, what is happening here is that we create an Express middleware checkJwt
that will check for a valid JSON web token. This is configured to use the information from the Auth0 API you created earlier.
Notice both issuer
and jwksUri
point to your System API account, which was created for you when you signed up. Again, there is one System API account per tenant, not per API. This account provides the keys (the JSON Web Key Set, in this case) to sign the auth information for specific APIs.
The audience
field will refer to the identifier for the API you created, not the System API account.
Finally, notice that there is also checkScopes
applied to the protected endpoint. This checks for the read:protected
permission.
Check your progress
At this point, if you return to the browser and click the “Members Only API” (or “Protected API”) button, the server will respond with an error:
UnauthorizedError: No authorization token was found.
That’s a good sign that things are starting to work.
Create an Auth0 client application
Just as you created an Auth0 API to model your back-end app, you’ll now create and configure a client, or consumer, of your secured endpoints. Again, Auth0 calls them SPAs (they used to be called just “Clients,” and still are in some of the Auth0 docs). Go to the Auth0 dashboard, and in the left-hand menu select “Applications -> Applications,” just above the API link you used before when configuring the server.
Now select the “Create Application” button. Give it a name (perhaps call it “Client” to distinguish it from the back-end app) and make sure to select “SPA” as the type. Hit “Create.”
Now open the client application by selecting from the list. Herein you’ll find the info you need to set up the client side of our test app: the domain and client ID. Make note of this information; we’ll use it in just a moment.
Configure the callback, logout, and Web Origin URLs in App Settings
First however, as seen in Figure 2, add the localhost address (http://localhost:3000) for the dev app to the allowed callbacks. This lets Auth0 know it can use your development URL for these purposes.
Figure 2. Adding localhost to client config
Build out the client auth
Now return to the app code and add the Auth0 SDK to the client, in index.html. In this case, we’ll use the CDN. Add the following to the header of the file:
<script src="https://cdn.auth0.com/js/auth0-spa-js/1.13/auth0-spa-js.production.js"></script>
Now we can tie in the auth. Begin by wiring up the log-in and log-out buttons. The handlers for them are seen in Listing 5.
Listing 5. Log-in and log-out handlers
const configureClient = async () => {
auth0 = await createAuth0Client({
domain: "[YOUR SYSTEM API URL].us.auth0.com",
client_id: "[YOUR CLIENT ID]",
audience: "[YOUR API IDENTIFIER]" // The backend api id
});
}
const login = async () => {
await auth0.loginWithRedirect({
redirect_uri: "http://localhost:3000"
});
};
const logout = () => {
auth0.logout({
returnTo: window.location.origin
});
};
With Listing 5, first you configure your Auth0 client using the settings information noted earlier. Notice again that the domain
field refers to your one-per-tenant System API.
Both handlers rely upon the Auth0 library you imported earlier. If you apply this and refresh the app, you can click the log-in button and be redirected to the Auth0 log-in page. This page is the “universal log-in” portal (Auth0 also supports integrating a “lock box” component). Notice that it automatically supports both username/password and social log-ins.
Show and hide content based on auth
Listing 6 has a few more script changes for index.html to implement show/hide functionality.