S
Supabase•2y ago
Jon3

Create Unique API Key For Users

Hi guys - quick question... I have have a next.js app deployed to vercel, all using SSR and auth with supabase. I am now adding in an API to that app. Thus, in doing this, I'd like to have users have their own individual API (like any other api youve used likely 🙂 and give the user the ability from my front end, to see their api and roll the key when needed. When I receive an API request for the serverless function, in that header i'll have to take this API key they have inputted and somehow use this to find the user i nthe users table with that API key... SO my question is this > I could just do this by adding an "api key" column (or secret and private key) type scenario to my users table right lol? I am just wondering if there is another best practice, or a common way to do this with supabase in terms of authenticating an "actual" key as opposed to just "matching on on file" with the user as a form of "auth".... thanks!
31 Replies
j4
j4•2y ago
Gist
Implement user API keys with Supabase
Implement user API keys with Supabase. GitHub Gist: instantly share code, notes, and snippets.
Jon3
Jon3OP•2y ago
@j4 Thanks man! Will check it out! @j4 @j4 This was an awesome guide! Thanks! -- Quesiton -- Did any of you guys run into any issue when running the "create JWT" part of this? Being that the JWT is in the auth schema as mentioned above as well, am I not supposed to see the JWT's created by the users? Ive added a "select"policy as described... But any which way I go to run this >> let { data, error } = await supabase .rpc('create_api_key', { id_of_user, key_description }) if (error) console.error(error) else console.log(data) When I try and run that request through API or within my JS (ive tried both) i check the logs and get a 204, but NOTHING gets created in the JWT it seems..... anyone else have this issue??
j4
j4•2y ago
I'll have to find my project for that and test again. @Jon3 actually, after thinking about this for a minute, you won't see the JWTs in auth.jwts because they're stored in Vault. The only thing in auth.jwts are references to them. This is by design, for security. However, you can craft a request to Vault and have them returned decrypted, if you're just wanting to verify they're there.
Jon3
Jon3OP•2y ago
@j4 This is the part that slightly loses me... So, ive confirme when i create a new user, it will create secrets in the vault for that new user. I have added the create_api_key SQL to supabase but for some reason, I cannot seem to invoke this....
No description
Jon3
Jon3OP•2y ago
I am supposed to use this JS to invoke that "create_or_replace api_key" function in supabase right?
No description
Jon3
Jon3OP•2y ago
AS a test, Ive made an API request to supabase to tst that function being called correctly... im getting a 204 BUT, seems nothing is happenng within supabase when i make that request... nothing is added to JWT, nor is anything added to secrets... could their be some sort of policy or whatnot dis-allowing this?
No description
j4
j4•2y ago
Here is the code in my demo app. Works for me. This is a SvelteKit page action.
create_api_key: async ({ request, url, locals: { supabase, getSession } }) => {
const session = await getSession()
if (!session) throw redirect(307, '/auth')

const formData = await request.formData();
const key_description = formData.get('description') as string;
/* PRODUCTION: sanitize description before continuing */

if (!key_description) return { error: 'Description required'}

const id_of_user = session!.user.id

const { error } = await supabase.rpc('create_api_key', { id_of_user, key_description })
if (error) return { error: error.message }

throw redirect(303, url.origin)
},
create_api_key: async ({ request, url, locals: { supabase, getSession } }) => {
const session = await getSession()
if (!session) throw redirect(307, '/auth')

const formData = await request.formData();
const key_description = formData.get('description') as string;
/* PRODUCTION: sanitize description before continuing */

if (!key_description) return { error: 'Description required'}

const id_of_user = session!.user.id

const { error } = await supabase.rpc('create_api_key', { id_of_user, key_description })
if (error) return { error: error.message }

throw redirect(303, url.origin)
},
Good question. I thought I had added all of the need steps on that Gist, but. There are no RLS policies for this in my project, because I don't use regular tables in the process.
Jon3
Jon3OP•2y ago
oh ok so you create the api key when they sign in it looks like? I was more/less jsut trying to test with an external api request to see how th ewhole JWT situation worked, but that jsut seesm to be the issue that the create_api_key doesnt seem to run when invoked
Jon3
Jon3OP•2y ago
So just to confirm @j4 , when you run that create_api_key, you DO see something that pops up in the JWT table right?
No description
Jon3
Jon3OP•2y ago
I do see the request is making it to supabase, which is the strangest thing - and its getting a 204 (excuse the other 404's those were jsut testing)
No description
j4
j4•2y ago
yes, I see entries in the table. I assume you've double-checked the function code? Just in case you missed a character or two at either end? Well, I built a page where if you're signed in, you can create as many keys as you'd like, delete them, etc. The function should work from wherever you call it.
Jon3
Jon3OP•2y ago
Yeah i jus tcut and pasted your code It must be something to do with the origin of the request coming from localhost or something.. maybe CORS issue...hmmm i mgonna have to troubleshoot this one
j4
j4•2y ago
My demo is on localhost too.
Jon3
Jon3OP•2y ago
hmmm
j4
j4•2y ago
No description
Jon3
Jon3OP•2y ago
So jsut to cofnirm, it SHOULD show up in that JWT table right if the request has been performed successfully?
j4
j4•2y ago
yep, I have three
Jon3
Jon3OP•2y ago
hmmm weird Well ill let you know if I figure it out... Also, let me know if you can think of any reason why the create_api_key wont run
Jon3
Jon3OP•2y ago
Ive done a "hello world" test to some basic SQL with the same methods (Localhost, API inputs, supabse url etc.) - jsut to see if I was making the request incorrectly. Seems that though that request went through - jsu tto rule out possibl e server errors etc. But it is correct etc which means, likely that the error is something is wrong within the SQL itself (for some reason) in my supabase account
No description
Jon3
Jon3OP•2y ago
Im goign to try playing around with that and add some debug points and whatnot
j4
j4•2y ago
Ok, I'm curious to see if you find anything.
Jon3
Jon3OP•2y ago
OK so FINALLY got it to work haha Nothing wrong with the SQL I have a supabase "types" file in my app that had to be updated due to the addition of the new functions So i updated that and it seems to work in my front end environemnt now i didnt even reaslize that when I generated that types file that it pulls i nteh funtions - Ill have to liekly regenerate the fiel (whiech is no biggie) to ake sure I add all the functions correctly For whateber reason, the request from a curl werent workign still but I think that had to do with the SQL for finding the auth.uid in the SQL - wasnt finding the user because i think that part of teh SQL is for authentication from the browser/ssr im guessing lol? no idea tho
Jon3
Jon3OP•2y ago
@j4 Quick follow up - when you run a "handle" request to use the code here >>
No description
Jon3
Jon3OP•2y ago
What does your response typically look like for example
Jon3
Jon3OP•2y ago
As in, when you perform this in your code - is the response supposed to be formatted a specific way where it shows the user_id??
No description
Jon3
Jon3OP•2y ago
Perhaps im testing with the wrong variable as the API key? Which column is the api_key that the user will use lol? There is ID, secret, and key_id - so its one of those jsut dont know exactly which one to use
j4
j4•2y ago
I'm not sure I'm understanding your question. Let me explain my perspective and we can go from there. Context: The user knows their API key and is sending it in a request header to your API server. The expectation is for you to grab the API key from the header and use it to make whatever DB request the user is asking for. The code screenshot is just showing an example for a GET request, where you'd theoretically take the API key, authenticate a Supabase client and request the user's data from some table. The structure of the data or error response you send back to them is up to you. The auth.key_uid() is a check you'd put in your RLS policy, so that it can handle authentication to the DB, based on the key, and return only that user's data. If you go to the vault schema in your table editor, the decrypted_secret is the JWT. It gets hashed in realtime, to form the API key. I'm sorry about all the other verbiage.. I finally re-read your last question and saw the bit about the column name. If you want to get an API key, then you need to call this function from a button on your frontend, say the user's dashboard or settings page: get_api_key(id_of_user text, secret_id text) e.g. from my SvelteKit demo app
<button on:click|preventDefault={(event) => copyApiKey(event)} id="copy-{key.id}">Copy</button>
<button on:click|preventDefault={(event) => copyApiKey(event)} id="copy-{key.id}">Copy</button>
const copyApiKey = async (event: MouseEvent & {
currentTarget: EventTarget & HTMLButtonElement;
}) => {
const btn_id = event.currentTarget.id
const btn = document.getElementById(btn_id)

const { data, error } = await supabase.rpc('get_api_key', {
id_of_user: session.user.id,
secret_id: btn_id.split('copy-')[1]
})

if (error) console.error(error)
if (data) {
await navigator.clipboard.writeText(data)

btn!.innerHTML = 'Copied'
setTimeout(() => {
btn!.innerHTML = 'Copy'
}, 1500)
}
}
const copyApiKey = async (event: MouseEvent & {
currentTarget: EventTarget & HTMLButtonElement;
}) => {
const btn_id = event.currentTarget.id
const btn = document.getElementById(btn_id)

const { data, error } = await supabase.rpc('get_api_key', {
id_of_user: session.user.id,
secret_id: btn_id.split('copy-')[1]
})

if (error) console.error(error)
if (data) {
await navigator.clipboard.writeText(data)

btn!.innerHTML = 'Copied'
setTimeout(() => {
btn!.innerHTML = 'Copy'
}, 1500)
}
}
Jon3
Jon3OP•2y ago
Ahhh... I had thought that code was fully pre-confiugred to look for sonething called 'table' in the api key, but i should have figured it was a placeholder for my OWN table - which is what made me confused. So to simplify, for the inbound API request its sounds like you (and the best practice in using the supabase secret) is really to just use the users API key to create the client and then the rest of the function is as it is? So, mainly to initialize the client is what that JWT is for...
j4
j4•2y ago
exactly They'll technically be sending an API key, but you could always use the exchange_api_key_for_jwt rpc call to grab the JWT, but that's an extra network request. I just figured adding a tad bit more to an RLS policy is better than making the extra network request.
Jon3
Jon3OP•2y ago
Oh yeah no you definitely made it the best way possible as the added JWT auth could be needed down the line
giorgia
giorgia•8mo ago
Did you find a solution in the end? It's a nightmare currently to just let users access the API with a custom api key

Did you find this page helpful?