Last active
August 28, 2023 20:45
-
-
Save kibolho/07c3571011a488f77049e9fdb5648381 to your computer and use it in GitHub Desktop.
Auth0 B2B Contexts and Hooks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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) => { | |
// }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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 | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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>; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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