import type { User, Auth0ContextInterface, GetTokenSilentlyOptions, GetIdTokenClaimsOptions, IdToken } from '@auth0/auth0-react' import { SDKModel } from '../core/model.js' import { SDK, TenantError, TenantModel } from '../index.js' export class AuthError extends Error { name = 'AuthError' } export interface QueryContext { tenantName?: string organization?: string systemId?: string } export enum TokenCustomClaimKeysEnum { ORG_DISPLAY_NAME = 'https://auth.sitecorecloud.io/claims/org_display_name', ORG_ID = 'https://auth.sitecorecloud.io/claims/org_id', ORG_NAME = 'https://auth.sitecorecloud.io/claims/org_name', ROLES = 'https://auth.sitecorecloud.io/claims/roles', ORG_TYPE = 'https://auth.sitecorecloud.io/claims/org_type' } export interface ITokenClaims { __raw: string name?: string given_name?: string family_name?: string middle_name?: string nickname?: string preferred_username?: string profile?: string picture?: string website?: string email?: string email_verified?: boolean gender?: string birthdate?: string zoneinfo?: string locale?: string phone_number?: string phone_number_verified?: boolean address?: string updated_at?: string iss?: string aud?: string exp?: number nbf?: number iat?: number jti?: string azp?: string nonce?: string auth_time?: string at_hash?: string c_hash?: string acr?: string amr?: string sub_jwk?: string cnf?: string sid?: string org_id?: string [TokenCustomClaimKeysEnum.ORG_DISPLAY_NAME]: string [TokenCustomClaimKeysEnum.ORG_ID]: string [TokenCustomClaimKeysEnum.ORG_DISPLAY_NAME]: string [TokenCustomClaimKeysEnum.ROLES]: string[] [TokenCustomClaimKeysEnum.ORG_TYPE]: string [key: string]: any } export interface ITokenClaimsWithAccountOrgId extends ITokenClaims { [TokenCustomClaimOrgAccountIdKey]: string } export const TokenCustomClaimOrgAccountIdKey = 'https://auth.sitecorecloud.io/claims/org_account_id' export interface Auth extends Omit, 'getAccessTokenSilently'> { getIdTokenClaims: (options?: GetIdTokenClaimsOptions) => Promise getAccessTokenSilently: (props?: GetTokenSilentlyOptions) => Promise login: (query: any) => void _logout: Auth['logout'] isExternal: () => boolean claims: ITokenClaims } export interface AuthModel extends Auth {} export function getAuthDefaults(sdk: SDK) { return { isAuthenticated: false, isLoading: false, user: { sub: 'something' } as User, loginWithRedirect: () => {}, logout: () => {} } } export class AuthModel extends SDKModel implements Auth { tenant?: TenantModel getDefaults(sdk: SDK) { return getAuthDefaults(sdk) } isExternal() { return !this.handleRedirectCallback && !!this.getAccessTokenSilently } get logout() { return this._logout } set logout(callback) { this._logout = (options) => { AuthModel.setLocalStorageContext(null) callback(options) } } async retrieveClaims() { this.claims = this.getIdTokenClaims ? ((await this.getIdTokenClaims()) as ITokenClaims) : null } /** Re-authenticate the user. This method is safe to call multiple times, it will only initiate authentication * if necessary: If user is not yet logged in, or the user logged in to wrong organization */ async login(query: any) { if (query?.error) { throw new Error(query.error_description) } if (!this.isLoading && this.loginWithRedirect) { const queryContext = this.getContext(query) const requestedOrganizationId = queryContext.organization const lastOrganizationId = this.isAuthenticated ? this.user['https://auth.sitecorecloud.io/claims/org_id'] : AuthModel.getContextFromLocalStorage().organization if ( // Need to log in? !this.isAuthenticated || // Need to switch organizations? (requestedOrganizationId && requestedOrganizationId != lastOrganizationId) ) { AuthModel.setLocalStorageContext(null) await this.loginWithRedirect({ ...AuthModel.formatContextForAuth0(queryContext), appState: { returnTo: location.pathname } }) return true } } return false } getUser() { if (!this.user) return {} return { id: this.user.sub?.replace('auth0|', ''), name: this.user.name, givenName: this.user.given_name, familyName: this.user.family_name, email: this.user.email, picture: this.user.picture, nickname: this.user.nickname } } getGainsightUserData() { if (!this.user) return null const org_account_id = this.claims?.[TokenCustomClaimOrgAccountIdKey] return { userFields: { id: this.user.email, email: this.user.email, firstName: this.user.given_name, lastName: this.user.family_name }, accountFields: { id: org_account_id, // Salesforce Account ID sfdcId: org_account_id } } } getGainsightGlobalContext() { const orgRoles = this.claims?.[TokenCustomClaimKeysEnum.ROLES] .filter((role: string) => role.includes('Organization')) .map((orgRole: string) => orgRole.split('\\')[1]) return { 'Organization DisplayName': this.claims?.[TokenCustomClaimKeysEnum.ORG_DISPLAY_NAME], OrganizationID: this.claims?.[TokenCustomClaimKeysEnum.ORG_ID], 'Organization Role': orgRoles.join(', '), 'Organization Type': this.claims?.[TokenCustomClaimKeysEnum.ORG_TYPE] || '' } } getDefaultContext(): Partial { return {} } getContext(query: QueryContext) { return { ...this.getDefaultContext(), ...AuthModel.getContextFromLocalStorage(), ...this.tenant?.getContext(), ...AuthModel.getContextFromUser(this.user), ...AuthModel.getContextFromQuery(query) } } initPreflight(query: any, audience: string) { if (!this.isAuthenticated) return const { sdk, getAccessTokenSilently } = this const context = AuthModel.formatContextForAuth0(this.getContext(query)) // sdk first, then query, then localstorage sdk.preflight = async function (url, options) { this.accessToken = await getAccessTokenSilently({ audience: audience, ...context }) return options } } /** * - Fetch tenant by tenantName (query or localstorage) * - If tenant has access to project/lib, use this tenant * - Else fetch all tenants for the user, search if there is a tenant that has access to project/lib and use this tenant * - If the user has no tenants at all, throw corresponding error * - If user has tenants but none has access to project/lib, throw corresponding error */ async findTenant(query: any, systemId: string, libraryId: string): Promise { const { tenantName: name } = this.getContext(query) return this.sdk.tenants.search({ name, systemId }).then(async (tenants) => { const foundTenants: TenantModel[] = tenants.length ? tenants : await this.sdk.tenants.search({ systemId }) if (!foundTenants.length) throw new TenantError('User does not have access to any components libraries') /** Find tenant that has access to project or that has access to a project (for non-xm-cloud-libraries) */ const activeTenant = foundTenants.find((tenant) => TenantModel.generateProjectIdFromTenant(tenant) === libraryId) || foundTenants.find((tenant) => TenantModel.generateProjectIdFromTenant(tenant)) AuthModel.setLocalStorageContext({ tenantName: activeTenant.name, organization: activeTenant.organizationId, systemId: activeTenant.systemId }) return activeTenant }) } static setLocalStorageContext(context: QueryContext) { localStorage.setItem('feaas:auth', JSON.stringify(context || '{}')) } static getContextFromQuery(query: QueryContext) { const context = {} as QueryContext if (query.organization) { context.organization = query.organization context.tenantName = query.tenantName || undefined } if (query.systemId) context.systemId = query.systemId return context } static getContextFromUser(user: typeof AuthModel.prototype.user) { const context = {} as QueryContext if (user?.['https://auth.sitecorecloud.io/claims/tenant_name']) context.tenantName = user['https://auth.sitecorecloud.io/claims/tenant_name'] if (user?.['https://auth.sitecorecloud.io/claims/org_id']) context.organization = user['https://auth.sitecorecloud.io/claims/org_id'] return context } static getContextFromLocalStorage() { const items = JSON.parse(localStorage.getItem('feaas:auth') || '{}') const context = {} as QueryContext if (items.tenantName) context.tenantName = items.tenantName if (items.organization) context.organization = items.organization if (items.systemId) context.systemId = items.systemId return context } static formatContextForAuth0(context: QueryContext) { return (({ tenantName: tenant_name, organization: organization_id, systemId: system_id }) => ({ tenant_name, organization_id, system_id }))(context) } }