Seneca-CDOT / telescope

A tool for tracking blogs in orbit around Seneca's open source involvement
https://telescope.cdot.systems
BSD 2-Clause "Simplified" License
96 stars 189 forks source link

Connect Supabase with our SSO login flow #2735

Closed DukeManh closed 2 years ago

DukeManh commented 2 years ago

What happened:

Part of #2555, once we config supabase and have it running, we want to make the Auth service communicate with the Supabase through the supabase-js package to handle new user registration and login.

How to reproduce it (as precise as possible):

humphd commented 2 years ago

This doc is really useful for understanding how Supabase uses JWTs:

https://supabase.com/docs/learn/auth-deep-dive/auth-deep-dive-jwts

Specifically:

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 anon key is OK to put in browser JS (e.g., it's not a secret). You send it with API requests when you talk to the database. It's signed, and you can't get passed Kong (the API gateway) without it. It depends on there being row-level security on the database. Supabase restricts what can be done on a given row of data in the db at the postgres level (e.g., you can say that the anon key can read from some some table, but not write). So we need to add this when we create our tables.

The service role key is only for use on the server. You can't expose this one, as it gives admin rights. It's useful when you need to do things in the database from a backend serivce.

The user access token is the one you get when you use supabase.auth.signIn(). It's an identity token for the user, and you'd pass that along with an apikey to make calls to the db on behalf of a user.

Based on my reading, we have to figure out which approach we want to take with respect to how we access data. If we do it from our microservices, we can use the service role key and it's easy. If, however, we want to do things from the browser directly, we'll have to add another JWT in the client-side code. So our auth provider in the client would hold a JWT for Telescope's microservices, and another for Supabase.

We could also merge the tokens, or choose to use the Supabase token as the same token our microservices accept. This might be the best way to go. I think it would end up looking like this:

  1. User goes to https://telescope.cdot.systems, clicks Sign In
  2. Telescope SSO service redirects user to Seneca's Microsoft SSO Login page
  3. User logs in with Microsoft, gets redirected back to SSO service
  4. SSO service calls Users service to see if we have info for this user in our Telescope db. If we do, a token is created, and the user is redirected back to https://telescope.cdot.systems with that token on the query string

The token we send back in step 4 is currently created by us, but we could use the one from Supabase instead. We'd just have to adjust some of our middleware for checking user roles (e.g., is this an admin). To be honest, that code is not very complex, so it's likely pretty easy to use the policies in the Supabase tokens.

In https://github.com/Seneca-CDOT/telescope/issues/2837 we need to make use of the Server-side auth API, see https://supabase.com/docs/reference/javascript/auth-api-createuser. I'll comment in that issue instead of here.

@joelazwar this probably affects your work on JWT signing. Read this stuff and see what you think, since Supabase seems to only sign with a secret.

joelazwar commented 2 years ago

Did some reading, couldn't find anything on RSA or public/private keys for JWTs in Supabase. I definitely agree with you though, let's use Supabase's token issuer to supply the JWT for all our services.

humphd commented 2 years ago

I'll update this with more details, so @joelazwar can finish it.

In #2844, we have the bulk of the work to get this started, but it's missing a critical piece: we need to be able to connect to Supabase from a browser without exposing the Supabase service role key (only appropriate on a server, where it can be protected). Our SSO service now uses the service role key in @DukeManh's PR to create rows in the telescope_profiles table. Here's what I think needs to happen next, based on the research Duke and I have done.

  1. We define a JWT secret in our environment, see https://github.com/Seneca-CDOT/telescope/blob/master/config/env.development#L279. This variable needs to be exposed to our SSO service via Docker (i.e., unless you explicitly tell Docker to allow access to a variable, it won't forward it to the container). Right now we expose a SECRET variable here https://github.com/Seneca-CDOT/telescope/blob/master/docker/docker-compose.yml#L98, and we need to switch this to use JWT_SECRET. Doing so will mean we can share this same secret between all of our backends (microservices, Supabase).
  2. Now that we have this JWT_SECRET in the SSO service, we need to use it to sign the token we create for a client in https://github.com/Seneca-CDOT/telescope/blob/master/src/api/sso/src/token.js#L51. This will mean that we've signed our token with the same secret that Supabase (i.e., Kong) expects to see when a request comes in.
  3. We need to update the secret we give each microservice to use when verifying a user request. We do that in the environment for each service in the docker/docker-compose.yml file, for example https://github.com/Seneca-CDOT/telescope/blob/master/docker/docker-compose.yml#L23-L26. Instead of using SECRET, we need to forward JWT_SECRET into these microservice's environments. This change needs to happen for every service.
  4. Satellite is expecting to see a SECRET environment variable, see https://github.com/Seneca-CDOT/satellite#configure. We need to modify this to JWT_SECRET, and update all uses of SECRET in the code, see https://github.com/Seneca-CDOT/satellite/search?q=SECRET. We then need to ship a new release of Satellite with these changes.
  5. Next we need to update our front-end code to authorize the Supabase client with the JWT we get back from the SSO service. We would do that by calling supabase.auth.setAuth() in the front-end when we get back the JWT from the SSO service, maybe here: https://github.com/Seneca-CDOT/telescope/blob/master/src/web/src/components/AuthProvider.tsx#L120-L122. We might decide to return a Supabase client object with the useAuth() hook, so callers can use it directly without having to figure out this authentication step.
  6. We need to update the SSO tests that I started to simulate what I just described in 5., see https://github.com/DukeManh/telescope/pull/60/files#diff-1a0d51d80b8a06d29578dde6cd6eb37ce97b5c770d977d60628bb54795c8c603R123. That is, we need to have a browser use the JWT from the SSO service, call supabase.auth.setAuth(token) and then use the authorized client to try doing some call to the Supabase API, and make sure it works. An example might be: update the user's name in the profiles table.
  7. We need to add Row Level Security (RLS) to our telescope_profiles and feeds tables (as well as any others we create). See https://blog.crunchydata.com/blog/a-postgresql-row-level-security-primer-creating-large-policies or other tutorials on how RLS works. If you run our containers now, you can go to http://localhost:8910 to see the Supabase console. Click on "Default Project" to see our Telescope project, then on the "Table editor" to see our tables, or "Authentication" to see our security setup. At http://localhost:8910/project/default/auth/policies you can see our Policies for each table. Currently, RLS isn't enabled for either table, but we can click Enable RLS to add it, and then click "New Policy" to define a policy. There are templates we can use to write these. For us, an example might be: "Users can only modify/delete records in feeds if the id in the JWT matches the id of the owner of the feed." We'll need to manually add these policies to our .sql so they get created automatically, but the GUI is nice for experimenting.
  8. We need to add tests to make sure that these policies work. For example, we could write a test that logs a user in, and tries to edit a feed they don't own, and make sure it fails. Same thing for one they do own, and it should work.

This is all that I can think of at the moment, but it should get you going, @joelazwar. Please jump into this and start knocking out PRs. Don't try to do it all in one big PR.

If you have questions, talk to @DukeManh and myself.

joelazwar commented 2 years ago

Thanks! this is really comprehensive. Will get to work on it and get a PR asap.