Supabase generic OIDC authentication
Using the Keycloak auth provider in Supabase to implement custom OIDC auth
Reader’s note: This post is more technical than usual, and probably useful to fewer than 20 people worldwide, but here we are anyway.
How did I end up in this auth hellscape again?
I’m building an app to help people play chess in-person (chessr.co). Naturally, it’d be really cool for people to log in with their chess.com account.
Chess.com has a guide for Getting started with Chess.com OAuth 2.0 Server1, however, since we use Supabase for authentication and our database, we’re limited by Supabase’s standard auth providers: Google, Apple, Facebook, etc.
Meanwhile, there is a laundry list of OAuth Provider requests to Supabase, and it’s unclear when or which OAuth providers they’ll support. There are ~100 comments that range from requesting providers from “log in with TikTok” to “log in with Quickbooks.”
As fortune would have it, I found a GitHub issue for Signing in with a generic OAuth2/OIDC provider, which describes a workaround using the Keycloak2 auth provider, but the details are light. I was eventually able to make this work, and wanted to share the results in case it’s helpful to others.
Now, time for one of those confusing diagrams
I’ve never been able to read sequence diagrams correctly, but I created one anyway, which seemed to help solidify my own understanding of what was happening.3
Here are the characters involved in this classic coming-of-age story:
Web UI is the frontend javascript where our user who wants to authenticate is clicking buttons.
Supabase is a managed service that provides Easy Auth™️ and our database and other things.
Backend API is our Node.js backend that acts as a Simple translation layer between (1) Supabase’s Keycloak integration, and (2) Chess.com’s OAuth2/OIDC APIs.4
OIDC Provider is the identity provider, in this case chess.com, that implements the OAuth2 authorization protocol.
A good framework for understanding these auth flows is: “a series of redirects until it finally works.”
Here we go:
Let’s crack some eggs
This is going to get messy, but I promise it’s as detailed as it needs to be in order to function properly, and also for you to understand how everything fits together.
The code for the frontend is pretty straightforward, and I’ll add the backend code to support this if anyone needs it.
(1) Enable the Keycloak auth provider
Enable the Keycloak provider in Supabase UI.
The "Client ID" and "Client Secret" should come from your OIDC identity provider. In my case, there isn’t a Client Secret, so I just filled in some nonsense characters.5
The "Realm URL" should be your app's backend URL. I used ngrok for dev, then switched to a prod URL later.
(2) Web client initiates OAuth flow to Supabase
The web browser kicks everything off with the signInWithOAuth library call from Supabase’s javascript library.
This tells Supabase to look at the Keycloak config we added in step 1 in order to use the REALM_URL to call our API in the next step.
(3) Supabase calls our backend API to initialize auth
This is main the reason for using Keycloak. In #6547, a user notes it as “one of the few providers where you can configure custom URL.”
Supabase calls "${REALM_URL}/protocol/openid-connect/auth" with a state query parameter.
(4) Our API handles the call and forwards the response
Our backend handles the call at "/protocol/openid-connect/auth" and forwards the call to the third-party OIDC /auth endpoint, e.g. "https://oauth.chess.com/authorize", and returns that to Supabase.
Apparently what you send to the OIDC "/auth" endpoint varies based on the provider and their requirements, but there are some themes:
state: Query parameter sent by Supabase. Forward it along to the third-party OIDC provider.
client_id: Should match the client_id you have with your OIDC provider, and the same one that is configured with "keycloak" in the Supabase UI.
redirect_uri: Where the web client should be redirected after the OAuth consent screen with a successful code. This should be a backend URL like "/auth/v1/callback", and it’s configured with your OIDC provider.
response_type: The string "code" (or potentially something else, depending on what your OIDC provider supports)
scope: The string "openid profile" (or whatever your OIDC docs recommends)
code_challenge: In some OAuth2 flows, such as PKCE, you need to generate a code verifier and code challenge during this phase to send to your third-party OIDC identity provider. You should store a mapping of "state" to the "code verifier" so you can look it up later.
code_challenge_method: Varies based on OIDC provider recommendations.
The end result of this is that the browser is redirected to the OIDC provider’s auth consent screen.
(5) Web client redirects to “/auth/v1/callback”
(This URL often needs to be configured with your OIDC provider)
Our web client is redirected to "/auth/v1/callback" with state and code query parameters populated by the OIDC provider after the consent is granted.
We need to use this info to call back to Supabase. See the "Callback URL (for OAuth)" from the Supabase UI "keycloak" configuration.
If you previously created a “code verifier,” then look it up using the “state” variable. Then, store a mapping of the "code" to the "code verifier" so we can look it up later.
Finally, redirect the client to the Supabase Callback URL, forwarding along the state and code query parameters.
(6) Supabase calls our backend API to get tokens
Supabase calls "${REALM_URL}/protocol/openid-connect/token" with the code query parameter.
(7) Our API handles and forwards the response
First we use the code sent by Supabase to look up the “code verifier,” if needed. Then we forward the call to the OIDC /token endpoint, e.g. "https://oauth.chess.com/token", and return the result to Supabase.
Chess.com returns a few tokens, including an ID token and an authorization token. Supabase will use the authorization token later for auth, and we'll need to be able to look up the associated ID.
So we parse the "/token" response and grab the "id_token" (which is a JWT), and store a lookup of the "idToken.sub" to the "idToken" itself in our database.
(8) Supabase calls our backend API to get user info
Supabase calls "${REALM_URL}/protocol/openid-connect/userinfo" with the Authorization header.
We parse the Authorization header to get the JWT, verify it, and pull out the "sub" field. Then we use the "sub" to look up the relevant idToken.
Finally, we return an object with:
sub: Subject from the ID token
email: Email for the user
email_verified: true (required by Supabase)
name: string of the user's name
picture: URL of the user's profile pic
Boom! Now our user is fully authenticated to Supabase with a user record. In this case, chess.com’s API doesn’t return the user’s email, so we had to make one up. Ideally, the identity provider can give you verified email info.
Wrapping up
I reached out to Supabase support about this, and eventually got ahold of someone on their Auth Team who said:
Currently the auth server cannot act as an OIDC relying party, but we do intend on adding it sometime this year.
Not the most helpful, but companies like Supabase always have a hundred priorities to juggle, so I totally understand where they’re coming from.
I eventually figured out the arcane wizardry above, and asked what they thought, and they responded:
If it unblocks you I won't say it's a bad idea. The main consideration would be understanding it may one day require you to migrate the users & provider if you decide to use our generic provider in the future.
Fair enough!
OAuth and OIDC are used somewhat interchangeably here. All I know is that OIDC is basically OAuth2 with extra sauce.
I still don’t know exactly what Keycloak is but glad it’s here.
With any luck, I’ll still understand this a month from now.
We didn’t have a backend before, and I tried to avoid creating one, but I’ll save you some time and tell you it’s necessary.
Apparently you don’t need a Client Secret for certain auth flows, which was news to me!
Thanks a lot for posting this. I have to implement this too soon and I couldn't find a good tutorial either. What kind of backend do you use for "Realm URL" etc.? I am just using supabase as backend so I only have edge-functions. Is it possible to set it up with them somehow?