Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to generate access tokens #86

Merged
merged 33 commits into from
Aug 30, 2023
Merged

Conversation

imor
Copy link
Contributor

@imor imor commented Aug 25, 2023

This PR adds a new feature to allow users to generate access tokens which will be used by the dbdev login command (in a separate PR).

In the UI the following changes have been made:

  1. In the navbar drop-down menu a new entry Access Tokens has been added.
image
  1. Clicking Access Tokens will take the user to the access tokens page (initially empty).
image
  1. Clicking New Token will open the new token form.
image
  1. Clicking Cancel will take the user back to the access tokens page. Filling a token name and clicking Create Token will take the user to the newly created token. On this page the user can copy the token to be pasted into the terminal when dbdev login command will be issued. This will also be the only opportunity user will get to copy the token, this is because tokens in the db are stored hashed and can't be shown in plaintext again.
image
  1. Clicking Close will take the user back to the access tokens page. Here user can click Revoke to delete/revoke a token.
image

In the db tokens are stored in app.access_tokens table. RLS policies are setup so that users can only create, read, or delete their own tokens. Tokens are stored salted and hashed in the db. The token returned to the user in the app.new_access_token is in plaintext. This token also contains the user id prefixed to the token. This is done so that a single token can act as both a "user id and password" when the app.redeem_access_token function is given this token. The app.redeem_access_token function validates the token presented to it and issues a jwt with an authenticated claim. This jwt can then be used by the client (dbdev in our case) to make api calls.

@vercel
Copy link

vercel bot commented Aug 25, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
dbdev ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 29, 2023 7:53am

Copy link
Member

@alaister alaister left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking 🔥🔥🔥
Mainly reviewed the FE code – just a couple of small comments 🙂

website/data/access-tokens/create-access-token.ts Outdated Show resolved Hide resolved
website/data/access-tokens/delete-access-token.ts Outdated Show resolved Hide resolved
website/pages/[handle]/_/access-tokens.tsx Outdated Show resolved Hide resolved
Copy link
Contributor

@olirice olirice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the SQL looks great!

lets wait for pgsodium review from michel and auth flow review from stojan before approving

I ran into some minor UI stuff during testing (video below)

Screen.Recording.2023-08-25.at.9.09.03.AM.mov

The 3 I noticed were:

  • Loading... not visible in dark mode
  • clicking Create Token did not redirect or have any visible effect
  • Error loading access tokens after creating an access token

supabase/migrations/20230817110845_access_token.sql Outdated Show resolved Hide resolved
supabase/migrations/20230817110845_access_token.sql Outdated Show resolved Hide resolved
supabase/migrations/20230817110845_access_token.sql Outdated Show resolved Hide resolved
supabase/migrations/20230817110845_access_token.sql Outdated Show resolved Hide resolved
supabase/migrations/20230817110845_access_token.sql Outdated Show resolved Hide resolved
Copy link

@hf hf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, though the password hashing is really not necessary for this use case and can actually severely tank performance of your database if someone puts their mind to it (which isn't that difficult). So I would just use SHA256 in the name of security (CIA model -- Availability is impacted by using password hashing when you don't need it without effective rate limiting or features providing for fairness).

Comment on lines +22 to +41
create policy access_tokens_select_policy
on app.access_tokens
as permissive
for select
to public
using ( auth.uid() = user_id );

create policy access_tokens_insert_policy
on app.access_tokens
as permissive
for insert
to authenticated
with check ( auth.uid() = user_id );

create policy access_tokens_delete_policy
on app.access_tokens
as permissive
for delete
to authenticated
using ( auth.uid() = user_id );
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be easier for maintenance to have one permissive policy for all that enforces the auth.uid() = user_id binding, and then add restrictive policies if something needs stopping (like deletes or the such).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you look carefully, the topmost policy is for the public role and the other two are for authenticated. So, adding a for all policy and then restricting individual operations will need more policies: two for all policies, three policies for restricting insert, update and, delete to public and two policies for restricting select and update to authenticated. Which makes seven policies in total.

supabase/migrations/20230817110845_access_token.sql Outdated Show resolved Hide resolved
Comment on lines 60 to 62
exception when unique_violation then
raise exception 'Token with name `%s` already exists', token_name;
end;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will quite literally never happen, so I would omit it and panic when you see the insert violation, as the token being inserted will never repeat while this universe exists.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sorry, it's about the name. It's fine, then though I don't see the point in enforcing name uniqueness -- it's just for the UX. By making the token's prefix available in the database, debugging will be easier for people.

Copy link
Contributor Author

@imor imor Aug 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have removed the unique constraint on (token_name, user_id) and the exception clause as well.

user_id uuid = auth.uid();
account app.accounts = account from app.accounts account where id = user_id;
token bytea = gen_random_bytes(16);
token_hash bytea = pgsodium.crypto_pwhash_str(token);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto, you don't need to do this. Just hash it with SHA256.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to a SHA256 impl now.

account app.accounts = account from app.accounts account where id = user_id;
token bytea = gen_random_bytes(16);
token_hash bytea = pgsodium.crypto_pwhash_str(token);
token_text text = encode(token, 'hex');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's often useful to encode some identifying information in the token's prefix. For example: dbdev_<token>.

Also what you may want to store in the database is the token's prefix (first 4-5 chars) to help people identify it quickly. It may also be a bit more compact to encode as Base64-URL or something.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented this, tokens now look like this:

image

Still a bit long due to token_id being uuids. I'm trying to convert them to an integer which should make the tokens smaller.

for token_hash in
select t.token_hash from app.access_tokens t where t.user_id = user_id
loop
token_valid := pgsodium.crypto_pwhash_str_verify(token_hash, token);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, no need to do password hash verification, which is also VERY CPU intensive. When you switch to SHA256, make sure you don't just do = as that's not constant-time comparison. I need to look into how this can be achieved in Postgres easily as I'm not finding it but @michelp this may be a good API to add to pgsodium.

In general though, this is really a fringe security benefit for this use case.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh also I suppose libsodium uses Argon2 which is incredibly CPU and memory intensive (depending on the settings). So this is really an easy way to crash the database -- just start generating / verifying tokens all the time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to a SHA256 based impl.

Comment on lines 167 to 171
'aud', 'authenticated',
'role', 'authenticated',
'sub', user_id,
'iat', issued_at,
'exp', expiry_at
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always useful to include the iss claim that will identify who issued the JWT.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, you may want to identify the key that signed it with the kid claim in the JWT's header. This can be useful to easily identity and rotate the key (in Supabase PostgREST we're just about to add support for this).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read the signing key with current_setting('app.settings.jwt_secret', true), but how do I get the key id?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added iss claim value of database.dev.

@imor
Copy link
Contributor Author

imor commented Aug 28, 2023

@olirice I have fixed the invisible Loading... text in dark mode. Regarding the other two problems, it looks like as if the API is not working. Did you run the migrations or reset the db?

Copy link
Member

@alaister alaister left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving on behalf of the frontend changes. LGTM!

One small code question but a non-blocker:

website/pages/[handle]/_/access-tokens.tsx Outdated Show resolved Hide resolved
))}
{isAccessTokensSuccess && accessTokens.length <= 0 && (
<p className="dark:text-white">
No access tokens found. Click &quot;New Token&quot; to create one.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@imor imor merged commit d8db5d1 into master Aug 30, 2023
4 checks passed
@imor imor deleted the feat/generate-access-tokens branch August 30, 2023 06:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants