Skip to content

Instantly share code, notes, and snippets.

@bradennapier
Created October 1, 2017 08:34
Show Gist options
  • Save bradennapier/7b4d765c927f2972f8a1762ef200c04b to your computer and use it in GitHub Desktop.
Save bradennapier/7b4d765c927f2972f8a1762ef200c04b to your computer and use it in GitHub Desktop.
/* @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