DEV Community

Ilia Mikhailov
Ilia Mikhailov

Posted on • Edited on • Originally published at codechips.me

Svelte Auth0 integration in 66 LOC

Yep, you read correctly. Pathetic 66 lines of code. You think I am lying? Well, code analysis tools don't lie. Let's ask one.

$ scc src/auth.js
───────────────────────────────────────────────────────────────────────────────
Language                     Files       Lines     Blanks    Comments      Code
───────────────────────────────────────────────────────────────────────────────
JavaScript                       1         107         19          22        66
───────────────────────────────────────────────────────────────────────────────
Total                            1         107         19          22        66
───────────────────────────────────────────────────────────────────────────────
Estimated Cost to Develop $1,556
Estimated Schedule Effort 1.314907 months
Estimated People Required 0.140209
───────────────────────────────────────────────────────────────────────────────

Enter fullscreen mode Exit fullscreen mode

Told ya! But I personally think other numbers are waaay more interesting. scc tells me that it takes around 1,5K USD to develop this in approximately 6 weeks by only 1/10 of me. It took me 20 minutes to write the code (I am very skilled at skimming the docs and copy-pasting). So, if I do the math correctly my hourly rate would be .... astronomic! Mind blown.

Note to self: raise the hourly rate pronto!

Ok, sorry for sidetracking. Back to the code. So Auth0 has a generous free tier in case you need to add authentication to your app. It also has all these cool starter packs for all these cool frameworks, but not for little Svelte. Turns out we don't really need one! Hah!

Show me the code!

Let's boot up and add Auth0 dependency.

$ npx degit sveltejs/template svelte-auth0
$ cd svelte-auth0 && npm i
$ npm add -D @auth0/auth0-spa-js

Enter fullscreen mode Exit fullscreen mode

Now, create an auth.js file in src dir. Here it is in all its glory with comments and all.


// src/auth.js

import {onMount, setContext, getContext} from 'svelte';
import {writable} from 'svelte/store';
import createAuth0Client from '@auth0/auth0-spa-js';

const isLoading = writable(true);
const isAuthenticated = writable(false);
const authToken = writable('');
const userInfo = writable({});
const authError = writable(null);
const AUTH_KEY = {};

// Default Auth0 expiration time is 10 hours or something like that.
// If you want to get fancy you can parse the JWT token and get
// token's actual expiration time.
const refreshRate = 10 * 60 * 60 * 1000;

function createAuth(config) {
  let auth0 = null;
  let intervalId = undefined;

  // You can use Svelte's hooks in plain JS files. How nice!
  onMount(async () => {
    auth0 = await createAuth0Client(config);

    // Not all browsers support this, please program defensively!
    const params = new URLSearchParams(window.location.search);

    // Check if something went wrong during login redirect
    // and extract the error message
    if (params.has('error')) {
      authError.set(new Error(params.get('error_description')));
    }

    // if code then login success
    if (params.has('code')) {
      // Let the Auth0 SDK do it's stuff - save some state, etc.
      await auth0.handleRedirectCallback();
      // Can be smart here and redirect to original path instead of root
      window.history.replaceState({}, document.title, '/');
      authError.set(null);
    }

    const _isAuthenticated = await auth0.isAuthenticated();
    isAuthenticated.set(_isAuthenticated);

    if (_isAuthenticated) {
      // while on it, fetch the user info
      userInfo.set(await auth0.getUser());

      // Get the access token. Make sure to supply audience property
      // in Auth0 config, otherwise you will soon start throwing stuff!
      const token = await auth0.getTokenSilently();
      authToken.set(token);

      // refresh token after specific period or things will stop
      // working. Useful for long-lived apps like dashboards.
      intervalId = setInterval(async () => {
        authToken.set(await auth0.getTokenSilently());
      }, refreshRate);
    }
    isLoading.set(false);

    // clear token refresh interval on component unmount
    return () => {
      intervalId && clearInterval(intervalId);
    };
  });

  // Provide a redirect page if you need.
  // It must be whitelisted in Auth0. I think.
  const login = async redirectPage => {
    await auth0.loginWithRedirect({
      redirect_uri: redirectPage || window.location.origin,
      prompt: 'login' // Force login prompt. No silence auth for you!
    });
  };

  const logout = () => {
    auth0.logout({
      returnTo: window.location.origin
    });
  };

  const auth = {
    isLoading,
    isAuthenticated,
    authToken,
    authError,
    login,
    logout,
    userInfo 
  };

  // Put everything in context so that child
  // components can access the state
  setContext(AUTH_KEY, auth);

  return auth;
}

// helper function for child components
// to access the auth context
function getAuth() {
  return getContext(AUTH_KEY);
}

export {createAuth, getAuth};

Enter fullscreen mode Exit fullscreen mode

In the onMount hook we are setting up a timer to refresh the access token so it doesn't expire and things suddenly stop working. This is useful for long running apps like dashboards.

Now replace App.svelte with the following contents.

<!-- App.svelte -->

<script>
  import { createAuth } from './auth';

  // Go to Auth0 to get the values and set everything up.
  // Make sure all callback urls are set correctly.
  const config = {
    domain: 'your-auth0-tenant.auth0.com',
    client_id: 'auth0-client-id',
    audience: 'https://my-facebook-killer.io'
  };

  const {
    isLoading,
    isAuthenticated,
    login,
    logout,
    authToken,
    authError,
    userInfo
  } = createAuth(config);

  $: state = {
    isLoading: $isLoading,
    isAuthenticated: $isAuthenticated,
    authError: $authError,
    userInfo: $userInfo ? $userInfo.name : null,
    authToken: $authToken.slice(0, 20)
  };
</script>

<style>
  main {
    padding: 1em;
    max-width: 240px;
    margin: 0 auto;
  }

  @media (min-width: 640px) {
    main {
      max-width: none;
    }
  }
</style>

<div>
  <div>
    {#if $isAuthenticated}
      <button on:click={() => logout()}>Logout</button>
    {:else}
      <button on:click={() => login()}>Login</button>
    {/if}
  </div>

  <h2>State</h2>
  <pre>{JSON.stringify(state, null, 2)}</pre>
</div>

Enter fullscreen mode Exit fullscreen mode

Done. Start the app (npm run dev) and hopefully everything should work.

Routing

If you need auth, you probably need routing too. Let's add one.

Now, there are quite a few routing solutions available. I chose one with the shortest name - yrv. Also because the slogan spoke to me - Your routing bro! Nobody has called me "bro" in a long time. Hey, I live in Sweden!

Yrv is small, sweet and with great documentation. The author says that "v" stands for Svelte (you make the connection), but I think that it secretly stands for vato. If you look at the author's GH profile pic he looks really badass, a true OG.

Ok, let's throw Yrv in the mix (npm add -D yrv) and add --single argument in your package.json in order to support SPA router.

 "start": "sirv public --single"
Enter fullscreen mode Exit fullscreen mode

Change your App.svelte to this.

<script>
  import { createAuth } from './auth';
  import { Link, Router, Route } from 'yrv';

  const config = {
    domain: 'your-auth0-tenant.auth0.com',
    client_id: 'auth0-client-id',
    audience: 'https://my-facebook-killer.io'
  };

  const {
    isLoading,
    isAuthenticated,
    login,
    logout,
    authError
  } = createAuth(config);

  $: disabled = !$isAuthenticated;
</script>

<style>
  main {
    padding: 1em;
    max-width: 240px;
    margin: 0 auto;
  }

  @media (min-width: 640px) {
    main {
      max-width: none;
    }
  }

  :global(a[aria-current]) {
    font-weight: 700;
  }
</style>

<main>
  <div>
    {#if $isLoading}
      <p>Loading ...</p>
    {:else if $authError}
      <p>Got error: {$authError.message}</p>
    {:else if !$isAuthenticated}
      <button on:click={() => login()}>Login</button>
    {:else}
      <button on:click={() => logout()}>Logout</button>

      <div>
        <Link href="/">Home</Link> |
        <Link href="/settings">Settings</Link> |
        <Link href="/hello/handsome">Hello!</Link>|
        <Link href="/foobar">Not Found</Link>

        <div>
          <Router {disabled}>
            <Route exact path="/">
              <h2>Home</h2>
              <p>This is the root page</p>
            </Route>
            <Route exact path="/settings">
              <h2>Settings</h2>
              <p>This is the settings page</p>
            </Route>
            <Route exact path="/hello/:name" let:router>
              <h2>Hola {router.params.name}!</h2>
              <p>Nice to see you</p>
            </Route>
            <Route fallback>
              <h2>404 Not Found</h2>
              <p>Sorry, page not found</p>
            </Route>
          </Router>
        </div>
      </div>
    {/if}
  </div>

</main>


Enter fullscreen mode Exit fullscreen mode

Now, when you start the app you should see our routing in action. Boom!

Conclusion

This example is bare and sloppy and might not solve all your problems. As the saying goes - there are many ways to skin a cat or ... peel a banana, or something. View it as inspiration. You can use it as base and adjust to your needs. The main point is that sometimes you don't need an NPM package because it's easier to write the code ourselves. I feel that this is often the case with Svelte. What Svelte community needs is more examples and inspiration and not NPM packages. But on the other hand, there is the discoverability aspect that NPM brings to the table, but that's a story for another day!

Hope you learned something new or got some inspiration and as always, thanks for reading!

Top comments (3)

Collapse
 
stordahl_ profile image
Jacob Stordahl

I'm getting an error that auth0 is null when attempting to call the login function. Any idea what would be causing this? Really love this implementation

Collapse
 
codechips profile image
Ilia Mikhailov

Sounds like auth0 is not intialized. Is onMount called? console.log is your friend here :)

Collapse
 
breucode profile image
breucode

Thanks for the article. That really helped me!