Next-auth: Is there a sane way to store tokens created by a service?

Created on 24 Jun 2020  ·  11Comments  ·  Source: nextauthjs/next-auth

Is it possible to use store provided by an existing non-OAuth authentication service?

I'm trying to use Providers.Credentials to integrate with an existing backend services that issues JWTs, but I'm having a heck of a time figuring out how to make this work.

When I POST /auth/tokens with the credentials, the access and refresh tokens are handed back to me. I need to have these tokens available to me in the API layer so that I can add them to the request headers of subsequent requests, but it's not clear to me how to sanely store these for later access (storing them in a global or singleton in a package feels side-effecty and generally "icky").

Here's what I have so far:

async function authorize({ username, password }) {
  const client = makeClient()

  try {
    const {
      data: {
        access,
        refresh,
      },
    } = await client.post('/auth/tokens', {
      email: username,
      password,
    })

    const { data: user } = await client.get('/users/me', {
      headers: {
        Authorization: makeAuthHeader(access),
      }
    })

    return user
  } catch (error) {
    console.error('ERROR: %o', error)
    return null
  }
}

Documentation feedback

  • [ ] Found the documentation helpful
  • [x] Found documentation but was incomplete
  • [ ] Could not find relevant documentation
  • [ ] Found the example project helpful
  • [x] Did not find the example project helpful

I've even dug through the source code a little to see if there was a way to "sneak" these tokens into the session, but I didn't see a clear path forward.

bug question

All 11 comments

Hey thanks for the detail and for feedback on documentation.

I think I understand what you want to do and it makes sense. I actually had to check to see what would happen when I tried it because it's a reasonable expectation but I wasn't sure it was supported or not.

~It turns out there is a bug with the credentials flow and user object isn't persisted when you sign in, but is supposed to be. You should be able to access the user object returned in the jwt() and signin() callbacks, but the user is coming though as a function rather than an object.~

^ Doh was wrong, the problem was with the example code in the Documentation not in NextAuth.js itself! It actually works and there is no bug.

@iaincollins Thanks, but the problem isn't that I can't get at the user object....the problem is that there's no easy way to store the access and refresh tokens I get back from my server. After much tinkering around, here's what I currently have. As you can see from the code below, I basically had to pass the res object all the way down into my authorize function so that I could set a pair of cookies. If you can think of a better solution, I'm all ears!

```es6
export default function handleRequest(req, res) {
return nextAuth(req, res, makeConfig(req, res))
}

function makeConfig(req, res) {
const authorize = makeAuthorizeFn(req, res)

// redacted
}

export function makeAuthorizeFn(res) {
return async function authorize({ username, password }) {
const client = makeClient()
try {
const {
data: {
access,
refresh,
},
} = await client.post('/auth/tokens', {
email: username,
password,
})

  // This is a bit hacky, but at least it gets the tokens back
  // to the client.
  res.setHeader('Set-Cookie', [
    serialize('access', access, { path: '/' }),
    serialize('refresh', refresh, { path: '/' }),
  ])

  //  redacted

}

Oh sure! The idea is that you should be able to set them on the user object and for that to get persisted (securely) in the JWT.

One thing to note here is that when I tried to set anything other than {id, name, email, image } to the user object, it would not make it into the session. I didn't dig far enough in the code to figure out why or where the other keys were be weeded out.

If you add a property to the user object in this scenario, then it's persisted to the JSON Web Token (assuming you use the work around above until the bug is fixed - when it's fixed you won't need to touch the jwt callback).

The session callback can be used to decide what properties can be safely exposed / exported from the JWT to the client side session object, if that makes sense.

@iaincollins, while I did notice that the example provided in the documentation was setting the user as a function, I do not have that problem because I'm already resolving the authorize function with the user object...but you couldn't know that because I redacted the code to keep the focus where it belongs. :-)

Here's the entirety of my makeAuthorizeFn higher-ordered function:

export function makeAuthorizeFn(res) {
  return async function authorize({ username, password }) {
    const client = makeClient()

    try {
      const {
        data: {
          access,
          refresh,
        },
      } = await client.post('/auth/tokens', {
        email: username,
        password,
      })

      // This is a bit hacky, but at least it gets the tokens back
      // to the client.
      res.setHeader('Set-Cookie', [
        serialize('access', access, { path: '/' }),
        serialize('refresh', refresh, { path: '/' }),
      ])

      const { data: user } = await client.get('/users/me', {
        headers: {
          Authorization: makeAuthHeader(access),
        }
      })

      // N.B.: Adding fields to the user object at this point does not work

      return user
    } catch (error) {
      console.error('ERROR: %o', error)
      return null
    }
  }
}

Just for grins and giggles, I tried adding user.foo = "hello, world" where I currently have the N.B. comment, but when the session is dumped, the foo datum is nowhere to be found:

{"user":{"name":"Dan Kreft","email":"[email protected]","image":null},"expires":"2020-07-24T23:20:32.599Z"}

This is what I was referring to in my previous comment.

I cannot simply use the jwt callback, because that callback does not have access to the tokens that were returned to me by the service.

Updated example below!

This is what your callbacks need to look like:

callbacks: { 
    session: async (session, token) => {
      // Copy properties from token contents to the client side session
      //
      // By default only 'safe' values like name, email and image which are
      // typically needed for presentation purposes (e.g. "you are logged in as…")
      // to avoid exposing sensitive information to the client inadvertently.
      session.user.data = token.user.data
      return Promise.resolve(session)
    }
}
  • The user object returned from the authorize callback is saved to the JWT (e.g. in token.user).
  • The session() callback controls what data is exposed from the JWT to the client session.

The stuff I wrote about the user response being wrong was totally incorrect! This amend example above works :-)

The example in the documentation I wrote for the Credentials plugin just a has a bug in it 🤦‍♂️

Note that async is not necessary (it actually causes eslint to complain) if you don't have an await in the function. I'll have another look at your proposed solution.

Okay, now I see what's going on here. It's a little confusing the way it's laid out here because I never would have thought that the session() callback's second argument would contain the thing returned by authorize().

At this point, though, I'm thinking that I'm probably going to stick with my current res.setHeader() solution because at least with the way I have it laid out now, my client doesn't have to worry about extracting the tokens from the session and figuring out where to put them (e.g. in cookies or in localStorage)...it only has to concern itself with pulling the tokens out of their respective cookies.

Thanks for your diligence...I feel more confident in using NextAuth knowing that you're so attentive. :-)

Note that async is not necessary (it actually causes eslint to complain)

The purpose of it whenever I give an example code is to make it clear the function is async and a promise is expected.

(Otherwise people then immediately ask "How can I do async calls?")

Okay, now I see what's going on here. It's a little confusing the way it's laid out here because I never would have thought that the session() callback's second argument would contain the thing returned by authorize().

To clarify, as per the docs it's always the contents of the JSON Web Token as the second argument to the session() callback. It is present if, and only if, JWT sessions are enabled. This is not always the same as what is returned by authorize() as what is saved to the JWT can be overridden in the jwt() callback.

For users who are using other configurations, other data will be stored in the JWT (e.g. a user object from a database and/or the profile response from an OAuth Provider).

The session() callback provides provide a way selectively expose things to the client securely, in a simple and uniform away that works for all providers and is almost certainly less code and more performant than other solutions.

my client doesn't have to worry about extracting the tokens from the session and figuring out where to put them (e.g. in cookies or in localStorage).

Any properties accessed via a session object are be automatically kept up to date, and kept in sync across tabs and windows, and are persisted across page navigation in a single page app so that people can avoid doing exactly this (which actually, you have done - you have put tem.

I'd recommend to anyone else reading this they use the session property, if nothing else it's much less code to maintain and makes it easier to avoid bugs.

To confirm for anyone reading this in future, this is all you need to do to add data to a session from if you return it in a user object from authorize():

callbacks: { 
  session: (session, token) => {
    session.user.data = token.user.data
    return session
  }
}

If you do that, they will be there when you access the session object from the client.

@iaincollins I'm taking another look at using the session (I was ignorant of getSession()) and this looks promising, but I don't see a way to update data in the session after login. If I'm to store my tokens in the session, I need to be able to write to it after I refresh my access token. I've tried Googling for a solution, but to no avail. Is there something else I'm missing?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

iaincollins picture iaincollins  ·  3Comments

iaincollins picture iaincollins  ·  3Comments

eatrocks picture eatrocks  ·  3Comments

simonbbyrne picture simonbbyrne  ·  3Comments

alex-cory picture alex-cory  ·  3Comments