Cognito, Lambda & S3
A complete serverless member portal solution
✧ Who am I?
✧ The problem
✧ The solution
✧ The details
✧ The future
✧ Questions?
I’m a software architect and developer by heart
✧ 15 countries
✧ 5 Tech locations
✧ 6 warehouse locations
✧ ~3.6B in revenue (2016)
✧ ~200M visits / month
The problem
✧ Perl “WebApp”
✧ Based on custom Framework
✧ Requires very old Perl
✧ Breaks every now and then
✧ Server maintenance
Low maintenance is a key non-functional requirement
Non-functional requirements
Sign-ups need approval
Audit-trail for changes
Low maintenance effort
Functional requirements
Sign-in with various providers
Update personal data
Share personal profile
Browse event photos
The solution
AWS Simple Storage Service
✧ Easy to use
✧ Security and Access Mgmt.
✧ Data Durability
✧ Scalability
✧ Well known :-)
AWS Lambda
✧ Build custom backends
✧ Automated administration
✧ Built-in fault tolerance
✧ Integrated security model
✧ Bring your own code
AWS Identity & Access Mgmt.
✧ Enhanced security
✧ Granular control
✧ Seamless integration
✧ External identity systems
✧ Temporary credentials
AWS Cognito
✧ Social identity
✧ Custom user pools
✧ Federated identities
✧ Synchronize data
✧ Web and native mobile
Build a single page app based on AWS technology
Authentication and Authorization via AWS Cognito
Web app stored on AWS S3
Delivered via AWS CloudFront
ReactJS & Redux web app
Login with Google, Facebook, Twitter and email
web app
S3
Cognito
IAM
Access is done fully based on IAM roles and policies
Cognito default Auth and Unauth roles
Auth role only allows for “sign up” by default
Once approved additional operations are permitted
UnAuth
WriteOwnProfileData
ReadAllProfileData
ReadAllPhotoData
Auth
WriteOwnAccountData
Cognito IDs
The details
Restricting unapproved access is key to prevent abuse
AuthRole allows only to write sign up information
Read rights are added after approval
{� "Action": [� "s3:GetObject"� ],� "Effect": "Allow",� "Resource": [� "arn:aws:s3:::my-bucket/accounts/${cognito-identity.amazonaws.com:sub}/base.json"� ],
"Condition": {
"StringLike": {
"cognito-identity.amazonaws.com:aud": "eu-central-1:UUID",
"cognito-identity.amazonaws.com:sub": [ "eu-central-1:UUID" ]
}
}�}
{� "Action": [� "s3:DeleteObject",� "s3:PutObject",� "s3:HeadObject"� ],� "Effect": "Allow",� "Resource": [� "arn:aws:s3:::my-bucket/accounts/${cognito-identity.amazonaws.com:sub}/base.json"� ]�}
Lambda function for approving a member
const AWS = require('aws-sdk');
exports.handler = (event, context, callback) => {
const iam = new AWS.IAM();
const cognitoId = event.cognito_id;
iam.getRolePolicy(getRolePolicyParams, (err, data) => {
if (err) callback(err);
else {
const policy = JSON.parse(decodeURIComponent(data.PolicyDocument));
policy.Statement = policy.Statement.map((statement) => {
if (statement.Condition.StringLike['cognito-identity.amazonaws.com:sub'].indexOf(cognitoId) == -1) {
statement.Condition.StringLike['cognito-identity.amazonaws.com:sub'].push(cognitoId);
}
return statement;
});
iam.putRolePolicy(putRolePolicyParams, (err, data) => {
if (err) callback(err);
else callback(null, { message: 'Member approved' });
});
}
});
};
Simplified ReactJS profile editing component
export const ProfileEditComponent = ({ name, number, email, links, update }) => {
return (
<div className="container">
<div className="row">
<div className="input-field col s12">
<input placeholder="Name" id="name" type="text" className="validate" value={ name } onChange={ update(name) } />
<label htmlFor="name">Name</label>
</div>
</div>
<div className="row">
<div className="input-field col s12">
<input placeholder="Number" id="number" type="text" className="validate" value={ number } onChange={ update(number) } />
<label htmlFor="number">Number</label>
</div>
</div>
{ links.map((link) => <LinkEditComponent link={ link } update={ update } />) }
</div>
);
};
Redux dispatch mapping and simplified update action
const mapDispatchToProps = (dispatch) => {
return {
update: (field) => (e) => dispatch(updateProfileField(field, e.target.val()))
}
};
export const updateProfileField = (field, newValue) => {
return (dispatch, getState) => {
const { profile: { data } } = getState();
return new Promise((resolve, reject) => {
data[field] = newValue;
AWS.config.credentials.refresh(() => {
const { webIdentityCredentials: { params: { IdentityId: sub } } } = AWS.config.credentials;
const S3 = new AWS.S3();
const Key = 'profiles/' + sub + '/private.json';
S3.putObject(
{Bucket: BUCKET_NAME, Key: Key, ContentType: 'application/json', Body: JSON.stringify(data)},
(err) => err ? reject(err) : resolve(data));
});
});
}
};
Stored JSON data in S3
Viewing a public profile on the client
export const loadProfile = (id) => {
return (dispatch) => {
dispatch({type: LOAD_PUBLIC_PROFILE_REQUEST, profile: id });
return new Promise((resolve, reject) => {
AWS.config.credentials.refresh(() => {
const { webIdentityCredentials: { params: { IdentityId: sub } } } = AWS.config.credentials;
new AWS.S3().getObject(
{Bucket: BUCKET_NAME, Key: 'profiles/' + id + '/public.json'},
(err, data) => {
if (err) {
dispatch({type: LOAD_PUBLIC_PROFILE_FAILURE, profile: id});
reject(err);
} else {
const profile = JSON.parse(new TextDecoder('UTF8').decode(data.Body));
dispatch({type: LOAD_PUBLIC_PROFILE_SUCCESS, profile: profile});
resolve(profile)
}
});
});
});
};
};
S3 beats DynamoDB due to data access possibilities
Plain text JSON files
Can be synced locally with AWS CLI tools
Easy to manipulate and transform into e.g. CSV
S3 was required for photo storage anyway
The future
Extend using Lambda functions and social logins
Update profile via Lambda functions to enforce constraints
Photo management section incl. albums, various sizes, etc.
Event management section incl. signup and link to photos
Add additional social logins (Twitter, Google, etc.)
Integrate social sharing functions
✧ Code on GitHub (soon)
✧ Blog posts coming