Created
October 1, 2017 08:34
-
-
Save bradennapier/7b4d765c927f2972f8a1762ef200c04b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* @flow */ | |
import PromiseQueue from 'promise-queue-observable'; | |
import { config as AWSConfig, CognitoIdentityCredentials, CognitoSyncManager } from 'aws-sdk'; | |
import 'amazon-cognito-js'; | |
import { | |
CognitoUserPool, | |
CognitoUser, | |
AuthenticationDetails, | |
CognitoUserAttribute, | |
} from 'amazon-cognito-identity-js'; | |
export type CognitoAuthenticator$Config = { | |
log: boolean | 'warn' | 'error' | 'debug', | |
}; | |
const tryJSONParse = (val: mixed): Object => | |
typeof val === 'object' | |
? val | |
: // $FlowIgnore | |
do { | |
let v; | |
try { | |
v = JSON.parse(val); | |
} catch (e) { | |
v = val; | |
} finally { | |
v; // eslint-disable-line | |
} | |
}; | |
export const toAWSAttribute = { | |
userLocale: 'locale', | |
userEmail: 'email', | |
firstName: 'name', | |
lastName: 'family_name', | |
username: 'preferred_username', | |
userPhoneNumber: 'phone_number', | |
userAddress: 'address', | |
phoneVerified: 'phone_number_verified', | |
emailVerified: 'email_verified', | |
dealerIdentityID: 'custom:dealerIdentityID', | |
company: 'custom: company', | |
latitude: 'custom:company_latitude', | |
longitude: 'custom:company_longitude', | |
}; | |
export const fromAWSAttribute = { | |
email: 'userEmail', | |
name: 'firstName', | |
family_name: 'lastName', | |
locale: 'userLocale', | |
preferred_username: 'username', | |
phone_number: 'userPhoneNumber', | |
email_verified: 'emailVerified', | |
phone_number_verified: 'phoneVerified', | |
address: 'userAddress', | |
'custom:dealerIdentityID': 'dealerIdentityID', | |
'custom:company': 'company', | |
'custom:company_latitude': 'companyLat', | |
'custom:company_longitude': 'companyLon', | |
}; | |
let Authenticator; | |
const handleSynchronizeUserData = (dataManager /* , initialRecords */) => | |
new Promise((resolve, reject) => { | |
dataManager.synchronize({ | |
onSuccess(dataset, newRecords) { | |
// if (observer) { | |
// observer.publish('data_sync', 'success', dataset, newRecords); | |
// } | |
resolve(['success', dataset, newRecords]); | |
}, | |
onFailure(error) { | |
// console.error('DataSync Error: ', error.message); | |
// if (observer) { | |
// observer.publish('data_sync', 'error', error.message); | |
// } | |
resolve(['error', error]); | |
}, | |
onConflict(dataset, conflicts, callback) { | |
console.warn('Dataset Conflict! ', conflicts); | |
// if (observer) { | |
// observer.publish('data_sync', 'conflict', conflicts, callback); | |
// } | |
resolve(['conflict', dataset, conflicts, callback]); | |
}, | |
observersetDeleted(dataset, datasetName, callback) { | |
// console.info('Dataset Deleted: ', datasetName); | |
// observer.publish('data_sync', 'deleted', datasetName); | |
resolve(['deleted', dataset, datasetName, callback]); | |
return callback(true); | |
}, | |
observersetMerged(dataset, datasetNames, callback) { | |
// console.info('Dataset Merged: ', datasetNames); | |
// observer.publish('data_sync', 'merged', datasetNames); | |
resolve(['merged', dataset, datasetNames, callback]); | |
return callback(true); | |
}, | |
}); | |
}); | |
const handleFormatDataRecords = records => | |
records.reduce((p, c) => { | |
p[c.key] = tryJSONParse(c.value); | |
return p; | |
}, {}); | |
const handleGetAllRecords = dataManager => | |
new Promise((resolve, reject) => { | |
handleSynchronizeUserData(dataManager) | |
.then(([result, ...args]) => | |
dataManager.getAllRecords((err, rawRecords) => { | |
if (err) { | |
console.error('Failed to Get All User Records: ', err.message); | |
return reject('Failed to Get User Data Records'); | |
} | |
const dealerIdentityID = dataManager.getIdentityId(); | |
const records = { | |
dealerIdentityID, | |
data: handleFormatDataRecords(rawRecords), | |
manager: dataManager, | |
sync: () => handleSynchronizeUserData(dataManager), | |
getAll: () => handleGetAllRecords(dataManager), | |
}; | |
return resolve(records); | |
}), | |
) | |
.catch(err => { | |
console.error('Failed to Get User Recors: ', err); | |
}); | |
}); | |
const getCredentials = (Username: string, Password: string): Class<AuthenticationDetails> => | |
new AuthenticationDetails({ | |
Username, | |
Password, | |
}); | |
const parseUserAttributes = attributes => | |
attributes.reduce((p, c = {}) => { | |
const { Name, Value } = c; | |
if (fromAWSAttribute[Name]) { | |
p[fromAWSAttribute[Name]] = Value; | |
} else { | |
p[Name] = Value; | |
} | |
return p; | |
}, {}); | |
const formatUserAttributes = attributes => { | |
const formatted = []; | |
for (const name of Object.keys(attributes)) { | |
formatted.push( | |
new CognitoUserAttribute({ | |
Name: toAWSAttribute[name] || name, | |
Value: attributes[name], | |
}), | |
); | |
} | |
return formatted; | |
}; | |
const getSessionIDToken = (session): string => session.getIdToken().getJwtToken(); | |
function publishConfirmedRegistrationCode(user, code: string, observer) { | |
// console.info('Submitting User Registration Code: ', code); | |
return user.confirmRegistration(String(code), true, (err, result) => { | |
if (err) { | |
console.error('Failed to Publish Confirmed Registration Code: ', err.message); | |
observer.publish('error', err.message); | |
} else { | |
observer.publish('success', result); | |
} | |
observer.cancel(); | |
}); | |
} | |
class CognitoAuthenticator { | |
constructor(config?: CognitoAuthenticator$Config = {}) { | |
this.config = { | |
region: 'us-west-2', | |
...config, | |
}; | |
AWSConfig.region = this.config.region; | |
this.setDefaults(); | |
} | |
setDefaults = (): void => { | |
this.state = { | |
pool: undefined, | |
mfa: undefined, | |
}; | |
// AWS Classes / Instances | |
this.aws = { | |
user: undefined, | |
userPool: undefined, | |
session: undefined, | |
identity: undefined, | |
sync: undefined, | |
dataManager: undefined, | |
}; | |
}; | |
setPool = (params): Class<CognitoAuthenticator> => { | |
const { ClientId, UserPoolId } = params; | |
this.state.pool = params; | |
if (ClientId && UserPoolId) { | |
this.aws.userPool = new CognitoUserPool(params); | |
} | |
return this; | |
}; | |
setIdentity = (params): Class<CognitoAuthenticator> => { | |
this.state.identity = params; | |
return this; | |
}; | |
refreshUserFromStorage = (): Class<CognitoAuthenticator> => { | |
if (this.aws.userPool) { | |
const user = this.aws.userPool.getCurrentUser(); | |
if (user) { | |
this.aws.user = user; | |
} | |
} | |
return this; | |
}; | |
setUser = (Username: string): Class<CognitoAuthenticator> => { | |
if (!this.aws.userPool) { | |
throw new Error('You must setup the User Pool before setting the user!'); | |
} else { | |
this.aws.user = new CognitoUser({ | |
Username, | |
Pool: this.aws.userPool, | |
}); | |
} | |
return this; | |
}; | |
setSession = (session): Class<CognitoAuthenticator> => { | |
if (!session.isValid()) { | |
throw new Error('Tried to set an invalid Session on Authenticator'); | |
} | |
this.aws.session = session; | |
return this; | |
}; | |
setPassword = (prevPassword: string, newPassword: string): Promise<*> => | |
new Promise((resolve, reject) => | |
this.refreshSessionIfNeeded().then(() => | |
this.getUser().changePassword(prevPassword, newPassword, (err, result) => { | |
if (err) { | |
console.error('Failed to Set User Password: ', err.message); | |
return reject(err.message); | |
} | |
return resolve(result); | |
}), | |
), | |
); | |
getPool = (required?: boolean = true) => { | |
if (this.aws.userPool) { | |
return this.aws.userPool; | |
} | |
if (required) { | |
throw new Error('Attempted to Get User Pool before it was setup!'); | |
} | |
}; | |
getUser = (required?: boolean = true) => { | |
if (this.aws.user) { | |
return this.aws.user; | |
} | |
if (required) { | |
throw new Error('Attempted to Get User before it was setup!'); | |
} | |
}; | |
getUsername = (required?: boolean = true) => { | |
if (this.aws.user) { | |
return this.aws.user.getUsername(); | |
} | |
if (required) { | |
throw new Error('Attempted to Get Username before it was setup!'); | |
} | |
}; | |
getSession = (required?: boolean = true) => { | |
if (this.aws.session && typeof this.aws.session.isValid === 'function') { | |
return this.aws.session; | |
} | |
if (required) { | |
throw new Error('Attempted to Get User Session before it was setup!'); | |
} | |
}; | |
getRefreshToken = (): string => this.getSession().getRefreshToken(); | |
getAccessToken = (): string => getSessionIDToken(this.getSession()); | |
logout = (): Class<CognitoAuthenticator> => { | |
const user = this.getUser(false); | |
if (user) { | |
user.signOut(); | |
} | |
const userPool = this.aws.userPool; | |
const poolParams = this.state.pool; | |
this.setDefaults(); | |
// dont reset the userPool on logout | |
this.aws.userPool = userPool; | |
this.state.pool = poolParams; | |
clearAWSCredentialsCache(); | |
return this; | |
}; | |
startSignup = (username: string, password: string, attributes: Object) => { | |
const observer = new PromiseQueue(); | |
this.getPool().signUp(username, password, attributes, null, (err, result) => { | |
if (err) { | |
console.error('Signup Error: ', err.message); | |
observer.publish('error', err, this); | |
} else { | |
this.setUser(username); | |
const callback = code => publishConfirmedRegistrationCode(this.getUser(), code, observer); | |
observer.publish('verify', result, callback, this); | |
} | |
}); | |
return { observer }; | |
}; | |
startLogin = (username: string, password: string) => { | |
// console.log('Start Login: ', username, password); | |
const observer = new PromiseQueue(); | |
const credentials = getCredentials(username, password); | |
clearAWSCredentialsCache(); | |
const onSuccess = session => { | |
observer.publish('success', this.setSession(session).getSession(), this); | |
observer.cancel(); | |
}; | |
const onFailure = err => { | |
console.error('Failed to Login User: ', err.message); | |
observer.publish('error', err, this); | |
observer.cancel(); | |
}; | |
const mfaRequired = data => { | |
console.info('Multi-Factor Validation Required: ', data); | |
const callback = code => { | |
const user = this.getUser(); | |
user.sendMFACode(String(code), { onSuccess, onFailure, newPasswordRequired }); | |
}; | |
observer.publish('verify', data, callback, this); | |
}; | |
const newPasswordRequired = (userAttributes, requiredAttributes) => { | |
const data = { userAttributes, requiredAttributes }; | |
// User was signed up by an admin and must provide new | |
// password and required attributes, if any, to complete | |
// authentication. | |
console.log('New Password Required'); | |
// the api doesn't accept this field back | |
delete userAttributes.email_verified; | |
const callback = newPassword => { | |
this.getUser().completeNewPasswordChallenge(newPassword, userAttributes, { | |
onSuccess, | |
onFailure, | |
mfaRequired, | |
}); | |
}; | |
observer.publish('reset_password', data, callback, this); | |
}; | |
this.setUser(username) | |
.getUser() | |
.authenticateUser(credentials, { | |
onSuccess, | |
onFailure, | |
mfaRequired, | |
newPasswordRequired, | |
}); | |
return { observer }; | |
}; | |
startForgotPassword = (username: string) => { | |
const observer = new PromiseQueue(); | |
this.setUser(username); | |
const onSuccess = () => { | |
// console.info('Forgot Password Success!'); | |
observer.publish('success', this); | |
observer.cancel(); | |
}; | |
const onFailure = err => { | |
console.error('Failed to Reset Password: ', err.message); | |
observer.publish('error', err, this); | |
observer.cancel(); | |
}; | |
const inputVerificationCode = data => { | |
console.info('Forgot Info Sent, Verification Required: ', data); | |
const callback = (code, newPassword) => { | |
this.getUser().confirmPassword(String(code), newPassword, { | |
onSuccess, | |
onFailure, | |
}); | |
}; | |
observer.publish('verify', data, callback); | |
}; | |
this.setUser(username) | |
.getUser() | |
.forgotPassword({ | |
onSuccess, | |
onFailure, | |
inputVerificationCode, | |
}); | |
return { observer }; | |
}; | |
startSetUserAttributes = (attributes: Object) => { | |
const observer = new PromiseQueue(); | |
const onSuccess = result => { | |
observer.publish('success', result); | |
observer.cancel(); | |
}; | |
const onFailure = err => { | |
console.error('Failed to Set User Attributes: ', err.message); | |
observer.publish('error', err); | |
observer.cancel(); | |
return err; | |
}; | |
const inputVerificationCode = async result => { | |
const deliveries = result.CodeDeliveryDetailsList; | |
while (deliveries.length > 0) { | |
const delivery = deliveries.shift(); | |
// eslint-disable-next-line | |
await new Promise((resolve, reject) => { | |
const callback = code => { | |
this.getUser().verifyAttribute(delivery.AttributeName, String(code), { | |
onSuccess: success => { | |
console.log('Attribute Verified: ', success); | |
if (deliveries.length === 0) { | |
resolve(onSuccess(success)); | |
} else { | |
resolve(); | |
} | |
}, | |
onFailure: err => { | |
reject(onFailure(err)); | |
}, | |
}); | |
}; | |
// simulate the same style that is used for standard verification of | |
// attributes. | |
observer.publish('verify', { CodeDeliveryDetails: delivery }, callback); | |
// finish promise | |
}); | |
// handle next delivery | |
} | |
}; | |
// eslint-disable-next-line | |
this.refreshSessionIfNeeded().then(() => { | |
const formatted = formatUserAttributes(attributes); | |
if (formatted.length === 0) { | |
console.error('Formatted Attributes was Empty'); | |
observer.publish('error', 'Formatted Attributes was Empty'); | |
return observer.cancel(); | |
} | |
return this.getUser().updateAttributes(formatted, { | |
onSuccess, | |
onFailure, | |
inputVerificationCode, | |
}); | |
}); | |
return { observer }; | |
}; | |
startVerifyAttribute = (attribute?: string = 'email') => { | |
const observer = new PromiseQueue(); | |
const onSuccess = result => { | |
observer.publish('success', result); | |
observer.cancel(); | |
}; | |
const onFailure = err => { | |
observer.publish('error', err); | |
observer.cancel(); | |
}; | |
const inputVerificationCode = result => { | |
const callback = code => { | |
if (code) { | |
return this.getUser().verifyAttribute(attribute, String(code), { | |
onSuccess, | |
onFailure, | |
}); | |
} | |
return observer.cancel(); | |
}; | |
observer.publish('verify', result, callback); | |
}; | |
this.refreshSessionIfNeeded() | |
.then(() => | |
this.getUser().getAttributeVerificationCode(attribute, { | |
onSuccess, | |
onFailure, | |
inputVerificationCode, | |
}), | |
) | |
.catch(err => observer.publish('error', err)); | |
return { observer }; | |
}; | |
refreshUserAttributes = (): Promise<*> => | |
new Promise((resolve, reject) => | |
this.refreshSessionIfNeeded().then(() => | |
this.getUser().getUserAttributes((err, attributes) => { | |
if (err) { | |
console.error('[getUserAttributes] Failed: ', err.message); | |
return reject(err.message); | |
} | |
resolve(parseUserAttributes(attributes)); | |
}), | |
), | |
); | |
refreshUserSession = (force?: boolean = false): Promise<*> => | |
new Promise((resolve, reject) => | |
this.getUser().getSession((err, session) => { | |
if (err) { | |
console.error('Failed to Refresh User Session from Library: ', err.message); | |
return reject('Refresh Session Failed'); | |
} | |
if (force || !session.isValid()) { | |
// manually set the session then attempt to refresh it | |
this.aws.session = session; | |
return resolve(this.refreshSession()); | |
} | |
resolve(this.setSession(session).getSession()); | |
}), | |
); | |
refreshSessionIfNeeded = (): Promise<*> => | |
new Promise(resolve => { | |
const session = this.getSession(); | |
if (session.isValid()) { | |
return resolve(session); | |
} | |
return resolve(this.refreshSession()); | |
}); | |
refreshSession = (): Promise<*> => | |
new Promise((resolve, reject) => { | |
this.getUser().refreshSession(this.getRefreshToken(), (err, session) => { | |
if (err) { | |
console.error('Failed to Refresh User Session: ', err.message); | |
return reject(err.message); | |
} else if (!session.isValid()) { | |
console.error('Refreshed Session is Not Valid!'); | |
return reject('Failed to Refresh Session'); | |
} | |
return resolve(this.setSession(session).getSession()); | |
}); | |
}); | |
refreshUserDevices = (limit?: number = 30, page = null): Promise<*> => | |
new Promise((resolve, reject) => | |
this.refreshSessionIfNeeded().then(() => | |
this.getUser().listDevices(limit, page, { | |
onSuccess: result => resolve(result), | |
onFailure: err => { | |
console.error('Failed to Refresh User Devices: ', err.message); | |
return reject(err.message); | |
}, | |
}), | |
), | |
); | |
refreshAccessTokensIfNeeded = (): Promise<string> => | |
this.refreshSessionIfNeeded().then(session => getSessionIDToken(session)); | |
refreshAccessTokens = (): Promise<string> => | |
this.refreshSession().then(session => getSessionIDToken(session)); | |
refreshIdentityPool = () => { | |
const identity = this.state.identity; | |
if (!identity) { | |
throw new Error( | |
'Tried to refresh Identity Pool before setting its params with setIdentity()!', | |
); | |
} | |
return this.refreshSession() | |
.then(session => { | |
const token = getSessionIDToken(session); | |
const userPool = this.getPool(); | |
const { region } = this.config; | |
this.aws.identity = new CognitoIdentityCredentials({ | |
...identity, | |
Logins: { | |
[`cognito-idp.${region}.amazonaws.com/${userPool.getUserPoolId()}`]: token, | |
}, | |
}); | |
AWSConfig.credentials = this.aws.identity; | |
return AWSConfig.credentials.refreshPromise(); | |
}) | |
.catch(err => { | |
console.error('Failed to Refresh Identity Pool: ', err.message); | |
throw err; | |
}); | |
}; | |
refreshIdentityPoolIfNeeded = () => { | |
const identity = this.state.identity; | |
if (!identity) { | |
throw new Error( | |
'Tried to refresh Identity Pool before setting its params with setIdentity()!', | |
); | |
} | |
return this.refreshSessionIfNeeded() | |
.then(session => { | |
const token = getSessionIDToken(session); | |
const userPool = this.getPool(); | |
const { region } = this.config; | |
this.aws.identity = new CognitoIdentityCredentials({ | |
...identity, | |
Logins: { | |
[`cognito-idp.${region}.amazonaws.com/${userPool.getUserPoolId()}`]: token, | |
}, | |
}); | |
AWSConfig.credentials = this.aws.identity; | |
return AWSConfig.credentials.refreshPromise(); | |
}) | |
.catch(err => { | |
console.error('Failed to Refresh Identity Pool: ', err.message); | |
throw err; | |
}); | |
}; | |
removeUserDevice = (deviceKey: string): Promise<string> => | |
new Promise((resolve, reject) => | |
this.refreshSessionIfNeeded().then(() => | |
this.getUser().forgetSpecificDevice(deviceKey, { | |
onFailure: err => reject(err.message), | |
onSuccess: () => resolve(deviceKey), | |
}), | |
), | |
); | |
refreshUserDataSync = (dataset?: string = 'settings', force?: boolean = false): Promise<*> => | |
new Promise((resolve, reject) => { | |
const getAWSCredentials = () => | |
AWSConfig.credentials.get(() => { | |
this.aws.sync = new CognitoSyncManager(); | |
return this.aws.sync.openOrCreateDataset(dataset, (err, dataManager) => { | |
if (err) { | |
console.error('Failed to Open User Data Set: ', dataset, err.message); | |
return reject('Failed to Open UserData'); | |
} | |
console.log(dataManager); | |
this.aws.dataManager = dataManager; | |
return resolve(handleGetAllRecords(dataManager)); | |
}); | |
}); | |
if (force) { | |
return this.refreshIdentityPool().then(() => getAWSCredentials()); | |
} | |
return this.refreshIdentityPoolIfNeeded().then(() => getAWSCredentials()); | |
}); | |
} | |
export function clearAWSCredentialsCache(): void { | |
if (AWSConfig.credentials && AWSConfig.credentials.clearCachedId) { | |
AWSConfig.credentials.clearCachedId(); | |
} | |
} | |
export function createCognitoAuthenticator( | |
config?: CognitoAuthenticator$Config, | |
): Class<CognitoAuthenticator> { | |
Authenticator = new CognitoAuthenticator(config); | |
return Authenticator; | |
} | |
export function getCognitoAuthenticator( | |
config?: CognitoAuthenticator$Config, | |
): Class<CognitoAuthenticator> { | |
if (!Authenticator && config) { | |
return createCognitoAuthenticator(config); | |
} | |
return Authenticator; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment