Redwood and Supabase Auth Example

Hi All,

I completed the full tutorial (parts 1 and 2) and switched out Supabase Auth for Netlify Identity. It is a pretty basic example but should help someone get up and running with Supabase and Redwood?

I built a super quick and dirty modal for the login/logout with Supabase - please don’t use this code in production as it needs work from an a11y perspective before it would be ready.

The big drawback is that I removed the roles functionality from the tutorial code because Supabase does not yet seem to have an easy way to implement roles like Netlify does. I am pretty sure this could be done by setting up a user table that was connected to the auth table - but will leave that for others to demo.

1 Like

Hi Eric and thanks for trying out RBAC in both Supabase and Netlify!

I am working on a Supabase-based app currently and will encounter this scenario soon, too.

Unlike Auth0 and Netlify, Supabase doesn’t send user role or permissions on the access_token.

Supabase has great security in the way of roles and policies that ensure only allowed actors can access or interact with your data at level specific level.

It’s important to recognize that when Prisma connects to the database it is doing so with service_role permission – it can query, delete, add, drop a table etc. Thus the app middleware needs to enforce things. Supabase’s js client passes along the JWT and then determines their PG role and policies from it and enforces what the can happen at a low level.

But, we don’t/can’t? set that JWT when querying – though it would be great if we can. I happened to run across this discussion yesterday asking for ’ Support for Postgres’ SET command in raw mode/PG RLS policies in general`.

But until then … :wink:

For example, the anon roles can’t do things that an authenticated roles can.

But - (as I understand thing) this is all done/setup/enforced at the Postgres database level.

There isn’t necessarily a mapping between this database role/policy and a user “app/business” role (ie, author) and permission (can add article – insert on articles, but not publish aka update the status).

Actually perhaps Paul @kiwicopple can help here to suggest the best way we might capture these “user roles” and map them (if needed to policies … since we don’t necessarily need to use them:

Policies are a tool. In the case of “serverless/Jamstack” setups, they are especially effective because you don’t have to deploy any middleware at all.

What is supa-nice is that users are in a separate auth schema and perhaps one can store UserRole there and maybe map to a user and some underlying role/policies?

This also make it harder for any user info to leak out via queries or GraphQL nested resolvers.

We’d have to make the getCurrentUser do a query per transaction (and maybe cache in context) to do a rawQuery to join in on the auth schema and User- > UserRole with the sub user id (Prisma can only query the public schema, not across schemas … which is another issue.

Any ideas @kiwicopple ? As I said, I am going to get into RBAC w/ RW and Supabase w/in the next week or two so would love to figure out a solid pattern and add to the RW Cookbook.

1 Like

@dthyresson Thank-you for such a thorough response! I will look forward to the cookbook/example you can share with the community in the coming weeks.

I am really excited about the potential Redwood + Supabase offers to solo-developers like me! I’ll keep you posted if I manage to discover anything along the way but I am probably going to just build out some ideas to learn RW better and then worry about the auth provider I use at a later date. I really like the idea that with Supabase I can manage all those “backend” services in one spot and would prefer to use them if I can!

I just happened across some RLS and JWT docs in postgraphile:

https://www.graphile.org/postgraphile/security/#how-it-works

begin;
set local role app_user;
set local jwt.claims.role to 'app_user';
set local jwt.claims.user_id to '2';

-- WE PERFORM GRAPHQL QUERIES HERE

commit;

If that could be set when Prisma runs that would be wonderful as would set that to have RLS.

2 Likes

@dthyresson Yeah I have been searching around for ways to implement this with Prisma. It looks like they have an open issue. Might also be worth throwing your support on the issue in the prisma repo Supporting Postgres' SET across queries of a request · Issue #5128 · prisma/prisma · GitHub

1 Like

Thanks for the tag @dthyresson

Can you elaborate a bit more on this one? Do you want to add content into the JWT, so that you can access it on the client? If that is the case then you can add it either to auth.users.raw_app_metadata or auth.users.raw_user_metadata - these will both be encoded into the JWT when the user session is created

Perhaps we work together to come up with a “definitive” example for this one? Now that we are past Launch Week I have some time to build up our examples. There are a few different ways to handle RBAC inside your app, and so it will be good to put our brains together to figure out which is the most viable option for most developers (here is a good article on RBAC with Policies: Designing the most performant Row Level Security schema in Postgres | by Caleb Brewer | Medium)

@kiwicopple and @dthyresson having an official example app would be great. As a front-end person who has played around with serverless functions and some “mid-stack” stuff I am loving the way that combining Supabase and Redwood is gradually exposes me to more backend thinking/tech.

The starting point I posted above is from the end of the second tutorial and I just swapped out the Netlify Auth for Supabase Auth (minus RBAC as stated above).

Hey All,

While I do really look forward to supabase’s row level secuirty to having Prisma support (or the other way around), one way you could implement RBAC is by storing roles in the DB. As described here: Cookbook - Role-based Access Control (RBAC) : RedwoodJS Docs

This is how I’m doing (very limited) RBAC on my project. I’m using enums for Roles, and its really helpful with typescript

enum Roles {
  OWNER
  ADMIN
  MEMBER
  VIEWER
}

model TeamMember {
  id     String  @id @default(cuid())
  user   User    @relation(fields: [userId], references: [id])
  userId String
  role   Roles
#. hide other attributes for brevity
}

I’d really welcome that @kiwicopple.

I should have started with the what and why and not the how.

  • Have a way to assign “app/business” user roles to users
  • That that made available in the RW’s currentUser’s user_metadata (either from JWT direct or enriched with info and set) so that the web side can determine if the user “hasRole()” to gain access to protected areas of the app

If that is the case then you can add it either to auth.users.raw_app_metadata or auth.users.raw_user_metadata - these will both be encoded into the JWT when the user session is created

  • when querying/updated data via Prisma, have some way of identifying the user so that RLS and policies can still be enforced at the PG level

Some points to know:

  • Prisma can query across schemas, so it’s not that easy to join the public and auth schemas in Supabase. You have to compose the SQL yourself via a rawQuery

Maybe we adapt the Blog w/ RBAC Tutorial that currently uses Netlify but a fork that uses Supabase PG and Auth instead? I could even add storage for photos next :wink:

The Uses Netlify Identity Trigger Serverless function calls to assign roles when signing up feature could also showcase some upcoming SB features when a new user is created (hint).

1 Like

Heyho, a wild Prisma (employee) appears in this thread 3 weeks after it got pointed out to him as he forgot about the tab… seriously. Hi!

My knowledge about RLS with Supabase is minimal, but while this tab was waiting to be rediscovered we at Prisma found a possible temporary workaround for how to execute the SET queries reliably before sending actual queries: github.com/prisma/prisma/issues/5128#issuecomment-826679950 But we need your help to confirm that would actually work. (If it does, we could further build another workaround with middlewares to make it easy to write all queries like that.)

Could you maybe take a look at this and see if this makes sense?

(And then of course we hope to get proper support for this into the Prisma Client API)

2 Likes

Thanks @janpio for this update an example:

const [ignore, userList, updateUser] = await prisma.$transaction([
  prisma.$executeRaw(`SET current_user_id = ${currentUser.id}`),
  prisma.post.findMany({ where: { title: { contains: 'prisma' } } }),
  prisma.post.count(),
])

@kiwicopple I looked at the RLS and policy docs here: Auth | Supabase Docs and

Supabase provides a special function in Postgres, auth.uid(), which extracts the user’s UID from the JWT. This is especially useful when creating Policies.

But since it looks like that relies on the request:

CREATE OR REPLACE FUNCTION auth.uid()
 RETURNS uuid
 LANGUAGE sql
 STABLE
AS $function$
  select nullif(current_setting('request.jwt.claim.sub', true), '')::uuid;
$function$

perhaps

create policy "Users can update their own profiles." 
  on profiles for update using (
    auth.uid() = id
  );

would be

create policy "Users can update their own profiles." 
  on profiles for update using (
   current_user_id = id
  );

given the above Prisma example when setting current_user_id ahead of the query?

I guess I’ll have to figure out a good way of testing this – will try this week.

1 Like

Following this thread with much interest. As already stated, RBAC is very important building apps today. I am working in the innovation sector and we are building new apps on a monthly basis. Having a toolset like Supabase in combo with RedwoodJS is something that gives us the necessary boost that I am really looking forward to.

Any updates on this one?

Hi @saendu and welcome!

I have not had a chance unfortunately, but I am working on an app where I could use RLS (row level security).

Note that RedwoodJS does have RBAC support Role-based Access Control (RBAC) | RedwoodJS Docs and .

What we’re wanting to try is to see if that role security can also extend into the Postgres level in policies rather than just in the service business logic api side.

For Supabase, you’d want to set your “app roles” in app_metadata as a collection of roles:

   "app_metadata": {
        "provider": "email"
      },
      "user_metadata": null,
      "role": "authenticated"
    },

as in:

{
  "exp": 1598628532,
  "sub": "1d271db5-f0cg-21f4-8b43-a01ddd3be294",
  "email": "example+author@example.com",
  "app_metadata": {
    "roles": ["author"]
  },
  "user_metadata": {
    "full_name": "Arthur Author",
  }
}

If you happen to try this approach out, please let us know how it worked out.

Hi @dthyresson

Sorry for the confusion with my statement about the “importance” of RBAC. I fully understand that redwood already has RBAC in place. I already tried using it with Supabase with success.
For that matter I was following the cookbook and enriching the user object with roles and for my case also with the uuid from the users table in supabase like so:

export const getCurrentUser = async (decoded) => {

  const uuid = decoded.sub

  const userRoles = await db.userRole.findMany({
    where: { user: { uuid: uuid } },
    select: { name: true },
  })

  const roles = userRoles.map((role) => {
    return role.name
  })

  const { id: userId } = await db.user.findUnique({
    where: { uuid: uuid },
    select: { id: true  },
  })

  const enrichedUserObj = { ...decoded, roles, userId };
  return enrichedUserObj
}

With this approach security is enforced on the service side rather than the Postgres side directly. Or in other words, hitting the the Supabase API directly would still return “unfiltered” data since no RLS is enforced. Enabling RLS and writing security rules like this create policy "Individuals can view their own todos." on todos for select using (auth.uid() = user_id); would not really do any good, since RW is always operating with a service token.

If I understood you correctly, you are trying to address this problem by finding a way combining RLS with RBAC, correct?

Yes, that’s correct - most/all implementations have sort of been done as you have, though with Supabase I do setup the trigger from auth.user to publuc.User and then keep roles also in the auth schema and set on the auth.user in raw_app_metadata. This saves all the db lookups get getUserRequest.

Also, when using Supabase be sure to use Connection Pooling.

Think this was just a misunderstanding in that when I thought you asked about “any updates” on RLS.

1 Like

@saendu Thanks for the example. I have implemented similar however I am having trouble extracting and using the accountId. What I would like to do is navigate users to an account page after logIn based on their accountId – see code snippets.

Should accountId be part of the currentUser object?

// api/src/lib/auth.js

export const getCurrentUser = async (decoded) => {
  const uuid = decoded.sub

  const { id: accountId } = await db.user.findUnique({
    where: { uuid: uuid },
    select: { accountId: true },
  })

  const enrichedUserObj = { ...decoded, accountId }
  return enrichedUserObj
}
// src/components/LoginForm

  const onSubmit = async (input) => {
    const email = input.email
    const password = input.password
    await logIn({ email, password }).then(() => {
      navigate(routes.account({ id: currentUser.accountId }))
    })

A more generic question… when logged-in as a Supabse user what is the expected result of the following?

query {
  redwood {
    currentUser
    }
  }

I get the following even though I can display the logged-in user email on screen

{
  "data": {
    "redwood": {
      "currentUser": null
    }
  }
}

This is what I see in the console

And this works as expected

{isAuthenticated && <>{currentUser.email}</>}

Hi @0x1a4f7d58 Yes, you should see the result of the getCurrentUser there.

You can have a look at the Auth Playground for Supabase to test signups and see the result.

https://redwood-playground-auth.netlify.app/supabase

Are you certain

  const { id: accountId } = await db.user.findUnique({
    where: { uuid: uuid },
    select: { accountId: true },
  })

  const enrichedUserObj = { ...decoded, accountId }

That accountId has a value here - perhaps

import { logger } from '@redwoodjs/api/logger'

and then logger.debug({accountId}, 'This is the user account id')?

in your getCurrentUser().

Also, another way to do this is to

const { userMetadata } = useAuth()

And look there.

That also has more info, specifically info from the raw_user_meta_data filed on the users table in Supabase’s auth schema.

image

Any data you set there (say from a trigger on the public schema side) would be available.

So you could set that and save a db call on every getCurrentUser request.

Also, if you are using Supabase in a serverless environment, please be sure to use connection pooling.

See: https://redwoodjs.com/docs/connection-pooling#supabase

1 Like

@dthyresson Thanks a lot. The logger turned out to be v helpful and I managed to resolve the issue. Good to know about {userMetadata} too :ok_hand:

1 Like

@saendu You can ignore this. I got it working with the following… and am no-longer trying to navigate based on currentUser. Using the context instead :slight_smile:

//api/src/lib/auth.js

export const getCurrentUser = async (
  decoded,
  { _token, _type },
  { _event, _context }
) => {
  const uuid = decoded.sub

  const { accountId } = await db.user.findUnique({
    where: { uuid: uuid },
    select: { accountId: true },
  })

  const { slug } = await db.account.findUnique({
    where: { id: accountId },
    select: { slug: true },
  })

  // logger.debug(
  //   {
  //     payload: { decoded, accountId, slug },
  //   },
  //   'Current User details'
  // )

  const enrichedUserObj = { ...decoded, accountId, slug }
  return enrichedUserObj
}