Build Your First Deno App with Authentication

Build Your First Deno App with Authentication

The creator of Node.js, Ryan Dahl, has authored a new framework for designing web applications. He went back and fixed some mistakes he made in hindsight, taking advantage of new technologies that were not available at the time he originally wrote Node. The result is Deno (pronounced DEH-no), a framework for writing “Node-like” web applications in TypeScript. Here, I will walk you through creating a basic web application with authentication.

You can find almost all the information you need at the Deno website—along with information on all the third-party libraries that are currently available for Deno. That is really the biggest drawback to the framework right now. It just hit version 1.0 on May 13th of 2020, so even though there are quite a few essential libraries, there are not nearly as many libraries as there are for Node. For those who are proficient in Node however, the transition to Deno should be pretty easy.

You can find the installation instructions at https://deno.land/#installation.

Create Your Deno Application

There aren’t any basic scaffolding libraries that I could find, so I just started with an empty folder. In the application’s root folder, create a file called index.ts that will be the starting point of your Deno application. You’ll use Opine, which is an Express clone for Deno to make building and routing easier.

One thing that is different about Deno is that there are no package managers for bringing in third-party libraries. You do this by using the library’s full URL. Do that at the top of the index.ts file, then set up a basic web application.

import { opine } from 'https://deno.land/x/opine@2.0.0/mod.ts';

const app = opine();

app.get('/', (req, res) => {
  res.send('Deno Sample');
});

app.listen(3000);
console.log('running on port 3000');

You can then run this very basic application by going to the terminal in the application’s folder and entering:

deno run -A index.ts

The -A is a shortcut for development purposes. Deno is completely locked down by default, so you’ll need to pass arguments to the run command to allow access like --allow-net to allow networking, and --allow-read to allow the application to read from the file system. The -A used here allows everything, effectively disabling all security. When you run this application and then go to http://localhost:3000 you should be greeted with Deno Sample on a blank page.

Build a Real Web Application with Deno

While this is a good first step, it’s not very useful. You’ll want to add some real functionality that’s a little more “real-world”, so change the index.ts file so that the contents are:

import { opine, serveStatic } from 'https://deno.land/x/opine@2.0.0/mod.ts';
import { renderFileToString } from 'https://deno.land/x/dejs@0.10.1/mod.ts';
import { join, dirname } from 'https://deno.land/x/opine@2.0.0/deps.ts';

import { ensureAuthenticated } from './middleware/authmiddleware.ts';
import users from './controllers/usercontroller.ts';
import auth from './controllers/authcontroller.ts';

const app = opine();
const __dirname = dirname(import.meta.url);

app.engine('.html', renderFileToString);
app.use(serveStatic(join(__dirname, 'public')));
app.set('view engine', 'html');

app.get('/', (req, res) => {
  res.render('index', { title: 'Deno Sample' });
});

app.use('/users', ensureAuthenticated, users);
app.use('/auth', auth)

app.listen(3000);
console.log('running on port 3000');

You’ll notice some more import statements which bring in some third-party libraries. Here, I am using dejs which is an EJS port for Deno. I’ve also included some utility classes from the Opine library for manipulating directory names. I will explain what the three files imported locally are in a moment. For now, just know that you’re importing them.

The line below the instantiation of the opine() app creates a reference to the local directory. The three lines below use this to set the view engine to DEJS for processing the HTML-like files, similar to the way EJS does for Node. The next section has been changed slightly to render one of those HTML template files, and the last two lines bring in some external routes. One thing of note is that the /users route has an ensureAuthenticated() middleware function. This will force users to log in before being allowed to visit the page. You’ll create that middleware shortly.

Fill In Your Deno Application

Now, you’ll want to create some of the missing pieces that you imported above. Start with the routes. Create a folder called controllers in the root of the application. Then add a usercontroller.ts file inside that folder with the following contents:

import { Router } from 'https://deno.land/x/opine@2.0.0/mod.ts';

const users = new Router();

// users routes
users.get('/me', (req, res) => {
  res.render('users/me', { title: 'My Profile', user: res.app.locals.user });
});

export default users;

This is a simple routing file. It gets the router from Opine and creates a new instance to hang routes from. Then there is code to add a route for /me to render the HTML view in users/me. The render() call also passes a title and the logged-in user to the page. This page will be protected so that there will always be a user to pass to the page.

Next, create some views to show when the routes are hit. In the root folder, add a views folder. Inside that, create a shared folder and a users folder. In the shared folder create a header.html and footer.html file. In the users folder add a me.html file. Finally, in the views folder itself create an index.html file.

These are pretty bare-bones, but it demonstrates how to create views that can be reused by other views. In the shared/header.html file add the following:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title><%= title %></title>
</head>

<body>

This outputs the top of an HTML page and injects the title into the page. Next, add the following to the shared/footer.html file:

</body>

</html>

Now you can use those partials in the index.html file:

<%- await include('views/shared/header.html', { title }); %>

<a href="/users/me">My Profile</a>

<%- await include('views/shared/footer.html'); %>

This includes the footer and header partials and adds a link to the profile page. The contents of the users/me.html file are:

<%- await include('views/shared/header.html', { title }); %>

<h1>My Profile</h1>

<ul>
<% for(var p in user){ %>
  <li><strong><%= p %>: </strong><%= user[p] %></li>
<% } %>
</ul>

<%- await include('views/shared/footer.html'); %>

Again, this page includes the header and footer, and loops through the properties of the user object. Granted, it’s not a super-sexy profile page, but it will let you know that the authentication steps all worked.

Manage Your Dependencies

Now that you’ve created your second file and imported the same dependencies, it’s easy to see what a chore managing and updating them will be. Deno recommends placing all imports in a single deps.ts file to centralize dependencies.

Create a file named deps.ts in the root of the application. This time you’ll export the dependencies you used, so the deps.ts file looks like this:

export {
    opine, serveStatic, Router
} from 'https://deno.land/x/opine@2.0.0/mod.ts';

export { renderFileToString } from 'https://deno.land/x/dejs@0.10.1/mod.ts';
export { join, dirname } from 'https://deno.land/x/opine@2.0.0/deps.ts';

Open index.ts to update the import statements. You’ll now import the dependencies from deps.ts. The library imports in the index.ts file becomes:

import { 
  opine,
  serveStatic,
  renderFileToString,
  join,
  dirname } from './deps.ts';

Open the controllers/usercontroller.ts file to update the import there. The import statement changes to:

import { Router } from './../deps.ts';

Add Authentication with Okta

Before you begin, you’ll need a free Okta developer account. Install the Okta CLI and run okta register to sign up for a new account. If you already have an account, run okta login. Then, run okta apps create. Select the default app name, or change it as you see fit. Choose Web and press Enter.

Select Other. Then, change the Redirect URI to http://localhost:3000/auth/callback and use http://localhost:3000 for the Logout Redirect URI.

What does the Okta CLI do?

The Okta CLI will create an OIDC Web App in your Okta Org. It will add the redirect URIs you specified and grant access to the Everyone group. You will see output like the following when it’s finished:

Okta application configuration has been written to: /path/to/app/.okta.env

Run cat .okta.env (or type .okta.env on Windows) to see the issuer and credentials for your app.

export OKTA_OAUTH2_ISSUER="https://dev-133337.okta.com/oauth2/default"
export OKTA_OAUTH2_CLIENT_ID="0oab8eb55Kb9jdMIr5d6"
export OKTA_OAUTH2_CLIENT_SECRET="NEVER-SHOW-SECRETS"

Your Okta domain is the first part of your issuer, before /oauth2/default.

NOTE: You can also use the Okta Admin Console to create your app. See Create a Web App for more information.

Make note of the ISSUER, CLIENT_ID, and CLIENT_SECRET values from the .okta.env file. You’ll need these 3 values for the next step.

Back in your application, create a new file in the root of the application called .env. The contents of the file will be:

issuer=https://{yourOktaOrgUrl}/oauth2/default
clientId={yourClientID}
clientSecret={yourClientSecret}
redirectUrl=http://localhost:3000/auth/callback
state=SuPeR-lOnG-sEcReT

Copy the Client ID, Client Secret, and Issuer from the .okta.env file and populate the values in the .env file.

First, open deps.ts and export a library for reading the .env file. The dotenv does this beautifully. Your deps.ts now looks like this:

export {
    opine, serveStatic, Router
} from 'https://deno.land/x/opine@2.0.0/mod.ts';

export { renderFileToString } from 'https://deno.land/x/dejs@0.10.1/mod.ts';
export { join, dirname } from 'https://deno.land/x/opine@2.0.0/deps.ts';
export { config } from 'https://deno.land/x/dotenv@v3.1.0/mod.ts';

You’re ready to start talking to Okta for authentication. Unfortunately, I couldn’t find any OpenID Connect (OIDC) libraries to make authentication with OAuth 2.0 and OIDC easier than this, so you’ll have to create it by hand. However, this can be an awesome exercise to help understand how OAuth and OIDC work. In the root folder of your application, create a new folder called middleware and add a file called authmiddleware.ts. Then add this content:

import { config } from './../deps.ts';

export const ensureAuthenticated = async (req:any, res:any, next:any) => {
  const user = req.app.locals.user;
  if (!user) {
    const reqUrl = req.originalUrl;
    const {issuer, clientId, redirectUrl, state} = config();
    const authUrl = `${issuer}/v1/authorize?client_id=${clientId}&response_type=code&scope=openid%20email%20profile&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${state}:${reqUrl}`;
    res.location(authUrl).sendStatus(302);
  }
  next();
}

Then you’ll implement the ensureAuthenticated() middleware that starts the first step of the authentication process. First, it checks to make sure the user isn’t already logged in. If they are, it just calls next() because there is nothing to do.

If there isn’t a currently logged in user, it builds a URL made up of the issuer, clientId, redirectUrl, and state properties from the .env file. It makes a call to the /v1/authorize endpoint of the issuer’s URL. It then redirects to that URL. This is a login page hosted by Okta. Kind of like when you’re redirected to Google to log in with Google as the identity provider. The URL that it will call when login is done is the http://localhost:3000/auth/callback URL that’s in the .env file. I’ve also tagged the original URL that the user was going to when they were redirected to the state query parameter. This will make it easy to direct them back there once they’ve logged in.

Next, you’ll need to implement the auth/callback route to handle the result from the login page and exchange the authorization code that you’ll receive from Okta. Create a file called authcontroller.ts in the controllers folder with the contents:

import { Router, config } from './../deps.ts';

const auth = new Router();

// users routes
auth.get('/callback', async (req, res) => {
  const { issuer, clientId, clientSecret, redirectUrl, state } = config();
 
  if (req.query.state.split(':')[0] !== state) {
    res.send('State code does not match.').sendStatus(400);
  }
 
  const tokenUrl: string = `${issuer}/v1/token`;
  const code: string = req.query.code;
 
  const headers = new Headers();
  headers.append('Accept', 'application/json');
  headers.append('Authorization', `Basic ${btoa(clientId + ':' + clientSecret)}`);
  headers.append('Content-Type', 'application/x-www-form-urlencoded');
 
  const response = await fetch(tokenUrl, {
    method: 'POST',
    headers: headers,
    body: `grant_type=authorization_code&redirect_uri=${encodeURIComponent(redirectUrl)}&code=${code}`
  });
 
  const data = await response.json();
  if (!response.ok) {
    res.send(data);
  } else {
    const user = parseJwt(data.id_token);
    req.app.locals.user = user;
    req.app.locals.isAuthenticated = true;
    res.location(req.query.state.split(':')[1] || '/').sendStatus(302);
  }
});

function parseJwt(token:string) {
  const base64Url = token.split('.')[1];
  const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
  const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
    return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
  }).join(''));

  return JSON.parse(jsonPayload);
};

export default auth;

There is actually a lot less going on here than you might think. First, the imports bring in the Router from Opine and read in the .env file again. Then they instantiate the router like in the usercontroller.ts file. The next thing I did was deconstruct the config object to make it easier to use the values. Next, I checked the state query parameter to make sure it matches. This helps ensure that Okta is the one who sent the authorization code. Then the authorization code gets pulled off the query string with req.query.code.

What happens next is a call to the token endpoint. You’ll send the authorization code in a POST request to Okta to exchange for an ID Token. So, here I’ve built some headers for the request. The most important is the Authorization header that has a value of Basic {yourClientId}:{yourClientSecret} the client ID and secret are base64 encoded. Then the POST call is finally made to the token endpoint with those headers and a body with a grant_type of authorization_code—the same redirect URL as before—and the authorization code I just received from Okta.

The fetch() call returns a promise resolved with the then() function. I get the response object’s JSON value, make sure the call was successful, parse the id_token value with the parseJwt() function below and stick it into a local variable called user. Finally, the user is sent to the URL they originally requested before being redirected for authentication.

Run the Deno Application

You can now run the application from the terminal again with:

deno run -A index.ts

Profile Page

Once it’s running you will be able to click on the profile link on the home page and will be redirected to Okta’s hosted login page. Once you’ve logged in, you’ll be directed back to the profile page and you’ll see your ID token’s properties displayed in a list.

Learn More About Deno and Node.js

The completed code for this post, is on GitHub. If you want to learn more about Deno and Node, check out the links below.

If you liked this post, follow us on Twitter and subscribe to our YouTube Channel so you never miss any of our awesome content!

Changelog:

Okta Developer Blog Comment Policy

We welcome relevant and respectful comments. Off-topic comments may be removed.