How to attach a policy to every Cognito identity - aws-lambda

I'd like to attach an IoT policy to the Cognito identities given to the federated users of my app. I'm tryng to do this with a Lambda function in the Post confirmation trigger of my user pool. Here's my function so far:
const AWS = require('aws-sdk');
const iot = new AWS.Iot();
exports.handler = async function(event, context) {
const policyName = 'arn:aws:iam::XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
const target = context.identity.cognitoIdentityId;
await iot.attachPolicy({ target, policyName }).promise();
const response = {
statusCode: 200,
body: JSON.stringify('Policy attached.'),
};
return response;
};
When this function runs I get an error:
"cannot read property 'cognitoidentityid' of undefined"
Similar error if I define principal as
const principal = context.cognito_identity_id; //error: "Missing required key 'target' in params"
According to the Iot docs, "The context object in your Lambda function contains a value for context.cognito_identity_id when you call the function with AWS credentials that you obtain through Amazon Cognito Identity pools." Can anyone tell me how to do that?
I should add that I would like to attach this policy for both desktop and mobile users.The Lambda docs imply that the identity property of the context object is provided for mobile apps only. If that is true then is there a different way to attach the IoT policy to all Cognito identites, mobile and desktop alike?
Thanks

To sum up, You will not be able to get identity id in Cognito's post confirmation trigger.
To overcome it, Client can invoke separate Lambda function (once user is confirmed) and in that Lambda you can attach policy, because here you will get identity id.
Other alternative which I don't prefer is client themselves attach a policy after user confirmation.
Attach Policy API - https://docs.aws.amazon.com/iot/latest/apireference/API_AttachPolicy.html

Related

Getting an error while retrieving a blob using user assigned managed identity

We have a C# code which used to retrieve a blob from storage account. The authentication is done using user assigned service principal. These things works till December. But now we are getting some weird error as follows.
ManagedIdentityCredential authentication unavailable. The requested identity has not been assigned to this resource.
Status: 400 (Bad Request)
Content:
{"error":"invalid_request","error_description":"Identity not found"}
The managed identity has storage data blob contributor access in the storage account.
Attaching the code for reference:
public static async Task<string> GetBlobAsync()
{
string storageName = "storage account name";
Uri blobUri = new Uri("blob uri");
TokenCredential cred = new ManagedIdentityCredential("client id");
var blobClient = new BlobClient(blobUri, cred, null);
try
{
var downloadInfo = await blobClient.DownloadAsync();
using (TextReader reader = new StreamReader(downloadInfo.Value.Content))
{
string metadataBlob = await reader.ReadToEndAsync();
return metadataBlob;
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
Console.WriteLine("");
return null;
}
P:S: the three environmental variables such as app id, app secret and tenant id are correct.
I have been stuck here for almost a month. Nothing works.
This document demonstrates how to use managed identity to access App Configuration from App Service, but you can replace the App Service with any other Azure services that support managed identity. https://learn.microsoft.com/en-us/azure/azure-app-configuration/howto-integrate-azure-managed-service-identity
Here are a few things I'd like to call out
Make sure the managed identity is enabled in the Azure service where your application runs.
When you are using system assigned managed identity, you don't need to provide the client Id. You only need to provide the client Id when you use user assigned managed identity.
Make sure the managed identity is granted either App Configuration Data Reader or App Configuration Data Owner role in the access control of your App Configuration instance.
Wait for at least 15 minutes after the role assignment for the permission to propagate.
Managed identity can ONLY work when your code is running in the Azure service. It will NOT work when running locally
Try this
Uri blobUri = new Uri("blob uri");
var cred = new DefaultAzureCredential(
new DefaultAzureCredentialOptions {
ManagedIdentityClientId = "your client id" });
var blobClient = new BlobClient(blobUri, cred, null);
ref: https://learn.microsoft.com/pt-br/dotnet/api/azure.identity.defaultazurecredential?view=azure-dotnet
Option 2 (work for me)
Create Identity and add in app service
Assign RBAC "Storage Blob Data Contributor" to your storage resource.
Add Key AZURE_CLIENT_ID (clientid of the identity that was created) in Environment App Service
Code to access blob
(you don't need to specify client id in the code because it will use the AZURE_CLIENT_ID configured in the AppService)
app.MapGet("/read", async () =>
{
Uri blobUri = new Uri("https://xxxx.blob.core.windows.net/texts/text.txt");
var cred = new DefaultAzureCredential();
var blobClient = new BlobClient(blobUri, cred, null);
var downloadInfo = await blobClient.DownloadAsync();
using (TextReader reader = new StreamReader(downloadInfo.Value.Content))
{
string metadataBlob = await reader.ReadToEndAsync();
return metadataBlob;
}
});
Result print

How to get the result of a payable transaction using near-api-js?

When calling a contract method with attached deposits, you are redirected to the NEAR wallet for approving the transaction. How can the contract frontend app get the result of the transaction after returning from the wallet?
I've faced the same problem. For this moment near-api set transaction info in the browser url. So you get the transaction hash from url after returning from the wallet. Then using transaction hash get info about it using near-api-js:
const { providers } = require("near-api-js");
//network config (replace testnet with mainnet or betanet)
const provider = new providers.JsonRpcProvider(
"https://archival-rpc.testnet.near.org"
);
const TX_HASH = "9av2U6cova7LZPA9NPij6CTUrpBbgPG6LKVkyhcCqtk3";
// account ID associated with the transaction
const ACCOUNT_ID = "sender.testnet";
getState(TX_HASH, ACCOUNT_ID);
async function getState(txHash, accountId) {
const result = await provider.txStatus(txHash, accountId);
console.log("Result: ", result);
}
Documentation: https://docs.near.org/docs/api/naj-cookbook#get-transaction-status
There are 2 options:
Use provider.txStatus like Tom Links said. But the cons : we only know transaction success or fail but not the response from smart contract.
Seperate deposit api and actions api -> User must deposit before call actions api, so we can read the response.

windows form calling graph api, Server error: User lookup by user id failed in AAD

My goal is to call graph api using some workflows. Right now I am trying to practice using a windows form. I registered the app with Azure and got appId, TenentId and Secret.
new GraphServiceClient(
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
IConfidentialClientApplication client = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantId)
.WithClientSecret(clientSecret)
.Build();
var result = await client.AcquireTokenForClient(scopes).ExecuteAsync();
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer",
result.AccessToken);
}));
When I call:
newonlineMeeting = await graphServiceClient.Me.OnlineMeetings
.Request()
.AddAsync(onlineMeeting);
I got "User lookup by user id failed in AAD" error. Do I still need to use username and password to login first? Please help.
Thanks
Holly

Get user attributes from Cognito User Pool through lambda

I've created a lambda that retrieves user attributes as (username, email, name...etc) however, I wonder how it's possible to get user attributes without explicitly hardcoding sub value to get all other related attributes? do I need to decode JWT Cognito token in frontend and use it in the lambda to determine the correct user and retrieve the related attributes?
here is my lambda in Node.JS:
const AWS = require('aws-sdk');
exports.handler = function(event, context) {
var cog = new AWS.CognitoIdentityServiceProvider();
var filter = "sub = \"" + "UserSUB" + "\"";
var req = {
"Filter": filter,
"UserPoolId": 'POOL here',
};
cog.listUsers(req, function(err, data) {
if (err) {
console.log(err);
}
else {
if (data.Users.length === 1){
var user = data.Users[0];
var attributes = data.Users[0].Attributes;
console.log(JSON.stringify(attributes));
} else {
console.log("error.");
}
}
});
}
I think the proper way to do this depends on whether you want to use API Gateway or not (It will make things simpler IMHO).
If you don't want to use APIG, and you are calling the lambda directly using temporary credentials, then you should pass the entire ID token and have the lambda do all of the validation and decoding (probably using a third party library for JWTs). It's not safe to do it in the frontend as that would mean you have a lambda that blindly accepts the attributes as facts from the frontend, and a malicious user could change them if they wanted.
If you are using API Gateway to put lambdas behind an API then I would create a cognito authorizer based on the User Pool, create a resource/method and configure it to use the authorizer, and enable Use Lambda Proxy Integration for the Integration Request. All the token's claims enabled for the client will be passed through on event.requestContext.authorizer.claims so long as it's valid.
There are some AWS docs here, although this does not use proxy integration. If you use proxy integration then you can skip 6b as the APIG will set the values for you. This is described in an answer here.

AWS Lambda API gateway with Cognito - how to use IdentityId to access and update UserPool attributes?

OK I am now days into this and have made significant progress but am still completely stumped about the fundamentals.
My application uses Cognito User Pools for creating and managing users - these are identified on S3 it seems by their IdentityId. Each of my users has their own S3 folder, and AWS automatically gives them a folder name that is equal to the user's IdentityId.
I need to relate the IdentityId to the other Cognito user information but cannot work out how.
The key thing I need is to be able to identify the username plus other cognito user attributes for a given IdentityId - and it's insanely hard.
So the first battle was to work out how to get the IdentityId when a Cognito user does a request via the AWS API Gateway. Finally I got that worked out, and now I have a Cognito user, who does a request to the API Gateway, and my Lambda function behind that now has the IdentityId. That bit works.
But I am completely stumped as to how to now access the Cognito user's information that is stored in the user pool. I can't find any clear information, and certainly no code, that shows how to use the IdentityId to get the Cognito user's attributes, username etc.
It appears that if I use a "Cognito user pool" to authorize my method in API Gateway, then the body mapping template can be used to put Cognito User information such as the sub and the username and email address into the context, BUT I do NOT get the IdentityId.
BUT if I use the AWS_IAM to authorize my method in the API gateway then the body mapping template does the inverse - it gives me the IdentityId but not the Cognito user fields such as sub and username and email.
It's driving me crazy - how can I get the IdentityId and all the Cognito users fields and attributes together into one data structure? The fact that I seem to be only able to get one or the other just makes no sense.
It turns out that to get the IdentityId AND user details at the same time using AWS Lambda/Cognito/API Gateway, you need to have a Lambda function that is authenticated using AWS_IAM (NOT COGNITO_USER_POOLS), you must send your request the AWS API Gateway, BUT it MUST be a signed request, you must then modify the integration request body mapping templates so that you are given the IdentityId in the event (maybe the context? can't remember). Now you have the IdentityId. Phew. Now you must submit the client's Cognito ID token from the front end to the back end. It is important to validate the token - you cannot trust that it has not been tampered with if you do not validate it. To decode and validate the token you must get the keys from your userpool, put them into your script, ensure that you have jwt decoding libraries plus signature validation libraries included in your AWS lambda zipfile. Now your script must validate the token submitted from the front end and then you can get the user details out of the token. Voila! Now you have both IdentityId plus user details such as their sub, username and email address. So easy.
The above is what is takes to get the username associated with an IdentityId using AWS Cognito/Lambda/API Gateway. This took me days to get working.
Can I please say to any Amazon employees who wander across this ........ well it's WAY too hard to get the user details associated with an IdentityId. You need to fix this. It made me angry that this was so hard and burned so much of my time.
The solution:
I did this by modifying an Amazon employees custom authorizer here:
https://s3.amazonaws.com/cup-resources/cup_custom_authorizer_lambda_function_blueprint.zip
as found and described here:
https://aws.amazon.com/blogs/mobile/integrating-amazon-cognito-user-pools-with-api-gateway/
use strict';
let util = require('util');
var jwt = require('jsonwebtoken');
var jwkToPem = require('jwk-to-pem');
var userPoolId = 'YOUR USERPOOL ID';
var region = 'YOUR REGION'; //e.g. us-east-1
var iss = 'https://cognito-idp.' + region + '.amazonaws.com/' + userPoolId;
//https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html
// DOWNLOAD FROM https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json
let userPoolKeys = {PUT YOUR DOWNLOADED USER POOL KEYS JSON HERE};
var pems = {};
let convertKeysToPems = () => {
var keys = userPoolKeys['keys'];
for(var i = 0; i < keys.length; i++) {
//Convert each key to PEM
var key_id = keys[i].kid;
var modulus = keys[i].n;
var exponent = keys[i].e;
var key_type = keys[i].kty;
var jwk = { kty: key_type, n: modulus, e: exponent};
var pem = jwkToPem(jwk);
pems[key_id] = pem;
}
}
exports.handler = function(event, context) {
convertKeysToPems()
console.log(event);
let token = event['body-json'].cognitoUserToken;
console.log(event['body-json'].cognitoUserToken);
ValidateToken(pems, event, context, token);
};
let ValidateToken = (pems, event, context, token) => {
//Fail if the token is not jwt
var decodedJwt = jwt.decode(token, {complete: true});
console.log(decodedJwt)
if (!decodedJwt) {
console.log("Not a valid JWT token");
context.fail("Unauthorized");
return;
}
//Fail if token is not from your UserPool
if (decodedJwt.payload.iss != iss) {
console.log("invalid issuer");
context.fail("Unauthorized");
return;
}
//Reject the jwt if it's not an 'Access Token'
if (decodedJwt.payload.token_use != 'id') {
console.log("Not an id token");
context.fail("Unauthorized");
return;
}
//Get the kid from the token and retrieve corresponding PEM
var kid = decodedJwt.header.kid;
var pem = pems[kid];
if (!pem) {
console.log(pems, 'pems');
console.log(kid, 'kid');
console.log('Invalid token');
context.fail("Unauthorized");
return;
}
//Verify the signature of the JWT token to ensure it's really coming from your User Pool
jwt.verify(token, pem, { issuer: iss }, function(err, payload) {
if(err) {
context.fail("Unauthorized");
} else {
let x = decodedJwt.payload
x.identityId = context.identity.cognitoIdentityId
//let x = {'identityId': context['cognito-identity-id'], 'decodedJwt': decodedJwt}
console.log(x);
context.succeed(x);
}
});
}
This problem -- the problem of using the user's sub instead of their identityId in S3 paths and how to set that up -- is 100% solved for me thanks to the help of #JesseDavda's solution to this problem in this issue: https://github.com/aws-amplify/amplify-js/issues/54
For all of you developers who have been trying to get the identityId in lambdas so that your Amplify default paths in S3 work - this solution simply ends up ignoring identityId altogether - it is a solution that sets up the paths in S3 based on sub instead of the identityId. At the end of this solution, you will never have to deal with more than one id for your users, you will never have to deal with identityId (hopefully) ever again.
If I'm understanding this correctly you want the CognitoIdentityId and the User attributes in the same place. How we do it is the following way:
From the event request context we get the IdentityId:
event.requestContext.identity.cognitoIdentityId
Also from the request context we get the user's sub:
event.requestContext.identity.cognitoAuthenticationProvider.split(':CognitoSignIn:')[1]
Then with the sub you can request the rest of the attributes the following way:
const AWS = require('aws-sdk');
let cognito = new AWS.CognitoIdentityServiceProvider();
let request = {
Username: userSub,
UserPoolId: process.env.userPoolId,
};
let result = await cognito.adminGetUser(request).promise();
const userAttributes = result.UserAttributes.reduce((acc, attribute) => {
const { Name, Value } = attribute;
acc[Name] = Value;
return acc;
}, {});
return userAttributes;

Resources