Empty JWT payload, persisting role permissions and "jankiness" with next-auth

Hello lads. I'm struggeling to get next-auth up and running. It has been working decently, but I have several issues that I'm unable to resolve, even after days of trying out different solutions, package versions and more. Here's what I'm trying to do: - Authentication with Discord & credentials - Implement a role system - Protect certain tRPC procedures for logged in users or users with specific roles Currently, the following issues are present: - When authenticating with Discord, a ID in the user table is generated. However, the session does not contain an ID. I am unable to use the ID in any of my tRPC procedures and therefore can't associate data in the DB with a user. - The payload section in the JWT is empty. Yes, completely empty. I'm not even sure why and how this is somewhat working, but it's definitely not secure nor usable. I do return valid data in all callbacks and copied them from the official documentation. - When an admin changes the roles of a user, the token will still be valid and the user still has the permissions of that role. That's obviously very bad, since the JWT only expires after 30 days and I wasn't able to work around this. The source code is available here: https://github.com/manuel-rw/kana Instructions for set up are located in the readme. It seems like there are major configuration issues with next-auth. The documentation is a bit vague and I'm not understanding, what I'm doing wrong. I don't want to treat users from OAuth providers any different than compared to the credentials in my tRPC routes. Please let me know if you have any questions, and I'll make sure to answer them asap! Environment: Node.js 18 Windows & Linux Versions of all dependencies in the package.json
GitHub
GitHub - manuel-rw/kana
Contribute to manuel-rw/kana development by creating an account on GitHub.
19 Replies
JulieCezar
JulieCezar17mo ago
I'm gonna write this in bullet points so you hopefully have an easier time to understand 😅 1. "When an admin changes the roles of a user, the token will still...." - yes this is obviously a problem, and the only way to go around this is to have a Database strategy instead of the JWT one, sou you can control it from your backend. Because of that you should be using a Database adapter which handles most of the logic for you. Loggin in should automatically create a session which is connected to the user. Then, when the Admin changes the user's role you need to delete that user's session so it will forcefully log him out.
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
2. "When authenticating with Discord, a ID in the user table is generated. However, the session does not contain an ID. I am unab...." - this should be solved if you use the adapter mentioned in 1. For example I use the prisma adapter.... 3. "The payload section in the JWT is empty. Yes, completely empty.... " - This is probably because of a mistake you made in the code. Write out every callback function with all of their props (signIn, session, jwt...) and console.log everything!!! You will see how it works then, because it's not so directly mention in the docs. For example, sometimes JWT gets called multiple times but the user and token objects don't always contain data, so you need to check if any data exists in token and only then overwrite what you want.. For example I check in JWT callback that i have a user.id and only then do I write to user object...
if (account?.provider === "google" && user?.id) {
token = {
user: {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
image: user.image,
message: user.message,
},
};
}
if (account?.provider === "google" && user?.id) {
token = {
user: {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
image: user.image,
message: user.message,
},
};
}
This is how I log every step..
jwt: async ({ token, account, isNewUser, profile, user }) => {
// console.log("--------------------------------------------");
// console.log("JWT CALLBACK");
// console.log("--------------------------------------------");

// console.log("token: ", token);
// console.log("account: ", account);
// console.log("isNewUser: ", isNewUser);
// console.log("profile: ", profile);
// console.log("user: ", user);

// For credentials login
})
jwt: async ({ token, account, isNewUser, profile, user }) => {
// console.log("--------------------------------------------");
// console.log("JWT CALLBACK");
// console.log("--------------------------------------------");

// console.log("token: ", token);
// console.log("account: ", account);
// console.log("isNewUser: ", isNewUser);
// console.log("profile: ", profile);
// console.log("user: ", user);

// For credentials login
})
4. regarding role based authentication for pages and trpc routes... There is no happier solution here but to check every time what the user's role is... so you can either put it in the session object, or fetch from db when you need it. For pages I use the getServerSideProps on each protected page to fetch the user's data before allowing him to enter or no... Again, this could be done through session object. If you use the session object, an alternative to GSSP is to use the "React" way of authentication.... You let the user to the page, display a loader, and then check the session object. If the role is ok then you hide loader and display data, else you redirect
Manicraft1001
Manicraft100117mo ago
Hi, thank you for your detailed reply. I appreciate your time. Let me quickly read all pullet points and reply to them...
JulieCezar
JulieCezar17mo ago
Np np I had the same question a year ago xD
Manicraft1001
Manicraft100117mo ago
1. I've read this multiple times. All tutorials and guides seem to use JWT, but sessions seem to have major advantages. I do understand how the basics of JWT works, but I am not experienced enough that I would understand everything. If you look at my Prisma schema, I already have the exact same model Session. My problem is, that I don't know how to use it, build sessions, refresh sessions and let sessions expire. I feel like there is too little information on this in the documentation - hence I asked here. I use Prisma as a DB adapter - which is the default for t3app. I do not understand how this should be done. The documentation only mentions the basic usage of Prisma: https://authjs.dev/reference/adapter/prisma 2. Okay, got it. I already use Prisma - it seems to work pretty okay, but I can't retrieve the user ID, nor any other information in any of my tRPC procedures. 3. Thanks for the suggestion. I have debugged many hours within the last few days. I tried deleting callbacks, modifying the content returned by the callbacks and to return different data depending on what provider was used. I looked at it today with an experienced friend - he has no idea, why my sesions are not working as they should. The JWT is empty and we weren't able at all to put any payload in the token, even though we return data from the callbacks. 4. Okay, got it. I noticed, that it's possible to fetch the user for each request by the user id. But for that, the other issues I'm having are blocking (eg. Discord not having an ID). I suggest, that you maybe have a look at the code. I'm simply not able to get any reliable results with the JWT and there is too little documentation about sessions. Thanks for your time btw. I was really disappointed the last few days, because none of my actions improved the state of the application. You're helping me out a lot 🙂
JulieCezar
JulieCezar17mo ago
1. Both have it's ups and downs, but the DB strategy is definitely the "safer" option and the only way to implement forcefull logout like you described.... The prisma adapter handles all that you just said. If you look at the bare T3 example... With only that logic and the adapter it will: Create a Account and User if you login for the first time, When you login it will create a session, Whenever you go to any page it will check if the sesison is still valid, If the session is invalid it will delete the session object 2. Open your database ,do you have the user's id in the sessio table? 3. Yep... I lost many days trying to figure it all out.... 4. Intersting... can you post a screenshot of your db of that user's object? Account, user and session tables after you log in Try to replace your /server/auth.ts with the example from the T3... you are doing some logic in the jwt callback which I don't know is 100% rright... I would need to actually try and test it... It could be like the problem I mention before, that you don't always have a token... Basically remove the jwt callback And I see you also have credentials login... delete that as well for now, just to see if all works only with discord Beacause combining oAuth and credentials is also a bit problematic 😅
Manicraft1001
Manicraft100117mo ago
1. Thanks, I will look at the examples. So far, all examples I've looked, either use JWT or only Google Oauth - which is significantly different compared to Discord. 2. Yes. If you want, I can show more details / screenshots. Feel free to start the app yourself too! 3. Yea.... Sucks 4. Sure. I'll send you after work
JulieCezar
JulieCezar17mo ago
The newest version of T3 uses Discord only as far as I can see, so check that out 👍 Try it first, then we will see haha Good luck!
Manicraft1001
Manicraft100117mo ago
Yes, there was only Discord by default. But it sucks that next auth is so restrictive with credentials. I'll try out the things you suggested. Can I tag you when I have updates? I'll post the solution here, when I figured it out, in case someone else has the same questions
Manicraft1001
Manicraft100117mo ago
This is my production database btw
Manicraft1001
Manicraft100117mo ago
Ok, some updates here. It seems, that the reason why Oauth and credentials are mostly incompatible is, that the crdentials provider does not support database sessions by design. The next-auth team designed it like this. Therefore, the easiest way to use Credentials with Discord auth is using JWT. However, there is a workaround, in which you create and manage your own sessions for credential logins. I got this from the following blog: https://nneko.branche.online/next-auth-credentials-provider-with-the-database-session-strategy/ I started implementing pieces and at least my Discord login is working again. However, I need to refactor some of the code, as I need access to the raw requests and responses to read and write headers & cookies - that is also described in the blog. One thing, that is preventing me from doing that, is this function, which is the default for t3app:
/**
* Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file.
*
* @see https://next-auth.js.org/configuration/nextjs
*/
export const getServerAuthSession = (ctx: {
req: GetServerSidePropsContext["req"];
res: GetServerSidePropsContext["res"];
}) => {
return getServerSession(ctx.req, ctx.res, authOptions);
};
/**
* Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file.
*
* @see https://next-auth.js.org/configuration/nextjs
*/
export const getServerAuthSession = (ctx: {
req: GetServerSidePropsContext["req"];
res: GetServerSidePropsContext["res"];
}) => {
return getServerSession(ctx.req, ctx.res, authOptions);
};
It seems that it isn't compatible with the advanced initialization. I will give it a shot to re-write myself. Sadly, none of the GH posts nor the blog uses this approach for the server auth session. I'll post my findings here Advanced initialization is btw described here: https://next-auth.js.org/configuration/initialization?ref=nneko.branche.online#advanced-initialization Also, where do I get the adapter from?
JulieCezar
JulieCezar17mo ago
Should be included in the next-auth package, or is a standalone package idk anymore... You can find examples in their docs Oh yea there was that, I forgot.... However, I've had the same case you have. I had to implement credentials and Google login options and managed to get it to work However, I also had the same problem where I couldn't invalidate a JWT token... so what I did was have a custom field in the database, e.g. active. On each page I make a call to the DB tol get the user's info a check if the account is active You could make it so that whenever a user gets a Role change you also set this property Not the prettiest solution but I didn't have any other options since the requirements were for both login options... Or another thing I did was the following. When the user logins in you can check if their role changed and display a notification somewhere that their account was changed and that they have to logout and login again to access all new possibilities However, this was specific to my usecase where it didn't matter if the user could still use the older role's functionalities It's on my other laptop but if you would like a short overview i can send you at the start of next week
Manicraft1001
Manicraft100117mo ago
Not a great idea in my opinion. I will try a few things out. It will probably take a few days, since most code snippets are outdated or not in combination used with tRPC Thanks for your help so far 🙂
JulieCezar
JulieCezar17mo ago
The easiest way would be to throw out credentials login... then you wont have these problems anymore 😂 Np np, and good luck
Manicraft1001
Manicraft100117mo ago
Not a viable solution :/ I'll post updates here for my findings
zenith
zenith17mo ago
How hard do we think it would be to just write our own "credentials plus" provider that supports DB this is an absolute punisher of a limitation
JulieCezar
JulieCezar16mo ago
It actually shouldn't be that hard... you have all the examples listed in their repo and have like 10 functions you need to define... Despite saying that I don't even know if it's actually possible... Because you have their normal credentials login, email login and oAuth login. And you can only create custom oAuth providers
JulieCezar
JulieCezar16mo ago
JulieCezar
JulieCezar16mo ago
So yea... If you need all of those maybe consider using Clerk as a Auth service
Manicraft1001
Manicraft100116mo ago
Wait, didn't I post my solution here two days ago? Or did someone delete it? I'm pretty sure that I posted it here. Anyhow, I'll explain my findings: - The blog above is correct. When tRPC is in use, one must refactor the authentication options to a function and pass the request and response object. Since the TS types do not overlap well, the errors must be suppressed. - The JWT callback must be removed. After that, the two new callbacks, described in the blog above, must be added. Basically, the only thing hindering you from using credentials with sessions is, that they don't have built in session management. Therefore, you must implement your own. Deletion of the session works implicit and no changes for that are required. - Modifying roles and immediately checking for them works absolutely fine with sessions.
Want results from more Discord servers?
Add your server