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
───────────────────────────────────────────────────────────────────────────────
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
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};
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>
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"
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>
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)
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 implementationSounds like auth0 is not intialized. Is onMount called? console.log is your friend here :)
Thanks for the article. That really helped me!