Part One: JWTs

About#

An introduction to JWTs and how they are used in Supabase Auth

Watch#

What are JSON Web Tokens (JWTs)?#

JWTs are JSON objects that are encoded and signed and sent around as a string. They are distributed to users of a service or website, who can later show the JWT to the website or service as proof that they have the right to access certain content.

What exactly do we mean when we say "encoded" and "signed"?

Well, the JSON object starts out looking something like this:

1{
2  "sub": "0001",
3  "name": "Sam Vimes",
4  "iat": 1516239022,
5  "exp": 1518239022
6}

sub is the "subject", which is usually the UUID of the user. name is self-explanatory, and iat is the Unix timestamp at which the token was created. Many JWTs will also have an exp, which is the date at which the token is set to expire and can no longer be used. These are some of the standard fields you may find in a JWT, but you can pretty much store whatever you want in there, for example:

1{
2  "sub": "0002",
3  "name": "Věra Hrabánková",
4  "iat": 1516239022,
5  "exp": 1518239022,
6  "theme": {
7      "primary" : "#D80C14",
8      "secondary" : "#FFFFFF"
9  }
10}

Just note that the more data you store in your token, the longer the encoded string will be.

When we want to send the JWT to the user, we first encode the data using an algorithm such as HS256. There are many libraries (and several different algorithms) that can be used to do this encoding/decoding, such as jsonwebtoken. I made a repl here so you can try it for yourself. The signing is as simple as:

1// from https://replit.com/@awalias/jsonwebtokens#index.js
2let token = jwt.sign({ name: 'Sam Vimes' }, 'some-secret')

And the resulting string will look like this:

1eyJhbGciOiJIUzI1NiJ9
2  .eyJzdWIiOiIwMDAxIiwibmFtZSI6IlNhbSBWaW1lcyIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE4MjM5MDIyfQ
3  .zMcHjKlkGhuVsiPIkyAkB2rjXzyzJsMMgpvEGvGtjvA

You will notice that the string is actually made up of three components, which we'll address one by one:

The first segment eyJhbGciOiJIUzI1NiJ9 is known as the "header", and when decoded just tells us which algorithm was used to do the encoding:

1{
2  "alg": "HS256"
3}

The second segment eyJzdWIiOiIwMDAxIiwibmFtZSI6IlNhbSBWaW1lcyIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE4MjM5MDIyfQ contains our original payload:

1{
2  "sub": "0001",
3  "name": "Sam Vimes",
4  "iat": 1516239022,
5  "exp": 1518239022
6}

The last segment zMcHjKlkGhuVsiPIkyAkB2rjXzyzJsMMgpvEGvGtjvA is the signature itself, which is the part used by the website or service provider to verify that a token sent by some user is legitimate. It is produced in the first instance by running the cryptographic function HS256 on the following input:

1HMACSHA256(
2  base64UrlEncode(header) + "." +
3  base64UrlEncode(payload)
4  <jwt_secret>
5)

You can test out minting your own tokens on https://jwt.io.

It is important to note that anyone who possesses the jwt_secret here can create new tokens, and also verify existing ones. More advanced JWT algorithms use two secrets: one for the creation of tokens, and a separate one to verify the validity of signed tokens.

You might wonder why JWTs are so popular all of a sudden. The answer is that with the mass adoption of microservice architecture, we were in a situation where several distinct microservices (APIs, websites, servers, etc.) want to easily validate that a user is who they say they are, or are in other words a "logged-in" user. Traditional session tokens are no use here, since they would require each microservice to either maintain a record of currently valid session tokens or to query a central database each time a user wants to access a resource in order to check the validity of the session token – very inefficient indeed. JWT-based auth in this sense is decentralized, since anyone with the jwt_secret can verify a token without needing access to a centralized database.

Note: One downside of JWTs is that they are not easily voidable, like session tokens. If a JWT is leaked to a malicious actor, they will be able to redeem it anywhere until the expiry date is reached – unless of course the system owner updates the jwt_secret (which will of course invalidate everyone's existing tokens).

JWTs in Supabase#

In Supabase we issue JWTs for three different purposes:

  1. anon key: This key is used to bypass the Supabase API gateway and can be used in your client-side code.
  2. service role key: This key has super admin rights and can bypass your Row Level Security. Do not put it in your client-side code. Keep it private.
  3. user specific jwts: These are tokens we issue to users who log into your project/service/website. It's the modern equivalent of a session token, and can be used by a user to access content or permissions specific to them.

The first token here, the anon key token, is for developers to send along with their API requests whenever they want to interact with their Supabase database.

Let's say you want to read the names of all the rows in a table colors. We would make a request like:

curl 'https://xscduanzzfseqszwzhcy.supabase.co/rest/v1/colors?select=name' \
-H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYxNDIwNTE3NCwiZXhwIjoxOTI5NzgxMTc0fQ.-NBR1WnZyQGpRLdXJfgfpszoZ0EeE6KHatJsDPLIX8c"

If we put this token into https://jwt.io, we see it decodes to:

1{
2  "role": "anon",
3  "iss": "supabase",
4  "iat": 1614205174,
5  "exp": 1929781174
6}

This JWT is signed by a jwt_secret specific to the developer's Supabase token (you can find this secret alongside this encoded "anon key" on your Dashboard under Settings > API page) and is required to get past the Supabase API gateway and access the developer's project.

The idea with this particular key is that it's safe to put into your client, meaning it's okay if your end users see this key – but only if you first enable Row Level Security, which is the topic of Part Two in this series.

The second key, service role key, should only ever be used on one of your own servers or environments, and should never be shared with end users. You might use this token to do things like make batch inserts of data.

The user access token is the JWT issued when you call for example:

1supabase.auth.signIn({
2  email: 'lao.gimmie@gov.sg',
3  password: 'They_Live_1988!',
4})

This token should be passed in addition to the apikey header as an Authorization Bearer header like:

curl 'https://xscduanzzfseqszwzhcy.supabase.co/rest/v1/colors?select=name' \
-H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYxNDIwNTE3NCwiZXhwIjoxOTI5NzgxMTc0fQ.-NBR1WnZyQGpRLdXJfgfpszoZ0EeE6KHatJsDPLIX8c" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjE1ODI0Mzg4LCJzdWIiOiIwMzM0NzQ0YS1mMmEyLTRhYmEtOGM4YS02ZTc0OGY2MmExNzIiLCJlbWFpbCI6InNvbWVvbmVAZW1haWwuY29tIiwiYXBwX21ldGFkYXRhIjp7InByb3ZpZGVyIjoiZW1haWwifSwidXNlcl9tZXRhZGF0YSI6bnVsbCwicm9sZSI6ImF1dGhlbnRpY2F0ZWQifQ.I-_oSsJamtinGxniPETBf-ezAUwDW2sY9bJIThvdX9s"

You'll notice that this token is quite a bit longer, since it contains information specific to the user such as:

1{
2  "aud": "authenticated",
3  "exp": 1615824388,
4  "sub": "0334744a-f2a2-4aba-8c8a-6e748f62a172",
5  "email": "d.l.solove@gmail.com",
6  "app_metadata": {
7    "provider": "email"
8  },
9  "user_metadata": null,
10  "role": "authenticated"
11}

Now that you understand what JWTs are and where they're used in Supabase, you can explore how to use them in combination with Row Level Security to start restricting access to certain tables, rows, and columns in your Postgres database: Part Two: Row Level Security

Resources#

Next steps#