Lets suppose we developed an Alexa Skill which uses AWS Lambda and AWS IoT to control a psychical device and we have a single IoT thing (with a hard-coded name like MyAwsomeDevice) which controls a single psychical device.
Now, lets suppose we want to publish this skill, so other people can use it too.
Publishing the skill in this form is probably is not the best idea:
- there is a single IoT thing and only we have access to it, since probably we are using a private key and a certificate to connect to it
- publishing the private key and the certificate is not an option, because other user will be able to access / control our device. This would be a huge security risk especially for some kind of devices (for ex. ones connected to mains)
- we would still have multiple users controlling a single IoT thing controlling multiple psychical devices. This would probably we end up in big mess.
Here is a minimal but still secure solution the prepare the skill for publishing:
Account Linking with Login with Amazon is used to identify the user.
Each user has it's own IoT thing with a certificate assigned to it. The certificate has a policy attached. The policy allows each user to access only their own (!) IoT thing
The IoT thing is created on-thy-fly for new users. The thing name, the thing's certificate and the public/private key pair is delivered to the user in a welcome e-mail sent using AWS Simple Email Service (SES).
Linking the Skill with Login with AmazonWe will use Login with Amazon, so will we need a security profile. Enter https://developer.amazon.com/lwa/sp/overview.html and check if there is a security profile for your skill. Create a new one if needed. Note the Client ID and Client Secret of your security profile.
Enable Account Linking for your skill in the Alexa Skills Kit Developer Console:
- Authorization Grant Type - Auth Code Grant
- Authorization URI - https://www.amazon.com/ap/oa
- Access Token URI - https://api.amazon.com/auth/o2/token
- Client ID / Client Secret - use the ones from the LWA security profile
- Scope - profile - this will give access to user's id, name and email
- Redirect URLs - check that your security profile have these in the Allowed Return URLs field (Web Settings)
Now, with the Account Linking enabled, we need to modify our AWS Lambda to ask new users to link their account. This a done by returning a LinkAccount card when the event['session']['user']['accessToken'] (we will use this later) is not provided:
def lambda_handler(event, context):
if not 'accessToken' in event['session']['user']:
# accessToken, ask the user to link their account
return build_response({}, {
'outputSpeech' : {
'type': 'PlainText',
'text': 'Please use the companion app to authenticate on Amazon to start using this skill'
},
'card': {
'type': 'LinkAccount'
},
'shouldEndSession': False
})
The Alexa App (web or mobile) will show the Link Account card:
After clicking on Link Account, logging into Amazon and following some simple steps we have our Skill linked with the user's Amazon account:
The following requests will have the event['session']['user']['accessToken'] populated. We can use this to query some info (userId, email, name) about the user:
if not 'accessToken' in event['session']['user']:
...
else:
userInfoResponse = urllib.request.urlopen("https://api.amazon.com/user/profile?access_token=" + event['session']['user']['accessToken']).read()
userInfo = json.loads(userInfoResponse.decode('utf-8'))
The result will be something like:
{'user_id': 'amzn1.account.AECOR7CU34SDXVJKWXXXXXXXXX', 'name': 'Attila', 'email': 't...@yahoo.com'}
Preparing the Skill to handle multiple Users / IoT thingsEach user will have their own IoT thing. For new users will create new IoT things dynamically.
To do this first we need to make some changes in our AWS IoT setup:
- create a new thing type (3d-printer) with a searchable attribute called UserId. This will be used to lookup IoT things based on their Amazon UserID.
- create a new policy (OneUserOneThing) that will allow each user (each with their own certificate) to access only their IoT thing. It is important to use such a restrictive policy to prevent to users to be able to access / control other users devices.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "iot:Connect",
"Resource": [
"arn:aws:iot:*:*:client/${iot:ClientId}"
],
"Condition": {
"Bool": {
"iot:Connection.Thing.IsAttached": [
"true"
]
}
}
},
{
"Effect": "Allow",
"Action": [
"iot:Publish",
"iot:Receive"
],
"Resource": [
"arn:aws:iot:*:*:topic/$aws/things/${iot:Connection.Thing.ThingName}/*"
]
},
{
"Effect": "Allow",
"Action": "iot:Subscribe",
"Resource": [
"arn:aws:iot:*:*:topicfilter/$aws/events/presence/*/${iot:ClientId}",
"arn:aws:iot:*:*:topicfilter/$aws/events/subscriptions/*/${iot:ClientId}",
"arn:aws:iot:*:*:topicfilter/$aws/things/${iot:Connection.Thing.ThingName}/*"
]
}
]
}
Now we need to the AWS Lambda function to work with multiple IoT things, instead using the same IoT thing regardless of who is the current user.
We will use the UserId attribute to lookup the user's IoT thing based their Amazon UserID. The AWS Pyhton SDK (boto3) can be used to do this:
iot_client = boto3.client('iot')
# IoT thing lookup
thingsResponse = iot_client.list_things(
maxResults = 1,
attributeName = "UserId",
attributeValue = userInfo['user_id']
)
If an a IoT thing with the required UserID was found we will use that thing.
if len(thingsResponse['things']) == 1:
# IoT thing found
thingName = thingsResponse['things'][0]['thingName']
print("Thing found: " + thingName)
If no IoT thing was found, it means that we are dealing with a new user, so we need to create on-the-fly an IoT thing with a certificate associated:
else:
# new-user, we create new IoT thing on-the-fly...
thingName = create_iot_thing(userInfo['user_id'], userInfo['email'], userInfo['name'])
print("Created new thing: " + thingName)
The 1st step is to create the thing:
def create_iot_thing(userId, userEmail, userName):
print("creating IoT Thing for new user " + userId)
thing_name = '3D_Printer-' + userId.replace(".", "-")
# 1. create the thing
thing = iot_client.create_thing(
thingName = thing_name,
thingTypeName = '3d-printer',
attributePayload = {
'attributes': {
'UserId': userId
},
}
)
print("Thing: {}".format(thing))
after which we generate a new certificate:
# 2. create keys and certificate
key_and_cert = iot_client.create_keys_and_certificate(setAsActive = True)
print("Key & Certificate: {}".format(key_and_cert))
assign the OneUserOneThing policy to the newly created certificate:
# 3. attach policy
iot_client.attach_policy(
policyName = "OneUserOneThing",
target = key_and_cert['certificateArn']
)
and we also need to assign the certificate to the newly created thing:
# 4. attach the certificate to the thing
iot_client.attach_thing_principal(
thingName = thing['thingName'],
principal = key_and_cert['certificateArn']
)
Now the user has it's own IoT thing.
The problem is that in this form the user is not able access the thing because does not have the certificate associated to it. To solve this we can send the certificate to the user in a welcome email, like:
I sent this using the AWS Simple Email Service (SES):
# Send email
msg = MIMEMultipart()
msg['Subject'] = 'Alexa - 3D Printing Skill'
msg['From'] = 'your@email.com'
msg['To'] = userEmail
msg.preamble = 'Multipart!\n'
# content
part = MIMEText(
"Hi " + userName + "!\n" \
"\n" \
"Welcome to the 3D Printing Skill!\n" \
"\n" \
"A 3D Printer IoT thing was created for you:\n" + thing['thingName'] + "\n" \
"\n" \
"The thing's certificates are attached to the email.\n" \
"\n" \
"Have a goog day!")
msg.attach(part)
# certificate
part = MIMEApplication(key_and_cert['certificatePem'])
part.add_header('Content-Disposition', 'attachment', filename='{}-certificate.pem.crt.txt'.format(thing_name))
msg.attach(part)
# private key
part = MIMEApplication(key_and_cert['keyPair']['PrivateKey'])
part.add_header('Content-Disposition', 'attachment', filename='{}-private.pem.key.txt'.format(thing_name))
msg.attach(part)
# public key key
part = MIMEApplication(key_and_cert['keyPair']['PublicKey'])
part.add_header('Content-Disposition', 'attachment', filename='{}-public.pem.key.txt'.format(thing_name))
msg.attach(part)
# root CA
part = MIMEApplication(
"-----BEGIN CERTIFICATE-----\n" \
"MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB\n" \
"yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL\n" \
"ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp\n" \
"U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW\n" \
"ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0\n" \
"aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL\n" \
"MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW\n" \
"ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln\n" \
"biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp\n" \
"U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y\n" \
"aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1\n" \
"nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex\n" \
"t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz\n" \
"SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG\n" \
"BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+\n" \
"rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/\n" \
"NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E\n" \
"BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH\n" \
"BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy\n" \
"aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv\n" \
"MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE\n" \
"p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y\n" \
"5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK\n" \
"WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ\n" \
"4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N\n" \
"hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq\n" \
"-----END CERTIFICATE-----"
)
part.add_header('Content-Disposition', 'attachment', filename='VeriSign-Class-3-Public-Primary-Certification-Authority-G5.pem.txt')
msg.attach(part)
ses_client = boto3.client('ses')
mailResult = ses_client.send_raw_email(
RawMessage = { 'Data' : msg.as_string().encode() },
Source = 'your@email.com',
Destinations = [userEmail]
)
print("Email sending: {}".format(mailResult))
Note: you need to verify the sending email address in the SES Management Console (Yahoo addresses may not work). The role used by the AWS Lambda needs to have the rights to send email. In IAM you can attach the AmazonSESFullAccess policy to the role.
Now the user can download the certificates to the Raspberry Pi (or other device) and has all it needs to safely connect to it's IoT thing.
For the full AWS Lambda code please see the following project:
Alexa / Walabot Controlled Smart 3D Printer
Enjoy!
(the implementation was mostly inspired by: https://www.hackster.io/dasbridge-team/dasbridge-129717)
Comments
Please log in or sign up to comment.