Skip to content

Instantly share code, notes, and snippets.

@kibolho
Last active August 28, 2023 20:45
Show Gist options
  • Save kibolho/07c3571011a488f77049e9fdb5648381 to your computer and use it in GitHub Desktop.
Save kibolho/07c3571011a488f77049e9fdb5648381 to your computer and use it in GitHub Desktop.
Auth0 B2B Contexts and Hooks
/**
* Handler that will be called during the execution of a PostLogin flow.
*
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
exports.onExecutePostLogin = async (event, api) => {
// This rule adds the authenticated user's email address to the access token.
if (event.authorization) {
const namespace = 'https://poolit.com';
const profile = {
email: event.user.email,
email_verified: event.user.email_verified,
user_metadata: event.user.user_metadata,
family_name: event.user.family_name,
given_name: event.user.given_name,
phone: event.user.phone_number,
phone_verified: event.user.phone_verified,
}
const org_metadata = {
...event.organization?.metadata,
"organization_id": event.organization?.id
}
const app_metadata = {
...event.client.metadata,
"name": event.client.name,
}
api.accessToken.setCustomClaim(`${namespace}/profile`, profile);
api.accessToken.setCustomClaim(`${namespace}/organization`, org_metadata);
api.accessToken.setCustomClaim(`${namespace}/app`, app_metadata);
api.accessToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
api.idToken.setCustomClaim(`${namespace}/organization`, org_metadata)
}
};
/**
* Handler that will be invoked when this action is resuming after an external redirect. If your
* onExecutePostLogin function does not perform a redirect, this function can be safely ignored.
*
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
// exports.onContinuePostLogin = async (event, api) => {
// };
'use client';
import {
AppState,
Auth0Provider,
Auth0ProviderOptions,
User as Auth0User,
AuthorizationParams,
useAuth0,
} from '@auth0/auth0-react';
import * as jwtDecode from 'jwt-decode';
import { ReactNode, createContext, useCallback, useEffect, useState } from 'react';
import { useTenant } from '../hooks';
type User = {
email: string;
name?: string;
picture?: string;
};
type TokenCollection = {
access_token: string;
id_token: string;
};
type TokenMetadata = {
organizationName: string;
};
export type AuthContextState = {
isLoading: boolean;
isAuthenticated: boolean;
user?: User;
error?: Error;
login: () => void;
logout: () => void;
tokenMetadata?: TokenMetadata
isInvitation: boolean
};
export type AuthProviderProps = {
children: ReactNode;
clientId?: string;
unAuthenticatedEntryPath?: string;
authenticatedEntryPath?: string;
onLoginSuccess?: (params: {
email: string;
avatar: string;
userName: string;
accessToken: string;
}) => void;
onLogoutSuccess?: () => void;
setCookieForCurrentDomain?: boolean;
};
const defaultAuthContextState: AuthContextState = {
isLoading: false,
isAuthenticated: false,
login: () => {},
logout: () => {},
isInvitation: false,
};
export const AuthContext = createContext<AuthContextState>(defaultAuthContextState);
const auth0ClientId = process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID || '';
const auth0Domain = process.env.NEXT_PUBLIC_AUTH0_DOMAIN || '';
const auth0Audience = process.env.NEXT_PUBLIC_AUTH0_AUDIENCE;
const auth0Namespace = process.env.NEXT_PUBLIC_AUTH0_NAMESPACE
const auth0CookieDomain =
process.env.NEXT_PUBLIC_AUTH0_COOKIE_DOMAIN
function InnerAuthProvider({ children, ...props }: AuthProviderProps) {
const {
error,
getAccessTokenSilently,
isAuthenticated,
isLoading,
loginWithRedirect,
logout,
user: userFromAuth0,
} = useAuth0<User & Auth0User>();
const { tenantConfig } = useTenant();
const [authError, setAuthError] = useState<string | null>(null);
const [user, setUser] = useState<User>();
const [tokenMetadata, setTokenMetadata] = useState<TokenMetadata>();
const {
authenticatedEntryPath,
onLoginSuccess,
onLogoutSuccess,
unAuthenticatedEntryPath = "",
} = props;
const url = typeof window !== 'undefined' ? window.location.origin : '';
const baseRedirectUrl = url;
const urlSearchParams = typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : undefined;
const organization = urlSearchParams?.get('organization') || undefined;
const invitation = urlSearchParams?.get('invitation') || undefined;
const handleLogin = async () => {
const authorizationParams: AuthorizationParams = {
redirect_uri: baseRedirectUrl + authenticatedEntryPath,
organization: organization || tenantConfig?.auth0_organization_id,
invitation,
};
loginWithRedirect({ authorizationParams });
};
const handleLogout = useCallback(async () => {
logout({
logoutParams: {
returnTo: baseRedirectUrl + unAuthenticatedEntryPath,
},
});
onLogoutSuccess?.();
}, [baseRedirectUrl, logout, onLogoutSuccess, unAuthenticatedEntryPath]);
const handleGetTokens = useCallback(async (): Promise<TokenCollection> => {
const tokens = await getAccessTokenSilently({
detailedResponse: true,
authorizationParams: {
scope: 'openid profile',
audience: auth0Audience,
redirect_uri: baseRedirectUrl + authenticatedEntryPath,
organization: tenantConfig?.auth0_organization_id,
},
});
if (!tokens.access_token) {
throw new Error('No tokens found');
}
return tokens;
}, [getAccessTokenSilently, baseRedirectUrl, authenticatedEntryPath, tenantConfig?.auth0_organization_id]);
const setAccessToken = useCallback(async () => {
try {
const { access_token, id_token } = await handleGetTokens();
const decodedIdToken = jwtDecode.default<any>(id_token);
const payloadKey = `${auth0Namespace}/organization`;
const organizationUrl = decodedIdToken[payloadKey]?.tenant_url || '';
const organizationName = decodedIdToken[payloadKey]?.tenant_name || '';
setTokenMetadata({organizationName})
if (userFromAuth0)
setUser({
email: userFromAuth0.email,
name: userFromAuth0.name,
picture: userFromAuth0.picture,
});
if (access_token) {
onLoginSuccess?.({
email: userFromAuth0?.email || '',
avatar: userFromAuth0?.picture || '',
userName: userFromAuth0?.sub || '',
accessToken: access_token,
});
if (
organizationUrl &&
organizationUrl !== url
) {
const url = organizationUrl + authenticatedEntryPath;
window.location.replace(url);
}
}
} catch (error) {
setAuthError('Authentication Error. Please contact support.');
}
}, [handleGetTokens, userFromAuth0, onLoginSuccess, url, authenticatedEntryPath]);
useEffect(() => {
if (!isLoading && isAuthenticated) {
setAuthError(null);
setAccessToken();
}
}, [setAccessToken, isAuthenticated, isLoading]);
if (authError) {
return (
<div>
<header>
<h1>Error</h1>
</header>
<main>
<span>{authError}</span>
</main>
</div>
);
}
return (
<AuthContext.Provider
value={{
error,
isLoading: isLoading && !error,
isAuthenticated: !isLoading && !error && isAuthenticated,
login: handleLogin,
logout: handleLogout,
user,
tokenMetadata,
isInvitation: !!invitation
}}
>
{children}
</AuthContext.Provider>
);
}
export function AuthProvider({ children, ...props }: AuthProviderProps) {
const providerConfig: Auth0ProviderOptions = {
domain: auth0Domain,
clientId: props.clientId ?? auth0ClientId,
authorizationParams: { audience: auth0Audience },
cookieDomain: props.setCookieForCurrentDomain ? undefined : auth0CookieDomain,
onRedirectCallback,
};
return (
<Auth0Provider {...providerConfig}>
<InnerAuthProvider {...props}>{children}</InnerAuthProvider>
</Auth0Provider>
);
}
const onRedirectCallback = (appState?: AppState) => {
if(appState?.returnTo)
window.location.href = appState?.returnTo
};
export type TenantConfig = {
name: string;
auth0_organization_id: string;
api_base_url?: string;
};
const empresa1: TenantConfig = {
"name": "Empresa 1",
"auth0_organization_id": 'org_id1',
}
const empresa2: TenantConfig = {
"name": "Empresa 1",
"auth0_organization_id": 'org_id2',
}
export const tenantConfigs: {[key: string]: TenantConfig} = {
"empresa1.localhost:3000": empresa1,
"empresa2.localhost:3000": empresa2,
"empresa1.b2b.abilioazevedo.com.br": empresa1,
"empresa2.b2b.abilioazevedo.com.br": empresa2
}
'use client';
import { ReactNode, createContext, useEffect, useState } from 'react';
import { TenantConfig, tenantConfigs } from './tenantConfigs';
export type TenantContextState = {
tenantConfig?: TenantConfig;
};
type TenantProviderProps = {
children: ReactNode;
isToMock?: boolean;
};
const defaultTenantContextState: TenantContextState = {
tenantConfig: undefined,
};
export const TenantContext = createContext<TenantContextState>(defaultTenantContextState);
export function TenantProvider({ children }: TenantProviderProps) {
const [contextValue, setContextValue] = useState<TenantContextState>(defaultTenantContextState);
const [tenantError, setTenantError] = useState<string | null>(null);
const getConfig = async () => {
try {
const clientDomain = window.location.host;
// You can fetch this from your backend
const tenantConfig = tenantConfigs[clientDomain]
setContextValue({ tenantConfig });
} catch (error) {
setTenantError('This tenant is not well configured');
}
};
useEffect(() => {
getConfig();
}, []);
if (tenantError) {
return (
<div>
<header>
<h1>Error</h1>
</header>
<main>
<span>{tenantError}</span>
</main>
</div>
);
}
return <TenantContext.Provider value={contextValue}>{children}</TenantContext.Provider>;
}
import { useContext } from 'react';
import { AuthContext } from '../contexts';
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('This component should be used inside <AuthProvider> component.');
}
return context;
}
import { useContext } from 'react';
import { TenantContext } from '../contexts';
export function useTenant() {
const context = useContext(TenantContext);
if (!context) {
throw new Error('This component should be used inside <TenantProvider> component.');
}
return context;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment