import { HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { SsoResources } from '@core/resources/sso.resources'; import { SSOConfigState } from '@core/states/sso-config.state'; import { Applicant } from '@core/typings/applicant.typing'; import { User } from '@core/typings/client-user.typing'; import { SSOConfiguration, SSOTokenKey } from '@core/typings/sso-configuration.typing'; import { TokenResponse } from '@core/typings/token.typing'; import { environment } from '@environment'; import { I18nService } from '@yourcause/common/i18n'; import { LogService } from '@yourcause/common/logging'; import { AttachYCState, BaseYCService } from '@yourcause/common/state'; import { AppInsightsService, GuidService } from '@yourcause/common/utils'; import { DeepLinkingService } from './deep-linking.service'; import { PortalDeterminationService } from './portal-determination.service'; import { SpinnerService } from './spinner.service'; import { AuthBehaviors } from './token/token-behaviors'; @AttachYCState(SSOConfigState) @Injectable({ providedIn: 'root' }) export class SSOService extends BaseYCService { constructor ( private appInsights: AppInsightsService, private spinnerService: SpinnerService, private i18n: I18nService, private portal: PortalDeterminationService, private deepLinkingService: DeepLinkingService, private guidService: GuidService, private authBehaviors: AuthBehaviors, private ssoResources: SsoResources, private logger: LogService ) { super(); } get ssoConfig () { return this.get('ssoConfig'); } private get id_token () { return localStorage.getItem(SSOTokenKey); } private set id_token (id_token: string) { if (id_token === null) { localStorage.removeItem(SSOTokenKey); } else { localStorage.setItem(SSOTokenKey, id_token); } } setSSOConfig (config: SSOConfiguration) { this.set('ssoConfig', config); } getIdToken () { return this.id_token; } setIdToken (token: string) { this.id_token = token; } logout () { if (this.id_token) { const prefix = this.portal.getCurrentPrefix(); const post_logout_redirect_uri = this.portal.isManager ? `https://${environment.locationBase}/management/ssologout${prefix ? `?prefix=${prefix}` : ''}` : `https://apply.${environment.locationBase}/apply/ssologout${prefix ? `?prefix=${prefix}` : ''}`; const id_token_hint = this.id_token; const result = { post_logout_redirect_uri, id_token_hint }; const url = environment.identityServer + '/connect/endsession?' + Object.keys(result) .map(key => key as keyof typeof result) .map((key) => `${key}=${encodeURIComponent(result[key])}`) .join('&'); this.id_token = null; return url; } return ''; } generateNonce () { let text = ''; const possible = '0123456789abcdef'; for (let i = 0; i < 64; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)); } return text; } generateUrl (tenantId: string, prefix: string) { if ( prefix === 'barings' || prefix === 'qa-chs' ) { this.appInsights.trackEvent('sso:debug', { event: 'GENERATE_URL', prefix }); } const locationBase: string = environment.isLocalhost ? 'yourcausegrantsqa.com' : environment.locationBase; const routeBase = this.portal.isManager ? 'management' : 'apply'; const result = { acr_values: `tenant:${tenantId}`, client_id: this.portal.isManager ? environment.ssoClientId : environment.ssoApplicantClientId, response_type: 'id_token token', scope: 'openid', nonce: this.guidService.nonce(), state: this.guidService.nonce(), redirect_uri: `https://${prefix}.${locationBase}/${routeBase}/sso_redirect` }; const url = environment.identityServer + '/connect/authorize?' + Object.keys(result) .map(key => key as keyof typeof result) .map((key) => `${key}=${result[key]}`) .join('&'); if ( prefix === 'barings' || prefix === 'qa-chs' ) { this.appInsights.trackEvent('sso:debug', { event: 'GENERATED_URL', url }); } return url; } private createAndWaitOnSubdomain (newHost: string, message: T): Promise { return new Promise((resolve, reject) => { const newPath = `${newHost}${location.pathname}`; const iframe = document.createElement('iframe'); iframe.src = newPath; async function listen ({ data: inboundMessage }: { data: { ycLoaded: boolean; } | { ycAcknowledged: boolean; ycMessage: T; }; }) { if (!inboundMessage) { return; } if ('ycLoaded' in inboundMessage && inboundMessage.ycLoaded) { iframe.contentWindow.postMessage(message, newHost); } else if ('ycAcknowledged' in inboundMessage) { document.body.removeChild(iframe); window.removeEventListener('message', listen as any); resolve(); } } window.addEventListener('message', listen as any); document.body.appendChild(iframe); }); } async handOffToSubdomain ( ...args: [ string, string, TokenResponse, User|Applicant, string, string ] ) { const [ prefix, nextRoute, token, user, clientIdentifier, ssoToken ] = args; const newHost = `https://${prefix}.${environment.locationBase}${+location.port > 443 ? `:${location.port}` : ''}`; this.spinnerService.startSpinner(); this.spinnerService.setLoadingMessage(this.i18n.translate( 'GLOBAL:lblTransferringToNewDomain', {}, 'You are now being transferred to your new domain' )); await this.createAndWaitOnSubdomain(newHost, { token, user, clientIdentifier, attemptedRoute: this.deepLinkingService.getAttemptedRoute(), ssoToken: ssoToken || undefined }); localStorage.removeItem(this.authBehaviors.current.userTokenKey); localStorage.removeItem(this.authBehaviors.current.userKey); const href = this.getSubdomainUrl(prefix, nextRoute); location.href = href; } getSubdomainUrl ( prefix: string, nextRoute: string ) { const newHost = `https://${prefix}.${environment.locationBase}${+location.port > 443 ? `:${location.port}` : ''}`; return newHost + nextRoute; } async getSSOConfigurationBySubDomain ( subDomain: string, skipSetOnState = false ) { try { const config = await this.ssoResources.getSSOConfigurationBySubDomain(subDomain); if (!skipSetOnState) { this.setSSOConfig(config); } return config; } catch (err) { const e = err as HttpErrorResponse; if (e.status === 404) { return null; } this.logger.error(e); throw e; } } /** * On login page, we get the SSO settings for the current subdomain * If we need to route to a newer subdomain, we do that here * * @param prefix: Current Subdomain Prefix * */ async handleSubDomainOnLoginPage (prefix: string) { const config = await this.getSSOConfigurationBySubDomain(prefix, true); if (!!config) { const needsUpdated = !!config.subDomain && !location.hostname.toLowerCase().includes(config.subDomain.toLowerCase()); if (needsUpdated) { const url = this.getSubdomainUrl(config.subDomain, location.pathname); location.href = url; } else { this.setSSOConfig(config); } } } }