Skip to content

Instantly share code, notes, and snippets.

@bpicolo
Last active July 2, 2021 13:38
Show Gist options
  • Save bpicolo/d339a4fb586090b22b71be917765ae21 to your computer and use it in GitHub Desktop.
Save bpicolo/d339a4fb586090b22b71be917765ae21 to your computer and use it in GitHub Desktop.
Data-race free Auth0 in any Javascript framework
/**
* I'm not a fan of Auth0's suggested implementation for Javascript presentation frameworks -
* they tie in too directly (using Vue lifecycle hooks, React useEffect).
* This leads to having to restructure your entire app around your authentication implementation
*
* React and Vue primarily serve as presentation layers, but your application domain logic often
* can and should live outside of your presentation layer. This makes authenticating with Vue lifecycle hooks
* or useEffect an antipattern for me, because it leads to either parameter drilling down to your business logic,
* or race conditions between your business logic and view lifecycles.
*
* This abstraction is agnostic of JS framework and guarantees we can manage
* the whole auth lifecycle without race conditions
*
* This is a fairly thin wrapper, which mostly exists for the sake of helping you manage
* the fact that `createAuth0Client` is an async function. Having a synchronous constructor
* which wraps that makes it easier to manage auth usage across the app
*/
import createAuth0Client, { Auth0Client } from "@auth0/auth0-spa-js";
const domain = process.env.VUE_APP_AUTH0_CLIENT_DOMAIN; // or wherever your env vars are located (REACT_APP_...)
const clientId = process.env.VUE_APP_AUTH0_CLIENT_ID;
const audience = process.env.VUE_APP_AUTH0_AUDIENCE;
export class AuthenticatedUser {
// first name and last name may be terrible naming choices
// depending on your target countries/audience
public firstName: string;
public lastName: string;
public email: string;
constructor(email: string, firstName: string, lastName: string) {
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
}
}
class AuthenticationService {
private domain: string;
private clientId: string;
private redirectUri: string;
private audience: string;
constructor(
domain: string,
clientId: string,
audience: string,
redirectUri: string
) {
this.domain = domain;
this.clientId = clientId;
this.redirectUri = redirectUri;
this.audience = audience;
}
/**
* Guarantee we only instantiate a single, shared client (nice)
* that's impossible to access any other way.
*/
private getClient = ((): (() => Promise<Auth0Client>) => {
let _clientPromise: Promise<Auth0Client> | null = null;
return (): Promise<Auth0Client> => {
if (!_clientPromise) {
_clientPromise = createAuth0Client({
domain: this.domain,
client_id: this.clientId,
redirect_uri: this.redirectUri,
audience: this.audience,
});
}
return _clientPromise;
};
})();
async getUser(): Promise<AuthenticatedUser | null> {
const user = await (await this.getClient()).getUser();
return user
? new AuthenticatedUser(
user.email || "",
user.given_name || "",
user.family_name || ""
)
: null;
}
async isAuthenticated(): Promise<boolean> {
const client = await this.getClient();
return client.isAuthenticated();
}
async handleRedirectCallbackIfNeeded(): Promise<{
appState?: unknown;
} | null> {
if (
window.location.search.includes("code=") &&
window.location.search.includes("state=")
) {
const client = await this.getClient();
return await client.handleRedirectCallback();
}
return null;
}
async getAccessToken(): Promise<string | null> {
const client = await this.getClient();
const token = await client.getTokenSilently();
return Promise.resolve(token);
}
async login(): Promise<void> {
const client = await this.getClient();
return client.loginWithRedirect();
}
async logout() {
const client = await this.getClient();
return client.logout();
}
export default new AuthenticationService(
domain,
clientId,
audience,
window.location.origin
);
// Usage with axios
import authenticationService from "@/services/authentication-service";
import axios from "axios";
const instance = axios.create({
baseURL: process.env.VUE_APP_API_HOST, // or REACT_APP_ / wherever you keep your env vars
});
instance.interceptors.request.use(async (config) => {
const token = await authenticationService.getAccessToken();
if (token) {
config.headers["Authorization"] = `Bearer ${token}`;
}
return config;
});
export default instance;
// usage with vue-router
import Vue from "vue";
import VueRouter, { RouteConfig } from "vue-router";
import Home from "@/views/Home.vue";
import authenticationService from "@/services/authentication-service";
Vue.use(VueRouter);
const routes: Array<RouteConfig> = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/app",
name: "App",
meta: {
requiresAuth: true,
},
component: () =>
import(/* webpackChunkName: "about" */ "../views/app/App.vue"),
children: [],
},
];
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes,
});
router.beforeEach(async (to, from, next) => {
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (await authenticationService.isAuthenticated()) {
return next();
}
return next({
path: "/login",
});
}
return next();
});
export default router;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment