Describe the bug
I am using the Amazon Cognito provider. The integration is working because when I use the signin callback I get the correct profile, account, and metadata back from cognito.
The problem is with the session. When the session tries to decode the token stored in the next-auth.session-token cookie, it gets the following back: { iat: 1593171452, exp: 1595763452 } with no user info. That means that the session returned is: { user: { name: null, email: null, image: null },
expires: '2020-07-26T11:37:32.936Z' }.
The error I find is: UnhandledPromiseRejectionWarning: TypeError [ERR_INVALID_ARG_TYPE]: The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type undefined.
Line of code: next-auth/dist/server/routes/session.js:250:9
To Reproduce
Steps to reproduce the behavior.
To reproduce, I downloaded the sample app and replaced the providers with the cognito provider. I'm able to login successfully but then the bug described sets in. One interesting side effect is that although the sign in on one browser fails, it causes other open windows to succeed. However, the success is based on the session described in the bug.
Include example code (or link to public repository) which can be used to reproduce the behaviour.
Expected behavior
A clear and concise description of what you expected to happen.
I would expect the session to be set correctly on a successful login with Cognito.
Screenshots or error logs
If applicable, add screenshots or error logs to help explain the problem.
Additional context
Add any other context about the problem here.
Documentation feedback
Documentation refers to searching through online documentation, code comments and issue history. The example project refers to next-auth-example.
Hmm, this is a new provider and we might have teething troubles with it, I think you are the first person to file a bug report about it. (In future we might start labelling them with something like new / beta so folks can try them out and feedback and know it might not be extensively tested yet.)
Could you try adding idToken: true to your provider token and try signing out / signing in again?
providers: [
Providers.Cognito({
clientId: process.env.COGNITO_CLIENT_ID,
clientSecret: process.env.COGNITO_CLIENT_SECRET,
domain: process.env.COGNITO_DOMAIN,
idToken: true
})
}
If that works, I'll update the provider config so it's set by default.
If it doesn't, we'll need to investigate it further.
Thanks for the bug report!
I updated the provider as requested and, unfortunately, it is still giving the same error. Not sure it helps, but I see in the function handling the session in session.js that the response is set to a default value but before the end of the function, it becomes "undefined" and that's what gets returned. I'm not sure where or how the next-auth.session-token is set. Let me know if there is anything else I can do to help. I'm really liking the work you've done here, though. Very helpful and when the kinks are ironed out it's going to be great.
@jeff0131 Thanks Jeff! Oh dear that sounds like a bug. I'll take a look and maybe ask @LoriKarikari for help if I get stuck.
I did some digging and found that when writing the cookie, the account has the accessToken and refreshToken so I thought the max cookie length might be exceeded. When I commented out including the account in the defaultJwtPayload (server/routes/callback.js line 72) , it started working. I'm sure that's not a solution, but it's the problem at least.
Thanks, that's interesting! I guess you have JWT sessions enabled (rather than a database)?
Usually 4 KB is about the limit for an individual cookie, but maybe it is that big. o.O
If you use the jwt callback you can actually control what gets set in the JWT token!
This callback is called 1. on sign in and 2. any time a session is accessed form the client.
The first time is is called (on sign in) it is passed a second argument of the raw OAuth profile from the provider so you can choose to copy over any additional data you want at that point.
It would be interesting to know what happens if you try accessing the token in that callback and seeing what it looks like (e.g. does it look large or contain a key with a big value, or does the object look messed up somehow).
Any modifications you do to the token object before it is returned determine what will be saved to the JWT (e.g. if you did delete token.user.email it would delete their email address from the the token before it is ever saved).
callbacks: {
jwt: async (token, oAuthProfile) => {
if (oAuthProfile) {
console.log("JWT callback on sign in", token, oAuthProfile)
} else {
console.log("JWT callback after sign in", token)
}
return Promise.resolve(token)
}
}
Just adding for clarity:
This sounds like a bug with this provider and I do plan to investigate, option above is only suggested as a workaround.
The callbacks are working correctly. The only challenge is when setting the session cookie. Thanks for looking into this.
@iaincollins I'm also being affected by this cookie size issue. I can confirm that using the JWT callback to truncate over 1000 characters from the refresh token, for example, will allow the __Secure-next-auth.session-token to be stored in the browser. Obviously this renders the refresh token worthless.
Is there any concrete plan on how to fix this or work around it?
I tested using a Pre Token Generation Lambda Trigger as suggested in an AWS Amplify issue to remove the cognito-groups claim from the token, but that did not shrink the token size enough. I'm not sure there's anything else I can remove from the token that wouldn't break the functionality I need.
It seems possible that storing the access token and refresh token in separate cookies would overcome the size limitations, but I'm not sure if that makes any sense from this library's point of view.
I'm looking forward to any kind of update you can provide. This has stopped my project dead in its tracks.
This also just came up in #392. Technically, we can't really do anything about it as it's a browser limit and pitfall of JWT session tokens. The limit is going to be different for different folks (depending on on their site).
It's actually the main reason why JWT sessions aren't the default (unless you don't specify a database) as, while JWT is super flexible, fast and cheap to run and very scaleable, it's such an annoying wall to run into.
To squeeze more space out of the token, you can can disable encryption (and only use signed JSON Web Tokens) if applicable, which uses less bytes, and is still secure if you are happy for the user to have read-only the contents of their token (e.g. if it only has their own access keys and roles and not a password or an API key shared between users, etc).
Unfortunately, often the only way to handle it is to use a database to store the data that won't fit in the token, and look up the database on demand to get that data.
i.e. create an entry in the database to store that info for the user on sign in (using the jwt callback in NextAuth.js) and then look it up on demand in subsequent requests.
Probably any database would do, something like Aurora Serverless (if deploying a Next.js site to AWS) or a regular Postgress, MySQL or Reddis instance would work. It only needs to be key / value (with the "key" being something like a User ID) so even a secure S3 bucket would work.
I appreciate that undermines a lot of the value people want from using JWT, but sadly it's an inherent limitation of them as session tokens in browsers. :-(
If you don't wan to use a database, it's also possible to use localStorage, so that might be an option if you were able to build an API endpoint your client calls after sign in, that could read in a JWT (the example project shows how to do that) and then returns some data for the client to persist in localStorage. That data could also be a signed JWT (one you create and sign yourself) which is secure for things like storing access rights, etc, just not suitable for actual session tokens (which is why NextAuth.js don't use it).
How can I get the OAuth access token? I see it in the accounts table in the database, but I haven't yet found a method that returns this information. Will this only be possible in v3 when the /api/auth/accounts is added as discussed in issue 179?
It seems to me that a very common use case is fetching data from third-party API's with an OAuth provider's access token used as the Bearer token. I'm unable to use JWT sessions with next-auth due to cookie size limitations, so with database sessions it's critical that a method is provided that returns the provider's access token.
How can I get the OAuth access token? I see it in the accounts table in the database, but I haven't yet found a method that returns this information.
If you want to use OAuth Refresh Tokens and Access Tokens that's currently something you'll need to write code for yourself.
I don't think this will be included in 3.0 but it might drop in a 3.x point release.
Is there any way to piggyback on the database connections next-auth already has or will I need to connect to the database from scratch?
Is there any way to piggyback on the database connections next-auth already has or will I need to connect to the database from scratch?
You'll need to use your own connection handling logic.
With serverless functions, connections are not shared between functions, so we don't export them.
The new default payload for the JWT in v3 is much smaller payload by default and doesn't include the full token (which is too big to fit into a JWT) so it shouldn't trigger this problem.
@iaincollins I'm experiencing I believe is the same issue where I use AWS Cognito and need to persist not only access token but also refresh token in the jwt callback. It works fine. I handle access token rotation inside the jwt callback, when it's expired use the persisted refresh token to get new access token. Except, I found every time I first time authenticated with Cognito, it gets oauth tokens and then it logs me out. A subsequent login is succesful without the need to enter username and password on Cognito hosted UI so that means it's already authenticated with it on the first login. As soon as I remove refresh token in the jwt callback everything works perfect. I understand this is a limitation of cookie size limit itself but also it's very common use case that both access & refresh tokens are required to be persisted in http only cookie. Do you have any suggestions or workaround, other than say persist one or both tokens in localstorage? But then that defects the purpose of using NextAuth in the first place as I think the way it automatically handles all those are great.
callbacks: {
jwt: async (token, user, account) => {
const isSignIn = !!user
if (isSignIn) {
const { accessToken, refreshToken } = account
setNextAuthToken(token, accessToken, refreshToken)
} else {
// Reaches here whenever session is accessed from client / server, eg. useSession, getSession etc
logger.info(`JWT callback after sign in ${JSON.stringify(token)}`)
const timeToRefresh = addMinutes(token.authTime * 1000, RefreshAuthInterval)
const shouldRefresh = isAfter(new Date(), timeToRefresh)
logger.info(`shouldRefresh: ${shouldRefresh}, timeToRefresh: ${timeToRefresh}`)
if (shouldRefresh) {
const refreshResult = await refreshAuthToken(token.savedRefreshToken)
if (refreshResult?.AccessToken) setNextAuthToken(token, refreshResult.AccessToken)
}
}
return Promise.resolve(token)
},
session: async (session, jwt: NextAuthJWT) => {
// Add property to session
session.accessToken = jwt.savedAccessToken
logger.info(`session callback ${JSON.stringify(session)}`)
return Promise.resolve(session)
}
}