anchorWhat is OAuth
For the sake of completeness of this article, let’s pretend you don’t already know. ;)
In simple words, OAuth is an approach for letting users log into your app without having to come up with a username and password. Instead, you rely on them being already authenticated with a popular third party service that offers OAuth (OAuth provider), such as Google and GitHub.
To do so, your app redirects the user to an OAuth provider. The user approves authentication there, and the provider redirects back to the app, where the user is now authenticated.
If you need to, you can look up more information on OAuth online, where you will find plenty of articles for your desired level of depth.
anchorWhat’s Auth.js
Auth.js is a library that aims to be a universal authentication layer for JS frontend and full-stack apps.
It started as NextAuth, built specifically for the Next.js framework (full-stack, React-based), but was later converted into a universal library.
anchorGotcha 0: Authentication vs Authorization
There’s a lot of confusion around the terms "authentication" and "authorization".
From an academic perspective, “authentication” means verifying who the user is, and “authorization” is verifying if the identified user has permission to access or do something.
A nice analogy is attending a regulated facility. In order to get in, you first need to visit a Pass and Registration Office. You show them your id and they issue a daily pass for you. Now you can enter, and every time you need go through a security checkpoint, you present your daily pass to a security guard. The security guard would then make sure that the pass is still valid and might even call the Registration Office to run your pass through their database.
In this analogy, presenting your id to the Pass and Registration office is “authentication” — checking that a person is who they claim to be. Presenting your daily pass to a security guard is “authorization” — checking if a person is allowed to perform an action such as visiting an office. Offices behind security checkpoints represent API endpoints.
The problem is that in practice people don’t really distinguish between “authentication” and “authorization” that much. You can find these terms used interchangeably.
People who know there’s a difference between the two terms but aren’t sure which one is appropriate in which context, have a loophole: simply say “auth”! 😬 The fact that the word “auth” perfectly fits any article and library documentation, proves that the two terms are interchangeable in practice.
anchorGotcha 1: “OAuth” means OAuth 2
There are two versions of the OAuth protocol which are incompatible with each other. The modern version is OAuth 2, naturally. That’s what everyone uses.
When OAuth is mentioned and the version is not specified, it normally means OAuth 2. This article does this, too.
anchorGotcha 2: OAuth Authentication Flow Types
The OAuth standard offers a choice of four authentication flows:
- Client credentials flow — implies passing a key and a secret, useful for server-to-server interactions, such as CI, CLI, etc.
- Device authorization flow — uses a separate device for user authorization, such as a mobile phone. Useful for authenticating devices that have limited user interface: servers, smart TVs, etc.
- Implicit flow — a simple flow, useful for classic frontend-less apps that render HTML on the backend. Should not be used with a frontend, as it would required storing the app secret code in localStorage, which is unsafe. Considered obsolete.
- Authorization code flow — a more involved flow, best suited for web apps. The app secret code is safely stored on the backend, the access token can be stored on the backend too.
We will be using the authorization code flow.
anchorGotcha 3: You Need a Backend!
OAuth authentication cannot be implemented in a frontend-only app without any backend, such as an SPA deployed to GitHub Pages.
This is because the app needs to pass a secret token (essentially, a password) to the OAuth provider, so that the provider recognizes the app. If you added this secret token into the frontend, it would be accessible from the browser to everyone on the internet. A malefactor would be able to extract it and implement a fake copy of your app that authenticates users for real, stealing their auth tokens. As a result, the malefactor would be able to access your app as another user.
For this reason, your frontend app should not authenticate with an OAuth provider directly, but rather leave this job to your backend. It’s your backend that safely stores the secret token and passes it to the OAuth provider.
Luckily, SvelteKit can work as a full-stack app, with a frontend and a backend!
anchorGotcha 4: The Authorization Code Flow Can Be Implemented in Different Ways
If you search the internet for diagrams explaining the OAuth authorization code flow, you’ll notice that they all vary in small details.
This is because this flow can be implemented in different ways.
For example, one could enable a frontend-only app to authenticate with OAuth by setting up a tiny backend microservice such as prose/gatekeeper (that you can even deploy to Heroku for free!) that will be used for one purpose: storing your app’s secret and passing it to an OAuth provider when requested. This approach is not very secure, though, as the access token will have to be stored in localStorage in the browser, unencrypted and easily accessible to physical users and scripts on the same domain.
Many apps and libraries store tokens in localStorage, but some developers and security professionals believe this to be unsafe.
Auth.js stores the access token (used to authorize requests to third party APIs such as GitHub or Google) on the backend side and uses a Secure, HttpOnly cookie to store a session token (used to authorize requests to your own backend). Such a cookie can only be intercepted at the moment of issuing. Once set, it cannot be accessed or copied from browsers' Dev Tools or JS scripts. This substantially improves security.
anchorOAuth Authorization Code Flow with Auth.js
This diagram is different from any other OAuth flow diagram on the internet. It precisely reflects the flow that Auth.js specifically is using.
The user opens your frontend in their browser.
The user clicks the "Sign in with GitHub" button.
Your frontend makes a POST request to your backend to
http://my-app.example.com:5173/auth/signin/github
. This passes the control flow to the backend, which computes sign-in options for the OAuth provider: callback URL, scopes, etc.Your backend makes a 302 redirect to GitHub to:
https://github.com/login/oauth/authorize
, passing a number of query params, including:scope
of permissions to request;client_id
– the id of your app at GitHub;response_type=code
— the type of OAuth flow to useredirect_uri=http://my-app.example.com:5173/auth/callback/github
– your backend endpoint to redirect to.
The user authenticates with GitHub using email and password, if they haven't already.
The user authorizes your app to authenticate with GitHub, if they haven't already.
Yeah, it's a bit of a Pimp My Ride type of situation. 😅GitHub makes a 302 redirect to your backend to
http://my-app.example.com:5173/auth/callback
, passing acode
query param containing a temporary authorization code.Two things happen in parallel here:
- The browser waits for your backend to respond.
- The backend makes a direct request to GitHub, passing the temporary authorization code along with the app's id and secret codes. The browser is not involved with this request.
GitHub verifies the id and secret of your app, as well as the temporary authorization code.
GitHub responds to your backend with an access token.Your backend responds to the broswer with a 302 redirect to a page where the user is supposed to land, e. g.
http://my-app.example.com:5173
.
Here, the backend sets a secure HttpOnly cookie containing a session token that your frontend will use to authorize requests to your backend.Your frontend takes over as the browser loads the page.
It can now use the cookie to authorize requests to your backend.
Requests to GitHub APIs are performed through your backend.
The curious thing to note here is that the authorization code seems redundant.
Normally, the authorization code is used as a measure to prevent the frontend from knowing the app secret and the access token.
But the way Auth.js implements the flow, the OAuth provider never responds to the frontend, only to the backend. This means that steps 7 and 8 could hypothetically be skipped.
Why has OAuth implmented the flow with the authorization code? Many OAuth providers do not support the Implicit flow for web apps because it’s potentially unsafe and only offer the Authorization code flow, so that’s what Auth.js is doing.
anchorGotcha 5: You Should “Create” a Separate “App” with each OAuth Provider for Every Deployment Environment
In order to let your app authenticate with an OAuth provider, you must access the provider’s admin panel and “create an app” in it. This will provide you with an ID and secret token for your app to use.
Some OAuth providers let you define multiple webapp hostnames and callback URLs per app, but some important ones such as GitHub, only allow one hostname and one callback URL per app. For such providers, you need to “create” a separate “app” with the provider for each deployment environment, e.g., local development, staging and production. You will end up with three or more sets of app ids and secret tokens.
anchorGotcha 6: you need a domain for local development
When you run the SvelteKit server for local development, it offers you this url: http://localhost:5173
.
The problem with it is that the localhost
domain is not unique, and some OAuth providers will refuse to work with it.
On top of that, building more than one app on http://localhost:5173
is an inconvenience, since cookies and localStorage entries mix up, forcing logouts and invalid state.
For these two reasons, you should use a unique domain that points at 127.0.0.1
or ::1
(note that multiple domains can do so).
You could use a subdomain of your business domain, such as local.my-app.dev
, but there’s another problem with this.
anchorGotcha 7: Some OAuth Providers Refuse to Work with Unencrypted http
The http
protocol is inherently unsafe and some OAuth providers refuse to work with it.
That’s a problem because setting up https
encryption certificates for local development is a tedious hassle, as it requires either paying for certs, or using short-lived free ones.
There’s a loophole, though! Use a subdomain on the example.com
domain, e. g. my-app.example.com
.
Google, for example, refuses to work with http://local.my-app.dev
because of insecure protocol, but makes an exception for http://my-app.example.com
, as the example.com
domain is dedicated for testing and development purposes and can’t harm any users.
Google, however, also rejects URLs on made-up top-level domains.
anchorHow to Create a Subdomain on Example.com
Of course, you can’t register a custom subdomain on the public example.com
domain for everyone to resolve. But you can fake it for local development.
To do so, locate the system-wide hosts
file in your operating system, open it for editing with superuser rights and append like this:
127.0.0.1 my-app.example.com
127.0.0.1 my-other-app.example.com # you can do more than one if needed
Changes should become effective immediately on save — on your machine only.
See the externals links section in this Wikipedia article for detailed instructions.
anchorConfiguring GitHub
- Visit https://github.com/settings/developers and click
New OAuth app
. - Fill in the form, using the following URLs:
- Application name:
My App (dev)
- Homepage URL: http://my-app.example.com:5173
- Authorization callback URL: http://my-app.example.com:5173/auth/callback/github
- Application name:
- On the next screen, copy the
Client ID
to your password manager. - Click
Generate a new client secret
. Copy the resulting secret token to your password manager. GitHub will not let you see it again!
Reference documentation for Auth.js GitHub Provider: https://authjs.dev/reference/core/providers_github
anchorConfiguring Google
- Visit https://console.cloud.google.com/.
- In the top-left corner of the page, expand the list of projects and click
New project
. TypeMy App (dev)
and clickCreate
. - On the
Credentials
tab, clickCreate credentials
→OAuth Client ID
. Then SelectWeb application
. - Visit
APIs and Services
→OAuth Consent Screen
https://console.cloud.google.com/apis/credentials/consent and change thePublishing status
of your app toTesting
. - Visit
APIs and Services
→Credentials
https://console.cloud.google.com/apis/credentials - Click
Add URI
forAuthorized Javascript Origins
, use this value: http://my-app.example.com:5173 - Click
Add URI
forAuthorized redirect URIs
, use this value: http://my-app.example.com:5173/auth/callback/google - Copy
Client ID
andClient Secret
from the right sidebar to your password manager. If noClient Secret
exists, create one. - Save.
Reference documentation for Auth.js Google Provider: https://authjs.dev/reference/core/providers_github
anchorConfiguring SvelteKit
Now we know all we need to set up OAuth in SvelteKit. Let’s proceed.
anchor0. Set Up a SvelteKit Codebase
…if you haven’t already.
npm create svelte@latest my-app
cd my-app/
npm i
I’ll be using my-app
as the name of the project and the subdomain. Substitute it for your app’s name.
anchor1. Install Auth.js
npm i -D @auth/core @auth/sveltekit
anchor2. Set Up the Environment Variables
Edit the .env
file in the root of your project, it should look like this:
AUTH_SECRET=77d597e9f8df83cfd5d8faef39f85e64213315ec2b38ef867a1b76f7585f5dda
AUTH_TRUST_HOST=true
GITHUB_ID=686fcbc5d5f00215ad9d
GITHUB_SECRET=1aafbc3a1f485cfaf5927753aaf54f58962b984d
GOOGLE_ID="5af337fd1334-139f9327d891dc5e9686a0d75b5f5662.apps.googleusercontent.com"
GOOGLE_SECRET="5E17CA-3250e5657D80d848eAa65954822C"
AUTH_SECRET
should be a random string. It’s used to encrypt tokens exchanged between your frontend and your backend. Sadly, it’s not used to encrypt the OAuth access token, since an OAuth provider should be able to read its unencrypted value. It should be at least 32 alphanumeric characters long, the longer the better. You can generate a random string with this console command:
If you don’t haveopenssl rand -base64 32
openssl
, you can generate a random string online here. That’s theoretically unsafe, but should be fine for pet projects.AUTH_TRUST_HOST
must betrue
for all enviromnents except Vercel deployments.GITHUB_ID
— the id of your “app” on GitHub.GITHUB_SECRET
— the secret token of your app on GitHub. Note that GitHub will only show it once! Don’t lose it, or you’ll have to regenerate it.GOOGLE_ID
— the id of your “app” on Google.GOOGLE_SECRET
— the secret token of your app on Google.
anchor3. Create a Server Hook to Initialize AuthJS
Create src/hooks.server.ts
:
import { SvelteKitAuth } from '@auth/sveltekit';
import GitHub from '@auth/core/providers/github';
import GoogleProvider from '@auth/core/providers/google';
import { GITHUB_ID, GITHUB_SECRET, GOOGLE_ID, GOOGLE_SECRET } from '$env/static/private';
export const handle = SvelteKitAuth({
providers: [
GitHub({ clientId: GITHUB_ID, clientSecret: GITHUB_SECRET }),
GoogleProvider({ clientId: GOOGLE_ID, clientSecret: GOOGLE_SECRET }),
],
});
VS Code with the official Svelte extension installed will automatically recognize the existence of $env
imports when you start the SvelteKit dev server with npm run dev
.
If you already have a handle
hook, you should use the sequence function to combine two hooks into one.
anchor4. Load the Session from Your Backend
In src/routes/+layout.server.ts
:
export const load = async (event) => {
return {
session: await event.locals.getSession()
};
};
anchor5. Use the Session in Your Frontend
In src/routes/+layout.svelte
or whatever layout or page you need it in:
<script>
import { page } from "$app/stores";
</script>
{#if $page.data.session} {#if $page.data.session.user?.image}
<span
style="background-image: url('{$page.data.session.user.image}')"
class="avatar"
/>
{/if}
<span>
<small>Signed in as</small>
<br />
<strong>
{$page.data.session.user?.email ?? $page.data.session.user?.name}
</strong>
</span>
<a href="/auth/signout" class="button" data-sveltekit-preload-data="off">
Sign out
</a>
{/if}
anchor6. Add a Sign-in Button
Add the {:else}
clause to {#if $page.data.session}
:
{:else}
<span class="notSignedInText"> You are not signed in </span>
<a href="/auth/signin" class="buttonPrimary" data-sveltekit-preload-data="off">
Sign in
</a>
{/if}
Note that /auth/signin
is a route automatically generated by @auth/sveltekit
. You don’t need to define it.
anchor7. Protect a Route from Anonymous Access
Any page such as src/routes/protected/+page.svelte
:
<script>
import { page } from "$app/stores";
</script>
{#if $page.data.session}
<h1>Protected page</h1>
<p>
This is protected content. You can access this content because you are signed
in.
</p>
<p>Session expiry: {$page.data.session?.expires}</p>
{:else}
<h1>Access Denied</h1>
<p>
<a href="/auth/signin" data-sveltekit-preload-data="off">
You must be signed in to view this page
</a>
</p>
{/if}
Alternatively, you can redirect from a +layout.js
, which arguably provides better access management. See this StackOverflow question for possible ways to do it.
anchor9. You Rock!
Run your dev server with npm run dev
and try it out.
If you have wired everything correctly, it should work!
anchorGotcha 8: Automatically Generated Routes
Note that routes under /auth
such as:
/auth/callback/[provider]
/auth/signin
…are routes automatically generated by @auth/sveltekit
. You don’t need to define them.
The former is a backend route where OAuth providers should redirect the user to.
The latter is a frontend route containing sign-in buttons for OAuth providers of your choice. The HTML/CSS of buttons will be generated for you by the package.
anchorGotcha 9: Auth.js Is Evolving
As of 2023, the library is relatively new. Its core is more or less stable, but its SvelteKit integration claims to be under active development.
The implication is that it may receive breaking changes. In order to upgrade to future versions you will need to follow the upgrade guide or release notes to rewrite some of your code accordingly.
Keep that in mind.
anchorDemo App
@auth/sveltekit
has an official demo app:
- Live: https://sveltekit-auth-example.vercel.app
- Source: https://github.com/nextauthjs/sveltekit-auth-example
anchorNext Steps
The goal of this article is to help you connect all the parts together to bootstrap your OAuth adventure.
Some of aspects not covered are: