S
Supabase8mo ago
Az3K

Edge Functions & JWT Claims Auth Hook timeouts

Hello, I'm currently implementing an edge functions which is being called as an auth hook to get custom claims. The code is working fine currently, but I would like to also update the user/app metadata in the supabase db for the users and when I try to call the supabase auth admin to update the user by id, it always timeouts, the logs shows that the hook took more than 5 seconds every single time I try to update and it's impossible to have great error handling, as the edge function is being called, logs are showing up to the supabase auth admin call, then timeouts so I have no idea why it can timeout. In the Auth log in supabase dashboard, I always get : 422: Failed to reach hook within maximum time of 5.000000 seconds And in the Postgres logs I get: process 65551 acquired ShareLock on transaction 1346 after 2707.894 ms process 65551 still waiting for ShareLock on transaction 1346 after 1000.072 ms connection authorized: user=supabase_auth_admin database=postgres And they corresponds to the UPDATE query made my the updateUserById call. Here is the code snippet:
Admin client
const supabaseClient = createClient(
Deno.env.get("SUPABASE_URL") ?? "",
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "",
{
auth: {
autoRefreshToken: false,
persistSession: false
}}
);

...

const { data: user, error } = await supabaseClient.auth.admin.updateUserById(
user_id,
{ app_metadata: { subscription: subscription } }
)
Admin client
const supabaseClient = createClient(
Deno.env.get("SUPABASE_URL") ?? "",
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "",
{
auth: {
autoRefreshToken: false,
persistSession: false
}}
);

...

const { data: user, error } = await supabaseClient.auth.admin.updateUserById(
user_id,
{ app_metadata: { subscription: subscription } }
)
Thank you.
41 Replies
Az3K
Az3KOP8mo ago
Just to let you know, executing the EXACT same query using the PostgREST api, it works totally fine... Here is the query extracted from the ShareLock logs:
[
{
"file": null,
"host": "{REDACTED}",
"metadata": [],
"parsed": [
{
"application_name": null,
"backend_type": "client backend",
"command_tag": "UPDATE waiting",
"connection_from": "127.0.0.1:57337",
"context": "while updating tuple (0,8) in relation \"users\"",
"database_name": "postgres",
"detail": "Process holding the lock: 65539. Wait queue: 65551.",
"error_severity": "LOG",
"hint": null,
"internal_query": null,
"internal_query_pos": null,
"leader_pid": null,
"location": null,
"process_id": 65551,
"query": "UPDATE \"users\" AS users SET \"raw_app_meta_data\" = $1, \"updated_at\" = $2 WHERE users.id = $3",
"query_id": -4854828171011581000,
"query_pos": null,
"session_id": "",
"session_line_num": 3,
"session_start_time": "2025-01-13 18:10:27 UTC",
"sql_state_code": "00000",
"timestamp": "2025-01-13 18:10:28.932 UTC",
"transaction_id": 1347,
"user_name": "supabase_auth_admin",
"virtual_transaction_id": "10/2343"
}
],
"parsed_from": null,
"project": null,
"source_type": null
}
]
[
{
"file": null,
"host": "{REDACTED}",
"metadata": [],
"parsed": [
{
"application_name": null,
"backend_type": "client backend",
"command_tag": "UPDATE waiting",
"connection_from": "127.0.0.1:57337",
"context": "while updating tuple (0,8) in relation \"users\"",
"database_name": "postgres",
"detail": "Process holding the lock: 65539. Wait queue: 65551.",
"error_severity": "LOG",
"hint": null,
"internal_query": null,
"internal_query_pos": null,
"leader_pid": null,
"location": null,
"process_id": 65551,
"query": "UPDATE \"users\" AS users SET \"raw_app_meta_data\" = $1, \"updated_at\" = $2 WHERE users.id = $3",
"query_id": -4854828171011581000,
"query_pos": null,
"session_id": "",
"session_line_num": 3,
"session_start_time": "2025-01-13 18:10:27 UTC",
"sql_state_code": "00000",
"timestamp": "2025-01-13 18:10:28.932 UTC",
"transaction_id": 1347,
"user_name": "supabase_auth_admin",
"virtual_transaction_id": "10/2343"
}
],
"parsed_from": null,
"project": null,
"source_type": null
}
]
and here is my new code to update the user app metadata:
const { data, error } = await supabaseClient
.from('auth.users')
.update({
raw_app_meta_data: { subscription },
updated_at: new Date().toISOString()
})
.eq('id', user_id)
const { data, error } = await supabaseClient
.from('auth.users')
.update({
raw_app_meta_data: { subscription },
updated_at: new Date().toISOString()
})
.eq('id', user_id)
So I assume something is wrong in the function called
garyaustin
garyaustin8mo ago
You can't update app metadata with .update. Your .from with auth.users will not run at all. Your code seems to be updating a public users table.... Do you have one? The auth schema is not available to the REST API. And the way you access other schemas is .schema() but that won't help for auth. Your edge function seems to be attempting the correct way. Do you have trigger functions on auth.users?
Az3K
Az3KOP8mo ago
Well I'm going to check again but I always get hook timeout when running the getUserById, can the hook timeout and the query still go through? My main issue here is that auth hook if timed out, the login is failed, and I really need to update the app metadata in the db
garyaustin
garyaustin8mo ago
You'll need to provide more info on what you are up to. It is possible there is a conflict with the auth hook calling your edge function and you trying to update auth.users with the admin call. What hook are you using?
Az3K
Az3KOP8mo ago
JWT Claims
garyaustin
garyaustin8mo ago
I would not attempt to call an edge function in that. That is run for every JWT refresh.
Az3K
Az3KOP8mo ago
The main logic is login via discord, and process additional check (roles and servers condition) before allowing the login
garyaustin
garyaustin8mo ago
What are you trying to update into app metadate? That is still not a good thing to do in the claim hook. It runs every JWT refresh which could be minutes if you have a faster expire time.
Az3K
Az3KOP8mo ago
What would you recommend then? Executing a server action in my nextjs app during exchange code process and signout if not allowed?
garyaustin
garyaustin8mo ago
Is this when a new user is created or each signin?
Az3K
Az3KOP8mo ago
The server is a paid community, and I need to gather the specific roles upon login to implement a RBAC webapp
garyaustin
garyaustin8mo ago
I would check in your serverside signin code. Your other option might be an update trigger on auth.users as that logs signins (updates a last signed column). A user can be signed in for months though by default.
Az3K
Az3KOP8mo ago
Yes but when that signin occurs and the auth.users is updated, will the JWT already be generated?
garyaustin
garyaustin8mo ago
You can kill the process at that point. I don't think you can modify the jwt other than to modify auth.users NEW data in the trigger, which would modify the jwt.
Az3K
Az3KOP8mo ago
Okay, I have to check. Because the whole point of it, is that the discord oauth would be successful but to allow the login to my app, I need to check the roles AND update the claims before the jwt is generated or signing out
garyaustin
garyaustin8mo ago
So you could do your check, then change raw_app_meta_data in the NEW object on the update trigger (checking the sign time column is changing) and then your JWT back to the user will reflect that. You can't add custom claims though there.
Az3K
Az3KOP8mo ago
Okay, I'll implemented this and get back to you. Even using the community package? for custom claims I've been using this for past projects
garyaustin
garyaustin8mo ago
You could also have the custom claims hook get data from a table and have the update trigger modify the table the hook uses. The community package would work in the update trigger. I would first add an update trigger and raise log 'new=%',NEW; to see if that fires when you need.
Az3K
Az3KOP8mo ago
I see, the edge functions were using mainly to make requests and code easier than just run everything within psql function
garyaustin
garyaustin8mo ago
Don't do that on auth calls. Especially jwt claim one.
Az3K
Az3KOP8mo ago
okay but trigger's fine?
garyaustin
garyaustin8mo ago
Auth times out in a couple of seconds if not finished. So there is risk if the edge function is slow or then signin will error out.
Az3K
Az3KOP8mo ago
That's why I created that thread :pain: but yeah I suppose I have to do everything within a trigger like I used to in other projects
garyaustin
garyaustin8mo ago
Your first one is interesting if somehow calling auth.users while auth is in process caused a deadlock.
Az3K
Az3KOP8mo ago
It happens everytime with that updateUserById inside the edge function more than 3s is not normal
garyaustin
garyaustin8mo ago
You might not be able to update auth.users if Auth has a transaction going on. I don't know the details of their process.
Az3K
Az3KOP8mo ago
for a simple query like this one
garyaustin
garyaustin8mo ago
That is why I think it is a deadlock. That is probably the sharelock message, but not something I've dealt with.
Az3K
Az3KOP8mo ago
I could also use a trigger before insert in auth.sessions right? but I think if session is created, jwt is already generated no?
garyaustin
garyaustin8mo ago
The JWT gets generated constantly. I don't know the order auth does all its things, except that app metadata can be changed on the auth.users TRIGGER by changing NEW.
Az3K
Az3KOP8mo ago
Ok I'll test and get back to you if I need anything else
garyaustin
garyaustin8mo ago
supabase/auth github has the source and I've been able to get around enough to see what is up for somethings. You just have to be careful on what you do as it can change at any point. But they document using auth.users trigger.
Az3K
Az3KOP8mo ago
Ok but when jwt is refreshed, auth.users isn't triggered?
garyaustin
garyaustin8mo ago
Sessions is not triggered for a refresh I don't believe. I think that is signin/out... but I've not looked at it. You would need to verify the times on the sessions.
Az3K
Az3KOP8mo ago
Yes, I'll check all good. Thank you
Az3K
Az3KOP8mo ago
@garyaustin Another question, if I move the discord api check within triggers, and maybe some cron psql function to regularly update the subscription value, could I still use the auth hook to just add the subscription claim to the jwt (like in the example in the docs, https://supabase.com/docs/guides/auth/auth-hooks/custom-access-token-hook?queryGroups=language&language=http) ? So everything updating the metadata for the subscription value will be handled within the db itself, and only the auth hook will be used to add to the jwt
Custom Access Token Hook | Supabase Docs
Customize the access token issued by Supabase Auth
garyaustin
garyaustin8mo ago
Yes. The hook is setup mainly to do a quick database operation (read a user privilege table) and set a custom claim in the jwt. It runs on every refresh of the token so needs to be as simple as possible. You could then do your update with cron or the auth.users trigger to update the table and on the next jwt generation it would be reflected. (I would assume the jwt hook would be called AFTER the auth.users trigger function.
Az3K
Az3KOP8mo ago
Hello @garyaustin, Unfortunately I tested both ways, using the community claims package and doing everything within triggers (both before update 'last_sign_in_at' in auth.users and before insert auth.sessions) and using the auth hook to just gather the app_metadata and apply there, but they all have the previous metadata value saved during the login. From what I can see in the supabase/auth source code, User is fetched always before everything during the auth process so it would be : 1. Fetch User data 2. Create Session, and followed by Refresh Token and then finally Access Token And because of both implementation I tried happen in step 2., the actual changes are not reflected when claims are assigned during JWT creation. Basically this logic for login process isn't working, it works fine on signup tho but I really would like to have it working for the login too. (https://github.com/supabase/auth/blob/master/internal/api/token.go#L287)
GitHub
auth/internal/api/token.go at master · supabase/auth
A JWT based API for managing users and issuing JWT tokens - supabase/auth
garyaustin
garyaustin8mo ago
So creating session would be too late to modify data. I would think though on login it changes the last signed in column with an update, so an update trigger on auth.users could check and change the metadata. I'm out for awhile so I can't continue to be less than helpful for a bit.
Az3K
Az3KOP8mo ago
Yes but that's what I already tested, an update trigger on auth.users but even in this case, user data is already pre-fetched. I didn't find other "event' being triggered just before fetching the user data in the auth process yet. Because the general flow is: 1. User data Fetched <-- Tried trigger here (before insert auth.sessions) 2. Session created 3. Refresh Token created (with FK to the newly session) <-- Tried also trigger here (before update 'last_sign_in_at') 5. Update the 'last_sign_in_at' in auth.users 4. Access token created (based on newly refresh token) One workaround I found working, just tested now, might not be the best but at least it works. It is basically forcing refresh session within the auth/callback when code is successfully exchanged for a session. In that case it would force it and re-trigger the jwt generation with the update sub value.
if (code) {
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
const { data, error } = await supabase.auth.refreshSession()
...
if (code) {
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
const { data, error } = await supabase.auth.refreshSession()
...
garyaustin
garyaustin8mo ago
Yeah if they update sign_in_at after they set the refresh token then no way to trigger.

Did you find this page helpful?