Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FR-13875 - support logout hosted login in middleware #312

Merged
merged 5 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/nextjs/src/api/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const ApiUrls = {
},
};

interface BuildRouteResult {
export interface BuildRouteResult {
asPath: string;
asUrl: URL;
}
Expand Down
8 changes: 3 additions & 5 deletions packages/nextjs/src/common/FronteggBaseProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<FronteggProviderProps> = ({ router, appName = 'default', ...props }) => {
const isSSR = typeof window === 'undefined';
Expand All @@ -30,11 +31,6 @@ const Connector: FC<FronteggProviderProps> = ({ 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 (
<AppContext.Provider value={app}>
Expand All @@ -49,6 +45,8 @@ const Connector: FC<FronteggProviderProps> = ({ router, appName = 'default', ...
};

export const FronteggBaseProvider: FC<FronteggProviderProps> = (props) => {
config.fronteggAppOptions = props ?? {};

return (
<Connector {...props} framework={'nextjs'}>
{props.children}
Expand Down
8 changes: 3 additions & 5 deletions packages/nextjs/src/common/FronteggRouterBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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')) {
Expand All @@ -54,6 +52,6 @@ export function FronteggRouterBase(props: FronteggRouterBaseProps) {
}
}
}
}, [app, queryParams, pathArr, loginWithRedirect, logoutHosted]);
}, [app, queryParams, pathArr, loginWithRedirect, logout]);
return <></>;
}
1 change: 1 addition & 0 deletions packages/nextjs/src/common/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
9 changes: 8 additions & 1 deletion packages/nextjs/src/common/useRequestAuthorizeSSR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
});
Expand Down
5 changes: 4 additions & 1 deletion packages/nextjs/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const setupEnvVariables = {
};

class Config {
public authRoutes: Partial<AuthPageRoutes> = {};
public fronteggAppOptions: Partial<WithFronteggAppOptions> = {};
constructor() {
if (typeof window === 'undefined') {
Expand Down Expand Up @@ -66,6 +65,10 @@ class Config {
return generateCookieDomain(this.appUrl);
}

get authRoutes(): Partial<AuthPageRoutes> {
return this.fronteggAppOptions?.authOptions?.routes ?? {};
}

private validatePassword() {
const passwordMaps = this.password;
for (let key of Object.keys(passwordMaps)) {
Expand Down
7 changes: 6 additions & 1 deletion packages/nextjs/src/middleware/ProxyResponseCallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -41,6 +41,11 @@ const ProxyResponseCallback: ProxyResCallback<IncomingMessage, NextApiResponse>
res,
req,
});
if (isFronteggOauthLogoutUrl(url) || config.isHostedLogin) {
const { asPath: hostedLogoutUrl } = getHostedLogoutUrl(req.headers['referer']);
res.status(302).end(hostedLogoutUrl);
return;
}
yuvalotem1 marked this conversation as resolved.
Show resolved Hide resolved
res.status(statusCode).end(bodyStr);
return;
}
Expand Down
33 changes: 28 additions & 5 deletions packages/nextjs/src/middleware/helpers.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/src/utils/routing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function getAuthRoutes(): { routesArr: string[]; routesObj: Record<string
export function isAuthRoute(pathname: string): boolean {
const { routesArr, routesObj } = getAuthRoutes();

if (config.fronteggAppOptions.hostedLoginBox) {
if (config.isHostedLogin) {
return (
routesObj.loginUrl === pathname ||
routesObj.logoutUrl === pathname ||
Expand Down
36 changes: 36 additions & 0 deletions packages/nextjs/tests/middleware/helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { test, expect } from '@playwright/test';
import config from '../../src/config';
import { getHostedLogoutUrl } from '../../src/middleware/helpers';

test.describe('middleware helpers tests', () => {
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`
)}`
);
});
});
Loading