MQTT connection for users authenticated via Cognito Identity Pool - javascript

Shortly: I have a Federated Identity Pool which has roles for both unauthenticated and authenticated access. I have no problems with unauthenticated access. But when it comes to authenticated access, the user logs in just fine, but my role for the authenticated users does not get actually applied.
I have an s3 bucket with simple index.html and index.js files which communicate via MQTT.
Both policies for authenticated and unauthenticated users look exactly the same right now, and are fairly permissive (of course it's not the way to go for production, but I'm just trying to make it work in any way so far). So, both policies look as follows:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
]
}
And in index.js I can establish MQTT connection as an unauthenticated user, as follows:
var region = 'eu-west-1';
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: 'my-identity-pool-id',
});
AWS.config.credentials.clearCachedId();
AWS.config.credentials.get(function(err) {
console.log('accessKeyId:', AWS.config.credentials.accessKeyId);
if(err) {
console.log(err);
return;
}
var requestUrl = SigV4Utils.getSignedUrl(
'wss',
'data.iot.' + region + '.amazonaws.com',
'/mqtt',
'iotdevicegateway',
region,
AWS.config.credentials.accessKeyId,
AWS.config.credentials.secretAccessKey,
AWS.config.credentials.sessionToken
);
initClient(requestUrl);
});
initClient() just establishes MQTT connection by means of Paho:
function initClient(requestUrl) {
var clientId = String(Math.random()).replace('.', '');
var rpcId = "smart_heater_" + String(Math.random()).replace('.', '');
var client = new Paho.MQTT.Client(requestUrl, clientId);
var connectOptions = {
onSuccess: function () {
console.log('connected');
// Now I can call client.subscribe(...) or client.send(...)
},
useSSL: true,
timeout: 3,
mqttVersion: 4,
onFailure: function (err) {
console.error('connect failed', err);
}
};
client.connect(connectOptions);
client.onMessageArrived = function (message) {
console.log("msg arrived: " + message);
};
}
And it works just fine: connected gets printed to the console, and I can actually send/subscribe.
Now, I'm trying to do the same for authenticated users. For that, I added Cognito User Pool, created a user there, created an "app", added Authentication provider "Cognito" to my Identity Pool (with appropriate User Pool ID and App Client ID), and the authentication itself works just fine: user gets logged in. Code looks as follows:
var region = 'eu-west-1';
var poolData = {
UserPoolId: 'eu-west-1_XXXXXXXXX',
ClientId: 'ZZZZZZZZZZZZZZZZZZZZZZZZZ',
};
var userPool = new AWSCognito.CognitoIdentityServiceProvider.CognitoUserPool(poolData);
var authenticationData = {
Username: 'myusername',
Password: 'mypassword',
};
var authenticationDetails = new AWSCognito.CognitoIdentityServiceProvider.AuthenticationDetails(authenticationData);
var userData = {
Username: 'myusername',
Pool: userPool
};
var cognitoUser = new AWSCognito.CognitoIdentityServiceProvider.CognitoUser(userData);
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: function (result) {
console.log('result:', result);
console.log('access token: ' + result.getAccessToken().getJwtToken());
var myUserPoolId = 'eu-west-1_XXXXXXXXX';
console.log('You are now logged in.');
// Add the User's Id Token to the Cognito credentials login map.
var logins = {};
logins['cognito-idp.' + region + '.amazonaws.com/' + myUserPoolId] = result.getIdToken().getJwtToken();
AWS.config.credentials.params.Logins = logins;
// finally, expire the credentials so we refresh on the next request
AWS.config.credentials.expired = true;
//call refresh method in order to authenticate user and get new temp credentials
AWS.config.credentials.refresh((error) => {
if (error) {
console.error(error);
} else {
console.log('Successfully logged!');
console.log('accessKeyId:', AWS.config.credentials.accessKeyId);
var requestUrl = SigV4Utils.getSignedUrl(
'wss',
'data.iot.' + region + '.amazonaws.com',
'/mqtt',
'iotdevicegateway',
region,
AWS.config.credentials.accessKeyId,
AWS.config.credentials.secretAccessKey,
AWS.config.credentials.sessionToken
);
initClient(requestUrl);
}
});
},
onFailure: function (err) {
alert(err);
},
newPasswordRequired: function(userAttributes, requiredAttributes) {
// User was signed up by an admin and must provide new
// password and required attributes, if any, to complete
// authentication.
// the api doesn't accept this field back
delete userAttributes.email_verified;
var newPassword = prompt('Enter new password ', '');
// Get these details and call
cognitoUser.completeNewPasswordChallenge(newPassword, userAttributes, this);
},
});
The logging-in part wasn't trivial to get working, but now it works fine: in the console, I see:
You are now logged in.
Successfully logged!
accessKeyId: ASIAIRV4HOMOH6DXFTWA
And then, at the connection time, I'm getting the following error:
connect failed: {invocationContext: undefined, errorCode: 8, errorMessage: "AMQJS0008I Socket closed."}
I assume this is because the role, which is actually applied to the authenticated user, does not allow the action iot:Connect; I believe so because I can reproduce exactly the same error for unauthenticated users, if I put the following in the policy for unauthenticated users:
{
"Action": [
"iot:Connect"
],
"Resource": "*",
"Effect": "Deny"
},
Then unauthenticated users will receive the same error AMQJS0008I Socket closed when they try to connect.
So, it looks like my policy for authenticated users does not actually get applied to the authenticated user.
I experimented a lot before writing this question. Currently, in my Identity Pool settings, in "Authenticated role selection" I have just "Use default role", which should indeed pick my authenticated role, which is:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
]
}
But I also tried to select "Choose role with rules" and write a rule which would set my role based on some matching clause.
I also tried to create a group in my User Pool, use the same role there, add my user to this group, and in the Identity Pool settings set "Choose role from token".
Nothing helped. I keep getting this errorMessage: "AMQJS0008I Socket closed." message for authenticated users, whereas for unauthenticated users everything works just fine, even though the policies are identical.
Any help is appreciated.

The problem was that, for authenticated Cognito users, attaching IAM policy to the Identity Pool is not enough: in addition to that, one must attach an IoT policy (not IAM policy) to each identity (basically, to each user), like this:
$ aws iot attach-principal-policy \
--policy-name Some-Policy \
--principal us-east-1:0390875e-98ef-420d-a52d-f4188ce3cf06
Check also this thread https://forums.aws.amazon.com/thread.jspa?messageID=726121

Related

Need help for cognito authentication flow with MFA to remember the device while challenging for MFA

I am using aws cognito service for authentication, followed https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js this documentation.
My login flow is something like this:
import {AuthenticationDetails,CognitoUser,CognitoUserPool} from 'amazon-cognito-identity-js';
const authenticationData = {
Username: <username>,
Password: <password>
};
const authenticationDetails = new AuthenticationDetails(
authenticationData
);
const userData = {
Username: <username>,
Pool: <userPoolId>,
};
const cognitoUser = new CognitoUser(userData);
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: (result) => {
const accessToken = result.getAccessToken().getJwtToken();
console.log('access token --> ', accessToken);
},
onFailure: (err) => {
console.log("error",err.message);
},
mfaRequired: function (codeDeliveryDetails) {
var verificationCode = prompt('Please input verification code', '');
cognitoUser.sendMFACode(verificationCode, this);
},
})
I've already created one AWS Cognito UserPool and the enabled MFA for the same.
In signin flow is working and the user is authenticated by multi-factor-authentication and I'm getting the access token.
But now I want not to verify mfa for the same device multiple times. Something like remember the device for the specific user for new 10-15 logins or next 7-15 days.
I checked the docs and seemed I can achieve this with aws amplify.
async function rememberDevice() {
try{
const result = await Auth.rememberDevice();
console.log(result)
}catch (error) {
console.log('Error remembering device', error)
}
}
But aws amplify is not working for me as it has an open issue.
https://github.com/aws-amplify/amplify-js/issues/9204
In my cognito user pool I tried to set this one, but it's not helping as well.
Is there any other option available in Cognito api to achieve my requirement?

Cognito getId: NotAuthorizedException: Invalid login token. Not a valid OpenId Connect identity token

I am new to Cognito (JWT tokens & whole auth thing in general) so pardon me for asking stupid questions. I am trying to use Cognito user pools with identity pools. I logged in a user using the default URL (https://testapi123.auth.us-east-2.amazoncognito.com/login?response_type=token&client_id=<>&scope=openid&redirect_uri=https://aws.amazon.com) and got a token. I am unable to figure out how to get the identity id from this token for use in getId() API. This I want to later use to get credentials from the federated identity pool (not sure if i have that part right either).
For reference, i have my code -
var AWS = require('aws-sdk');
var cognitoidentity = new AWS.CognitoIdentity({apiVersion: '2014-06-30'});
exports.handler = (event, context, callback) => {
// TODO implement
var params = {
IdentityPoolId: 'us-east-2:<xxxxxx>', /* required */
AccountId: 'xxxxxxx',
Logins: {
'cognito-idp.us-east-2.amazonaws.com/us-east-2_xxxxx': '<**identityID???**>',
}
};
cognitoidentity.getId(params, function(err, data) {
if (err) console.log('Error3 : ' + err, err.stack); // an error occurred
else {
console.log('retval:' + JSON.stringify(data)); // successful response
var idenId = data.idenId;
var params = {
IdentityId: idenId,
CustomRoleArn: 'arn:aws:iam::xxxxxxxxx:role/cc_admin',
Logins: {
'CognitoIdentity': 'cognito-idp.us-east-2.amazonaws.com/us-east-2_xxxxxxx'
}
};
cognitoidentity.getCredentialsForIdentity(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
}
});
callback(null, 'Hello from Lambda 1');
};
The error - "2018-06-06T18:59:58.614Z ce0fc216-69bb-11e8-bbfc-4fff0d953dd4 Error3 : NotAuthorizedException: Invalid login token. Token signature invalid. NotAuthorizedException: Invalid login token. Token signature invalid."
I have tried parsing the JWT token received (with jwt.io). It shows me some details but none of them seem to be identity id to be used in the request. I have also tried using the entire token as identity id.
Really need help. Thanks.
First, you only need to pass Identity Pool Id in params for getId function to work. It will return IdentityId associated with your identity pool. Replace
var params = {
IdentityPoolId: 'us-east-2:<xxxxxx>', /* required */
AccountId: 'xxxxxxx',
Logins: {
'cognito-idp.us-east-2.amazonaws.com/us-east-2_xxxxx': '<**identityID???**>',
}
};
with
var params = {
IdentityPoolId: 'us-east-2:<xxxxxx>'
}
Second, you can't create credentials unless you have idToken i.e JWT (JSON Web Token which is returned to client after successful authentication by user pool).
It looks like this..
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
You can get this token only by signing in...Run this command on terminal after configuring aws-cli or use aws-sdk and run function InitiateAuth
aws cognito-idp admin-initiate-auth --user-pool-id ap-south-1_xxxxxxx --client-id AAAAAAAAAAAAAAAAAAAAA --auth-flow ADMIN_NO_SRP_AUTH --auth-parameters USERNAME="abc#123.com",PASSWORD="********"
Finally,Change your params function passing to getCRedentials method 'Logins' key to:
Logins: {
"IdentityId": <result from getId>
"cognito-idp.us-east-2.amazonaws.com/us-east-2_xxxxxxx": <your idToken>
}

How to turn an CognitoAuth auth object into AWS-SDK credentials

I have a cognito userpool and i can successfully log into my app with the following code:
const authData = {
ClientId : '2222222222222', // Your client id here
AppWebDomain : '1111111111.auth.us-east-1.amazoncognito.com',
TokenScopesArray : ['openid'],
RedirectUriSignIn : 'https://app.domain.com',
RedirectUriSignOut : 'https://app.domain.com'
};
const CognitoAuth = AmazonCognitoIdentity.CognitoAuth;
const auth = new CognitoAuth(authData);
auth.userhandler = {
/**onSuccess: <TODO: your onSuccess callback here>,
onFailure: <TODO: your onFailure callback here>*/
onSuccess: function(result: any) {
console.log("COGNITO SUCCESS!");
console.log(result);
},
onFailure: function(err: any) {
console.log("COGNITO FAIL!");
console.log(err);
}
};
auth.getSession();
const curUrl = window.location.href;
auth.parseCognitoWebResponse(curUrl);
I now have an auth object that I would like to parlay into some sort of credentials for the aws-sdk that i have so that i can list items into an S3 bucket, assuming the correct policies in my attached roles.
something like this, but realize this doesn't work:
AWS.config.credentials = auth.toCredentials(); //<== hoping for magic
const s3 = new AWS.S3();
s3.listObjectsV2(listObjectsV2Params, function(err: any, data: any) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data.Contents[0]); // successful response
});
Is this possible, and if so how do i do that?
UDPATE
Accepted answer worked and was a big help, adding some additions for clarity along the lines of trouble I ran into.
const creds = new AWS.CognitoIdentityCredentials({
IdentityPoolId: 'us-east-1:b111111-1111-1111-1111-1111111', // <-- This is in your Federated Identity if you have that set up, you have to "edit" the identity pool to get it, logging into cognito its a different screen.
Logins: {
"cognito-idp.us-east-1.amazonaws.com/us-east-1_BBBB1BBBBV2B": result.idToken.jwtToken // <- this login [POOL ID] is not the pool ARN, you need it in this format.
}
});
this link helped me.
This is possible and following are the steps.
To allow AWS resources access for AWS Userpool Users, it also requires to configure AWS Identity Pools registering the UserPool as a provider.
The IAM Role assigned for the authenticated user needs to have access to S3.
Using the AWS SDK for Identity Pools, UserPools JWT token can be exchanged for temporal AccessKey and SecretKey to use AWS SDK for S3.
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: IDENTITY_POOL_ID,
Logins: {
[USER_POOL_TOKEN]: result.idToken.jwtToken
}
});
AWS.config.credentials.refresh((error) => {
if (error) {
console.error(error);
} else {
console.log('Successfully logged!');
}
});
Inside the AWS.config.credentials.refresh callback you can call S3 since, the method internally will handle getting temporal credentials.

Retrieve the access token, secret access key and session token from aws cognito

AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: 'us-east-1:2ce7b2c2-898f-4a26-9066-d4feff8ebfe4'
});
// Make the call to obtain credentials
AWS.config.credentials.get(function(){
// Credentials will be available when this function is called.
var accessKeyId = AWS.config.credentials.accessKeyId;
var secretAccessKey = AWS.config.credentials.secretAccessKey;
var sessionToken = AWS.config.credentials.sessionToken;
//var identityId = AWS.config.credentials.identityId;
return res.send({
accessKeyId: accessKeyId
});
});
All of the variables have null value. Why? What am i doing wrong? Is there another way to access it?
Also i am supposed to send a secretkey and token to retrieve a session key
UPDATE:
When i try this method, I get an error saying:
Error: NotAuthorizedException: Unauthenticated access is not supported for this identity pool.
AWS.config.credentials.get(function(err) {
if (err) {
console.log("Error: "+err);
return;
}
console.log("Cognito Identity Id: " + AWS.config.credentials.identityId);
// Other service clients will automatically use the Cognito Credentials provider
// configured in the JavaScript SDK.
var cognitoSyncClient = new AWS.CognitoSync();
cognitoSyncClient.listDatasets({
IdentityId: AWS.config.credentials.identityId,
IdentityPoolId: ""
}, function(err, data) {
if ( !err ) {
console.log(JSON.stringify(data));
}
return res.send({
data: data
});
});
});
The exception you are seeing means that you have not set up your identity pool to allow unauthenticated identities.
You are not passing any logins in the logins map when you call get credentials, which means your user is un authenticated (which is not allowed by your identity pool).
Here is some documentation describing how to authenticate using external identity providers:
http://docs.aws.amazon.com/cognito/latest/developerguide/external-identity-providers.html

The security token included in the request is invalid. aws js sdk

I posted here on the AWS forum
I'm using the aws-js-sdk v2.2.3 with the following code. I get data back with Credentials populated. When I try to use the credentials I get the error that they are invalid. I'm using the developer authenticated identities flow. I have both roles Auth & UnAuth. My identity pool looks like it's correct. The trust relationships look like they are pointing to the correct identity pool id. There are policies attached to the Auth role for S3 & DynamoDB. I'm at a loss. Any help would be appreciated.
javascript client side:
var cognitoidentity = new AWS.CognitoIdentity({region: 'us-east-1'});
var params = {
IdentityId: user.cognito_id,
Logins: {
'cognito-identity.amazonaws.com': user.cognito_token
}
};
cognitoidentity.getCredentialsForIdentity(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data.Credentials);
});
I console.log the Id & SecretKey and they are filled in.
var aws_creds = StateService.get('user').aws_creds;
console.log(aws_creds.AccessKeyId);
console.log(aws_creds.SecretKey);
AWS.config.update({ accessKeyId: aws_creds.AccessKeyId,
secretAccessKey: aws_creds.SecretKey,
endpoint: ENV.aws_dyndb_endpoint,
region: 'us-east-1'
});
var dynamodb = new AWS.DynamoDB();
console.log("user obj: ", StateService.get('user'));
var params = {
TableName: games_table_name,
KeyConditionExpression: "Id = :v1",
ExpressionAttributeValues: {
":v1": {"N": id}
}
};
return dynamodb.query(params);
My Solution
What I came up with was to explicitly refresh the credentials versus get them lazily when I created a DynamoDb object for instance. Here's the function I use which returns a promise & resolves when the credentials are refreshed.
refresh: function() {
var deferred = $q.defer();
AWS.config.region = 'us-east-1';
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: COGNITO_IDENTITY_POOL_ID,
IdentityId: COGNITO_ID,
Logins: 'cognito-identity.amazonaws.com'
});
AWS.config.credentials.refresh(function(error) {
if ((error === undefined) || (error === null)) {
$log.debug("Credentials Refreshed Success: ", AWS.config.credentials);
var params = {
region: 'us-east-1',
apiVersion: '2012-08-10',
credentials: AWS.config.credentials
};
$rootScope.dynamodb = new AWS.DynamoDB({params: params});
deferred.resolve();
}
else {
$log.debug("Error refreshing AWS Creds:, ", error);
deferred.reject(error);
}
});
return deferred.promise;
}
If you want to use Cognito credentials to call other AWS services, I recommend you use the high-level AWS.CognitoIdentityCredentials object from the Javascript SDK, instead of calling the service API directly.
You can find more information about how to initialize and use AWS.CognitoIdentityCredentials in the Cognito Developer Guide:
Developer Authenticated Identities
Albert
The flow is like this: You ask the CognitoIdentityCredentials for a IdentityId, the IDentityId is supposed to track users accross devices and across Identities providers like (Facebook, Google, TWitter, etc.) then you with that ID you ask for a role attached to your pole CognitoIdentity, after you get the token, you ask the STS.assumeRoleWithWebIdentity for a temporary credentials with the appropriate roles attached to your pole.
Here is an example of how I did it:
// set the Amazon Cognito region
AWS.config.region = 'us-east-1';
// initialize the Credentials object with our parameters
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: 'us-east-1:YMIDENTITYPOLEID',
});
// We can set the get method of the Credentials object to retrieve
// the unique identifier for the end user (identityId) once the provider
// has refreshed itself
AWS.config.credentials.get(function(err) {
if (err) {
console.log("Error: "+err);
return;
}
console.log("Cognito Identity Id: " + AWS.config.credentials.identityId);
params = {
IdentityId: AWS.config.credentials.identityId
}
// Other service clients will automatically use the Cognito Credentials provider
// configured in the JavaScript SDK.
// Get the Role associated with the id coming from the pool
var cognitoidentity = new AWS.CognitoIdentity();
cognitoidentity.getOpenIdToken(params, function(err, data) {
if (err){
console.log(err, err.stack); // an error occurred
}else{
// Get temporoarly credientials form STS to access the API
var params = {
RoleArn: 'ROLE_OF_YOUR_POLE_ARN', /* required */
RoleSessionName: 'WHATEVERNAME', /* required */
WebIdentityToken: data.Token, /* required */
};
var sts = new AWS.STS()
console.log(data); // successful response
console.log(data.Token)
sts.assumeRoleWithWebIdentity(params, function(err, data) {
if (err){
console.log(err, err.stack); // an error occurred
}else{
console.log(data); // successful response
// Now we need these credentials that we got for this app and for this user
// From here we can limit the damage by
// Burst calling to the API Gateway will be limited since we now that this is a single user on a single device
// If suspicious activities we can drop this user/device
// The privileges are limited since the role attached to this is only the API GateWay calling
// This creds are temporary they will expire in 1h
var apigClient = apigClientFactory.newClient({
accessKey: data.Credentials.AccessKeyId,
secretKey: data.Credentials.SecretAccessKey,
sessionToken: data.Credentials.Token, //OPTIONAL: If you are using temporary credentials you must include the session token
region: AWS.config.region // OPTIONAL: The region where the API is deployed, by default this parameter is set to us-east-1
});
// Call the get to test
apigClient.deviceGet({}, {})
.then(function(result){
//This is where you would put a success callback
console.log(result)
}).catch( function(result){
//This is where you would put an error callback
});
}
});
}
});
});
NB: This was a test to get access to the API Gateway service, but it is not different to get access to other services, it depends on the pole you configure it and its attached services.
If you have credential for a user created in IAM you don't need the temporary token, but if you use this flow you have to include it.
Another point, limit the access to the services on your pole, keep in mind that is a publicly given key, every one can use it to get access to your stuff.
STS.assumeRoleWithWebIdentity is used because we are on the web, in the AWS JS SDK, if you use iOS or android/java or Boto, you have to use STS.assumeRole.
Hope this helps.

Categories