Serverless my custom authorizer is not working - amazon-web-services

I have the following serverless.yaml:
getSth:
handler: src/handlers/getSth.getSth
events:
- http:
path: getSth
method: get
cors: true
private: true
authorizer: authorizerFunc
authorizerFunc:
handler: src/handlers/authorizer.authorizer
getSth handler:
module.exports.getSth = async (event, context) => {
const response = {
statusCode: 200,
body: JSON.stringify({message: "nice you can call this});
}
return response;
}
authorizerFunc:
module.exports.authorizer = async (event, context) => {
console.log('i will fail your authorization');
let response = {
isAuthorized: false,
context: {
stringKey: "value",
numberKey: 1,
booleanKey: true,
arrayKey: ["value1", "value2"],
mapKey: { value1: "value2" },
},
};
return response;
}
That results in getting respons 200 in spite of the fact authorizer should not allow to execute that getSth function. Also console log 'I will fail your authorization' is not logged.
What am I doing wrong ?

I have tried to analyse your code and find several points where you can start digging.
Private functions
The key private: true actually makes API Gateway require an API key. I did now not try myself but perhaps private: true and an authorizer do not go together.
Strange still that you are able to call the function then. How do you call the function? From the CLI or through API Gateway and an API testing tool such as Postman or Insomnia?
Authorizer Configuration
Your authorizer configuration is definitely correct. We do have the very same setup in our code.
Authorizer Events
An authorizer function gets an APIGatewayTokenAuthorizerEvent in and should reply with a APIGatewayAuthorizerResult. The latter looks closely like an IAM statement and we do not use the field isAuthorized: false as per your example. I do not understand where this field is coming from. Our result to allow a request looks more or less like the following:
{
"principalId": "<our auth0 user-id>",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [{
"Action": "execute-api:Invoke",
"Effect": "Allow",
"Resource": "*"
}]
}
}
Note how the field principalId refers to the username we get from our identity provider (Auth0). And in reality looks something like this: auth0|6f84a3z162c72d0d0d000a00.
Further, we can allow or deny the function call via the Effect field which can hold the values Allow or Deny.
Finally, you can specify which resource the caller is permitted to call. For simplicity of this answer I put * there. Of course in the real world you can pull the ARN of the called function from the event and context and pass that into the policy document.
Opinion
We also had a hard way of figuring this out via documentation from AWS. Of course for AWS the preferred integration would be via AWS Cognito (which I also do prefer due to the more streamlined integration. We benefited quite a bit form the use of TypeScript here which we use to enforce types in and out of our Serverless functions. This way it was rather easy to figure out how the response needs to look like.
Background
We use the custom authorizer integration to allow a user base already existing in Auth0 consume our Serverless based APIs via application clients or single page applications.

Related

Cannot give aws-lambda access to aws-appsync API

I am working on a project where users can upload files into a S3 bucket, these uploaded files are mapped to a GraphQL key (which was generated by Amplify CLI), and an aws-lambda function is triggered. All of this is working, but the next step I want is for this aws-lambda function to create a second file with the same ownership attributes and POST the location of the saved second file to the GraphQL API.
I figured that this shouldn't be too difficult but I am having a lot of difficulty and can't understand where the problem lies.
BACKGROUND/DETAILS
I want the owner of the data (the uploader) to be the only user who is able to access the data, with the aws-lambda function operating in an admin role and able to POST/GET to API of any owner.
The GraphQL schema looks like this:
type FileUpload #model
#auth(rules: [
{ allow: owner}]) {
id: ID!
foo: String
bar: String
}
And I also found this seemingly-promising AWS guide which I thought would give an IAM role admin access (https://docs.amplify.aws/cli/graphql/authorization-rules/#configure-custom-identity-and-group-claims) which I followed by creating the file amplify/backend/api/<your-api-name>/custom-roles.json and saved it with
{
"adminRoleNames": ["<YOUR_IAM_ROLE_NAME>"]
}
I replaced "<YOUR_IAM_ROLE_NAME>" with an IAM Role which I have given broad access to, including this appsync access:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"appsync:*"
],
"Resource": "*"
}
]
}
Which is the role given to my aws-lambda function.
When I attempt to run a simple API query in my aws-lambda function with the above settings I get this error
response string:
{
"data": {
"getFileUpload": null
},
"errors": [
{
"path": [
"getFileUpload"
],
"data": null,
"errorType": "Unauthorized",
"errorInfo": null,
"locations": [
{
"line": 3,
"column": 11,
"sourceName": null
}
],
"message": "Not Authorized to access getFileUpload on type Query"
}
]
}
my actual python lambda script is
import http
API_URL = '<MY_API_URL>'
API_KEY = '<>MY_API_KEY'
HOST = API_URL.replace('https://','').replace('/graphql','')
def queryAPI():
conn = http.client.HTTPSConnection(HOST, 443)
headers = {
'Content-type': 'application/graphql',
'x-api-key': API_KEY,
'host': HOST
}
print('conn: ', conn)
query = '''
{
getFileUpload(id: "<ID_HERE>") {
description
createdAt
baseFilePath
}
}
'''
graphql_query = {
'query': query
}
query_data = json.dumps(graphql_query)
print('query data: ', query_data)
conn.request('POST', '/graphql', query_data, headers)
response = conn.getresponse()
response_string = response.read().decode('utf-8')
print('response string: ', response_string)
I pass in the API key and API URL above in addition to giving AWS-lambda the IAM role. I understand that only one is probably needed, but I am trying to get the process to work then pare it back.
QUESTION(s)
As far as I understand, I am
providing the appropriate #auth rules to my GraphQL schema based on my goals and (2 below)
giving my aws-lambda function sufficient IAM authorization (via both IAM role and API key) to override any potential restrictive #auth rules of my GraphQL schema
But clearly something is not working. Can anyone point me towards a problem that I am overlooking?
I had similar problem just yesterday.
It was not 1:1 what you're trying to do, but maybe it's still helpful.
So I was trying to give lambda functions permissions to access the data based on my graphql schema. The schema had different #auth directives, which caused the lambda functions to not have access to the data anymore. Even though I gave them permissions via the cli and IAM roles. Although the documentation says this should work, it didn't:
if you grant a Lambda function in your Amplify project access to the GraphQL API via amplify update function, then the Lambda function's IAM execution role is allow-listed to honor the permissions granted on the Query, Mutation, and Subscription types.
Therefore, these functions have special access privileges that are scoped based on their IAM policy instead of any particular #auth rule.
So I ended up adding #auth(rules: [{ allow: custom }]) to all parts of my schema that I want to access via lambda functions.
When doing this, make sure to add "lambda" as auth mode to your api via amplify update api.
In the authentication lambda function, you could then check if the user, who is invoking the function, has access to the requested query/S3 Data.

AWS - HTTP API Gateway - How do I block favicon requests?

I'm using a HTTP API Gateway to trigger a lambda invocation. When I use the url from postman, no issues. When I use it from my browser, it always makes a 2nd request, for the favicon.
Is there anyway in the gateway itself to block the favicon request from getting to the lambda?
I'm using the following terraform:
resource "aws_apigatewayv2_api" "retry_api" {
name = "${var.environment}_${var.cdp_domain}_retry_api"
protocol_type = "HTTP"
description = "To pass commands into the retry lambda."
target = module.retry-support.etl_lambda_arn
}
resource "aws_lambda_permission" "allow_retry_api" {
statement_id = "AllowAPIgatewayInvokation"
action = "lambda:InvokeFunction"
function_name = module.retry-support.etl_lambda_arn
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.retry_api.execution_arn}/*/*"
}
This won't block the favicon request made from the browser, rather won't invoke the Lambda for those requests.
Assuming the API endpoint is /hello and the http method is GET, you can restrict api-gateway to invoke the lambda for only this URL. The format would be like this.
arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/hello
So the source_arn in aws_lambda_permission would change to something like this
source_arn = "${aws_apigatewayv2_api.retry_api.execution_arn}/*/*/GET/hello"
The answer assumes the existing / in the end is for apiId and stage respectively. Otherwise check the value for ${aws_apigatewayv2_api.retry_api.execution_arn} and make modifications accordingly.
This answer can also help. You can provide the openapi specification in the body for your supported path only. For the above case the relevant path section of the openapi specification invoking a Lambda named HelloWorldFunction would look like
"paths": {
"/hello": {
"get": {
"x-amazon-apigateway-integration": {
"httpMethod": "POST",
"type": "aws_proxy",
"uri": {
"Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations"
},
"payloadFormatVersion": "2.0"
},
"responses": {} //Provide the expected response model
}
}
}
Here is a link to OpenApi Specification.
Normally, I would do this by putting cloudfront in front of the API gateway, and map the favicon.ico to an S3 bucket.
If you really want to handle it at the API GW level, you can create a /favicon.ico route, and set the integration to MOCK - this will return a specific value, and not invoke lambda (or any other back end).

How can I use IAM to invoke AppSync wtihin AWS Lambda?

I'm currently in the process of implementing a subscription mutation within AWS Lambda using AppSync. I want to use IAM and avoid using any other type of AUTH mechanism as I'm calling it within the AWS stack. Unfortunately, I'm receiving the following 403 error:
(Excerpt from an SQS' CloudWatch log)
{
"errorMessage": "Response not successful: Received status code 403",
"name": "ServerError",
"errorType": "UnrecognizedClientException",
"message": "The security token included in the request is invalid."
}
I've tried following these to no avail, but I don't know what I'm missing:
https://medium.com/#jan.hesters/how-to-use-aws-appsync-in-lambda-functions-e593a9cef1d5
https://www.edwardbeazer.com/using-appsync-client-from-lambda/
https://adrianhall.github.io/cloud/2018/10/26/backend-graphql-trigger-appsync/
How to send GraphQL mutation from one server to another?
AWS Appsync + HTTP DataSources + AWS IAM
AWS Appsync Invoke mutate from Lambda?
Here's the code that I'm currently calling it from:
import AWS from "aws-sdk";
import { AWSAppSyncClient } from "aws-appsync";
import { Mutation, mutations } from "./mutations/";
import "cross-fetch/polyfill";
/**
*
*/
AWS.config.update({
region: Config.region,
});
export class AppSyncClient {
client: AWSAppSyncClient<any>;
constructor() {
if (!env.APPSYNC_ENDPOINT) {
throw new Error("APPSYNC_ENDPOINT not defined");
}
/**
* We create the AppSyncClient with the AWS_IAM
* authentication.
*/
this.client = new AWSAppSyncClient({
url: env.APPSYNC_ENDPOINT,
region: Config.region,
auth: {
credentials: AWS.config.credentials!,
type: "AWS_IAM",
},
disableOffline: true,
});
}
/**
* Sends a mutation on the AppSync Client
* #param mutate The Mutation that will be sent with the variables.
* #returns
*/
sendMutation(mutate: Mutation) {
const mutation = mutations[mutate.type] as any;
const variables = mutate.variables;
console.log("Sending the mutation");
console.log("Variables is ", JSON.stringify(variables));
return this.client.mutate({
mutation,
fetchPolicy: "network-only",
variables,
});
}
}
Here's the current IAM from the Lambda SQS:
{
"Statement": [
{
"Action": [
"appsync:GraphQL"
],
"Effect": "Allow",
"Resource": [
"arn:aws:appsync:us-east-2:747936726382:apis/myapi"
]
}
],
"Version": "2012-10-17"
}
I know it is not an IAM problem from the lambda, because I've tried momentarily giving it full access, and I still got the 403 error.
I've also verified that AppSync has the IAM permission configured (as an additional provider).
Do you guys have any ideas? I'm impressed that this is a ghost topic with such little configuraiton references.
I finally nailed it. I went and re-read for third time Adrian Hall's post, and it did lead me to the solution.
Please note that I installed the AWS AppSync client which is not needed but simplifies the process (otherwise you'd have to sign the URL yourself. For that see Adrian Hall's post).
There are a couple of things:
You need to polyfill "fetch" by including either cross-fetch (Otherwise you're going to get hit by Invariant Violation from the Apollo Client which AppSync internally uses).
You need to pass the lambda's internal IAM credentials (Which I didn't even know existed) to the configuration portion of the AppSyncClient.
You need to add the proper permission to the IAM role of the lambda, in this case: ["appsync:GraphQL"] for the action.
Here's some code:
This is the AppSync code.
// The code is written in TypeScript.
// https://adrianhall.github.io/cloud/2018/10/26/backend-graphql-trigger-appsync/
// https://www.edwardbeazer.com/using-appsync-client-from-lambda/
import { env } from "process";
import { Config, env as Env } from "../../../../shared";
// This is such a bad practice
import AWS from "aws-sdk";
import { AWSAppSyncClient } from "aws-appsync";
import { Mutation, mutations } from "./mutations/";
// Very important, otherwise it won't work!!! You'll have Invariant Violation
// from Apollo Client.
import "cross-fetch/polyfill";
/**
*
*/
AWS.config.update({
region: Config.region,
credentials: new AWS.Credentials(
env.AWS_ACCESS_KEY_ID!,
env.AWS_SECRET_ACCESS_KEY!,
env.AWS_SESSION_TOKEN!
),
});
export class AppSyncClient {
client: AWSAppSyncClient<any>;
constructor() {
// Your AppSync endpoint - The Full URL.
if (!Env.APPSYNC_ENDPOINT) {
throw new Error("APPSYNC_ENDPOINT not defined");
}
/**
* We create the AppSyncClient with the AWS_IAM
* authentication.
*/
this.client = new AWSAppSyncClient({
url: Env.APPSYNC_ENDPOINT,
region: Config.region,
auth: {
credentials: AWS.config.credentials!,
type: "AWS_IAM",
},
disableOffline: true,
});
}
/**
* Sends a mutation on the AppSync Client
* #param mutate The Mutation that will be sent with the variables.
* #returns
*/
// The mutation is a object that holds the mutation in
// the `gql` tag. You can ommit this part.
sendMutation(mutate: Mutation) {
const mutation = mutations[mutate.type] as any;
const variables = mutate.variables;
// This is the important part.
return this.client.mutate({
mutation,
// Specify "no-cache" in the policy.
// network-only won't work.
fetchPolicy: "no-cache",
variables,
});
}
}
We need to enable IAM in the AppSync authorization mechanism. Yes, it is possible to have multiple Authentication enabled. I'm currently using OPEN_ID and IAM simultaneously.
https://us-east-2.console.aws.amazon.com/appsync/home?region=us-east-2#/myappsync-id/v1/settings
Here's the Lambda's IAM policy that executes the GQL:
{
"Statement": [
{
"Action": [
"appsync:GraphQL"
],
"Effect": "Allow",
"Resource": [
"arn:aws:appsync:us-east-2:747936726382:apis/ogolfgja65edlmhkcpp3lcmwli/*"
]
}
],
"Version": "2012-10-17"
}
You can further restrict here in the following fashion:
arn:${Partition}:appsync:${Region}:${Account}:apis/${GraphQLAPIId}/types/${TypeName}/fields/${FieldName}
arn:aws:appsync:us-east-2:747936726382:apis/ogolfgja65edlmhkcpp3lcmwli/types/Mutation/field/myCustomField"
Note, we need to better restrict this as we are currently giving it entire access to the API.
In your .gql file (AppSync GraphQL schema), add the #aws_iam directive to the mutation that is being used to send the subscriptions to, in order to restrict access from the front-end.
type Mutation {
addUsersMutationSubscription(
input: AddUsersSagaResultInput!
): AddUsersSagaResult #aws_iam
}

Lambda is not authorized to perform: dynamodb:Query on resource - But scan works just fine

I am new to Lambda, I was playing with the lambda samples given in the AWS.
In nodeJS 6.10 Runtime,
dynamo.query({ TableName: 'my_table', KeyConditionExpression: 'id = :id',ExpressionAttributeValues: {':id': '123'} }, done); is erroring out with xxxxLambda is not authorized to perform: dynamodb:Query on resource.
But dynamo.scan({ TableName: 'my_table'}, done); works just fine, so does the PUT operation dynamo.putItem(JSON.parse(event.body), done); I haven't modified my IAM policies.
BTW, definition of done variable is
const done = (err, res) => callback(null, {
statusCode: err ? '400' : '200',
body: err ? err.message : JSON.stringify(res),
headers: {
'Content-Type': 'application/json',
},
});
Edit
My Lambda-IAM Original roles:
AWSLambdaMicroserviceExecutionRole-xxxx
AWSLambdaBasicExecutionRole-xxxx-xxxx-xx-xx-xx
When I attached "AmazonDynamoDBFullAccess" policy, 'query' also started to work fine.
But what I am wondering is how scan and put works but not query?
Edit-2
Dear downvoter, please add a comment so that I can improve my question.
This seems like a permission issue.
Double check the IAM policy inside the IAM Role attached to the Lambda function, whether it has AmazonDynamoDBFullAccess or Custom Policy including 'query' action granted.
"Effect": "Allow",
"Action" [
dynamodb:Query
]
I had the same issue, but the accepted solution alone did not work for me. I got everything to work fine if I did not provide a table arn in the permissions, but just made it work for all tables. Sure, it works, but that's hardly a secure solution.
I found that I had to specify the arn for the GSI I was querying on in the IAM permissions. So instead of specifying the arn for the table, I specified the arn for the specific index I wanted to query on. And then it worked. Maybe there's a more elegant solution, but this is what worked for me.

How to get the Cognito Identity id in AWS Lambda

How can I get the identity id of the user (logged in by AWS Cognito) that invoked an AWS Lambda function? Do I have to use the SDK on the Lambda function to get the identity id?
In AWS javascript SDK inside lambda function just use
context.identity.cognitoIdentityId
It is working for me
If anyone else stumbles upon this, I think this will help you a lot.
Note this only applies if you're using the Cognito User Pool Authorizer. If you want to use AWS_IAM with Cognito Identitys check out my github example https://github.com/VictorioBerra/js-cognito-auth-example (read down to EDIT area below)
If you have "Use Lambda Proxy Integration" checked then you wont have access to Request Template Mappings. But you can get to the claims inside the token in your lambda function:
exports.handler = (event, context, callback) => {
//create a response
const response = {
statusCode: 200,
body: JSON.stringify({
"user email": event.requestContext.authorizer.claims.email,
}),
};
callback(null, response);
};
EDIT - More information for using AWS_IAM as APIG authorizer
Basically you need to have your APIG secured with AWS_IAM AND you must auth via a Cognito Federated Identity which will return a sessionToken example using user pools. This is what makes the AWS IAM credentials temporary. Now you have everything you need to auth to your APIG.
To test this, download the desktop version of postman, toss in your API URI (grab this from the stages area) 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 built 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 it can be a little complicated 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.
IMO professional opinion I think using the AWS_IAM temp creds authorizer is preferred. This way, you can use as many different IdPs as you want in Cognito Identities (Facebook, Twitter, pools, etc.)
If you go through API Gateway, you can pass the cognito id (as well as the user arn and other useful information) to Lambda. This solved the issue for me.
http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html
Per the docs, it looks like information about the identity provider would only be available for an invoke through the Mobile SDK.
To get around this, one option is to pass the identity ID to the function manually as part of the event. Assuming you are doing something like AWS.config.credentials = new AWS.CognitoIdentityCredentials(...) then you should be able to get the ID via AWS.config.credentials.identityId (after the credentials are refreshed).
EDIT: A better option for identity validation is to let Cognito/IAM handle it, and assume that if a user can successfully invoke a Lambda function, that means they are allowed to. In this case to manage per-user validation, take a look at whitelisting.
For anyone else still struggling to obtain the IdentityId of a Cognito User in a Lambda Function, after many hours of exploration, I discovered that the IdentityId can be obtained within a Lambda Function in the following ways depending on the method you use to invoke the Lambda Function:
Invocation via an API-Gateway trigger with a Cognito User Pool Authorizer
The IdentityId can be obtained in the following way:
const IDENTITY_POOL_ID = "us-west-2:7y812k8a-1w26-8dk4-84iw-2kdi849sku72"
const USER_POOL_ID = "cognito-idp.us-west-2.amazonaws.com/us-west-2_an976DxVk"
const { CognitoIdentityClient } = require("#aws-sdk/client-cognito-identity");
const { fromCognitoIdentityPool } = require("#aws-sdk/credential-provider-cognito-identity");
exports.handler = async (event,context) => {
const cognitoidentity = new CognitoIdentityClient({
credentials: fromCognitoIdentityPool({
client: new CognitoIdentityClient(),
identityPoolId: IDENTITY_POOL_ID,
logins: {
[USER_POOL_ID]:event.headers.Authorization
}
}),
});
var credentials = await cognitoidentity.config.credentials()
console.log(credentials)
// {
// identityId: 'us-west-2:d393294b-ff23-43t6-d8s5-59876321457d',
// accessKeyId: 'ALALA2RZ7KTS7STD3VXLM',
// secretAccessKey: '/AldkSdt67saAddb6vddRIrs32adQCAo99XM6',
// sessionToken: 'IQoJb3JpZ2luX2VjEJj//////////...', // sessionToken cut for brevity
// expiration: 2022-07-17T08:58:10.000Z
// }
var identity_ID = credentials.identityId
console.log(identity_ID)
// us-west-2:d393294b-ff23-43t6-d8s5-59876321457d
const response = {
statusCode: 200,
headers: {
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods" : "OPTIONS,POST,GET,PUT"
},
body:JSON.stringify(identity_ID)
};
return response;
}
Why?
Invoking a Lambda Function using an API-Gateway trigger with a Cognito User Pool Authorizer will result in the following fields (among additional fields which have been removed for brevity) being available in the event of the invocation:
{
"resource": "/{proxy+}",
"path": "/_",
"httpMethod": "POST",
"headers": {
"Authorization": "eyJrmlaod8(kyC9zdspo0jZIYjRENFlCNVBidnksl89DNm9USDJrT25MSDhjXC9EYkIzMzQ8389iYWxnIjoiUlMyNTYifQ.eyJzdWIiOiI8389yNDg4Ny0wYTRdkiuiOGQtODA4My1kNmIyYzZhYWNhN2YiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6XC9cL2NvZ25pdG8taWRwLnVzLXdlc3QtMi5hbWF6b25hd3MuY29tXC91cy13ZXN0LTJfZ3dPMjlMckR4IiwiY3VzdG9tOmZpcnN0X2tleSI6ImZhbHNlIiwiY29nbml0bzp1c2VybmFtZSI6IjJjNjI0ODg3LTBhNGItNGE4ZC04MDgzLWQ2YjJjNmFhY2E3ZiIsIm9yaWdpbl9qdGkiOiI0ODEzYzk1Ni01MDM0LTQzNmItYjE5OS00MWZjN2U1ZTQ0ODciLCJhdWQiOiJyNjdwOGdxNnQ5cHJrc2JyYmtxMDVoM2NuIiwiZXZlbnRfaWQiOiJmOWRlNWMxOC0yMTNlLTQ2NDgtOGY4MC1mODdhYWJhNWM4NGUiLCJjdXN0b206YXBpX2tleV9JRCI6Imhoa2hpMmw5cTkiLCJjdXN0b206YXBpX2tleSI6InVzLXdlc3QtMjplOTBkYmUzZC0wN2FhLTRjODQtOGQ0Ny04NjJmZDJhNzQ1MGZfcm5Zalk2YmhaT0lkWXhDcDBDcjNYZWVaYzFuT3ViZnkzdEs5aEtFRFVIZ3FJd1BSTVJuQ2pyWlJYdGNBSVpqWTVnMFFyejN3ek1keGoiLCJ0b2tlbl91c2UiOiJpZCIsImF1dGhfdGltZSI6MTY1ODA0ODg1OSwiZXhwIjoxNjU4MTM2MTk3LCJpYXQiOjE2NTgxMzI1OTcsImp0aSI6IjdmNTdiYWZlLTA1ZGUtNDgwZC1hMDgzLWY1MjlhN2YzNmI2YiIsImVtYWlsIjoidHJldmVuYXc3QGdtYWlsLmNvbSJ9.hk-8ajGP2jO0RQvzwZWp2d5T1BLiWL9q6vvrbXemLBbd2kb1kkBSvklfC_7WWvJoy1ukwNoq8Cx63U2hQfjJB077AHrHfN2PkJu4DG86vSdtSzrZVDQmle331UxopLQvzDZ1mejfmSFbo6x1ZeTbo39PRpox4pzsfeUAM1Rf8H6y8OrdPZa7Gh6gRkiN2IcwdBnXI4-Q6HX5QqiVzr2O4zEnNsqMFfsFA3aO05hnp7EYRWHgS6EgaQjfBirCoyerBkjFJTXynl76Jj4fK3-3KY4tw5EHplxkgAih7a9QTxy8SbRee8kh7fwrIGEO6CtgkM9v0XL7jBPtNtGTpqBRzw"
},
"requestContext": {
"authorizer": {
"claims": {
"sub": "231df9d7-0aab-4dsd-8389-d315d9daca7f",
"email_verified": "true",
"iss": "https://cognito-idp.us-west-2.amazonaws.com/us-west-2_an976DxVk",
"custom:first_key": "false",
"cognito:username": "231df9d7-0aab-4dsd-8389-d315d9daca7f",
"origin_jti": "8131rlpo-5123-436b-b199-41315d9d4487",
"aud": "r6315d9dt9sdls315d9d5h3cn",
"event_id": "f3dsp9d8-213e-4648-8f80-f8315d9dc84e",
"custom:name": "my_name",
"token_use": "id",
"auth_time": "1658148859",
"exp": "Mon Jul 18 09:23:17 UTC 2022",
"iat": "Mon Jul 18 08:23:17 UTC 2022",
"jti": "982sdafe-97rs-996l-i899-k315d9d09n7n",
"email": "jeff#amazon.com"
}
},
"extendedRequestId": "dSiT_dLdJdad9oP=",
"requestTime": "18/Jul/2022:08:23:27 +0000",
"requestTimeEpoch": 1658132607750,
"requestId": "387ss00s3-add0-62i9-l9s0-9a8dj8976dfa",
"identity": {
"cognitoIdentityPoolId": null,
"accountId": null,
"cognitoIdentityId": null,
"caller": null,
"sourceIp": "91.83.769.200",
"principalOrgId": null,
"accessKey": null,
"cognitoAuthenticationType": null,
"cognitoAuthenticationProvider": null,
"userArn": null,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0",
"user": null
},
"domainName": "mskd8fpni2.execute-api.us-west-2.amazonaws.com",
"apiId": "mskds98ni2"
},
"body": "{\"name\":\"my_name\"}",
"isBase64Encoded": false
}
and the following fields being available in the context of the invocation:
{
"callbackWaitsForEmptyEventLoop": true,
"functionVersion": "$LATEST",
"functionName": "hello-world-jsHelloWorldFunction-kTdLswTRrkdS",
"memoryLimitInMB": "512",
"logGroupName": "/aws/lambda/hello-world-jsHelloWorldFunction-kTdLswTRrkdS",
"logStreamName": "2022/07/15/[$LATEST]95edi84719944bdi84a6cdi8451di849",
"invokedFunctionArn": "arn:aws:lambda:us-west-2:598763214571:function:hello-world-jsHelloWorldFunction-kTdLswTRrkdS",
"awsRequestId": "d8s57juq-3g39-49sk-8625-5849sk9be44I"
}
Notice the following things:
The Cognito User's IdentityId is not available in either the event or the context
Other information about the Cognito User is available in the event (email, sub, custom attributes, etc.)
The request invocation time is available in the event (the time that the lambda function was invoked which is available in event.requestContext.requestTime and event.requestContext.requestTimeEpoch)
Note: Please leave a comment if you are having a hard time setting up an API-Gateway trigger with a Cognito User Pool Authorizer for your Lambda Function, if people are interested I will add instructions on how to accomplish this.
Direct invocation using a Cognito User's credentials and the AWS SDK for JavaScript v3
The IdentityId can be obtained in the following way:
exports.handler = async (event,context) => {
var identity_ID = context.identity.cognitoIdentityId
console.log(identity_ID)
// us-west-2:d393294b-ff23-43t6-d8s5-59876321457d
const response = {
statusCode: 200,
headers: {
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods" : "OPTIONS,POST,GET,PUT"
},
body:JSON.stringify(identity_ID)
};
return response;
}
Why?
Directly invoking a Lambda Function using the AWS SDK for Javascript v3 and a Cognito User's credentials will result in no additional information being available in the event of the invocation:
{
"name": "my_name"
}
however, the IdentityId is available in the context of the invocation:
{
"callbackWaitsForEmptyEventLoop": true,
"functionVersion": "$LATEST",
"functionName": "hello-world-jsHelloWorldFunction-kTdLswTRrkdS",
"memoryLimitInMB": "512",
"logGroupName": "/aws/lambda/hello-world-jsHelloWorldFunction-kTdLswTRrkdS",
"logStreamName": "2022/07/15/[$LATEST]95edi84719944bdi84a6cdi8451di849",
"identity": {
"cognitoIdentityId": "us-west-2:d393294b-ff23-43t6-d8s5-59876321457d",
"cognitoIdentityPoolId": "us-west-2:7y812k8a-1w26-8dk4-84iw-2kdi849sku72"
},
"invokedFunctionArn": "arn:aws:lambda:us-west-2:598763214571:function:hello-world-jsHelloWorldFunction-kTdLswTRrkdS",
"awsRequestId": "d8s57juq-3g39-49sk-8625-5849sk9be44I"
}
Notice the following things:
The Cognito User's IdentityId is available in context.identity.cognitoIdentityId
Additional information about the Cognito User is not available in the event or the context (email, sub, custom attributes, etc.)
The request invocation time is not available in either the event or context
Note: To directly invoke a Lambda Function using a Cognito User's credentials and the AWS SDK for JavaScript v3, you need to add a custom IAM Permission to the IAM Role assumed by authenticated Users in your Cognito User Pool. To do this:
Go to IAM -> Roles, and find the role for your user pool such as amplify-myapp-prod-12987-authRole
Click "Add permissions"
Click "Create inline policy"
Click "JSON"
Insert the following policy and replace the Function ARN with the ARN of your Lambda Function:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"lambda:InvokeFunctionUrl",
"lambda:InvokeFunction"
],
"Resource": [
"arn:aws:lambda:*:598763214571:function:hello-world-jsHelloWorldFunction-kTdLswTRrkdS"
]
}
]
}
Click "Review Policy"
Save the policy
Invocation methods that don't currently support authenticating users from a Cognito User Pool:
Invoking with caller credentials (this only works if you are using AWS_IAM authentication, not a Cognito User Pool Authorizer)
Invoking via a function URL (this only supports AWS_IAM authentication as well)
My observation is the following.
If you call the API Gateway with a signed Request where you actually provide the accesskey, secret and sessionToken which you can extract via (JS SDK):
AWS.config.credentials = new AWS.CognitoIdentityCredentials(...)
AWS.config.credentials.get(..)
And assumed that your lambda is called from API-Gateway via LAMBDA_PROXY and Authorizer AWS_IAM. You can only access user stuff in lambda with:
exports.create = function (event, context) {
secdata = event.requestContext.identity.cognitoAuthenticationProvider;
}
Then you will get, apart from other stuff, the "sub" of the cognito UserPool User. So if you really want to know more about the user, it seems you need to ask AWS again via SDK call.
For a Python Lambda, invoked via Javascript AWS SDK / Cognito / Amplify...
https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html
context.identity.cognito_identity_id
It should look something like this:
{aws region}:{ GUID }
Assuming you are using an Identity Pool, this will return the Cognito Federated Identity, that can be used for fine grained access control. This is safer than relying on the Javascript payload containing the identity id.
The Cognito Identity Pool Auth Role will need to have Lambda:InvokeFunction policy, otherwise the user won't be able to invoke the function in the first place.
Edit: This works when calling the Lambda function DIRECTLY, not via API Gateway.
Edit2: The Cognito user is allowed to call the lambda because it is explicitly set in the IAM Cognito Auth role.
I was using Kotlin and my Lambda handler was
override fun handleRequest(event: APIGatewayProxyRequestEvent, context: Context): APIGatewayProxyResponseEvent
But event.requestContext had no authorizer. The solution was to upgrade the dependency in build.gradle from
com.amazonaws:aws-lambda-java-events:2.1.0 to com.amazonaws:aws-lambda-java-events:2.2.7. After that, I got the username as follows.
val claims = requestContext.authorizer["claims"] as Map<String, String>
println(claims["cognito:username"])
println(claims["email"])