AWS IoT MQTT over Websocket with STS temporary credentials - amazon-web-services

I am having issues using the temporary credentials to initiate a connection to AWS IoT using STS temporary credentials, whilst keeping things secure.
I have already successfully connected embedded devices using certificates with policies.
But when I come to try connecting via the browser, using a pre-signed URL, I have hit a stumbling block.
Below is a code snippet from a Lambda function which first authenticates the request (not shown), and then builds the url using STS credentials via assumeRole.
Using my generated URL along with Paho javascript client, I have been successful up to the point of receiving a response of "101 Switching Protocols" in the browser. But the connection is terminated instead of switching to websockets.
Any help or guidance anyone out there can provide me with would be much appreciated.
const iot = new AWS.Iot();
const sts = new AWS.STS({region: 'eu-west-1'});
const params = {
DurationSeconds: 3600,
ExternalId: displayId,
Policy: JSON.stringify(
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iot:*"
],
"Resource": [
"*"
]
},
/*{
"Effect": "Allow",
"Action": [
"iot:Connect"
],
"Resource": [
"arn:aws:iot:eu-west-1:ACCID:client/" + display._id
]
},
{
"Effect": "Allow",
"Action": [
"iot:Receive"
],
"Resource": [
"*"
]
}*/
]
}
),
RoleArn: "arn:aws:iam::ACCID:role/iot_websocket_url_role",
RoleSessionName: displayId + '-' + Date.now()
};
sts.assumeRole(params, function(err, stsData) {
if (err) {
fail(err, db);
return;
}
console.log(stsData);
const AWS_IOT_ENDPOINT_HOST = 'REDACTED.iot.eu-west-1.amazonaws.com';
var url = v4.createPresignedURL(
'GET',
AWS_IOT_ENDPOINT_HOST,
'/mqtt',
'iotdata',
crypto.createHash('sha256').update('', 'utf8').digest('hex'),
{
key: stsData.Credentials.AccessKeyId,
secret: stsData.Credentials.SecretAccessKey,
protocol: 'wss',
expires: 3600,
region: 'eu-west-1'
}
);
url += '&X-Amz-Security-Token=' + encodeURIComponent(stsData.Credentials.SessionToken);
console.log(url);
context.succeed({url: url});
});
Edit: If it helps, I just checked inside the "Frames" window in Chrome debugger, after selecting the request which returns a 101 code. It shows a single frame: "Binary Frame (Opcode 2, mask)".
Does this Opcode refer to MQTT control code 2 AKA "CONNACK"? I am not an expert at MQTT (yet!).

I realised my mistake by reading the docs on STS.
If you pass a policy to this operation, the temporary security credentials that are returned by the operation have the permissions that are allowed by both the access policy of the role that is being assumed, and the policy that you pass.
The RoleARN that is supplied must also allow the actions that you are requesting via STS assumeRole.
i.e. The RoleARN could allow iot:*, then when you assume role, you can narrow the permissions down to, for instance iot:Connect and for specific resources.

Related

403 Error while invoking Lamda function calling S3 for ob ject retrieval

Most of threads on similar topic has advised creating an IAM role to be assigned to Lamda function and creating a bucket level policy in S3 console to allow access for above role.
Have created these as below
Role for my Lamda function -
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stmtlamda",
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::referencedataepiko/*",
"arn:aws:s3:::referencedataepiko"
]
}
]
}
I have a policy configured at a bucket level -
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Statement1",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::************:role/$role_name_lamda"
},
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::referencedataepiko/*",
"arn:aws:s3:::referencedataepiko"
]
}
]
}
Thought these would have been sufficient:) Post above configurations tried testing the Lamda from its Test tab. But still get the same error ""error": "Access Denied (Service: Amazon S3; Status Code: 403; Error Code: AccessDenied.."
Would appreciate if i can know whats missing above.
(Note - There is no server side encryption for s3 buckets)
1 more addition to the post - I am using a Java AWS sdk for writing the Lamda function. Have tried few combinations on setting up the client object for S3. Am including this to know if this is not a problem..
//com.amazonaws.services.s3.model.Region region = com.amazonaws.services.s3.model.Region.US_East_2;
//AmazonS3 client = AmazonS3ClientBuilder.standard().build();
//AmazonS3 client = AmazonS3ClientBuilder.standard().withRegion(Regions.AP_SOUTH_1).build();
Creds creds = new Creds();
AmazonS3 client = new AmazonS3Client(new BasicAWSCredentials(creds.getAWSAccessKeyId(), creds.getAWSSecretKey()))
.withRegion(Region.getRegion(Regions.US_EAST_1));
The problem is here.
Creds creds = new Creds();
AmazonS3 client = new AmazonS3Client(new BasicAWSCredentials(creds.getAWSAccessKeyId(), creds.getAWSSecretKey()))
.withRegion(Region.getRegion(Regions.US_EAST_1));
You are creating Creds object and in next line using getAWSSecretKey and getAWSAcccessKey method. That might be null.
There is no need to pass accessKey and secretKey separately while creating client of any AWS service inside Lambda function. You can try this.
AmazonS3 s3Client = AmazonS3ClientBuilder.standard()
.withCredentials(new DefaultAWSCredentialsProviderChain())
.build();

Identity Pool Role Can't Access S3 Bucket Access Point

Summary: I am using AWS Amplify Auth class with a pre-configured Cognito User Pool for authentication. After authentication, I am using the Cognito ID token to fetch identity pool credentials (using AWS CredentialProviders SDK) whose assumed role is given access to an S3 access point. I then attempt to fetch a known object from the bucket's access point using the AWS S3 SDK. The problem is that the request returns a response of 403 Forbidden instead of successfully getting the object, despite my role policy and bucket (access point) policy allowing the s3:GetObject action on the resource.
I am assuming something is wrong with the way my policies are set up. Code snippets below.
I am also concerned I'm not getting the right role back from the credentials provider, but I don't allow unauthenticated roles on the identity pool so I am not sure, and I don't know how to verify the role being sent back in the credentials' session token to check.
I also may not be configuring the sdk client objects properly, but I followed the documentation provided to the best of my understanding from the documentation (I am using AWS SDK v3, not v2, so slightly different syntax and uses modular imports)
Backend Configurations - IAM
Identity Pool: Authenticated Role Trust Policy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
},
"Action": [
"sts:AssumeRoleWithWebIdentity",
"sts:TagSession"
],
"Condition": {
"StringEquals": {
"cognito-identity.amazonaws.com:aud": "<MY_IDENTITY_POOL_ID>"
},
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": "authenticated"
}
}
}
]
}
Identity Pool: Authenticated Role S3 Access Policy
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:GetObjectVersion"
],
"Resource": "arn:aws:s3:::<MY_ACCESS_POINT_NAME>/object/*"
}
]
}
Backend Configurations - S3
S3 Bucket and Access Points: Block All Public Access
S3 Bucket CORS Policy:
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"PUT",
"HEAD"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": [],
"MaxAgeSeconds": 300
}
]
S3 Bucket Policy (Delegates Access Control to Access Points):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DelegateAccessControlToAccessPoints",
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": "*",
"Resource": [
"arn:aws:s3:::<MY_BUCKET_NAME>",
"arn:aws:s3:::<MY_BUCKET_NAME>/*"
],
"Condition": {
"StringEquals": {
"s3:DataAccessPointAccount": "<MY_ACCT_ID>"
}
}
}
]
}
Access Point Policy
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowAccessPointToGetObjects",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<ACCT_ID>:role/<MY_IDENTITY_POOL_AUTH_ROLE_NAME>"
},
"Action": [
"s3:GetObject",
"s3:GetObjectVersion"
],
"Resource": "arn:aws:s3:<REGION>:<ACCT_ID>:accesspoint/<MY_ACCESS_POINT_NAME>/object/*"
}
]
}
Front End AuthN & AuthZ
Amplify Configuration of User Pool Auth
Amplify.configure({
Auth: {
region: '<REGION>',
userPoolId: '<MY_USER_POOL_ID>',
userPoolWebClientId: '<MY_USER_POOL_APP_CLIENT_ID>'
}
})
User AuthZ process:
On user login event, call Amplify's Auth.signIn() which returns type CognitoUser:
// Log in user (error checking ommitted here for post)
const CognitoUser = await Auth.signIn(email, secret);
// Get ID Token JWT
const CognitoIdToken = CognitoUser.signInUserSession.getIdToken().getJwtToken();
// Use #aws-sdk/credentials-provider to get Identity Pool Credentials
const credentials = fromCognitoIdentityPool({
clientConfig: { region: '<REGION>' },
identityPoolId: '<MY_IDENTITY_POOL_ID>',
logins: {
'cognito-idp.<REGION>.amazonaws.com/<MY_USER_POOL_ID>': CognitoIdToken
}
})
// Create S3 SDK Client
client = new S3Client({
region: '<REGION>',
credentials
})
// Format S3 GetObjectCommand parameters for object to get from access point
const s3params = {
Bucket: '<MY_ACCESS_POINT_ARN>',
Key: '<MY_OBJECT_KEY>'
}
// Create S3 client command object
const getObjectCommand = new GetObjectCommand(s3params);
// Get object from access point (execute command)
const response = await client.send(getObjectCommand); // -> 403 FORBIDDEN

I want to create an S3 bucket that can only be accessed by users created with Cognito when they sign in

I want to be accessible in the following ways:
const cognitoUser = userPool.getCurrentUser();
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: "us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx,
Logins: {
'cognito-idp.us-east-1.amazonaws.com/us-east-1_xxxxxxxxx': result.getIdToken().getJwtToken()
}
});
// このようにアクセスしたい。
window.location.href = "https://nabezokodaikon-private.s3.amazonaws.com/private/index.html";
I have created the following S3 bucket policy.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
},
"Action": "s3:*",
"Resource": "arn:aws:s3:::nabezokodaikon-private/*"
},
{
"Effect": "Deny",
"NotPrincipal": {
"AWS": "arn:aws:iam::xxxxxxxxxxxx:user/nabezokodaikon"
},
"Action": "s3:*",
"Resource": "arn:aws:s3:::nabezokodaikon-private/index.html"
}
]
}
But this is not accessible.
How can I do what I want?
While your code is obtaining credentials from Cognito, this line is not supplying any information that identifies the request as coming from the Cognito credentials:
window.location.href = "https://nabezokodaikon-private.s3.amazonaws.com/private/index.html";
That URL is just a normal anonymous request, which should be rejected (as it is).
To gain access to the private content, you will need to create a pre-signed URL that uses the credentials supplied by Cognito to 'sign' the request. A pre-signed URL provides time-limited access to private content in Amazon S3.
See: Amazon S3 pre-signed URLs
You will also need to remove the Deny statement, since it will block any access that doesn't come from the stated user. Amazon S3 buckets are private by default, so Deny should only be used in situations where an Allow needs to be overridden.

AWS: Restrict Cognito Authorized User to specific Lambda Functions

I'm working with AWS and I've the following setup: UserPool; API Gateway, Lambda Functions
The api gateway is using a UserPool authorizer to protect the lambda functions. This is working so far. Now I want to restrict every lambda function to a specific group of users. Therefore I've created two user groups in the CognitoPool (user and admin) and I've assigned a specific role to each group with a policy. Afterwards I've created a user in the UserPool and added him to the user group. That user is still able to submit requests to each route/lambda function.
How do I submit a request?
Postman
set IdToken (of the authenticated user) in the Authorization header
without Authorization header the response is a 401 (as expected)
with Authorization header every lambda function can be triggered (not expected)
Configuration of the UserPool Groups:
Group User:
Arn: Role ARN: arn:aws:iam::xxxxxx:role/User
UserRole is specified as
{
"Version": "2012-10-17",
"Statement": [
"Action": [
"lambda:InvokeFunction",
"lambda:InvokeAsync"
],
"Resource": [
"arn:aws:lambda:region:xxxxxx:function:api-dev-getItems
],
"Effect": "Allow"
]
}
Group Admin:
Arn: Role ARN: arn:aws:iam::xxxxxx:role/Admin
AdminRole is specified as
{
"Version": "2012-10-17",
"Statement": [
"Action": [
"lambda:InvokeFunction",
"lambda:InvokeAsync"
],
"Resource": [
"arn:aws:lambda:region:xxxxxx:function:api-dev-getItems
"arn:aws:lambda:region:xxxxxx:function:api-dev-getUsers
],
"Effect": "Allow"
]
}
The payload of the id token also contains:
'cognito:roles': [ 'arn:aws:iam::xxxxxx:role/User' ]
So I've found a solution to my problem. Here is the summary of my experiences:
Cognito Authorizer is more like a yes/no authorizer (authenticated or not; user groups are not evaluated)
Therefore I went with AWS IAM Authorizer in the API Gateway, which will evaluate the user group roles
Instead of a JWT the AWS signature v4 authorization has to be passed (there is a plugin for postman and several packages on npm)
Since I am using an API Gateway I had to change the role policy resources to execute-api:Invoke
In detail:
UserRole:
{
"Version": "2012-10-17",
"Statement": [
"Action": [
"lambda:InvokeFunction",
"lambda:InvokeAsync"
],
"Resource": [
"arn:aws:execute-api:region:accountid:api-id/stage/GET/items
],
"Effect": "Allow"
]
}
AdminRole:
{
"Version": "2012-10-17",
"Statement": [
"Action": [
"lambda:InvokeFunction",
"lambda:InvokeAsync"
],
"Resource": [
"arn:aws:execute-api:region:accountid:api-id/stage/GET/items
"arn:aws:execute-api:region:accountid:api-id/stage/*/users
],
"Effect": "Allow"
]
}
Instead of passing the ID Token into the Authorization header, I had to use Postman AWS Signature, which requires at least an AccessKey and a SecretKey. Those two can be retrieved when I sign in my user using the aws-sdk. aws-sdk-js with TypeScript as example:
import { CognitoUserPool, CognitoUser, AuthenticationDetails } from 'amazon-cognito-identity-js';
const userPool = new CognitoUserPool({
UserPoolId: 'my pool id',
ClientId: 'my client id'
});
function signIn(username: string, password: string) {
const authData = {
Username: username,
Password: password,
};
const authDetails = new AuthenticationDetails(authData);
const userData = {
Username: username,
Pool: userPool,
};
const cognitoUser = new CognitoUser(userData);
cognitoUser.authenticateUser(authDetails, {
onSuccess: (result) => {
const cognitoIdpKey = `cognito-idp.${region}.amazonaws.com/${userPool.getUserPoolId()}`;
const credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: 'identity pool id,
Logins: {
[cognitoIdpKey]: result.getIdToken().getJwtToken(),
}
});
AWS.config.update({
credentials,
});
credentials.refreshPromise()
.then(() => {
console.log('Success refresh. Required data:', (credentials as any).data.Credentials);
})
.catch(err => console.error('credentials refresh', err));
}
});
}

Can't use AWS temporary credentials (STS) - forbidden

I need to give temporary Access Keys to my clients to connect to IoT services (publish, receive, etc.). To provide this access, I've created a Lambda function that calls sts.assumeRole to create temporary STS keys. Those keys are being created and look fine.
I'm using assumeRole with Lambda in a role with the following inline policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"iot:Connect",
"iot:Subscribe",
"iot:Publish",
"iot:Receive"
],
"Resource": "*",
"Effect": "Allow"
},
{
"Action": [
"ec2:*"
],
"Resource": "*",
"Effect": "Allow"
},
{
"Action": [
"sts:AssumeRole"
],
"Resource": "*",
"Effect": "Allow"
}
]
}
Note: I've added ec2 permissions to try a secondary (simplified) test.
This role has an open trust relationship:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": "sts:AssumeRole"
}
]
}
However, in my client code (browser), I can't connect to IoT and I receive the following error:
WebSocket connection to 'wss://my-endpoint.iot.us-east-1.amazonaws.com/mqtt?X-Amz-Algorithm=AWS4-H…Signature=my-signature' failed: Error during WebSocket handshake: Unexpected response code: 403
Trying a simplified test, I've used EC2, but received another permission error. The code used follows below (used browserify to bundle in browse).
const AWS = require('aws-sdk');
// connect to Lambda to retrieve accessKeyId and secretAccessKey
$.ajax({
method: 'GET',
url: 'my-url',
success: function(res) {
// connect to EC2
AWS.config.update({accessKeyId: res.accessKeyId, secretAccessKey: res.secretAccessKey, region: res.region});
const ec2 = new AWS.EC2();
ec2.describeInstances({}, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
});
Error:
POST https://ec2.us-east-1.amazonaws.com/ 401 (Unauthorized)
Error: AWS was not able to validate the provided access credentials(…) "AuthFailure: AWS was not able to validate the provided access credentials"
Found the error. When connecting with the client, I need to provide the sessionToken created by the assumeRole.
Client code:
// connect to EC2
AWS.config.update({accessKeyId: res.accessKeyId, secretAccessKey: res.secretAccessKey, sessionToken: res.sessionToken, region: res.region});
const ec2 = new AWS.EC2();
ec2.describeInstances({}, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});