FusionAuth + NextAuth refresh tokens



  • I'm new to FusionAuth. I came to know about it because of NextAuth and I love it! Now I'm looking to build a boilerplate app as my go to full-stack platform: Next.js + NextAuth + FusionAuth + Hasura.

    My only issue so far is how to implement refresh tokens. NextAuth provides a demo for Google as OAuth provider, but I couldn't find anything inside FusionAuth Docs that would help me. Any hints please?



  • I think I had some success so far. I was able to hit the right endpoint, found here after all. Below is the code, however I still have to implement the token refresh logic. I will update the code when I'm done. At this point I have another question though:

    In order to enhance NextAuth token with required Hasura custom claims, I need to set a new iat and exp value. These values are passed to NextAuth by FusionAuth in the account.accessToken property on the jwt callback event. Is there a way and is it safe to decode and grab all values from account.accessToken? It looks like it contains exactly all props I need, together with exact iat and exp.

    // [...nextauth].js api route
    
    import {cleanEnv, host, str} from 'envalid'
    import jwt from 'jsonwebtoken'
    import NextAuth from 'next-auth'
    import Providers from 'next-auth/providers'
    
    const env = cleanEnv(process.env, {
      FUSIONAUTH_DOMAIN: host(),
      FUSIONAUTH_CLIENT_ID: str(),
      FUSIONAUTH_SECRET: str(),
      FUSIONAUTH_TENANT_ID: str(),
      SECRET: str(),
    })
    
    const FUSIONAUTH_REFRESH_TOKEN_URL = `${FUSIONAUTH_DOMAIN}/oauth2/token?`
    
    async function refreshAccessToken(token) {
      try {
        const url =
          FUSIONAUTH_REFRESH_TOKEN_URL +
          new URLSearchParams({
            client_id: env.FUSIONAUTH_CLIENT_ID,
            client_secret: env.FUSIONAUTH_SECRET,
            tenant_id: env.FUSIONAUTH_TENANT_ID,
            grant_type: "refresh_token",
            refresh_token: token.refreshToken,
          });
    
        const response = await fetch(url, {
          headers: {
            "Content-Type": "application/x-www-form-urlencoded",
          },
          method: "POST",
        });
    
        const refreshedTokens = await response.json();
    
        if (!response.ok) {
          throw refreshedTokens;
        }
    
        return {
          ...token,
          accessToken: refreshedTokens.access_token,
          accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
          refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, // Fall back to old refresh token
        };
      } catch (error) {
        console.log(error);
    
        return {
          ...token,
          error: "RefreshAccessTokenError",
        };
      }
    }
    
    export default NextAuth({
      providers: [
        Providers.FusionAuth({
          id: 'fusionauth',
          name: 'FusionAuth',
          domain: env.FUSIONAUTH_DOMAIN,
          tenantId: env.FUSIONAUTH_TENANT_ID,
          clientId: env.FUSIONAUTH_CLIENT_ID,
          clientSecret: env.FUSIONAUTH_SECRET,
          scope: 'offline_access',
        }),
      ],
    
      secret: env.SECRET,
    
      session: {jwt: true},
    
      jwt: {
        secret: env.SECRET,
        async encode({secret, token}) {
          const jwtClaims = {
            ...token,
            iat: Date.now() / 1000,
            exp: Math.floor(Date.now() / 1000) + 60 * 30,
            'https://hasura.io/jwt/claims': {
              'x-hasura-allowed-roles': token.roles,
              'x-hasura-default-role': 'user',
              'x-hasura-role': 'user',
              'x-hasura-user-id': token.sub,
            },
          }
    
          return jwt.sign(jwtClaims, secret, {algorithm: 'RS512'})
        },
        async decode({secret, token}) {
          return jwt.verify(token, secret, {algorithms: ['RS512']})
        },
      },
    
      pages: {},
    
      callbacks: {
        async jwt(token, user, account, profile) {
          if (user && profile) {
            token.id = user.id
            token.roles = profile.roles
          }
    
          return token
        },
    
        async session(session, token) {
          if (token) {
            const encodedToken = jwt.sign(token, env.SECRET, {algorithm: 'RS512'})
            session.id = token.id
            session.token = encodedToken
            session.error = token.error
          }
    
          return session
        },
      },
    
      events: {},
    })
    
    


  • Hi @naughtly-keller,

    Glad you got some traction on refresh tokens!

    I am not familiar with NextAuth and its requirements. If you are looking to read the access token's iat and exp value issued by FusionAuth -- that should be achievable. In fact, if you are using an OAuth workflow you can use the userinfo and introspect to decode that access token and associated claims. All of this is linked in the documentation as well as concisely here under OAuth.

    If you want to access your user via API, you can also use endpoints like the registration to get more information about your user.

    But modifying a token might be tricky.

    Off the top of my head, if you can customize claims on a token with FusionAuth's lambda functionality:

    However, the populate lambda documentation prohibits modifying the iat and exp claims.

    You may add or modify anything in the jwt object. However, you may not modify the header keys or values of the JWT. FusionAuth also protects certain reserved claims. The following claims are considered reserved and modifications or removal will not be reflected in the final JWT payload:

    exp

    iat

    sub

    I will let you know if anything else pops to mind. In my growing experience, those iat and exp are reserved (for security reasons). You may want to review how you are connecting all your architecture pieces to determine why you need to modify those claims. I have used NextJS and NextAuth only sparingly, but similar example applications are built using the OAuth protocol in React (here) and VueJS (here). They might prove useful to you.

    This is a bit longer because I wanted to give you some general guidance and feedback. I hope it helps!

    Thanks,
    Josh

    Additional Links about JWT that might be helpful:



  • Hey @joshua thank you so much 🙏
    I spent the weekend on it, and I have been successful. Honestly, FusionAuth has been a revelation. It's extremely powerful, and because of that I struggled a bit getting to know all the features. I apologize for my previous questions... They sound a bit silly now.

    I was not yet aware of both the userinfo and introspect apis, so thanks again for letting me know about them. I like the userinfo api more, although introspect does return all the claims, including iat and exp. But before I get into it, please let me describe what I did. I'm no security expert, and I'd be happy if you could vet the process at a high level to make sure I'm not doing anything blatantly naive. Once I complete my implementation, I plan to write a tutorial about Next.js + FusionAuth + Hasura.

    After signing in, I grab the accessToken prop and very manually run it through jsonwebtoken.verify using the public key stored on FusionAuth and hold a reference to that object. I then add to it Hasura's custom claims, and then sign this thing using a different RSA key, known by the Hasura server. When it's time to refresh the token I simply repeat the process: decoding the token, enhancing it with custom claims and signing it again. The resulting access token is stored in session and used to talk to the server.

    The next step would be trying to implement the userinfo verification api you suggested instead of manually verifying the token.

    I have a couple of questions now, which I hope you can answer:

    • You mentioned both iat and exp are reserved. I currently use them in my enhanced token to let hasura know when it's time to expire the token. As of my current understanding, every newly signed token would have at least a new iat set. What do you think I should use to set exp? Maybe the account.expires_in prop I get from FusionAuth?
    • The sign in process right now requires to call FusionAuth, then I would have to hit the userinfo endpoint and finally call hasura with an upsert operation in case of a new user. Wouldn't that be too chatty and slow? Would you suggest a different flow?
    • I'm currently using 2 different key pairs in this process: the FusionAuth's application RSA256 key for the login process, and an RSA512 key to sign/verify the final access token I send to hasura. Should I just use one key? Having FusionAuth manage keys would be great, but how can I use a key managed by FusionAuth to sign a token from the outside? I wasn't able to retrieve the private key or find an api for that.
    • Finally, is it possible to rotate keys automatically in FusionAuth like Firebase does for example?

    I apologize for the long winded post and number of questions. Thank you very much for your time and kind support!



  • Hey @joshua ,
    After complicating my life in a number of painful ways (please, check above) 🙄☝️ which was still worthy for my learning journey, I ended up following all your advices FTW:

    • Using lambdas resulted in a much cleaner and faster implementation;
    • You can also get away without lambdas as well (look ma, no lambdas): all you need to do is map the available access token claims to those required by hasura;
    • You need no extra RSA keys and thanks to FusionAuth you don't even need to pass a public key to hasura: all you need is set the jwk_url parameter.

    At the end of the day, the correct implementation was so smooth I doubt it will need a tutorial 🤔



  • @naughtly-keller!

    Glad you got it worked out!

    Let us know if there is anything that we can add to the documentation, etc to make it less painful for others 🙂.

    As I was reviewing I did see that others have similar setups using adjacent tech stacks (Might be interesting to you):

    Anyways, glad you got it working, and thanks for sharing the journey. 👍

    Thanks,
    Josh



  • @naughtly-keller Please share your application when it is live!



  • @naughtly-keller Good one,

    We are currently building an app on the same stack.

    For user management we have written triggers and functions between hasura and fusionauth to keep the userdata in fusionauth up to date, we have also written all login, refresh and create user actions in hasura and functions.

    If you want I can take you trough a tour...... and show you how we unified everything behind our Hasura gateway.

    There is one issue we are currently working on before our stack is complete. The ability to use organizations. Maybe we can Collab on it and we will gladly share our code with you. @robotdan if @naughtly-keller wants to opensource the stack we are more than willing to contribute. We do have one issue in our implementation tho. And a little inspiration from your side would go a long way.

    in our case all user management, syncing, login and other ops are executed by directly talking to our GQL endpoint( Hasura) this is all in working order. But we need to add organizations in the mix. We have an issue finding how we can leverage Fusionauth for this.

    Ohh a preview test version of our app is available on https://thisisfashion.tv 😉



  • We'd love to highlight such an application or post if you wanted to share it..

    As for organizations, I see a few options:

    • Groups/roles
    • Entities (you have to buy a license for this)
    • Model it in your own database and sync back and forth

    It all depends on your use case, but each of these may work.



  • @dan I thought the difficulty was subjective, due to my inexperience with FusionAuth, but I can write down a tutorial and push a demo if you like 🙂



  • Hey @sander Can you be more precise about how you want to use organizations? Do you mean implementing some sort of multi-tenancy? I'd be happy to collaborate 🙂



  • @dan
    Ill give some more info on how we want to do it soon. as in a user story. whatever we come up with i woudl also like to incorporate in the blogpost / repos that we will share with @naughtly-keller.

    Long story short would be.

    How we solved users:
    Create user request ====> graphql ===> azure function ( created user in FA, returns info, uses uuid and email to create user in our hasura DB users table. From the moment of creation of a user in DB, if in our DB something gets changed ( EG name usermane email etc, this request triggers a function to update and sync that change with Fusionauth.

    For auth we include and X-hasura-user and role in the JWT. inside of hasura we pull this from the auth header and then a user is allowed to modify things in its own row and read information that goes with the role.

    what we want with organizations:
    This is fashion has a studio application much like Youtube studio to manage uploads to our platform. on this platform we want organizations to be able to manage their own content ( think brands, and their catwalks, films, shortfilms ) which then will be available for users on our platform to consume.

    The prettiest solution would be if we can add an x-hasura-org with an organization UUID to the token. and include the role the user has in the organization as well. We would like handle organization details synchronization in the same way as we do with users at the moment. This way we could let a user switch organizations by changing their token. reason for this is we are planning on using microservices, and this way we can just pass the needed info in headers to those services.

    within hasura there would be an organizations table very much like we have our users table now.

    @naughtly-keller you can shoot me an email to setup a call and demo if you want sander@thisisfashion.tv



  • Hey @sander your issues seem to be related more with how Hasura's authorization system works than FusionAuth authentication. However, you can populate your JWT with the values you need. As @dan suggested, I would probably use Groups to model organizations, then it's easy to add the relative claims on the access token. So yes, you can populate your token with claims for both a set of roles and one or more organizations. Everything else should be in the authorization system domain, and considering you are using Hasura you can probably model those relationships in the database.



  • @naughtly-keller or @sander

    Not sure if one of you were the creator of this?

    https://next-auth.js.org/providers/fusionauth

    or if not, at the very least I wanted to pass along the additional info that is now available 🙂

    Thanks,
    Josh


Log in to reply