FusionAuth
    • Home
    • Categories
    • Recent
    • Popular
    • Pricing
    • Contact us
    • Docs
    • Login

    nextAuth SignOut and revoking app sessions

    Scheduled Pinned Locked Moved Unsolved
    Q&A
    2
    5
    136
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • L
      laurahernandez
      last edited by

      I'm trying to use FusionAuth with nextAuth, and when I call SignOut() from my app, the nextAuth token is destroyed, and then I have it redirect to my api/logout route where I call FusionAuth's oauth2/logout url which destroys the FusionAuth SSO session; however, I can't destroy the application sessions that I still see for that user via the admin console. I have a logout url configured for the tenant which calls my api/endSession route, but I was hoping to get the userId from the nextAuth token here to destroy all refreshTokens; however, at this point, the nextAuth token is gone.

      What is the correct way to sign out of all sessions when using nextAuth?

      mark.robustelliM 1 Reply Last reply Reply Quote 0
      • mark.robustelliM
        mark.robustelli @laurahernandez
        last edited by

        @laurahernandez What is the Logout behavior set to for your Application?

        4e6f9037-3b95-400f-859e-73815720a759-image.png

        L 1 Reply Last reply Reply Quote 0
        • L
          laurahernandez @mark.robustelli
          last edited by

          @mark-robustelli I have it set to "All applications".

          I got this working, but please let me know if there is a better way.

          Basically the flow is: hit the sign out button, this navigates to api/auth/global-logout which reads the refreshTokenId that had been stored in the nextAuth token (because of the jwt callback defined in authOptions), and uses that to revoke that session for that user. Then the fusionAuth OAuth logout URL (oauth2/logout) is called which revokes the FusionAuth SSO session and then that calls the tenant Logout URL which is the /logout page. Then the logout page calls nextAuth's signOut() to destroy the nextAuth cookies on the client and redirect to /login.

          This GitHub conversation helped me get this done: https://github.com/nextauthjs/next-auth/discussions/3938#discussioncomment-2165150
          Reading the whole dialog before that post really helped me understand what was going on.
          Then this FusionAuth example shows how to revoke refresh tokens: https://github.com/FusionAuth/fusionauth-example-node-centralized-sessions/blob/main/changebank/src/index.ts
          (the premise of the setup: https://github.com/FusionAuth/fusionauth-example-node-centralized-sessions)
          And of course the background information as to why we have to do all this: https://fusionauth.io/docs/lifecycle/authenticate-users/logout-session-management

          I'm still working on it, but it does work already, and the code looks like this:
          For the nextAuth setup:

          api/lib/authOptions.ts

          import FusionAuthProvider from 'next-auth/providers/fusionauth';
          const fusionAuthIssuer = process.env.FUSIONAUTH_ISSUER;
          export const fusionAuthClientId = process.env.FUSIONAUTH_CLIENT_ID;
          const fusionAuthClientSecret = process.env.FUSIONAUTH_CLIENT_SECRET;
          export const fusionAuthUrl = process.env.FUSIONAUTH_URL;
          export const fusionAuthTenantId = process.env.FUSIONAUTH_TENANT_ID;
          export const fusionAuthRefreshTokenScopedApiKey =
            process.env.FUSION_AUTH_REFRESHTOKEN_API_KEY;
          
          const missingError = 'missing in environment variables.';
          if (!fusionAuthIssuer) {
            throw Error('FUSIONAUTH_ISSUER' + missingError);
          }
          if (!fusionAuthClientId) {
            throw Error('FUSIONAUTH_CLIENT_ID' + missingError);
          }
          if (!fusionAuthClientSecret) {
            throw Error('FUSIONAUTH_CLIENT_SECRET' + missingError);
          }
          if (!fusionAuthUrl) {
            throw Error('FUSIONAUTH_URL' + missingError);
          }
          if (!fusionAuthTenantId) {
            throw Error('FUSIONAUTH_TENANT_ID' + missingError);
          }
          if (!fusionAuthRefreshTokenScopedApiKey) {
            throw Error('FUSION_AUTH_REFRESHTOKEN_API_KEY' + missingError);
          }
          export const authOptions = {
            providers: [
              FusionAuthProvider({
                issuer: fusionAuthIssuer,
                clientId: fusionAuthClientId,
                clientSecret: fusionAuthClientSecret,
                wellKnown: `${fusionAuthUrl}/.well-known/openid-configuration/${fusionAuthTenantId}`,
                tenantId: fusionAuthTenantId, // Only required if you're using multi-tenancy
                authorization: {
                  params: {
                    scope: 'openid offline_access email profile',
                  },
                },
              }),
            ],
            callbacks: {
              async session({ session, token }: any) {
                session.user = token.user;
                return session;
              },
              async jwt({ token, account, user }: any) {
                // console.log({ token, account, user });
                if (user) {
                  token.user = user;
                }
                // Persist the OAuth access_token to the token right after signin to use in token revocation
                if (account) {
                  // token.accessToken = account.access_token;
                  token.refreshTokenId = account.refresh_token_id;
                }
                return token;
              },
            },
            pages: {
              signIn: '/login', // Custom login page
            },
            debug: true,
          };
          

          And at api\auth[...nextauth]\route.ts

          import NextAuth from 'next-auth';
          import { authOptions } from '../lib/authOptions';
          
          const handler = NextAuth(authOptions);
          
          export { handler as GET, handler as POST };
          

          Then in your FusionAuth setup:
          Configure the tenant "logout URL" (under OAuth tab) to http://localhost:3000/logout (I did it at the tenant level, but it can also be specified per application), and define that page in your Next app:

          /logout.ts:

          'use client';
          import { signOut } from 'next-auth/react';
          import { useRouter } from 'next/navigation';
          import { useEffect } from 'react';
          
          export default function LogoutPage() {
            const router = useRouter();
            useEffect(() => {
              async function signout() {
                await signOut({ redirect: false });
                router.push('/login');
                // ...
              }
              signout();
            }, []);
            return (
              <>
                <div>Please wait while you are being logged out</div>
              </>
            );
          }
          

          The sign out button on your app:

          {session && (
                      <button
                        onClick={async () => {
                          window.location.href = '/api/auth/global-logout';
                        }}
                        type='button'
                      >
                        Sign Out
                      </button>
                    )}
          

          Your api/auth/global-logout route

          import { NextRequest, NextResponse } from 'next/server';
          import jwt from 'next-auth/jwt';
          import FusionAuthClient from '@fusionauth/typescript-client';
          import {
            fusionAuthClientId,
            fusionAuthRefreshTokenScopedApiKey,
            fusionAuthTenantId,
            fusionAuthUrl,
          } from '../lib/authOptions';
          
          export async function GET(req: NextRequest) {
            try {
              const token = await jwt.getToken({
                req,
                secret: process.env.NEXTAUTH_SECRET,
              });
              console.debug('token', token);
              if (!token) {
                console.warn('No JWT token found when calling /global-logout endpoint');
                return NextResponse.redirect(process.env.NEXTAUTH_URL!);
              }
              console.log('token?.user', token?.user);
          
              if (!token.idToken)
                console.warn(
                  "Without an id_token the user won't be redirected back from the IdP after logout.",
                );
          
              const fusionAuthLogoutUrl = getLogoutRedirectUrl(req, token.idToken);
          
              //revoke this token ID. Should I revoke more?
              const refreshTokenId = token?.refreshTokenId;
              if (refreshTokenId && typeof refreshTokenId === 'string') {
                if (fusionAuthRefreshTokenScopedApiKey && fusionAuthUrl) {
                  console.debug('revoking refresh token with ID: ', refreshTokenId);
                  const client = new FusionAuthClient(
                    fusionAuthRefreshTokenScopedApiKey,
                    fusionAuthUrl,
                    fusionAuthTenantId,
                  );
                  const revokeResponse = await client.revokeRefreshTokenById(
                    refreshTokenId,
                  );
                  console.debug(revokeResponse);
                }
              }
          
              // revoke ALL active user sessions regardless of device, application, etc
              // const user = token?.user;
              // if (user && typeof user === 'object' && 'id' in user) {
              //   console.log('got user info: ', user);
              //   const userId = (user as { id: string; email: string }).id;
          
              //   if (userId && fusionAuthRefreshTokenScopedApiKey && fusionAuthUrl) {
              //     console.log('revoking all user refreshTokens (sessions)');
              //     const client = new FusionAuthClient(
              //       fusionAuthRefreshTokenScopedApiKey,
              //       fusionAuthUrl,
              //       fusionAuthTenantId,
              //     );
              //     console.log('revoking ');
              //     const revokingResponse = await client.revokeRefreshTokensByUserId(
              //       userId,
              //     );
              //     console.log(revokingResponse.wasSuccessful());
              //     console.log(revokingResponse.exception);
              //   }
              // }
              // to initiate the process of ending FusionAuth's SSO session and follow the configured logout URLs for the application. See https://github.com/FusionAuth/fusionauth-example-node-centralized-sessions/blob/main/changebank/src/index.ts
              return NextResponse.redirect(fusionAuthLogoutUrl);
            } catch (error) {
              console.error(error);
              console.error(JSON.stringify(error));
              return NextResponse.redirect(process.env.NEXTAUTH_URL!);
            }
          }
          
          function getLogoutRedirectUrl(req: NextRequest, idToken: any): string {
            const { searchParams } = new URL(req.url);
            const postLogoutRedirectUri = searchParams.get('post_logout_redirect_uri');
          
            const params = new URLSearchParams();
          
            if (postLogoutRedirectUri) {
              params.set('post_logout_redirect_uri', postLogoutRedirectUri);
            }
          
            if (fusionAuthTenantId) {
              params.set('tenantId', fusionAuthTenantId);
            }
          
            if (fusionAuthClientId) {
              params.set('client_id', fusionAuthClientId);
            } else if (idToken) {
              params.set('id_token_hint', idToken);
            }
          
            return `${fusionAuthUrl}/oauth2/logout?${params.toString()}`;
          }
          

          There is also commented out code that shows how to revoke all sessions like in the FusionAuth example, but I guess that would revoke sessions that are from another device as well which I'd rather not do.

          mark.robustelliM 1 Reply Last reply Reply Quote 0
          • mark.robustelliM
            mark.robustelli @laurahernandez
            last edited by

            @laurahernandez This appears to be the right approach. I am a bit confused on whether you got it working or not in terms of no longer seeing the sessions in the AdminUI after your flow. Please let me know.

            L 1 Reply Last reply Reply Quote 0
            • L
              laurahernandez @mark.robustelli
              last edited by

              @mark-robustelli Yes, it's all working. After the logout flow executes, the sessions that were being left behind on FusionAuth are now being revoked properly. Thanks.

              1 Reply Last reply Reply Quote 0
              • First post
                Last post