Skip to content

Commit

Permalink
feat(cognito): verify SSO users and migrate
Browse files Browse the repository at this point in the history
  • Loading branch information
samueljamesbliss committed Oct 17, 2023
1 parent fee65f9 commit 6861789
Show file tree
Hide file tree
Showing 7 changed files with 2,202 additions and 53 deletions.
44 changes: 44 additions & 0 deletions examples/javascript/src/lib/jane-service.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,49 @@ const verifyCredentials = async (data, token) => {
return result
}

/** ----- GET SSO USER ATTRIBUTES ----- */

const verifySSOUser = async (data, token) => {
/*
user_attributes.identities is already a stringified object and
unfortunately if you call JSON.stringify on it, the object breaks
on the backend when it attempts to re-parse it.
The solution here is to parse identities before stringifying it.
*/
const parsedData = {
...data,
user_attributes: {
...data.user_attributes,
identities: JSON.parse(data.user_attributes.identities)
}
}
const response = await apiService.post(
`${COGNITO_API}/verify_sso_user`,
parsedData,
token
)

const result = {
errorMessage: "",
user: response.body?.user,
}

switch (response.statusCode) {
case 200:
break
case 404:
result.errorMessage = "User not found"
break
default:
result.errorMessage = buildErrorMessage(
"Error verifying SSO user",
response
)
}

return result
}

/** ----- VALIDATE USER ----- */


Expand Down Expand Up @@ -171,5 +214,6 @@ export default {
userExists,
ensureExternalUserExists,
verifyCredentials,
verifySSOUser,
validateUser
}
12 changes: 12 additions & 0 deletions examples/javascript/src/lib/utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,15 @@ export const mapUserAttributes = (userAttributes) => {

return userData
}

export const addAreaCodeToPhone = (phone) => {
let partial = phone.startsWith("+") ? phone.substring(1) : phone

// Missing + and country code, 2223334444
if (phone.length === 10) {
return `+1${partial}`
}

// If was already correct, just return the +
return `+${partial}`
}
13 changes: 1 addition & 12 deletions examples/javascript/src/migration-lambda/index.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { addAreaCodeToPhone } from '../lib/utils.mjs';
import Jane from "../lib/jane-service.mjs";
import apiService from "../lib/api-service.mjs";

Expand Down Expand Up @@ -96,15 +97,3 @@ export const handler = async (event) => {

return event;
};

const addAreaCodeToPhone = (phone) => {
let partial = phone.startsWith("+") ? phone.substring(1) : phone;

// Missing + and country code, 2223334444
if (phone.length === 10) {
return `+1${partial}`;
}

// If was already correct, just return the +
return `+${partial}`;
};
104 changes: 97 additions & 7 deletions examples/javascript/src/post-confirmation-lambda/index.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mapUserAttributes } from '../lib/utils.mjs';
import { addAreaCodeToPhone, mapUserAttributes } from '../lib/utils.mjs';
import Jane from '../lib/jane-service.mjs';
import apiService from '../lib/api-service.mjs';

Expand Down Expand Up @@ -26,12 +26,17 @@ export const handler = async (event) => {
return event;
}

const { success, errorMessage } = await Jane.createUser({
pool_id: event.userPoolId,
external_id: event.userName,
app_client_id: event.callerContext.clientId,
...mapUserAttributes(event.request.userAttributes),
}, token);
event = await handleUserMigration(event, token);

const { success, errorMessage } = await Jane.createUser(
{
pool_id: event.userPoolId,
external_id: event.userName,
app_client_id: event.callerContext.clientId,
...mapUserAttributes(event.request.userAttributes),
},
token
);

if (!success) {
throw new Error(`User creation was not successful: ${errorMessage}`);
Expand All @@ -41,3 +46,88 @@ export const handler = async (event) => {

return event;
};
/* Cognito SSO flows do not go through our migration handler
instead we handle those migrations here, after signup.
If a user is signing up via sso, we check for a Jane SSO user
associated with this client and use that users data for the migration */
const handleUserMigration = async (event, token) => {
let userIdentities;
try {
userIdentities = JSON.parse(event.request.userAttributes.identities);
} catch (err) {
console.error("userIdentities unable to parse", err);
return event;
}

const userGoogleIdentity = userIdentities.find(
(i) => i.providerType === "Google"
);
if (!userGoogleIdentity) {
return event;
}

const { errorMessage, user } = await Jane.verifySSOUser(
{
email: event.request.userAttributes.email,
user_attributes: event.request.userAttributes,
app_client_id: event.callerContext.clientId,
},
token
);
if (errorMessage === "User not found") {
// Jane user for this client was not found, continue normal sign up
return event;
} else if (errorMessage || !user) {
// something went wrong, continue normal sign up and log error
console.error(`failed to retrieve data for migration: ${errorMessage}`);
return event;
}
const attributes = {};
const { first_name, last_name, phone, birth_date } = user;

const attributesToUpdate = [];
first_name &&
(attributes.given_name = first_name) &&
attributesToUpdate.push({
Name: "given_name",
Value: first_name,
});
last_name &&
(attributes.family_name = last_name) &&
attributesToUpdate.push({
Name: "family_name",
Value: last_name,
});
phone &&
(attributes.phone_number = addAreaCodeToPhone(phone)) &&
attributesToUpdate.push({
Name: "phone_number",
Value: addAreaCodeToPhone(phone),
});
birth_date &&
(attributes.birthdate = birth_date) &&
attributesToUpdate.push({
Name: "birthdate",
Value: birth_date,
});
const cognitoIdServiceProvider = new CognitoIdentityProviderClient({
region: "us-east-1",
});
const command = new AdminUpdateUserAttributesCommand({
UserAttributes: attributesToUpdate,
UserPoolId: event.userPoolId,
Username: event.userName,
});
await cognitoIdServiceProvider
.send(command)
.then((data) => console.log("Cognito user updated!", data))
.catch((err) => {
console.error("Cognito Attribute Update Unsuccessful", err);
});

event.request.userAttributes = {
...event.request.userAttributes,
...attributes,
};
return event;
};
14 changes: 14 additions & 0 deletions examples/javascript/src/pre-signup-lambda/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ export const handler = async (event) => {
}

if (userExists) {
// Caveat, flow is "wrong" because SSO is sinup/sign in is the same thing, so it calls signup when
// we actually want is signin

// Eg:
// Here we should check event.triggerSource === 'PreSignUp_ExternalProvider'
// User should be confirmed (just like a migration)
// No message to user since it already had an account before (just like a migration)
// And move flow along to Post-Confirmation, where it can finish the migration
if (event.triggerSource === 'PreSignUp_ExternalProvider') {
event.response.autoConfirmUser = true
event.response.autoVerifyEmail = true

return event
}
throw Error('User already exists, please log in')
}

Expand Down
Loading

0 comments on commit 6861789

Please sign in to comment.