Discord OAuth2 with Astro

Use Discord as authentication for your Astro site

We are going to go over some steps to put Login functionality into our Astro website.

Pieces needed that we will cover:

Setup a Discord App

You can create a new Discord App by logging into discord.com/developers and going to the Applications section.

I created app Astronaut

Astronaut App

Allowed Redirects

Go into the OAuth2 > General section

You will need to enter 2 urls that Discord will verify as white-listed

Discord allowed redirects

These are urls verified by Discord so that only you can use this mechanism from allowed sites.

In our case we are setting up 2 so that we can use it from development (locally) and once deployed.

This is all you need for the Discord App.

auth.html

auth.html is a static html page that takes a successful discord login and stores information about the user into the browser’s localStorage. This information can then be used by our site to know who our visitor is.

As you may have noticed in the app redirects, I am pointing to /auth.html, we will create this file in /public/auth.html

Here is its content:

/*  /public/auth.html  */
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.0/axios.min.js"></script>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
    <title>Discord Auth</title>
  </head>
  <body>
  </body>
  <script type="text/javascript">
    // dependencies: axios moment
    // grab url hash coming back from Discord Authentication
    var rt = document.location.hash;
    var token = false;
    var expires = -99;
    // split it and look for access_token, and expires_in
    var parts = rt.split('&');
    parts.forEach(function (p) {
      var vals = p.split('=');
      if (vals.length === 2 && vals[0] === 'access_token') {
        token = vals[1];
      }
      if (vals.length === 2 && vals[0] === 'expires_in') {
        expires = parseInt(vals[1]);
      }
    });
    if (token && expires > 0) {
      // exchange access_token for user_info
      var m = moment().add(2, 'days');
      axios.get('https://discordapp.com/api/users/@me', {
        headers: {
          'Authorization': 'Bearer ' + token
        }
      })
      .then((response) => { // store info client-side
        localStorage.setItem('discord_user', JSON.stringify(response.data));
        localStorage.setItem('expires', m.format());
        var redirTo = localStorage.getItem('afterLogin');
        // redirect to afterLogin value or homepage
        if (!redirTo) redirTo = '/';
        document.location = redirTo;
      });
    }
  </script>
</html>

This page does not have body markup and its purpose is to catch the user-action after they have authenticated to discord. Discord will redirect here and pass to our site an access_token that we can use to grab some information about the user.

Hopefully there is sufficient /*commentary*/ to follow the logic.

After a successful login, this leaves us with a localStorage like:

localStorage state

AuthUser.astro

Before taking a look at the component code, we will setup env variables to not lose developer experience. This is because when developing locally, we will need to use the local redirect url, and use the other one when building for deploy.

Env Variables

Astro can handle this in an elegant way by leveraging what Vite does. Here is Astro’s docs.

So we will create 2 files in our project root:

env files

Be sure to use your Discord App’s Client Id

app client_id

With these 2 files, Astro will know which one to use depending if it is using the dev server, or if it is building the site.

Component code

/*  /src/components/AuthUser.astro  */
---
// grab values from .env file
const { DISCORD_CLIENT_ID, DISCORD_REDIRECT } = import.meta.env;
// build discord auth url
const discord_oauth_url = 
      'https://discordapp.com/oauth2/authorize?client_id='
      + DISCORD_CLIENT_ID
      + '&redirect_uri='
      + DISCORD_REDIRECT
      + '&scope=identify&response_type=token';
let logoutPage = Astro.props.logoutPage || '/logout';
---
<div id="user-logged-out" style="display:none;">
  <a href={discord_oauth_url}>Login <i class="fab fa-discord"></i></a>
</div>
<div id="user-logged-in" style="display:none;">
  <div class="dropdown">
    <span class="btn btn-sm btn-secondary dropdown-toggle" data-bs-toggle="dropdown">
      <img id="user-avatar" height="28" style="margin:0 0 0 -6px;" />
      <span id="username"></span>
    </span>
    <ul class="dropdown-menu dropdown-menu-end">
      <li><a class="dropdown-item" id="action-logout" style="cursor:pointer">Logout</a></li>
    </ul>
  </div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js" type="text/javascript"></script>
<script type="text/javascript" define:vars={{logoutPage}}>
  var expires = localStorage.getItem('expires') || 0;
  var m_expires = moment(expires);
  var now = moment();
  if (now.isBefore(m_expires)) {
    document.getElementById('user-logged-in').style.display = '';
    var user = JSON.parse(localStorage.getItem('discord_user'));
    // document.getElementById('username').innerText = user.username;
    document.getElementById('user-avatar').src = 
      'https://cdn.discordapp.com/avatars/' + user.id + '/' + user.avatar + '.png';
  } else {
    document.getElementById('user-logged-out').style.display = '';
  }
  document.getElementById('action-logout').addEventListener('click', function () {
    localStorage.setItem('discord_user', false);
    localStorage.setItem('expires', '0');
    window.location = logoutPage;
  });
</script>

<style scoped>
#user-logged-out a, #user-logged-out a:visited { color:#ffa; text-decoration:none; }
</style>

front-matter

We are building the url to send the user to discord to authenticate using our Discord App. Our app’s id and redirect url are taken from the proper environment variables file.

We are also setting our logout page, could be specified in the component’s props, or it defaults to /logout

html

The html section contains two main divs that are hidden.

div#user-logged-out will be toggled to show depending on what the javascript finds in the browser’s localStorage.

Logged out

Same with div#user-logged-in, it’ll be toggled on depending on the localStorage state. This div has a bootstrap dropdown with an option to logout.

Logged In

javascript

The javascript will look into localStorage and depending on what it finds, it will show the respective div.

It checks if the expiration time has elasped. If it has expired, then the div#user-logged-out is shown, else div#user-logged-in is shown.

The localStorage state has been set from auth.html after a successful discord authentication.

style

¯\_(ツ)_/¯

Using the component

Now that we have AuthUser.astro, we can use it in our Layout

---
import AuthUser from './../components/AuthUser.astro';
---
<!-- Somewhere in your Layout  -->
<div class="float-end text-light">
  <AuthUser logoutPage="/logout" />
</div>

logout

The logout.astro page will just serve to tell the user that we no longer know who they are.

/*  /src/pages/logout.astro  */
---
import BaseLayout from './../layouts/BaseLayout.astro';
---
<BaseLayout title="See you later :eyes:">
  <h1>Who are you o_O</h1>
</BaseLayout>

Some unexplained things

Where from here

Now with this on your site, tell us what you plan on doing in the comments… Just kidding, no commenting here yet.



Back to Post List
ok!