For my ongoing project Review Planner I had been relying on in-app authentication, storing every piece of user information in the application database. It works for simple authentication, but it becomes very cumbersome when you want to add more features like password reset, email verification, and so on.
There were some options like using an authentication library like Auth.js directly in the app, or using a third-party service like Firebase or Supabase. However, I chose Authentik for the following reasons:
- Self-hosted: I wanted to challenge myself to self-host everything
- Modular: the ability to swap to another provider in the future, if needed
- Well-documented: Authentik seemed to be pretty mature and well-documented
Prerequisites
- A Kubernetes cluster
- A domain name (I used
junyi.mefor this example) - Default storage class set up in the cluster (For example, ceph-rbd) for Authentik’s PostgreSQL and Redis databases
Overview
This post will cover:
- Setting up Authentik
- Implementing OIDC authentication in SvelteKit for existing users
User registration, password reset, and email verification can be added later (consult Authentik documentation), which can all be handled by Authentik.
The flow will look like this:

In our setup, the SvelteKit app will be the RP (Relying Party) and Authentik will be the OP (OpenID Provider).
As shown in steps 2-3, the user will be redirected to Authentik for authentication, so they will see the Authentik login page once they hit the login button in the SvelteKit app.
Set up Authentik
I used the Authentik Helm chart to deploy Authentik on my cluster, with the following configuration:
|
|
Make sure the secret for TLS is created in your cluster using something like cert-manager.
Create a new application in Authentik
When accessing Authentik for the first time, you will be prompted to create an admin user. After that, you can log in to the Authentik dashboard.
On https://auth.junyi.me/if/admin/#/core/applications (replace with your Authentik domain), create a new application with provider.


For the OIDC provider, you can use the default settings, with the following changes:
- Use the explicit grant type
- Add the offline_access scope to allow refresh tokens

Once the application is created, you can see the provider details in the providers tab.

The URLs on the right are used later to configure the SvelteKit app.
Make sure to set the Redirect URIs to your SvelteKit app’s OIDC callback URL, which is usually something like <your-app-domain>/auth/callback. We will implement this in the SvelteKit app later.
At this point, we are done setting up the Authentik OIDC provider. It’s time to move on to the application side.
SvelteKit + OIDC
For my application, I chose not to use another library for authentication. Just some fetches and cookies to get the job done.
The login flow will look like this:
- Create a login button that redirects to the Authentik OIDC provider
- Handle the OIDC callback in the SvelteKit app, then
- Get the access token and refresh token from the URL parameters
- Store the tokens in cookies
- Validate the access token on each request to the SvelteKit app
When the access token expires, the following happens:
- Backend on any route detects the expired/missing access token, and redirects user to
/auth/refresh - Browser automatically sends the refresh token cookie to the
/auth/refreshroute - The
/auth/refreshroute exchanges the refresh token for a new access token- If success, the new access token is stored in the cookie, and the user is redirected back to the original route
- If failed, the user is redirected to
/auth/logout, which will clear the cookies and redirect the user to the main page
By automatically redirecting the user to the /auth/refresh route and redirecrting them back once refresh is done, we can provide a seamless experience for the user without being taken to different places because of expired access tokens.
Configuration
I usually keep my SvelteKit config params in environment variables, and for this project, I have the following:
|
|
These can all be found in the Authentik application provider details page, as shown above.
Login button
For the login button, I just added a simple link on the navbar:
Login button link
|
|
which links to the /auth/login route:
src/routes/auth/login/+page.server.ts
|
|
This will redirct the user to the Authentik login page.
Callback handler
Once the user successfully logs in, they will be redirected back to the SvelteKit app at /auth/callback with a code in the URL parameters.
This route is handled by:
rc/routes/auth/callback/+page.server.ts
|
|
Here, several things happen:
- Get the code from the URL parameters
- Exchange the code for tokens by making a POST request to the token endpoint
- Use the access token to fetch user info from the userinfo endpoint
- Persist the user into the application DB, to store user-specific data in the app later
- Store the access token and refresh token in cookies
Authorization
For each request to a protected endpoint, a validation logic can be added:
src/hooks.server.ts
|
|
decodeAccessToken will validate the access token using the JWKS endpoint, and return the token payload if valid.
src/lib/server/jwt.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { env } from '$env/dynamic/private';
import { jwtVerify, createRemoteJWKSet } from 'jose';
const JWKS = createRemoteJWKSet(new URL(env.AUTH_JWKS_URL));
export type AccessTokenPayload = {
iss: string;
sub: string;
aud: string;
exp: number;
iat: number;
auth_time: number;
acr: string;
}
export async function decodeToken(token: string) {
if (!token) {
return null;
}
try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: env.AUTH_ISSUER,
audience: env.CLIENT_ID,
});
return payload;
} catch (err) {
console.error('Invalid token:', err);
return null;
}
}
export async function decodeAccessToken(token: string): Promise<AccessTokenPayload | null> {
return decodeToken(token) as Promise<AccessTokenPayload | null>;
}
Token refresh
The /auth/refresh route will handle the token refresh logic:
src/routes/auth/refresh/+server.ts
|
|
On a successful refresh, it redirects user to the originally requested page.
If unsuccessful, it redirects user to /auth/logout, which clears the cookies and redirects user to the main page.
It also handles JSON requests, for which the retry logic can be implemented on the client side, so it simply returns a 200 response.
Logout
The logout route does two things:
- Call the Authentik logout endpoint to invalidate the session
- Clear the cookies and user data in the SvelteKit app
First, on the backend, the cookies are cleared and the logout endpoint is called:
src/routes/auth/logout/+page.server.ts
|
|
Then, on the frontend, user data is cleared and user is redirected to the main page:
Logout link
|
|