Building a serverless connected BBQ as SaaS - Part 2 - User Creation
In part two of the series about the world of BBQ, where tradition and technology rarely cross paths. The future of grilling is here, and it’s connected, smart, and runs on the cloud! I continue with user management using an serverless and event-driven approach with Cognito User Pool together with Lambda, EventBridge, and StepFunctions.
Post Confirmation
. The function will put a an event on the application event-bus that a user was created. The user service will react on this event and store information about the user in a DynamoDB table. User service ends by posting a new event on the bus saying a new user was created.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: Connected BBQ Application Common Infra
Parameters:
Application:
Type: String
Description: Name of owning application
Default: bbq-iot
Resources:
EventBridgeBus:
Type: AWS::Events::EventBus
Properties:
Name: !Sub ${Application}-application-eventbus
Tags:
- Key: Application
Value: !Ref Application
Outputs:
EventBridgeName:
Description: The EventBus Name
Value: !Ref EventBridgeBus
Export:
Name: !Sub ${AWS::StackName}:eventbridge-bus-name
EventBridgeArn:
Description: The EventBus ARN
Value: !GetAtt EventBridgeBus.Arn
Export:
Name: !Sub ${AWS::StackName}:eventbridge-bus-arn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: Connected BBQ Application idP setup Authentication
Parameters:
ApplicationName:
Type: String
Description: The application that owns this setup.
HostedAuthDomainPrefix:
Type: String
Description: The domain prefix to use for the UserPool hosted UI <HostedAuthDomainPrefix>.auth.[region].amazoncognito.com
CommonStackName:
Type: String
Description: The name of the common stack that contains the EventBridge Bus and more
Resources:
UserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: !Sub ${ApplicationName}-user-pool
UsernameConfiguration:
CaseSensitive: false
UsernameAttributes:
- "email"
AutoVerifiedAttributes:
- email
Policies:
PasswordPolicy:
MinimumLength: 12
RequireLowercase: true
RequireUppercase: true
RequireNumbers: true
RequireSymbols: true
AccountRecoverySetting:
RecoveryMechanisms:
- Name: "verified_email"
Priority: 1
- Name: "verified_phone_number"
Priority: 2
Schema:
- Name: email
AttributeDataType: String
Mutable: false
Required: true
- Name: name
AttributeDataType: String
Mutable: true
Required: true
- Name: tenant
AttributeDataType: String
DeveloperOnlyAttribute: true
Mutable: true
Required: false
GenerateSecret: False
set. Now let's add the client to the template from before.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
UserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref UserPool
GenerateSecret: False
AllowedOAuthFlowsUserPoolClient: true
CallbackURLs:
- http://localhost:3000
#- !Sub https://${DomainName}/signin
AllowedOAuthFlows:
- code
- implicit
AllowedOAuthScopes:
- phone
- email
- openid
- profile
SupportedIdentityProviders:
- COGNITO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"metadata": {
"domain": "idp",
"application": "application_name",
"event_type": "signup",
"version": "1.0",
},
"data": {
"email": "user e-mail",
"userName": "user name",
"name": "name",
"verified": "verified",
"status": "status",
},
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
PostSignUpHook:
Type: AWS::Serverless::Function
Properties:
AutoPublishAlias: "true"
CodeUri: ./PostSignUpLambda
Handler: hook.handler
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- EventBridgePutEventsPolicy:
EventBusName:
Fn::ImportValue: !Sub ${CommonStackName}:eventbridge-bus-name
Environment:
Variables:
EventBusName:
Fn::ImportValue: !Sub ${CommonStackName}:eventbridge-bus-name
ApplicationName: !Ref ApplicationName
PostSignUpHookPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt PostSignUpHook.Arn
Principal: cognito-idp.amazonaws.com
UserPool:
Type: AWS::Cognito::UserPool
Properties:
.....
LambdaConfig:
PostConfirmation: !GetAtt PostSignUpHook.Arn
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import boto3
import os
import json
def handler(event, context):
application_name = os.environ["ApplicationName"]
event_bus = os.environ["EventBusName"]
event_bus_client = boto3.client("events")
user_event = {
"metadata": {
"domain": "idp",
"application": application_name,
"event_type": "signup",
"version": "1.0",
},
"data": {
"email": event["request"]["userAttributes"]["email"],
"userName": event["userName"],
"name": event["request"]["userAttributes"]["name"],
"verified": event["request"]["userAttributes"]["email_verified"],
"status": event["request"]["userAttributes"]["cognito:user_status"],
},
}
response = event_bus_client.put_events(
Entries=[
{
"Source": f"{application_name}.idp",
"DetailType": "signup",
"Detail": json.dumps(user_event),
"EventBusName": event_bus,
},
]
)
return event

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: Connected BBQ Application User Service
Parameters:
ApplicationName:
Type: String
Description: Name of owning application
Default: bbq-iot
CommonStackName:
Type: String
Description: The name of the common stack that contains the EventBridge Bus and more
Resources:
UserSignUpHookStateMachineLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub ${ApplicationName}/userservice/signuphookstatemachine
RetentionInDays: 5
UserSignUpHookExpress:
Type: AWS::Serverless::StateMachine
Properties:
DefinitionUri: statemachine/statemachine.asl.yaml
Tracing:
Enabled: true
Logging:
Destinations:
- CloudWatchLogsLogGroup:
LogGroupArn: !GetAtt UserSignUpHookStateMachineLogGroup.Arn
IncludeExecutionData: true
Level: ALL
DefinitionSubstitutions:
EventBridgeBusName:
Fn::ImportValue: !Sub ${CommonStackName}:eventbridge-bus-name
UserTable: !Ref UserTable
ApplicationName: !Ref ApplicationName
Policies:
- Statement:
- Effect: Allow
Action:
- logs:*
Resource: "*"
- EventBridgePutEventsPolicy:
EventBusName:
Fn::ImportValue: !Sub ${CommonStackName}:eventbridge-bus-name
- DynamoDBCrudPolicy:
TableName: !Ref UserTable
Events:
UserSignUp:
Type: EventBridgeRule
Properties:
EventBusName:
Fn::ImportValue: !Sub ${CommonStackName}:eventbridge-bus-name
Pattern:
source:
- !Sub ${ApplicationName}.idp
detail-type:
- signup
Type: EXPRESS
UserTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Sub ${ApplicationName}-users
AttributeDefinitions:
- AttributeName: userid
AttributeType: S
KeySchema:
- AttributeName: userid
KeyType: HASH
BillingMode: PAY_PER_REQUEST
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Comment: User service - User Signup Hook State Machine
StartAt: Debug
States:
Debug:
Type: Pass
Next: Create User
Create User:
Type: Task
Resource: arn:aws:states:::dynamodb:putItem
Parameters:
TableName: ${UserTable}
Item:
userid:
S.$: $.detail.data.userName
name:
S.$: $.detail.data.name
email:
S.$: $.detail.data.email
status:
S.$: $.detail.data.status
verified:
S.$: $.detail.data.verified
ResultPath: null
Next: Post Event
Post Event:
Type: Task
Resource: arn:aws:states:::events:putEvents
Parameters:
Entries:
- Source: ${ApplicationName}.user
DetailType: created
Detail.$: $
EventBusName: ${EventBridgeBusName}
End: true
create-react-app
. For styling we will use Tailwind CSS.1
2
3
4
5
6
7
8
9
10
import { getCurrentUser } from "aws-amplify/auth";
export const isAuthenticated = async () => {
try {
await getCurrentUser();
return true;
} catch {
return false;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React, { useEffect } from "react";
import { Navigate, Route, Routes, useNavigate } from "react-router-dom";
import { isAuthenticated } from "../utils/auth";
import Header from "../components/Header";
import Footer from "../components/Footer";
import { Authenticator } from "@aws-amplify/ui-react";
import "@aws-amplify/ui-react/styles.css";
const Login = () => {
const navigate = useNavigate();
useEffect(() => {
isAuthenticated().then((loggedIn) => {
if (loggedIn) {
navigate("/dashboard");
}
});
}, [navigate]);
return (
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-grow flex items-center justify-center">
<Authenticator signUpAttributes={["name"]} loginMechanisms={["email"]}>
{({ signOut, user }) => (
<Routes>
<Route path="/" element={<Navigate replace to="/dashboard" />} />
</Routes>
)}
</Authenticator>
</main>
<Footer />
</div>
);
};
export default Login;

Create Account
and fill in e-mail and password, in the next step the e-mail address must be verified.
Profile
tab, also not that the login button now changes to logout.