Skip to content

Commit

Permalink
Add support for application id and option to use appId in isolated mode
Browse files Browse the repository at this point in the history
  • Loading branch information
frontegg-david committed Oct 16, 2024
1 parent 0d4e49f commit 1631534
Show file tree
Hide file tree
Showing 16 changed files with 206 additions and 23 deletions.
33 changes: 32 additions & 1 deletion packages/example-app-directory/app/UserState.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
'use client';
import { useAuthUserOrNull, useLoginWithRedirect, AdminPortal, useLogoutHostedLogin } from '@frontegg/nextjs';
import {
useAuthActions,
useAuthUserOrNull,
useLoginWithRedirect,
AdminPortal,
useLogoutHostedLogin,
} from '@frontegg/nextjs';
import Link from 'next/link';

export const UserState = () => {
const user = useAuthUserOrNull();
const loginWithRedirect = useLoginWithRedirect();
const logoutHosted = useLogoutHostedLogin();
const { switchTenant } = useAuthActions();

/*
* Replace the elements below with your own.
Expand All @@ -18,6 +25,30 @@ export const UserState = () => {

<br />
<br />
<div>
<h2>Tenants:</h2>
{(user?.tenants ?? []).map((tenant) => {
return (
<div key={tenant.tenantId}>
<b>Tenant Id:</b>
{tenant['tenantId']}
<span style={{ display: 'inline-block', width: '20px' }} />
{tenant.tenantId === user?.tenantId ? (
<b>Current Tenant</b>
) : (
<button
onClick={() => {
switchTenant({ tenantId: tenant.tenantId });
}}
>
Switch to tenant
</button>
)}
</div>
);
})}
</div>
<br />
<button
onClick={() => {
AdminPortal.show();
Expand Down
6 changes: 1 addition & 5 deletions packages/example-app-directory/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<head></head>
<body>
{/* @ts-expect-error Server Component for more details visit: https://github.com/vercel/next.js/issues/42292 */}
<FronteggAppProvider
authOptions={{ keepSessionAlive: true }}
customLoginOptions={{ paramKey: 'organization' }}
hostedLoginBox
>
<FronteggAppProvider authOptions={{ keepSessionAlive: true }} customLoginOptions={{ paramKey: 'organization' }}>
{children}
</FronteggAppProvider>
</body>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@
import { FronteggApiMiddleware } from '@frontegg/nextjs/middleware';

export default FronteggApiMiddleware;

/**
* Option to support multiple origins in single nextjs backend
*
* import type { NextApiRequest, NextApiResponse } from 'next';
*
* export default function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
* // Add CORS headers after the handler has run
* const allowedOrigins = ['http://localapp1.davidantoon.me:3000', 'http://localapp2.davidantoon.me:3000'];
* const origin = req.headers.origin ?? '';
*
* if (allowedOrigins.includes(origin)) {
* res.setHeader('Access-Control-Allow-Origin', origin);
* } else {
* res.removeHeader('Access-Control-Allow-Origin');
* }
*
* res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,PATCH,OPTIONS');
* res.setHeader(
* 'Access-Control-Allow-Headers',
* 'Content-Type, Authorization, x-frontegg-framework, x-frontegg-sdk, frontegg-source'
* );
* res.setHeader('Access-Control-Allow-Credentials', 'true');
*
* return FronteggApiMiddleware(req, res);
* }
*/

export const config = {
api: {
externalResolver: true,
Expand Down
28 changes: 28 additions & 0 deletions packages/example-pages/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useState } from 'react';

export function Index() {
const { user, isAuthenticated } = useAuth();
const { switchTenant } = useAuthActions();
const loginWithRedirect = useLoginWithRedirect();
const [state, setState] = useState({ userAgent: '', id: -1 });
const logoutHosted = useLogoutHostedLogin();
Expand Down Expand Up @@ -65,6 +66,33 @@ export function Index() {
<br />
<br />
<Link href='/account/logout'>logout embedded</Link>
<br />
<br />
<br />
<div>
<h2>Tenants:</h2>
{(user?.tenants ?? []).map((tenant) => {
return (
<div key={tenant.tenantId}>
<b>Tenant Id:</b>
{tenant['tenantId']}
<span style={{ display: 'inline-block', width: '20px' }} />
{tenant.tenantId === user?.tenantId ? (
<b>Current Tenant</b>
) : (
<button
onClick={() => {
switchTenant({ tenantId: tenant.tenantId });
}}
>
Switch to tenant
</button>
)}
</div>
);
})}
</div>
<br />
</div>
);
}
Expand Down
19 changes: 18 additions & 1 deletion packages/nextjs/src/api/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function removeInvalidHeaders(headers: Record<string, string>) {
* These header is used to identify the tenant for login per tenant feature
*/
export const CUSTOM_LOGIN_HEADER = 'frontegg-login-alias';

/**
* Build fetch request headers, remove invalid http headers
* @param headers - Incoming request headers
Expand All @@ -56,10 +57,23 @@ export function buildRequestHeaders(headers: Record<string, any>): Record<string
let cookie = headers['cookie'];
if (cookie != null && typeof cookie === 'string') {
cookie = cookie.replace(/fe_session-[^=]*=[^;]*$/, '').replace(/fe_session-[^=]*=[^;]*;/, '');

if (config.rewriteCookieByAppId && config.appId) {
cookie = cookie.replace(
`fe_refresh_${config.appId.replace('-', '')}`,
`fe_refresh_${config.clientId.replace('-', '')}`
);
}
}
if (cookie != null && typeof cookie === 'object') {
cookie = Object.entries(cookie)
.map(([key, value]) => `${key}=${value}`)
.map(([key, value]) => {
if (config.rewriteCookieByAppId && config.appId && key === `fe_refresh_${config.appId.replace('-', '')}`) {
return `fe_refresh_${config.clientId.replace('-', '')}=${value}`;
} else {
return `${key}=${value}`;
}
})
.join('; ');
}

Expand All @@ -77,6 +91,9 @@ export function buildRequestHeaders(headers: Record<string, any>): Record<string
'x-frontegg-sdk': `@frontegg/nextjs@${sdkVersion.version}`,
};

if (headers['frontegg-requested-application-id']) {
preparedHeaders['frontegg-requested-application-id'] = headers['frontegg-requested-application-id'];
}
if (headers[CUSTOM_LOGIN_HEADER]) {
preparedHeaders[CUSTOM_LOGIN_HEADER] = headers[CUSTOM_LOGIN_HEADER];
}
Expand Down
13 changes: 13 additions & 0 deletions packages/nextjs/src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ export enum EnvVariables {
*/
FRONTEGG_CLIENT_ID = 'FRONTEGG_CLIENT_ID',

/**
* Your Frontegg application ID, get it by visit:
* - For Dev environment [visit](https://portal.frontegg.com/development/applications)
* - For Prod environment [visit](https://portal.frontegg.com/production/applications)
*/
FRONTEGG_APP_ID = 'FRONTEGG_APP_ID',

/**
* Rewrite the cookie name by the Frontegg application ID
* to support multiple Frontegg applications with same domain
*/
FRONTEGG_REWRITE_COOKIE_BY_APP_ID = 'FRONTEGG_REWRITE_COOKIE_BY_APP_ID',

/**
* Your Frontegg application's Client Secret, get it by visit:
* - For Dev environment [visit](https://portal.frontegg.com/development/settings/general)
Expand Down
23 changes: 21 additions & 2 deletions packages/nextjs/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const setupEnvVariables = {
FRONTEGG_BASE_URL: process.env.FRONTEGG_BASE_URL,
FRONTEGG_TEST_URL: process.env.FRONTEGG_TEST_URL,
FRONTEGG_CLIENT_ID: process.env.FRONTEGG_CLIENT_ID,
FRONTEGG_APP_ID: process.env.FRONTEGG_APP_ID,
FRONTEGG_REWRITE_COOKIE_BY_APP_ID: process.env.FRONTEGG_REWRITE_COOKIE_BY_APP_ID,
FRONTEGG_CLIENT_SECRET: process.env.FRONTEGG_CLIENT_SECRET,
FRONTEGG_ENCRYPTION_PASSWORD: process.env.FRONTEGG_ENCRYPTION_PASSWORD,
FRONTEGG_COOKIE_NAME: process.env.FRONTEGG_COOKIE_NAME,
Expand Down Expand Up @@ -54,6 +56,18 @@ class Config {
return getEnv(EnvVariables.FRONTEGG_CLIENT_ID) ?? setupEnvVariables.FRONTEGG_CLIENT_ID;
}

get appId(): string | undefined {
return getEnvOrDefault(EnvVariables.FRONTEGG_APP_ID, setupEnvVariables.FRONTEGG_APP_ID);
}
get rewriteCookieByAppId(): boolean {
return (
getEnvOrDefault(
EnvVariables.FRONTEGG_REWRITE_COOKIE_BY_APP_ID,
setupEnvVariables.FRONTEGG_REWRITE_COOKIE_BY_APP_ID ?? 'false'
) === 'true'
);
}

get clientSecret(): string | undefined {
let clientSecret = undefined;
try {
Expand Down Expand Up @@ -88,7 +102,12 @@ class Config {
EnvVariables.FRONTEGG_COOKIE_NAME,
setupEnvVariables.FRONTEGG_COOKIE_NAME ?? 'fe_session'
);
return `${cookieNameEnv}-${this.clientId.replace(/-/g, '')}`;

if (this.rewriteCookieByAppId && this.appId) {
return `${cookieNameEnv}-${this.appId.replace(/-/g, '')}`;
} else {
return `${cookieNameEnv}-${this.clientId.replace(/-/g, '')}`;
}
}

get cookieDomain(): string {
Expand Down Expand Up @@ -152,9 +171,9 @@ class Config {
envAppUrl: this.appUrl,
envBaseUrl: this.baseUrl,
envClientId: this.clientId,
envAppId: this.appId,
secureJwtEnabled: this.secureJwtEnabled,
};
console.log('this.appEnvConfig', config);
return config;
}
}
Expand Down
7 changes: 6 additions & 1 deletion packages/nextjs/src/edge/getSessionOnEdge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,13 @@ export const handleHostedLoginCallback = async (
expires: new Date(decodedJwt.exp * 1000),
secure: isSecured,
});

let cookieName = `fe_refresh_${config.clientId.replace('-', '')}`;
if (config.rewriteCookieByAppId && config.appId) {
cookieName = `fe_refresh_${config.appId.replace('-', '')}`;
}
const refreshCookie = CookieManager.create({
cookieName: `fe_refresh_${config.clientId.replace('-', '')}`,
cookieName,
value: refreshToken ?? '',
expires: new Date(decodedJwt.exp * 1000),
secure: isSecured,
Expand Down
13 changes: 11 additions & 2 deletions packages/nextjs/src/middleware/ProxyRequestCallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,17 @@ const ProxyRequestCallback: ProxyReqCallback<ClientRequest, NextApiRequest> = (p
});

logger.debug(`${req.url} | proxy FronteggCookies (${fronteggCookiesNames.join(', ')})`);
fronteggCookiesNames.forEach((cookieName: string) => {
proxyReq.setHeader(cookieName, allCookies[cookieName]);
fronteggCookiesNames.forEach((requestCookieName: string) => {
let cookieName = requestCookieName;
if (config.rewriteCookieByAppId && config.appId) {
cookieName = requestCookieName
.replace(config.appId, config.clientId)
.replace(config.appId.replace(/-/g, ''), config.clientId.replace(/-/g, ''))
.replace(config.appId.replace('-', ''), config.clientId.replace('-', ''));

logger.debug(`cookieName ${requestCookieName} replaced with appId ${cookieName}`);
}
proxyReq.setHeader(cookieName, allCookies[requestCookieName]);
});

proxyReq.setHeader('x-frontegg-framework', req.headers['x-frontegg-framework'] ?? `next@${NextJsPkg.version}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const withFronteggApp = (app: FronteggCustomAppClass, options?: WithFront
};

function CustomFronteggApp(appProps: AppProps) {
const { user, tenants, activeTenant, session, envAppUrl, envBaseUrl, envClientId, secureJwtEnabled } =
const { user, tenants, activeTenant, session, envAppUrl, envBaseUrl, envClientId, secureJwtEnabled, envAppId } =
appProps.pageProps;
return (
<FronteggProvider
Expand All @@ -71,6 +71,7 @@ export const withFronteggApp = (app: FronteggCustomAppClass, options?: WithFront
envBaseUrl,
secureJwtEnabled,
envClientId,
envAppId,
}}
>
{app(appProps) as any}
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export interface FronteggProviderOptions extends Omit<FronteggAppOptions, 'conte
envAppUrl: string;
envBaseUrl: string;
envClientId: string;
envAppId?: string;
secureJwtEnabled?: boolean;
contextOptions?: Omit<FronteggAppOptions['contextOptions'], 'baseUrl'>;
}
Expand Down
20 changes: 15 additions & 5 deletions packages/nextjs/src/utils/cookies/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,18 @@ export const getCookieHeader = (request: RequestType): string => {
return cookieHeader;
};

export const getRefreshTokenCookieNameVariants = () => [
`fe_refresh_${config.clientId}`,
`fe_refresh_${config.clientId.replace('-', '')}`,
`fe_refresh_${config.clientId.replace(/-/g, '')}`,
];
export const getRefreshTokenCookieNameVariants = () => {
if (config.rewriteCookieByAppId && config.appId) {
return [
`fe_refresh_${config.appId}`,
`fe_refresh_${config.appId.replace('-', '')}`,
`fe_refresh_${config.appId.replace(/-/g, '')}`,
];
} else {
return [
`fe_refresh_${config.clientId}`,
`fe_refresh_${config.clientId.replace('-', '')}`,
`fe_refresh_${config.clientId.replace(/-/g, '')}`,
];
}
};
20 changes: 17 additions & 3 deletions packages/nextjs/src/utils/cookies/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ class CookieManager {
cookieNumber ? getIndexedCookieName(cookieNumber, cookieName) : cookieName;

get refreshTokenKey(): string {
return `fe_refresh_${config.clientId}`.replace(/-/g, '');
if (config.rewriteCookieByAppId && config.appId) {
return `fe_refresh_${config.appId.replace(/-/g, '')}`;
} else {
return `fe_refresh_${config.clientId.replace(/-/g, '')}`;
}
}

/**
Expand Down Expand Up @@ -266,10 +270,20 @@ class CookieManager {
return (
cookie
.map((property) => {
if (property.toLowerCase() === `domain=${config.baseUrlHost}`) {
if (property.startsWith(`fe_refresh_${config.clientId.replace('-', '')}`)) {
if (config.rewriteCookieByAppId && config.appId) {
return property.replace(
`fe_refresh_${config.clientId.replace('-', '')}`,
`fe_refresh_${config.appId.replace('-', '')}`
);
} else {
return property;
}
} else if (property.toLowerCase() === `domain=${config.baseUrlHost}`) {
return `Domain=${config.cookieDomain}`;
} else {
return property;
}
return property;
})
.join(';') + ';'
);
Expand Down
7 changes: 6 additions & 1 deletion packages/nextjs/src/utils/fetchUserData/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AllUserData, FronteggNextJSSession } from '../../types';
import { getTenants, getMe, getMeAuthorization, getEntitlements } from '../../api';
import { calculateExpiresInFromExp } from '../common';
import fronteggLogger from '../fronteggLogger';
import config from '../../config';

const FULFILLED_STATUS = 'fulfilled';

Expand All @@ -23,7 +24,11 @@ export default async function fetchUserData(options: FetchUserDataOptions): Prom

const { accessToken } = session;
const reqHeaders = await getHeaders();
const headers = { ...reqHeaders, authorization: `Bearer ${accessToken}` };
const headers: Record<string, string> = { ...reqHeaders, authorization: `Bearer ${accessToken}` };

if (config.appId) {
headers['frontegg-requested-application-id'] = config.appId;
}

logger.debug('Retrieving user data...');
const [baseUserResult, tenantsResult, entitlementsResult, meAuthorizationResult] = await Promise.allSettled([
Expand Down
Loading

0 comments on commit 1631534

Please sign in to comment.