import { TokenService } from '@abp/auth/token.service'; import { LogService } from '@abp/log/log.service'; import { MessageService } from '@abp/message/message.service'; import { UtilsService } from '@abp/utils/utils.service'; import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { AppConsts } from '@shared/AppConsts'; import { UrlHelper } from '@shared/helpers/UrlHelper'; import { AuthenticateModel, AuthenticateResultModel, ExternalAuthenticateModel, ExternalAuthenticateResultModel, ExternalLoginProviderInfoModel, TokenAuthServiceProxy } from '@shared/service-proxies/service-proxies'; import { ScriptLoaderService } from '@shared/utils/script-loader.service'; import * as _ from 'lodash'; import { finalize } from 'rxjs/operators'; import { OAuthService, AuthConfig } from 'angular-oauth2-oidc'; import * as AuthenticationContext from 'adal-angular/lib/adal'; import { NgxSpinnerService } from 'ngx-spinner'; declare const FB: any; // Facebook API declare const gapi: any; // Facebook API declare const WL: any; // Microsoft API export class ExternalLoginProvider extends ExternalLoginProviderInfoModel { static readonly FACEBOOK: string = 'Facebook'; static readonly GOOGLE: string = 'Google'; static readonly MICROSOFT: string = 'Microsoft'; static readonly OPENID: string = 'OpenIdConnect'; static readonly WSFEDERATION: string = 'WsFederation'; icon: string; initialized = false; constructor(providerInfo: ExternalLoginProviderInfoModel) { super(); this.name = providerInfo.name; this.clientId = providerInfo.clientId; this.additionalParams = providerInfo.additionalParams; this.icon = providerInfo.name.toLowerCase(); } } @Injectable() export class LoginService { static readonly twoFactorRememberClientTokenName = 'TwoFactorRememberClientToken'; authenticateModel: AuthenticateModel; authenticateResult: AuthenticateResultModel; externalLoginProviders: ExternalLoginProvider[] = []; rememberMe: boolean; wsFederationAuthenticationContext: any; constructor( private _tokenAuthService: TokenAuthServiceProxy, private _router: Router, private _utilsService: UtilsService, private _messageService: MessageService, private _tokenService: TokenService, private _logService: LogService, private oauthService: OAuthService, private spinnerService: NgxSpinnerService ) { this.clear(); } authenticate(finallyCallback?: () => void, redirectUrl?: string, captchaResponse?: string): void { finallyCallback = finallyCallback || (() => { this.spinnerService.hide(); }); // We may switch to localStorage instead of cookies this.authenticateModel.twoFactorRememberClientToken = this._utilsService.getCookieValue(LoginService.twoFactorRememberClientTokenName); this.authenticateModel.singleSignIn = UrlHelper.getSingleSignIn(); this.authenticateModel.returnUrl = UrlHelper.getReturnUrl(); this.authenticateModel.captchaResponse = captchaResponse; this._tokenAuthService .authenticate(this.authenticateModel) .subscribe({ next: (result: AuthenticateResultModel) => { this.processAuthenticateResult(result, redirectUrl); finallyCallback(); }, error: (err: any) => { finallyCallback(); } }); } externalAuthenticate(provider: ExternalLoginProvider): void { this.ensureExternalLoginProviderInitialized(provider, () => { if (provider.name === ExternalLoginProvider.FACEBOOK) { FB.login(response => { this.facebookLoginStatusChangeCallback(response); }, { scope: 'email' }); } else if (provider.name === ExternalLoginProvider.GOOGLE) { gapi.auth2.getAuthInstance().signIn().then(() => { this.googleLoginStatusChangeCallback(gapi.auth2.getAuthInstance().isSignedIn.get()); }); } else if (provider.name === ExternalLoginProvider.MICROSOFT) { WL.login({ scope: ['wl.signin', 'wl.basic', 'wl.emails'] }); } }); } init(): void { this.initExternalLoginProviders(); } private processAuthenticateResult(authenticateResult: AuthenticateResultModel, redirectUrl?: string) { this.authenticateResult = authenticateResult; if (authenticateResult.shouldResetPassword) { // Password reset this._router.navigate(['account/reset-password'], { queryParams: { userId: authenticateResult.userId, tenantId: abp.session.tenantId, resetCode: authenticateResult.passwordResetCode } }); this.clear(); } else if (authenticateResult.requiresTwoFactorVerification) { // Two factor authentication this._router.navigate(['account/send-code']); } else if (authenticateResult.accessToken) { // Successfully logged in if (authenticateResult.returnUrl && !redirectUrl) { redirectUrl = authenticateResult.returnUrl; } this.login( authenticateResult.accessToken, authenticateResult.encryptedAccessToken, authenticateResult.expireInSeconds, this.rememberMe, authenticateResult.twoFactorRememberClientToken, redirectUrl ); } else { // Unexpected result! this._logService.warn('Unexpected authenticateResult!'); this._router.navigate(['account/login']); } } private login(accessToken: string, encryptedAccessToken: string, expireInSeconds: number, rememberMe?: boolean, twoFactorRememberClientToken?: string, redirectUrl?: string): void { let tokenExpireDate = rememberMe ? (new Date(new Date().getTime() + 1000 * expireInSeconds)) : undefined; this._tokenService.setToken( accessToken, tokenExpireDate ); this._utilsService.setCookieValue( AppConsts.authorization.encrptedAuthTokenName, encryptedAccessToken, tokenExpireDate, abp.appPath ); if (twoFactorRememberClientToken) { this._utilsService.setCookieValue( LoginService.twoFactorRememberClientTokenName, twoFactorRememberClientToken, new Date(new Date().getTime() + 365 * 86400000), // 1 year abp.appPath ); } if (redirectUrl) { location.href = redirectUrl; } else { let initialUrl = UrlHelper.initialUrl; if (initialUrl.indexOf('/account') > 0) { initialUrl = AppConsts.appBaseUrl; } location.href = initialUrl; } } private clear(): void { this.authenticateModel = new AuthenticateModel(); this.authenticateModel.rememberClient = false; this.authenticateResult = null; this.rememberMe = false; } private initExternalLoginProviders(callback?: any) { this._tokenAuthService .getExternalAuthenticationProviders() .subscribe((providers: ExternalLoginProviderInfoModel[]) => { this.externalLoginProviders = _.map(providers, p => new ExternalLoginProvider(p)); if (callback) { callback(); } }); } ensureExternalLoginProviderInitialized(loginProvider: ExternalLoginProvider, callback: () => void) { if (loginProvider.initialized) { callback(); return; } if (loginProvider.name === ExternalLoginProvider.FACEBOOK) { new ScriptLoaderService().load('//connect.facebook.net/en_US/sdk.js').then(() => { FB.init({ appId: loginProvider.clientId, cookie: false, xfbml: true, version: 'v2.5' }); FB.getLoginStatus(response => { this.facebookLoginStatusChangeCallback(response); if (response.status !== 'connected') { callback(); } }); }); } else if (loginProvider.name === ExternalLoginProvider.GOOGLE) { new ScriptLoaderService().load('https://apis.google.com/js/api.js').then(() => { gapi.load('client:auth2', () => { gapi.client.init({ clientId: loginProvider.clientId, scope: 'openid profile email' }).then(() => { callback(); }); }); }); } else if (loginProvider.name === ExternalLoginProvider.MICROSOFT) { new ScriptLoaderService().load('//js.live.net/v5.0/wl.js').then(() => { WL.Event.subscribe('auth.login', this.microsoftLogin); WL.init({ client_id: loginProvider.clientId, scope: ['wl.signin', 'wl.basic', 'wl.emails'], redirect_uri: AppConsts.appBaseUrl, response_type: 'token' }); }); } else if (loginProvider.name === ExternalLoginProvider.OPENID) { const authConfig = this.getOpenIdConnectConfig(loginProvider); this.oauthService.configure(authConfig); this.oauthService.initImplicitFlow('openIdConnect=1'); } else if (loginProvider.name === ExternalLoginProvider.WSFEDERATION) { let config = this.getWsFederationConnectConfig(loginProvider); this.wsFederationAuthenticationContext = new AuthenticationContext(config); this.wsFederationAuthenticationContext.login(); } } private getWsFederationConnectConfig(loginProvider: ExternalLoginProvider): any { let config = { clientId: loginProvider.clientId, popUp: true, callback: this.wsFederationLoginStatusChangeCallback.bind(this) } as any; if (loginProvider.additionalParams['Tenant']) { config.tenant = loginProvider.additionalParams['Tenant']; } return config; } private getOpenIdConnectConfig(loginProvider: ExternalLoginProvider): AuthConfig { let authConfig = new AuthConfig(); authConfig.loginUrl = loginProvider.additionalParams['LoginUrl']; authConfig.issuer = loginProvider.additionalParams['Authority']; authConfig.skipIssuerCheck = loginProvider.additionalParams['ValidateIssuer'] === 'false'; authConfig.clientId = loginProvider.clientId; authConfig.responseType = 'id_token'; authConfig.redirectUri = window.location.origin + '/account/login'; authConfig.scope = 'openid profile'; authConfig.requestAccessToken = false; return authConfig; } private facebookLoginStatusChangeCallback(resp) { if (resp.status === 'connected') { const model = new ExternalAuthenticateModel(); model.authProvider = ExternalLoginProvider.FACEBOOK; model.providerAccessCode = resp.authResponse.accessToken; model.providerKey = resp.authResponse.userID; model.singleSignIn = UrlHelper.getSingleSignIn(); model.returnUrl = UrlHelper.getReturnUrl(); this._tokenAuthService.externalAuthenticate(model) .subscribe((result: ExternalAuthenticateResultModel) => { if (result.waitingForActivation) { this._messageService.info('You have successfully registered. Waiting for activation!'); return; } this.login(result.accessToken, result.encryptedAccessToken, result.expireInSeconds, false, '', result.returnUrl); }); } } public openIdConnectLoginCallback(resp) { this.initExternalLoginProviders(() => { let openIdProvider = _.filter(this.externalLoginProviders, { name: 'OpenIdConnect' })[0]; let authConfig = this.getOpenIdConnectConfig(openIdProvider); this.oauthService.configure(authConfig); this.spinnerService.show(); this.oauthService.tryLogin().then(() => { let claims = this.oauthService.getIdentityClaims(); const model = new ExternalAuthenticateModel(); model.authProvider = ExternalLoginProvider.OPENID; model.providerAccessCode = this.oauthService.getIdToken(); model.providerKey = claims['sub']; model.singleSignIn = UrlHelper.getSingleSignIn(); model.returnUrl = UrlHelper.getReturnUrl(); this._tokenAuthService.externalAuthenticate(model) .pipe(finalize(() => { this.spinnerService.hide(); })) .subscribe((result: ExternalAuthenticateResultModel) => { if (result.waitingForActivation) { this._messageService.info('You have successfully registered. Waiting for activation!'); return; } this.login(result.accessToken, result.encryptedAccessToken, result.expireInSeconds, false, '', result.returnUrl); }); }); }); } private googleLoginStatusChangeCallback(isSignedIn) { if (isSignedIn) { const model = new ExternalAuthenticateModel(); model.authProvider = ExternalLoginProvider.GOOGLE; model.providerAccessCode = gapi.auth2.getAuthInstance().currentUser.get().getAuthResponse().access_token; model.providerKey = gapi.auth2.getAuthInstance().currentUser.get().getBasicProfile().getId(); model.singleSignIn = UrlHelper.getSingleSignIn(); model.returnUrl = UrlHelper.getReturnUrl(); this._tokenAuthService.externalAuthenticate(model) .subscribe((result: ExternalAuthenticateResultModel) => { if (result.waitingForActivation) { this._messageService.info('You have successfully registered. Waiting for activation!'); return; } this.login(result.accessToken, result.encryptedAccessToken, result.expireInSeconds, false, '', result.returnUrl); }); } } public wsFederationLoginStatusChangeCallback(errorDesc, token, error, tokenType) { let user = this.wsFederationAuthenticationContext.getCachedUser(); const model = new ExternalAuthenticateModel(); model.authProvider = ExternalLoginProvider.WSFEDERATION; model.providerAccessCode = token; model.providerKey = user.profile.sub; model.singleSignIn = UrlHelper.getSingleSignIn(); model.returnUrl = UrlHelper.getReturnUrl(); this._tokenAuthService.externalAuthenticate(model) .subscribe((result: ExternalAuthenticateResultModel) => { if (result.waitingForActivation) { this._messageService.info('You have successfully registered. Waiting for activation!'); this._router.navigate(['account/login']); return; } this.login(result.accessToken, result.encryptedAccessToken, result.expireInSeconds, false, '', result.returnUrl); }); } /** * Microsoft login is not completed yet, because of an error thrown by zone.js: https://github.com/angular/zone.js/issues/290 */ private microsoftLogin() { this._logService.debug(WL.getSession()); const model = new ExternalAuthenticateModel(); model.authProvider = ExternalLoginProvider.MICROSOFT; model.providerAccessCode = WL.getSession().access_token; model.providerKey = WL.getSession().id; // How to get id? model.singleSignIn = UrlHelper.getSingleSignIn(); model.returnUrl = UrlHelper.getReturnUrl(); this._tokenAuthService.externalAuthenticate(model) .subscribe((result: ExternalAuthenticateResultModel) => { if (result.waitingForActivation) { this._messageService.info('You have successfully registered. Waiting for activation!'); return; } this.login(result.accessToken, result.encryptedAccessToken, result.expireInSeconds, false, '', result.returnUrl); }); } }