You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Preface: I'm using a prisma database adapter with Twitch as my auth provider.
Like others before me (#2066#4726), I've ran into a challenging situation when trying to upgrade/expand the scopes my next-auth application was allowed to use.
The current suggestions I've seen say to create/copy-paste custom providers with the extra scopes. This works well if your application only needs two or three different set of static scopes, like if you had "regular scopes" (openid user:read:email) and "admin scopes" (openid user:read:email channel:manage:all email:send), but quickly gets out of hand if you need to apply a combination of scopes in truly incremental fashion. To provide a concrete example, the application I'm writing involves a bunch of independent widgets, and each widget may need different scopes from the auth provider. If we had 5 widgets requesting 5 different scopes, we would need 120 different copy-pasted custom providers to cover all possible combinations.
The core problem is, when the user signs in, next-auth does not update the Account DB model to reflect the new scopes/access_tokens/refresh_tokens.
Here's 2 ways how this might happens:
A developer uses the default scopes, and ships the application to production. Some users sign up. The developer modifies the requested scopes in the provider options and redeploys. The DB entries for existing users, even after they've signed in again, will contain tokens with the old scopes.
Using the signIn() function with a scope override (signIn('twitch', undefined, {scope: 'old:scopes ' + 'my:new:scope'}). After the going through the Oauth flow, the new tokens with the expanded scopes are lost since they were never persisted.
Hopefully you understand the issue by now. Anyway, here's a how I overcame this problem:
I wanted to get a hold of the new tokens that are obtained when the user first signs in. Unfortunately, none of the existing callbacks contain them.
After inspecting the source code for next-auth, I noticed that the OauthUserConfig type has an optional method profile, which is invoked when the user signs in. The second parameter of that function is a TokenSet. This contains the id_token, access_token, refresh_token, and so on.
Knowing this, I implemented the following:
TwitchProvider({clientId: env.NEXT_PUBLIC_TWITCH_CLIENT_ID,clientSecret: env.TWITCH_CLIENT_SECRET,authorization: {params: {scope: "openid user:read:email",},},// This is the only function that exposes the TokenSet, so below is a hack to update the database with the new id_token, refresh_token, scopes, and so on// @ts-expect-error The type of the tokens argument is being forcedasyncprofile(profile: TwitchProfile,tokens: TwitchTokenSet){constacc=awaitprisma.account.findUnique({select: {scope: true},where: {provider_providerAccountId: {provider: "twitch",providerAccountId: profile.sub,},},});// Happens when logging in with an account for the first timeif(acc==null){return{id: profile.sub,name: profile.preferred_username,email: profile.email,image: profile.picture,};}constaccScopes=acc.scope?.split(" ")??[];conststoredScopes=newScopeSet(accScopes);constnewScopes=tokens.scope.split(" ");if(!storedScopes.isSupersetOf(newScopes)){storedScopes.merge(newScopes);awaitprisma.account.update({data: {id_token: tokens.id_token,access_token: tokens.access_token,refresh_token: tokens.refresh_token,expires_at: tokens.expires_at,scope: storedScopes.toString(),token_type: tokens.token_type,},where: {provider_providerAccountId: {provider: "twitch",providerAccountId: profile.sub,},},});}return{id: profile.sub,name: profile.preferred_username,email: profile.email,image: profile.picture,};},})
And in the client, whenever I detect that the user does not have the necessary scopes to use a widget, I invoke
The suggestion:
This works, but I feel like it could be much cleaner if the TokenSet was exposed in one of the callbacks. Since it would have to work for both the database and jwt session strategy, I don't think it would be enough to add the token parameter in the existing jwt callback.
Since I've noticed that a lot of the hooks have arguments which are only ever populated when the user signs in, why not have a callback which is intended exactly for this scenario? That way you'd be able to remove a lot of the unnecessary parameters from the other callbacks, and you would be able to place the tokenset in this callback as well.
Edit: Apparently the account parameter that gets passed to the jwt callback is stale because of the update made in the profile function, so you might want to refetch from the DB if you make use this hack.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Preface: I'm using a prisma database adapter with Twitch as my auth provider.
Like others before me (#2066 #4726), I've ran into a challenging situation when trying to upgrade/expand the scopes my next-auth application was allowed to use.
The current suggestions I've seen say to create/copy-paste custom providers with the extra scopes. This works well if your application only needs two or three different set of static scopes, like if you had "regular scopes" (
openid user:read:email
) and "admin scopes" (openid user:read:email channel:manage:all email:send
), but quickly gets out of hand if you need to apply a combination of scopes in truly incremental fashion. To provide a concrete example, the application I'm writing involves a bunch of independent widgets, and each widget may need different scopes from the auth provider. If we had 5 widgets requesting 5 different scopes, we would need 120 different copy-pasted custom providers to cover all possible combinations.The core problem is, when the user signs in, next-auth does not update the Account DB model to reflect the new scopes/access_tokens/refresh_tokens.
Here's 2 ways how this might happens:
signIn()
function with a scope override (signIn('twitch', undefined, {scope: 'old:scopes ' + 'my:new:scope'})
. After the going through the Oauth flow, the new tokens with the expanded scopes are lost since they were never persisted.Hopefully you understand the issue by now. Anyway, here's a how I overcame this problem:
OauthUserConfig
type has an optional methodprofile
, which is invoked when the user signs in. The second parameter of that function is aTokenSet
. This contains the id_token, access_token, refresh_token, and so on.Knowing this, I implemented the following:
And in the client, whenever I detect that the user does not have the necessary scopes to use a widget, I invoke
The suggestion:
This works, but I feel like it could be much cleaner if the
TokenSet
was exposed in one of the callbacks. Since it would have to work for both the database and jwt session strategy, I don't think it would be enough to add the token parameter in the existingjwt
callback.Since I've noticed that a lot of the hooks have arguments which are only ever populated when the user signs in, why not have a callback which is intended exactly for this scenario? That way you'd be able to remove a lot of the unnecessary parameters from the other callbacks, and you would be able to place the tokenset in this callback as well.
Edit: Apparently the
account
parameter that gets passed to thejwt
callback is stale because of the update made in theprofile
function, so you might want to refetch from the DB if you make use this hack.Beta Was this translation helpful? Give feedback.
All reactions