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
JavaScript
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