UNPKG

angularx-social-login

Version:

Social login and authentication module for Angular 9+. Supports authentication with Google, Facebook, Amazon, and VK. Can be extended to other providers also.

863 lines (850 loc) 32.1 kB
import { Injectable, Inject, NgModule, Optional, SkipSelf } from '@angular/core'; import { ReplaySubject, AsyncSubject } from 'rxjs'; import { CommonModule } from '@angular/common'; class BaseLoginProvider { constructor() { } loadScript(id, src, onload, parentElement = null) { // get document if platform is only browser if (typeof document !== 'undefined' && !document.getElementById(id)) { let signInJS = document.createElement('script'); signInJS.async = true; signInJS.src = src; signInJS.onload = onload; if (!parentElement) { parentElement = document.head; } parentElement.appendChild(signInJS); } } } class SocialUser { } class GoogleLoginProvider extends BaseLoginProvider { constructor(clientId, initOptions = { scope: 'email' }) { super(); this.clientId = clientId; this.initOptions = initOptions; } initialize() { return new Promise((resolve, reject) => { try { this.loadScript(GoogleLoginProvider.PROVIDER_ID, 'https://apis.google.com/js/platform.js', () => { gapi.load('auth2', () => { this.auth2 = gapi.auth2.init(Object.assign(Object.assign({}, this.initOptions), { client_id: this.clientId })); this.auth2 .then(() => { resolve(); }) .catch((err) => { reject(err); }); }); }); } catch (err) { reject(err); } }); } getLoginStatus(loginStatusOptions) { return new Promise((resolve, reject) => { if (this.auth2.isSignedIn.get()) { let user = new SocialUser(); const profile = this.auth2.currentUser.get().getBasicProfile(); user.id = profile.getId(); user.name = profile.getName(); user.email = profile.getEmail(); user.photoUrl = profile.getImageUrl(); user.firstName = profile.getGivenName(); user.lastName = profile.getFamilyName(); user.response = profile; if (loginStatusOptions && loginStatusOptions.refreshToken) { this.auth2.currentUser.get().reloadAuthResponse().then(authResponse => { user.authToken = authResponse.access_token; user.idToken = authResponse.id_token; resolve(user); }); } else { const authResponse = this.auth2.currentUser.get().getAuthResponse(true); user.authToken = authResponse.access_token; user.idToken = authResponse.id_token; resolve(user); } } else { reject(`No user is currently logged in with ${GoogleLoginProvider.PROVIDER_ID}`); } }); } signIn(signInOptions) { const options = Object.assign(Object.assign({}, this.initOptions), signInOptions); return new Promise((resolve, reject) => { const offlineAccess = options && options.offline_access; let promise = !offlineAccess ? this.auth2.signIn(signInOptions) : this.auth2.grantOfflineAccess(signInOptions); promise .then((response) => { let user = new SocialUser(); if (response && response.code) { user.authorizationCode = response.code; } else { let profile = this.auth2.currentUser.get().getBasicProfile(); let token = this.auth2.currentUser.get().getAuthResponse(true) .access_token; let backendToken = this.auth2.currentUser .get() .getAuthResponse(true).id_token; user.id = profile.getId(); user.name = profile.getName(); user.email = profile.getEmail(); user.photoUrl = profile.getImageUrl(); user.firstName = profile.getGivenName(); user.lastName = profile.getFamilyName(); user.authToken = token; user.idToken = backendToken; user.response = profile; } resolve(user); }, (closed) => { reject(closed); }) .catch((err) => { reject(err); }); }); } signOut(revoke) { return new Promise((resolve, reject) => { let signOutPromise; if (revoke) { signOutPromise = this.auth2.disconnect(); } else { signOutPromise = this.auth2.signOut(); } signOutPromise .then((err) => { if (err) { reject(err); } else { resolve(); } }) .catch((err) => { reject(err); }); }); } } GoogleLoginProvider.PROVIDER_ID = 'GOOGLE'; /** @dynamic */ class SocialAuthService { constructor(config) { this.providers = new Map(); this.autoLogin = false; this._user = null; this._authState = new ReplaySubject(1); /* Consider making this an enum comprising LOADING, LOADED, FAILED etc. */ this.initialized = false; this._initState = new AsyncSubject(); if (config instanceof Promise) { config.then((config) => { this.initialize(config); }); } else { this.initialize(config); } } get authState() { return this._authState.asObservable(); } get initState() { return this._initState.asObservable(); } initialize(config) { this.autoLogin = config.autoLogin !== undefined ? config.autoLogin : false; const { onError = console.error } = config; config.providers.forEach((item) => { this.providers.set(item.id, item.provider); }); Promise.all(Array.from(this.providers.values()).map((provider) => provider.initialize())) .then(() => { if (this.autoLogin) { const loginStatusPromises = []; let loggedIn = false; this.providers.forEach((provider, key) => { let promise = provider.getLoginStatus(); loginStatusPromises.push(promise); promise .then((user) => { user.provider = key; this._user = user; this._authState.next(user); loggedIn = true; }) .catch(console.debug); }); Promise.all(loginStatusPromises).catch(() => { if (!loggedIn) { this._user = null; this._authState.next(null); } }); } }) .catch((error) => { onError(error); }) .finally(() => { this.initialized = true; this._initState.next(this.initialized); this._initState.complete(); }); } refreshAuthToken(providerId) { return new Promise((resolve, reject) => { if (!this.initialized) { reject(SocialAuthService.ERR_NOT_INITIALIZED); } else if (providerId !== GoogleLoginProvider.PROVIDER_ID) { reject(SocialAuthService.ERR_NOT_SUPPORTED_FOR_REFRESH_TOKEN); } else { const providerObject = this.providers.get(providerId); if (providerObject) { providerObject .getLoginStatus({ refreshToken: true }) .then((user) => { user.provider = providerId; this._user = user; this._authState.next(user); resolve(); }) .catch((err) => { reject(err); }); } else { reject(SocialAuthService.ERR_LOGIN_PROVIDER_NOT_FOUND); } } }); } signIn(providerId, signInOptions) { return new Promise((resolve, reject) => { if (!this.initialized) { reject(SocialAuthService.ERR_NOT_INITIALIZED); } else { let providerObject = this.providers.get(providerId); if (providerObject) { providerObject .signIn(signInOptions) .then((user) => { user.provider = providerId; resolve(user); this._user = user; this._authState.next(user); }) .catch((err) => { reject(err); }); } else { reject(SocialAuthService.ERR_LOGIN_PROVIDER_NOT_FOUND); } } }); } signOut(revoke = false) { return new Promise((resolve, reject) => { if (!this.initialized) { reject(SocialAuthService.ERR_NOT_INITIALIZED); } else if (!this._user) { reject(SocialAuthService.ERR_NOT_LOGGED_IN); } else { let providerId = this._user.provider; let providerObject = this.providers.get(providerId); if (providerObject) { providerObject .signOut(revoke) .then(() => { resolve(); this._user = null; this._authState.next(null); }) .catch((err) => { reject(err); }); } else { reject(SocialAuthService.ERR_LOGIN_PROVIDER_NOT_FOUND); } } }); } } SocialAuthService.ERR_LOGIN_PROVIDER_NOT_FOUND = 'Login provider not found'; SocialAuthService.ERR_NOT_LOGGED_IN = 'Not logged in'; SocialAuthService.ERR_NOT_INITIALIZED = 'Login providers not ready yet. Are there errors on your console?'; SocialAuthService.ERR_NOT_SUPPORTED_FOR_REFRESH_TOKEN = 'Chosen login provider is not supported for refreshing a token'; SocialAuthService.decorators = [ { type: Injectable } ]; SocialAuthService.ctorParameters = () => [ { type: undefined, decorators: [{ type: Inject, args: ['SocialAuthServiceConfig',] }] } ]; class SocialLoginModule { constructor(parentModule) { if (parentModule) { throw new Error('SocialLoginModule is already loaded. Import it in the AppModule only'); } } static initialize(config) { return { ngModule: SocialLoginModule, providers: [ SocialAuthService, { provide: 'SocialAuthServiceConfig', useValue: config } ] }; } } SocialLoginModule.decorators = [ { type: NgModule, args: [{ imports: [ CommonModule ], providers: [ SocialAuthService ] },] } ]; SocialLoginModule.ctorParameters = () => [ { type: SocialLoginModule, decorators: [{ type: Optional }, { type: SkipSelf }] } ]; // Simulates login / logout without actually requiring an Internet connection. // // Useful for certain development situations. // // For example, if you want to simulate the greatest football referee England has ever produced: // // const dummyUser: SocialUser = { // id: '0123456789', // name: 'Howard Webb', // email: 'howard@webb.com', // firstName: 'Howard', // lastName: 'Webb', // authToken: 'dummyAuthToken', // photoUrl: 'https://en.wikipedia.org/wiki/Howard_Webb#/media/File:Howard_Webb_march11.jpg', // provider: 'DUMMY', // idToken: 'dummyIdToken', // authorizationCode: 'dummyAuthCode' // }; // // let config = new AuthServiceConfig([ // { ... }, // { // id: DummyLoginProvider.PROVIDER_ID, // provider: new DummyLoginProvider(dummyUser) // Pass your user into the constructor // }, // { ... } // ]); class DummyLoginProvider extends BaseLoginProvider { constructor(dummy) { super(); if (dummy) { this.dummy = dummy; } else { this.dummy = DummyLoginProvider.DEFAULT_USER; } // Start not logged in this.loggedIn = false; } getLoginStatus() { return new Promise((resolve, reject) => { if (this.loggedIn) { resolve(this.dummy); } else { reject('No user is currently logged in.'); } }); } initialize() { return new Promise((resolve, reject) => { resolve(); }); } signIn() { return new Promise((resolve, reject) => { this.loggedIn = true; resolve(this.dummy); }); } signOut(revoke) { return new Promise((resolve, reject) => { this.loggedIn = false; resolve(); }); } } DummyLoginProvider.PROVIDER_ID = 'DUMMY'; DummyLoginProvider.DEFAULT_USER = { id: '1234567890', name: 'Mickey Mouse', email: 'mickey@mouse.com', firstName: 'Mickey', lastName: 'Mouse', authToken: 'dummyAuthToken', photoUrl: 'https://en.wikipedia.org/wiki/File:Mickey_Mouse.png', provider: 'DUMMY', idToken: 'dummyIdToken', authorizationCode: 'dummyAuthCode', response: {} }; class FacebookLoginProvider extends BaseLoginProvider { constructor(clientId, initOptions = { scope: 'email,public_profile', locale: 'en_US', fields: 'name,email,picture,first_name,last_name', version: 'v4.0', }) { super(); this.clientId = clientId; this.initOptions = initOptions; } initialize() { return new Promise((resolve, reject) => { try { this.loadScript(FacebookLoginProvider.PROVIDER_ID, `//connect.facebook.net/${this.initOptions.locale}/sdk.js`, () => { FB.init({ appId: this.clientId, autoLogAppEvents: true, cookie: true, xfbml: true, version: this.initOptions.version, }); resolve(); }); } catch (err) { reject(err); } }); } getLoginStatus() { return new Promise((resolve, reject) => { FB.getLoginStatus((response) => { if (response.status === 'connected') { let authResponse = response.authResponse; FB.api(`/me?fields=${this.initOptions.fields}`, (fbUser) => { let user = new SocialUser(); user.id = fbUser.id; user.name = fbUser.name; user.email = fbUser.email; user.photoUrl = 'https://graph.facebook.com/' + fbUser.id + '/picture?type=normal'; user.firstName = fbUser.first_name; user.lastName = fbUser.last_name; user.authToken = authResponse.accessToken; user.response = fbUser; resolve(user); }); } else { reject(`No user is currently logged in with ${FacebookLoginProvider.PROVIDER_ID}`); } }); }); } signIn(signInOptions) { const options = Object.assign(Object.assign({}, this.initOptions), signInOptions); return new Promise((resolve, reject) => { FB.login((response) => { if (response.authResponse) { let authResponse = response.authResponse; FB.api(`/me?fields=${options.fields}`, (fbUser) => { let user = new SocialUser(); user.id = fbUser.id; user.name = fbUser.name; user.email = fbUser.email; user.photoUrl = 'https://graph.facebook.com/' + fbUser.id + '/picture?type=normal'; user.firstName = fbUser.first_name; user.lastName = fbUser.last_name; user.authToken = authResponse.accessToken; user.response = fbUser; resolve(user); }); } else { reject('User cancelled login or did not fully authorize.'); } }, options); }); } signOut() { return new Promise((resolve, reject) => { FB.logout((response) => { resolve(); }); }); } } FacebookLoginProvider.PROVIDER_ID = 'FACEBOOK'; class AmazonLoginProvider extends BaseLoginProvider { constructor(clientId, initOptions = { scope: 'profile', scope_data: { profile: { essential: false }, }, redirect_uri: location.origin, }) { super(); this.clientId = clientId; this.initOptions = initOptions; } initialize() { let amazonRoot = null; if (document) { amazonRoot = document.createElement('div'); amazonRoot.id = 'amazon-root'; document.body.appendChild(amazonRoot); } window.onAmazonLoginReady = () => { amazon.Login.setClientId(this.clientId); }; return new Promise((resolve, reject) => { try { this.loadScript('amazon-login-sdk', 'https://assets.loginwithamazon.com/sdk/na/login1.js', () => { resolve(); }, amazonRoot); } catch (err) { reject(err); } }); } getLoginStatus() { return new Promise((resolve, reject) => { let token = this.retrieveToken(); if (token) { amazon.Login.retrieveProfile(token, (response) => { if (response.success) { let user = new SocialUser(); user.id = response.profile.CustomerId; user.name = response.profile.Name; user.email = response.profile.PrimaryEmail; user.response = response.profile; resolve(user); } else { reject(response.error); } }); } else { reject(`No user is currently logged in with ${AmazonLoginProvider.PROVIDER_ID}`); } }); } signIn(signInOptions) { const options = Object.assign(Object.assign({}, this.initOptions), signInOptions); return new Promise((resolve, reject) => { amazon.Login.authorize(options, (authResponse) => { if (authResponse.error) { reject(authResponse.error); } else { amazon.Login.retrieveProfile(authResponse.access_token, (response) => { let user = new SocialUser(); user.id = response.profile.CustomerId; user.name = response.profile.Name; user.email = response.profile.PrimaryEmail; user.authToken = authResponse.access_token; user.response = response.profile; this.persistToken(authResponse.access_token); resolve(user); }); } }); }); } signOut(revoke) { return new Promise((resolve, reject) => { try { amazon.Login.logout(); this.clearToken(); resolve(); } catch (err) { reject(err.message); } }); } persistToken(token) { localStorage.setItem(`${AmazonLoginProvider.PROVIDER_ID}_token`, token); } retrieveToken() { return localStorage.getItem(`${AmazonLoginProvider.PROVIDER_ID}_token`); } clearToken() { localStorage.removeItem(`${AmazonLoginProvider.PROVIDER_ID}_token`); } } AmazonLoginProvider.PROVIDER_ID = 'AMAZON'; class VKLoginProvider extends BaseLoginProvider { constructor(clientId, initOptions = { fields: 'photo_max,contacts', version: '5.124', }) { super(); this.clientId = clientId; this.initOptions = initOptions; this.VK_API_URL = '//vk.com/js/api/openapi.js'; this.VK_API_GET_USER = 'users.get'; } initialize() { return new Promise((resolve, reject) => { try { this.loadScript(VKLoginProvider.PROVIDER_ID, this.VK_API_URL, () => { VK.init({ apiId: this.clientId, }); resolve(); }); } catch (err) { reject(err); } }); } getLoginStatus() { return new Promise((resolve, reject) => this.getLoginStatusInternal(resolve, reject)); } signIn() { return new Promise((resolve, reject) => this.signInInternal(resolve, reject)); } signOut() { return new Promise((resolve, reject) => { VK.Auth.logout((response) => { resolve(); }); }); } signInInternal(resolve, reject) { VK.Auth.login((loginResponse) => { if (loginResponse.status === 'connected') { this.getUser(loginResponse.session.mid, loginResponse.session.sid, resolve); } }); } getUser(userId, token, resolve) { VK.Api.call(this.VK_API_GET_USER, { user_id: userId, fields: this.initOptions.fields, v: this.initOptions.version, }, (userResponse) => { resolve(this.createUser(Object.assign({}, { token }, userResponse.response[0]))); }); } getLoginStatusInternal(resolve, reject) { VK.Auth.getLoginStatus((loginResponse) => { if (loginResponse.status === 'connected') { this.getUser(loginResponse.session.mid, loginResponse.session.sid, resolve); } }); } createUser(response) { const user = new SocialUser(); user.id = response.id; user.name = `${response.first_name} ${response.last_name}`; user.photoUrl = response.photo_max; user.authToken = response.token; return user; } } VKLoginProvider.PROVIDER_ID = 'VK'; /** * Protocol modes supported by MSAL. */ var ProtocolMode; (function (ProtocolMode) { ProtocolMode["AAD"] = "AAD"; ProtocolMode["OIDC"] = "OIDC"; })(ProtocolMode || (ProtocolMode = {})); const COMMON_AUTHORITY = 'https://login.microsoftonline.com/common/'; /** * Microsoft Authentication using MSAL v2: https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-browser */ class MicrosoftLoginProvider extends BaseLoginProvider { constructor(clientId, initOptions) { super(); this.clientId = clientId; this.initOptions = { redirect_uri: location.origin, authority: COMMON_AUTHORITY, scopes: ['openid', 'profile', 'User.Read'], knownAuthorities: [], protocolMode: ProtocolMode.AAD, clientCapabilities: [], cacheLocation: 'sessionStorage' }; this.initOptions = Object.assign(Object.assign({}, this.initOptions), initOptions); } initialize() { return new Promise((resolve, reject) => { this.loadScript(MicrosoftLoginProvider.PROVIDER_ID, 'https://alcdn.msauth.net/browser/2.1.0/js/msal-browser.js', () => { try { const config = { auth: { clientId: this.clientId, redirectUri: this.initOptions.redirect_uri, authority: this.initOptions.authority, knownAuthorities: this.initOptions.knownAuthorities, protocolMode: this.initOptions.protocolMode, clientCapabilities: this.initOptions.clientCapabilities }, cache: !this.initOptions.cacheLocation ? null : { cacheLocation: this.initOptions.cacheLocation } }; this._instance = new msal.PublicClientApplication(config); resolve(); } catch (e) { reject(e); } }); }); } getSocialUser(loginResponse) { return new Promise((resolve, reject) => { //After login, use Microsoft Graph API to get user info let meRequest = new XMLHttpRequest(); meRequest.onreadystatechange = () => { if (meRequest.readyState == 4) { try { if (meRequest.status == 200) { let userInfo = JSON.parse(meRequest.responseText); let user = new SocialUser(); user.provider = MicrosoftLoginProvider.PROVIDER_ID; user.id = loginResponse.idToken; user.name = loginResponse.idTokenClaims.name; user.email = loginResponse.account.username; user.idToken = loginResponse.idToken; user.response = loginResponse; user.firstName = userInfo.givenName; user.lastName = userInfo.surname; resolve(user); } else { reject(`Error retrieving user info: ${meRequest.status}`); } } catch (err) { reject(err); } } }; //Microsoft Graph ME Endpoint: https://docs.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http meRequest.open('GET', 'https://graph.microsoft.com/v1.0/me'); meRequest.setRequestHeader('Authorization', `Bearer ${loginResponse.accessToken}`); try { meRequest.send(); } catch (err) { reject(err); } }); } getLoginStatus() { return new Promise((resolve, reject) => { const accounts = this._instance.getAllAccounts(); if (accounts.length > 0) { try { this._instance.ssoSilent({ scopes: this.initOptions.scopes, loginHint: accounts[0].username }) .then(loginResponse => { this.getSocialUser(loginResponse) .then(user => resolve(user)) .catch(err => reject(err)); }) .catch(err => reject(err)); } catch (err) { reject(err); } } else { reject(`No user is currently logged in with ${MicrosoftLoginProvider.PROVIDER_ID}`); } }); } signIn() { return new Promise((resolve, reject) => { try { this._instance.loginPopup({ scopes: this.initOptions.scopes }) .then(loginResponse => { this.getSocialUser(loginResponse) .then(user => resolve(user)) .catch(err => reject(err)); }) .catch(err => reject(err)); } catch (err) { reject(err); } }); } signOut(revoke) { return new Promise((resolve, reject) => { try { const accounts = this._instance.getAllAccounts(); //TODO: This redirects to a Microsoft page, then sends us back to redirect_uri... this doesn't seem to match other providers //Open issues: // https://github.com/abacritt/angularx-social-login/issues/306 // https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/2563 this._instance.logout({ account: accounts[0], postLogoutRedirectUri: this.initOptions.redirect_uri }) .then(() => { resolve(); }) .catch(err => { reject(err); }); } catch (err) { reject(err); } }); } } MicrosoftLoginProvider.PROVIDER_ID = 'MICROSOFT'; /** * Generated bundle index. Do not edit. */ export { AmazonLoginProvider, BaseLoginProvider, DummyLoginProvider, FacebookLoginProvider, GoogleLoginProvider, MicrosoftLoginProvider, SocialAuthService, SocialLoginModule, SocialUser, VKLoginProvider }; //# sourceMappingURL=angularx-social-login.js.map