amazon-cognito-identity-js
amazon-cognito-identity-js copied to clipboard
Accessing user claims in lambda with AWS_IAM authorizer.
I am able to successfully use Cognito User pool, federated identities , APIG and Lambda. I am using AWS_IAM authoriser on APIG, I get the IAM policy and accordingly I can execute APIs.
I would like to get user claims like email, name in the lambda function. I did request body template mapping in integration request like below.
"email": "$context.authorizer.claims.email", "cognitoIdentityPoolId": "$context.identity.cognitoIdentityPoolId",
I can access cognitoIdentityPoolId in lambda as event.cognitoIdentityPoolId but I dont get any value for email. Am I misiing anything? Will I get $context.authorizer.claims.email in case of AWS_IAM authorizer in APIG or using $context.identity.cognitoIdentityPoolId I need to get user claims?
Thanks in advance.
There's a lot of different places that you could be having trouble, but one that has got me before is whether the attribute (in this case email
) is set to be readable for your application in Cognito.
It's probably handy to know that those authorizer claims are available in the event.request
, no mapping required 😌
exports.handler = function(event, context, callback) {
console.log(event.request.userAttributes.email)
...
}
@iwritesomecode i checked my attributes are set to be readable in cognito application. In my case I am using AWS_IAM authorizer and not cognito pool .. i tried console.log(event.request.userAttributes.email) but I dont get these values in lambda ..
I am getting cognitoIdentityId using $context.identity.cognitoIdentityId but not sure how to get user attributes from here ..
Ahh, my mistake, I wrote that thinking you were authorizing with a Cognito User Pool (read the title 🙄).
Do you get anything at regarding identity in event.request?
@outmarch Did you get any solution? I'm facing the same problem. :(
it would really be nice if the user claims of the federated identity would be wired to the cognito user automagically and be accessible in lambda.
I am authorizing with a Cognito User Pool and am struggling to get the Cognito user in my lambda function.
I'm curious to know if this answer from SO answers your question: https://stackoverflow.com/a/42405528/19020
Thanks @danesparza I actually answered my own question recently and created a modern front-end project that shows exactly how this is done. For the most part the user's comment on that post was right but with the libraries I am using it is a lot easier (depending on what exactly you are trying to do).
Basically you need to have your APIG secured with AWS_IAM like the user says AND you must auth via a Cognito Federated Identity which should hand you back a sessionToken this is what makes the AWS IAM credentials temporary (unless refreshed). Now you have everything you need to auth to your APIG and you don't need any of the extra lambda validation the OP was talking about.
To test this, download the desktop version of postman, toss in your API URI and then under Authorization fill out the 5 fields you need for Sig4 signing. You will see 'event.identity' object in your lambda function is loaded up with properties such as the user
object.
If you want to use the APIG auto-generated SDK it comes build in with a factory that takes the accessKey, secret, and token and signs everything for you. Same with the aws-sdk. You can init the credentials with those three items and it will automatically sign all requests for you with those temp creds. If you want to straight up manually hit your API with window.fetch, request, curl, (insert http client here) you can calculate your own Sig4 (beware, complicated af or use a modern library to do it for you.
Also for the record, while doing my research I noticed that if you want to NOT use AWS_IAM as an APIG authorizer, and you want to use "Cognito Identity Pool Authorizer" which is a fancy new option in the dropdown in APIG you can still get a ton of info on the user in the lambda event if you just pass the JWT gained from a successful Cognito popl auth to the APIG as the Authorization header. Inside that JWT is a lot of attributes which you can customize in your pool settings.
Hope this helps!
@VictorioBerra That event.identity object still will not have the claims like email and sub populated. I see the user value, but it is just a string like "AROJJLLMYRO5HLYRTYULU:CognitoIdentityCredentials".
@deanpeterson I have spent some time here and there in the last 7 days you asked this question trying to work out some way to make this happen. Unfortunately what I have found is that Amazon Cognito is still very much in its infancy and while we have all sorts of information like the user, the identity-id, and several other pieces of identifying information the only real way to get data on the user with only the AWS-SDK is to use AdminGetUser() but the problem with that is, its wants a username. Which is silly because Amazon loads up event.requestContext.identity
with every single piece of information besides that.
What's more is we cant just authenticate them to the pool all over again because we don't have their secrets or tokens so that avenue would not work either.
Amazon should really provide an admin way to get a user with more than just the username. People are currently unable to do things like verify a user exists, what if we wanted to show a bunch of "user profiles" in a web app on our page? We cant even get a listing of users with attributes right now. With Amazon holding the keys to the user pool and heavily restricting access to the data from the outside i'm not sure how else to solve this.
I feel like we are so close because they are going the extra mile here and giving us so much data about the identity of the request automatically just from the access ID, the secret and the session token. I would like to know, at what other points or events do we ever see the cognitoIdentityId again? I suppose if you knew that you could hook into events and store the ID into a custom lookup table for later use. http://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html#cognito-user-pools-lambda-trigger-syntax-post-confirmation
If anyone can solve this I would love to know how they did it. The goal here is to use APIG with AWS_IAM Authorization and then get some usable details about the user in the lambda without additional boilerplate like cracking open an AccessToken or using a custom Authorizer. IMO the AdminGetUser() command simply needs to take one of the IDs we get back in the event.
EDIT: See Gbahaa's answer below for using admin search and filtering on the sub!
Hi, I am not sure if this will help you but it helped me to get the user attributes from a code in Lambda invoked by APIG with AWS_IAM Authorization. (inspired by an answer here )
If you log the event passed to lambda, you will find that event.requestContext.identity.cognitoAuthenticationProvider looks like
cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_xxxxxxx,cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_xxxxxx:CognitoSignIn:SSSSSSSS
The SSSSSSSS is the sub of the user in User Pool. Then, by decoding the string, you can easily get the sub. After that you use it in the filter of listUsers
const provider = event.requestContext.identity.cognitoAuthenticationProvider;
const sub=provider.split(':')[2];
const Params = {
UserPoolId: 'xxxxxxxxx', /* required */
Filter: "sub=\""+ sub + "\"",
Limit: 1
};
cognitoidentityserviceprovider.listUsers(Params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data.Users[0].Attributes); // successful response
});
The data.Users[0].Attributes in the callback will have all your user attributes The result is
[ { Name: 'sub', Value: 'SSSSSSS' },
{ Name: 'address', Value: 'xxxxxxxxi' },
{ Name: 'email_verified', Value: 'true' },
{ Name: 'phone_number_verified', Value: 'false' },
{ Name: 'phone_number', Value: 'xxxxxxxxi' },
{ Name: 'given_name', Value: 'xxxxxxxxi' },
{ Name: 'family_name', Value: 'xxxxxxxxi' },
{ Name: 'email', Value: 'xxxxxxxxi' } ]
Note that you can also filter the returned attributes by using
AttributesToGet: [
'STRING_VALUE',
/* more items */
],
in Params.
Thanks so much @Gbahaa! I didn't realize the sub was hiding in there. This is very useful!
@deanpeterson ^
http://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ListUsers.html
Hey guys, I wanted to jump in here because I've been working on this same problem for a while and I finally have a solution that I'm kind of happy with.
First, here is my problem with the server-side filter-based solution offered above. If your server gets the user's sub
claim and then uses that on the back end to access the Cognito User Pool in order to pull the user's attributes, what happens when you're using an identity provider that's not the Cognito User Pool (e.g. Google. FB, Twitter, etc.)? If someone logs in via Google, you still want their email address and whatever other claims you need. So what then?
The other problem I have with this is that it seems like duplicate effort. If you decode the access token that the client gets from Cognito on logging in successfully, it has all the claims you need. So if all the claims are there on the client, why send a single claim (i.e. the sub
attribute) back up to the server and then look up those same claims again? It's duplicate effort.
So the real answer, for me at least, is to get the claims on the client and then just send them up to the server with each request. If you're using HTTPS (and you are using HTTPS, right?) then this isn't a problem. There are two ways to do this.
One way is to take the access token you got and put it into a header, and pass that up and decode it on the server side with jwt-decode
. That's the solution offered in an answer for this SO post, and I've tried it and it works pretty well.
However, I spent some time with this example from Amazon, which I'm sure you've all looked at as well, and I realized something: this example is sending the claims (email
, surname
, and username
from what I recall) right up to APIG by attaching them directly to every POST request body.
So the flow goes something like this:
- The user logs into Google or whatever IDP on the client, and gets an access token from that provider with some claims.
- The client attaches those claims directly to the root of the JSON object that makes up the request body.
- An APIG request mapping template pulls them off the request body and sends them to the Lambda as part of the payload.
- Now those claims, like
email
andusername
, are available in the Lamba right on theevent
object!
Regarding step 3, look at the request mapping template he's using:
#set($inputRoot = $input.path('$'))
{
"operation": "create",
"payload": {
"Item" : {
"userId" : "$context.identity.cognitoIdentityId",
"name" : "$inputRoot.name",
"surname" : "$inputRoot.surname",
"email" : "$inputRoot.email",
"provider": "Google"
}
}
}
The first line of this template declares $inputRoot
as an object version of the whole request body, so that you can call its various fields like $inputRoot.myFieldName
. Then when it's putting together this payload
object it pulls in the claims name
, surname
, and email
right from the main request body.
I've tried this, and it works! I've not done it with a third-party IDP, but it's working well enough with the Cognito User pool
So to wrap up, the approach of looking up the claims on the server side is all wrong IMO. You already have all the claims you need on the client, which you got when the user logged in. You just need to send them up to the server securely as part of the request, by sending them as a header or as part of the request body over HTTPS.
There is one question mark in my mind about the above, tho. The SO answer I linked is pretty paranoid and suggests you encrypt the token and so on. What the AWS engineer is doing seems less paranoid -- he's just sticking the claims right onto the body and sending that stuff up over HTTPS. I'm assuming that he knows what he's doing and that this is ok, especially since the claims aren't super sensitive (i.e. there's no password there, and if you intercepted those claims you wouldn't be able to use them to do anything without a password.)
Anyway, I hope this helps someone.
Edit: To flesh this out with some code, here is what I'm doing on the client (this is extracted from a React/Relay app's fetch query):
import jwt_decode from 'jwt-decode';
const token = getUserTokenHoweverYouDoIt();
if (token) {
const claims = jwt_decode(token);
body.username = claims['cognito:username']
body.email = claims.email
body.sub = claims.sub
}
invokeApig({
method,
path,
headers,
body
})
.then(response => response.json())
.then((data) => {
if (data.errors) {
throw data.errors.map(({ message }) => message)
}
return data
})
.catch(err => console.log(err))
On the APIG side, I'm using the LAMBDA
integration but not LAMBDA_PROXY
so that I can use custom request mapping templates. Here is the template I'm using, which is just a variant on the default one that the serverless framework uses:
#set( $body = $input.json("$") )
#set( $inputRoot = $input.path("$") )
#define( $loop )
{
#foreach($key in $map.keySet())
#set( $k = $util.escapeJavaScript($key) )
#set( $v = $util.escapeJavaScript($map.get($key)).replaceAll("\\'", "'") )
"$k":
"$v"
#if( $foreach.hasNext ) , #end
#end
}
#end
{
"body": $body,
"method": "$context.httpMethod",
"principalId": "$context.authorizer.principalId",
"stage": "$context.stage",
"idpAttributes": {
"email": "$inputRoot.email",
"username": "$inputRoot.username",
"sub": "$inputRoot.sub"
},
#set( $map = $input.params().header )
"headers": $loop,
#set( $map = $input.params().querystring )
"query": $loop,
#set( $map = $input.params().path )
"path": $loop,
#set( $map = $context.identity )
"identity": $loop,
#set( $map = $stageVariables )
"stageVariables": $loop
}
So in my lambda function handler, I can access event.idpAttributes
and get the claims that the client has sent over (i.e. event.idpAttributes.username
or event.idpAttributes.email
, etc.)
This all seems a bit dubious - what about untrustworthy clients?
If we identify the client based on a token on the server, then they can't make random claims about who they are, but if we just use the token to authorize them, and then trust the client to send ok values for "email" for example, they could potentially send a damaging request.
(I could have the wrong end of the stick though)