diff --git a/packages/nextjs/src/api/urls.ts b/packages/nextjs/src/api/urls.ts index d93236c7..25f78777 100644 --- a/packages/nextjs/src/api/urls.ts +++ b/packages/nextjs/src/api/urls.ts @@ -11,7 +11,7 @@ export const ApiUrls = { }, }; -interface BuildRouteResult { +export interface BuildRouteResult { asPath: string; asUrl: URL; } diff --git a/packages/nextjs/src/common/FronteggBaseProvider.tsx b/packages/nextjs/src/common/FronteggBaseProvider.tsx index c521c0e0..b2239485 100644 --- a/packages/nextjs/src/common/FronteggBaseProvider.tsx +++ b/packages/nextjs/src/common/FronteggBaseProvider.tsx @@ -8,6 +8,7 @@ import AppContext from './AppContext'; import initializeFronteggApp from '../utils/initializeFronteggApp'; import useRequestAuthorizeSSR from './useRequestAuthorizeSSR'; import useOnRedirectTo from '../utils/useOnRedirectTo'; +import config from '../config'; const Connector: FC = ({ router, appName = 'default', ...props }) => { const isSSR = typeof window === 'undefined'; @@ -30,11 +31,6 @@ const Connector: FC = ({ router, appName = 'default', ... ); ContextHolder.setOnRedirectTo(onRedirectTo); - // useEffect(() => { - // if(window.location.pathname == '/account/login') { - // app.store.dispatch({ type: 'auth/requestAuthorize', payload: true }); - // } - // }, [app]); useRequestAuthorizeSSR({ app, user, tenants, activeTenant, session }); return ( @@ -49,6 +45,8 @@ const Connector: FC = ({ router, appName = 'default', ... }; export const FronteggBaseProvider: FC = (props) => { + config.fronteggAppOptions = props ?? {}; + return ( {props.children} diff --git a/packages/nextjs/src/common/FronteggRouterBase.tsx b/packages/nextjs/src/common/FronteggRouterBase.tsx index ff895a3d..664e60dc 100644 --- a/packages/nextjs/src/common/FronteggRouterBase.tsx +++ b/packages/nextjs/src/common/FronteggRouterBase.tsx @@ -8,7 +8,6 @@ import { FRONTEGG_AFTER_AUTH_REDIRECT_URL } from '../utils/common/constants'; import AppContext from './AppContext'; import React from 'react'; import { ParsedUrlQuery } from 'querystring'; -import { useLogoutHostedLogin } from './hooks'; interface FronteggRouterBaseProps { queryParams?: ParsedUrlQuery; @@ -20,8 +19,7 @@ export function FronteggRouterBase(props: FronteggRouterBaseProps) { const { queryParams = {}, pathArr, isAppDirEnabled } = props; const app = useContext(AppContext); const loginWithRedirect = useLoginWithRedirect(); - const { requestAuthorize } = useLoginActions(); - const logoutHosted = useLogoutHostedLogin(); + const { requestAuthorize, logout } = useLoginActions(); useEffect(() => { if (!app) { @@ -40,7 +38,7 @@ export function FronteggRouterBase(props: FronteggRouterBaseProps) { } loginWithRedirect(); } else if (pathname === routesObj.logoutUrl) { - logoutHosted(window.location.origin + window.location.search); + logout(); } } else { if (pathname.startsWith(routesObj.hostedLoginRedirectUrl ?? '/oauth/callback')) { @@ -54,6 +52,6 @@ export function FronteggRouterBase(props: FronteggRouterBaseProps) { } } } - }, [app, queryParams, pathArr, loginWithRedirect, logoutHosted]); + }, [app, queryParams, pathArr, loginWithRedirect, logout]); return <>; } diff --git a/packages/nextjs/src/common/hooks.ts b/packages/nextjs/src/common/hooks.ts index b2df7abc..db04399e 100644 --- a/packages/nextjs/src/common/hooks.ts +++ b/packages/nextjs/src/common/hooks.ts @@ -6,6 +6,7 @@ import { buildLogoutRoute } from '../api/urls'; * Hook to logout client side for hosted login * @returns {Function} logout function to be used in the client side for hosted login * @param redirectUrl - The URL to redirect to after successful logout will be window.location.href by default. + * @deprecated use `const { logout } = useLoginActions();` */ export const useLogoutHostedLogin = () => { diff --git a/packages/nextjs/src/common/useRequestAuthorizeSSR.ts b/packages/nextjs/src/common/useRequestAuthorizeSSR.ts index 44603f49..82c5f9d8 100644 --- a/packages/nextjs/src/common/useRequestAuthorizeSSR.ts +++ b/packages/nextjs/src/common/useRequestAuthorizeSSR.ts @@ -5,12 +5,19 @@ import { FronteggApp } from '@frontegg/js'; import { AllUserData } from '../types'; export default function useRequestAuthorizeSSR({ app, user, tenants, session }: { app: FronteggApp } & AllUserData) { + const userWithTokensOrNull = user + ? { + ...user, + refreshToken: session?.refreshToken, + accessToken: user.accessToken ?? session?.accessToken, + } + : null; useEffect(() => { app?.store.dispatch({ type: 'auth/requestAuthorizeSSR', payload: { accessToken: session?.accessToken, - user: user ? { ...user, refreshToken: session?.refreshToken } : null, + user: userWithTokensOrNull, tenants, }, }); diff --git a/packages/nextjs/src/config/index.ts b/packages/nextjs/src/config/index.ts index d34cca53..3a87a86c 100644 --- a/packages/nextjs/src/config/index.ts +++ b/packages/nextjs/src/config/index.ts @@ -18,7 +18,6 @@ const setupEnvVariables = { }; class Config { - public authRoutes: Partial = {}; public fronteggAppOptions: Partial = {}; constructor() { if (typeof window === 'undefined') { @@ -66,6 +65,10 @@ class Config { return generateCookieDomain(this.appUrl); } + get authRoutes(): Partial { + return this.fronteggAppOptions?.authOptions?.routes ?? {}; + } + private validatePassword() { const passwordMaps = this.password; for (let key of Object.keys(passwordMaps)) { diff --git a/packages/nextjs/src/middleware/ProxyResponseCallback.ts b/packages/nextjs/src/middleware/ProxyResponseCallback.ts index 61368ccb..79598583 100644 --- a/packages/nextjs/src/middleware/ProxyResponseCallback.ts +++ b/packages/nextjs/src/middleware/ProxyResponseCallback.ts @@ -4,7 +4,7 @@ import { NextApiResponse } from 'next'; import config from '../config'; import CookieManager from '../utils/cookies'; import { createSessionFromAccessToken } from '../common'; -import { isFronteggLogoutUrl } from './helpers'; +import { getHostedLogoutUrl, isFronteggLogoutUrl, isFronteggOauthLogoutUrl } from './helpers'; import fronteggLogger from '../utils/fronteggLogger'; import { isSSOPostRequest } from '../utils/refreshAccessToken/helpers'; @@ -41,6 +41,11 @@ const ProxyResponseCallback: ProxyResCallback res, req, }); + if (isFronteggOauthLogoutUrl(url) || config.isHostedLogin) { + const { asPath: hostedLogoutUrl } = getHostedLogoutUrl(req.headers['referer']); + res.status(302).end(hostedLogoutUrl); + return; + } res.status(statusCode).end(bodyStr); return; } diff --git a/packages/nextjs/src/middleware/helpers.ts b/packages/nextjs/src/middleware/helpers.ts index 388ee841..aadd320f 100644 --- a/packages/nextjs/src/middleware/helpers.ts +++ b/packages/nextjs/src/middleware/helpers.ts @@ -1,3 +1,7 @@ +import { BuildRouteResult, buildLogoutRoute } from '../api/urls'; +import config from '../config'; +import { authInitialState } from '@frontegg/redux-store'; + /** * If pattern information matching the input url information is found in the `pathRewrite` array, * the url value is partially replaced with the `pathRewrite.replaceStr` value. @@ -28,9 +32,28 @@ export const rewritePath = ( return url; }; -export const isFronteggLogoutUrl = (url: string) => { - return url.endsWith('/logout'); - // return ( - // fronteggAuthApiRoutesRegex.filter((path) => path.endsWith('/logout')).findIndex((route) => url.endsWith(route)) >= 0 - // ); +/** + * Checks If route is a logout route + * @param url + */ +export const isFronteggLogoutUrl = (url: string) => url.endsWith('/logout'); + +/** + * Checks If route is a hosted logout route + * @param url + */ +export const isFronteggOauthLogoutUrl = (url: string) => url.endsWith('/oauth/logout'); + +/** + * Returns url to be redirected for hosted logout + * @param referer the route to redirect to after logout + */ +export const getHostedLogoutUrl = (referer = config.appUrl): BuildRouteResult => { + const logoutPath = config.authRoutes?.logoutUrl ?? authInitialState.routes.logoutUrl; + const refererUrl = new URL(referer); + const isLogoutRoute = refererUrl.toString().includes(logoutPath); + + const redirectUrl = isLogoutRoute ? refererUrl.origin + refererUrl.search : refererUrl.toString(); + + return buildLogoutRoute(redirectUrl, config.baseUrl); }; diff --git a/packages/nextjs/src/pages/withFronteggApp/withFronteggApp.tsx b/packages/nextjs/src/pages/withFronteggApp/withFronteggApp.tsx index 5b94260a..208cefb6 100644 --- a/packages/nextjs/src/pages/withFronteggApp/withFronteggApp.tsx +++ b/packages/nextjs/src/pages/withFronteggApp/withFronteggApp.tsx @@ -41,9 +41,6 @@ export const withFronteggApp = (app: FronteggCustomAppClass, options?: WithFront }; }; - config.authRoutes = options?.authOptions?.routes ?? {}; - config.fronteggAppOptions = options ?? {}; - function CustomFronteggApp(appProps: AppProps) { const { user, tenants, activeTenant, session, envAppUrl, envBaseUrl, envClientId } = appProps.pageProps; return ( diff --git a/packages/nextjs/src/utils/routing/index.ts b/packages/nextjs/src/utils/routing/index.ts index 8d8015a7..8f5eb988 100644 --- a/packages/nextjs/src/utils/routing/index.ts +++ b/packages/nextjs/src/utils/routing/index.ts @@ -16,7 +16,7 @@ export function getAuthRoutes(): { routesArr: string[]; routesObj: Record { + test('getHostedLogoutUrl returns the appUrl as post_logout_redirect_uri if no referer', () => { + const hostedLogoutUrl = getHostedLogoutUrl().asPath; + expect(hostedLogoutUrl).toBe( + `${config.baseUrl}/oauth/logout?post_logout_redirect_uri=${encodeURIComponent(config.appUrl + '/')}` + ); + }); + + test('getHostedLogoutUrl returns the correct url with "session" in post_logout_redirect_uri', () => { + const redirectUrl = 'https://test.recirect.io/session'; + const hostedLogoutUrl = getHostedLogoutUrl(redirectUrl).asPath; + expect(hostedLogoutUrl).toBe( + `${config.baseUrl}/oauth/logout?post_logout_redirect_uri=${encodeURIComponent(redirectUrl)}` + ); + }); + + test('getHostedLogoutUrl should return the appUrl url as post_logout_redirect_uri if referer is logout path', async () => { + const hostedLogoutUrl = getHostedLogoutUrl(`${config.appUrl}/account/logout`).asPath; + expect(hostedLogoutUrl).toBe( + `${config.baseUrl}/oauth/logout?post_logout_redirect_uri=${encodeURIComponent(config.appUrl)}` + ); + }); + + test('getHostedLogoutUrl should return base url in post_logout_redirect_uri if logout path', async () => { + const hostedLogoutUrl = getHostedLogoutUrl(`${config.appUrl}/account/logout?organization=osem`).asPath; + expect(hostedLogoutUrl).toBe( + `${config.baseUrl}/oauth/logout?post_logout_redirect_uri=${encodeURIComponent( + `${config.appUrl}?organization=osem` + )}` + ); + }); +});