supabase / supabase-flutter

Flutter integration for Supabase. This package makes it simple for developers to build secure and scalable products.
https://supabase.com/
MIT License
735 stars 184 forks source link

Sign in with Custom (third party) JWT #479

Closed henry2man closed 1 year ago

henry2man commented 1 year ago

Is your feature request related to a problem? Please describe. In our project the authentication must be managed by a third party service. We want to enable RLS and use our own tokens with custom policies in order to fetch data, so we need to use a custom JWT in order to access Supabase.

We was trying to reproduce this article (https://medium.com/@gracew/using-supabase-rls-with-a-custom-auth-provider-b31564172d5d) but we don't find the apropiate API. In a nutshell:

  1. Obtain the Supabase JWT Secret: The JWT secret, which is used to sign JWTs, can be found in the Supabase Dashboard under Settings > API > Config.
  2. Store the signed JWT when a user authenticates.
  3. Call supabase.auth.setAuth() (or Dart/Flutter equivalent API) from the client: When making calls to the Supabase API, it's necessary to call setAuth using the JWT from step 2 to make calls as the user.
  4. Use the JWT data: The data from the JWT payload can be used in either your RLS policy or in column default values.

Describe the solution you'd like

I want to set my custom session JWT in Supabase, so I can implement custom RLS policies. I want to do it only once (during login process).

Ideally we should have a public method API for setting our custom token into auth.currentSession.accessToken, so _getAuthHeaders() will take it into consideration (so no header "override" will be needed):

https://github.com/supabase/supabase-flutter/blob/0b7b1c6d0a84047de5dc3f295c3e1c18ff61dd1d/packages/supabase/lib/src/supabase_client.dart#L287-L295

Describe alternatives you've considered

N/A, the external Auth provider is a requisite. We didn't found an alternative in current API

EDIT: @bdlukaa contributed a possible workaround:

Supabase.instance.client.rest.setAuth(...).from(...)...

EDIT2: HERE is a fully working workaround: https://github.com/supabase/supabase-flutter/issues/479#issuecomment-1559170692

EDIT3: Another possible workaround that can be applied only once until a proper API is added:

client.headers['Authorization'] = "Bearer $jwt";

Additional context

https://github.com/supabase/supabase-flutter/issues/479#issuecomment-1559117327 https://medium.com/@gracew/using-supabase-rls-with-a-custom-auth-provider-b31564172d5d https://github.com/supabase/supabase-js/issues/553 https://github.com/supabase/gotrue-js/issues/701 https://supabase.com/docs/guides/realtime/extensions/postgres-changes#custom-tokens

bdlukaa commented 1 year ago

As a workaround, you can try to do Supabase.instance.client.rest.setAuth(...).from(...)....

henry2man commented 1 year ago

As a workaround, you can try to do Supabase.instance.client.rest.setAuth(...).from(...)....

@bdlukaa I didn't know you were here too! 😊 Thanks, I'll try it and see if it works...

henry2man commented 1 year ago

I've been trying this to work but there is something missing and/or broken.

With this snippet:

print(await client.rest.setAuth(
  null,
).from('rls_enabled_table').select());

print(await client.rest.setAuth(
  "xxx.yourcustomjwt.ssss",
).from('rls_enabled_table').select());

And this policy:

I'm sure the custom jwt is valid because Supabase is accepting it (I made a mistake setting the signing key and the service returned a 401 error code). The snippet execution returns always an empty array. It only returns data in both calls if I disable RLS for the table.

The token payload is this:

{
  "iss": "https://example.com",
  "iat": 1684481355,
  "nbf": 1684481355,
  "exp": 1685086155,
  "data": {
    "user": {
      "id": "xxxx"
    }
  }
}
henry2man commented 1 year ago

Got it 🚀! This is the workaround (at least for now):

Function name: active_user Schema: public Definition:

DECLARE
    _user_id bigint;
BEGIN
    _user_id := (current_setting('request.jwt.claims', true)::json->'data'->'user'->>'id')::bigint;
    RETURN EXISTS (
        SELECT 1 FROM "public"."users" 
        WHERE id = _user_id AND enabled = true
    );
EXCEPTION WHEN others THEN
    RETURN false;
END;
Captura de pantalla 2023-05-23 a las 14 08 15

With this code, using JWT tokens generated with Supabase secret will give you RLS enabled with custom authentication.

henry2man commented 1 year ago

Another possible workaround that can be applied only once until a proper API is added:

Supabase.instance.client.headers['Authorization'] = "Bearer $jwt";
henry2man commented 1 year ago

In an internal conversation @bdlukaa recommended me to use setAuth() method on each call.

But I've double checked source code and, actually, I think that setting headers in SupabaseClient seems a better option because they apply not only for PostgresClient, but also to functions, storage, realtime...

https://github.com/supabase/supabase-flutter/blob/0b7b1c6d0a84047de5dc3f295c3e1c18ff61dd1d/packages/supabase/lib/src/supabase_client.dart#L73-L102

If Supabase ensures that setting headers will be always an override of any other possible header, this could be the safest method. In this was the case, we may add some clarification / documentation at least in source code

https://github.com/supabase/supabase-flutter/blob/0b7b1c6d0a84047de5dc3f295c3e1c18ff61dd1d/packages/supabase/lib/src/supabase_client.dart#LL72C4-L72C4

and/or, even better, in docs.

What do you think?

taylorjdawson commented 1 year ago

How would one accomplish this using the javascript sdk?

henry2man commented 1 year ago

How would one accomplish this using the javascript sdk?

In the original article, the author said this:

https://medium.com/@gracew/using-supabase-rls-with-a-custom-auth-provider-b31564172d5d#:~:text=3.%20Call%20setAuth,Cookies.get(%22mytoken%22))

Call setAuth() from the client When making calls to the Supabase API, make sure to call setAuth using the JWT from step 2 to make calls as the user.

Using the js-cookie package (imported as Cookies):

supabase.auth.setAuth(Cookies.get("mytoken"))

NOTE: in this case a cookie is used to store the token, but this could change in other platforms.

henry2man commented 1 year ago

Interesting, it looks that setAuth() were removed from current goTrue client implementation:

https://github.com/supabase/gotrue-js/blob/master/src/GoTrueClient.ts

But it was present on previous versions:

https://github.com/supabase/gotrue-js/blob/edcd2ffde6dfe81d02700a52a79e635a5ab8839d/src/GoTrueClient.ts#L389

henry2man commented 1 year ago

Interesting, it looks that setAuth() were removed from current goTrue client implementation:

Confirmed: https://github.com/supabase/gotrue-js/pull/340

I have to read the PR well because there was a lot of controversy when removing this method

henry2man commented 1 year ago

More on this:

https://github.com/supabase/supabase-js/issues/553 https://github.com/supabase/gotrue-js/issues/701 https://supabase.com/docs/guides/realtime/extensions/postgres-changes#custom-tokens

TL;DR: Two approaches are discussed: per request custom client vs one time "signInWithToken"

Ideally I think we should have a common solution between clients in order to have some coherence between SDKs. @bdlukaa @taylorjdawson can you ask Javascript folks?

We could survive with workarounds...

taylorjdawson commented 1 year ago

Looking through the JS code the access_code from the session gets passed to the same headers as the original setAuth method did. However, you cannot simply set the access_code as you also have to set some refresh_token this is where I am blocked


const { data: result, error } = await supabase.auth.setSession({
  access_token: "",
  refresh_token: ""
})
``
dshukertjr commented 1 year ago

@taylorjdawson It would be great if you could open an issue on supabase-js repo and discuss this there!

dshukertjr commented 1 year ago

@henry2man Yup, using the heasers setter is the way to go for customer JWT! Note that you are responsible for refreshing the JWT by yourself.

PR is always welcome if you want to add any comments to the SDK!

henry2man commented 1 year ago

And this policy:

  • table: public.rls_enabled_table
  • PERMISSIVE TO authenticated
  • USING (true)

@dshukertjr There is one point that is still unresolved.

With a custom token (using headers for now) I'm able to pass Supabase security controls if I pass custom tokens signed with the same secret as Supabase.

BUT when I try to implement RLS policies, I'm not able to get the authenticated role in Postgres. I don't think this is expected in any manner.

Do tokens need any special payload in order get that role? If this is the case we need to add more context and docs in order to use custom authentication.

Please review my previous experiments:

dshukertjr commented 1 year ago

@henry2man The role comes from the role property of the JWT.

Here is a sample JWT provided by Supabase auth.

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

You can read more about it in our auth deep dive guide here.

henry2man commented 1 year ago

@dshukertjr Thanks, this all makes sense now.