How to provide custom data from API gateway endpoint to lambda authorizer - amazon-web-services

The API Gateway endpoints we are using shall be restricted via permissions to a specific audience.
The idea is to use the lambda authorizer to fetch the permissions from an external service and then create the policy to allow or deny access to the endpoint.
For the matching of the permissions to the API endpoint the endpoint would need to provide the permissions it needs to the authorizer.
My question is now how can I enrich the endpoint data with its own required permissions and use them in the authorizer lambda(probably via the event) for further validation.
Example:
User1 is forwarded to the first endpoint GET/petstore/pets(this endpoint needs the permission -> View:Pets)
Lambda authorizer requests the user permissions from the external service
The service returns: [View:Pets , View:Somethingelse, etc.]
The lambda authorizer matches the user permissions against the required endpoint permission and creates the Allow policy on a match
User2 does the same but does not have the permission for viewing pets, no match -> Deny
Here is my code for the lambda:
import {Callback, Context} from 'aws-lambda';
import {Authorizer} from './authorizer';
export class App {
constructor(private authorizer: Authorizer = new Authorizer()) {
}
public handleEvent(event, callback: Callback): Promise<void> {
return this.authorizer.checkAuthorization(event, callback)
.then((policy) => callback(null, policy))
.catch((error) => callback(error, null));
}
}
const app: App = new App();
module.exports.lambda_handler = async (event) => {
return await app.handleEvent(event);
};
Code for the checkAuthorization method:
export class Authorizer {
public resourceAuthorizer: ResourceAuthorizer = new ResourceAuthorizer();
public authenticationChecker: AuthenticationChecker = new AuthenticationChecker();
public checkAuthorization(event, callback): Promise<object> {
const endpointPermissions = event.endpointPermissions; // <== this is what I need, a custom field in the event which
// is provided from the api endpoint in some way
// in my example this whould contain a string or json
// with 'View:Pets' and 'View:Somethingelse'
return this.authenticationChecker.check(event)
.then((decodedJwt) => {
const principalId: string = decodedJwt.payload.sub;
return Promise.resolve(decodedJwt)
.then((jwt) => this.resourceAuthorizer.check(jwt, event.endpointPermissions))
.then((payload) => callback(null,
getAuthorizationPolicy(principalId, 'Allow', event.endpointPermissions, payload)))
.catch((payload) => callback(null,
getAuthorizationPolicy(principalId, 'Deny', event.endpointPermissions, payload)));
}).catch((error) => {
console.log(error);
callback('Unauthorized');
});
}
}
The event.endpointPermissions is basically what I am looking for. Depending on the API endpoint this should be filled with the permissions neccessary for that endpoint. The resourceAuthorizer then fetches the users Permissions from the external service and compares them to the endpointPermissions and then creates the Allow or Deny policies.
So where can I enter the endpointPermissions in my API Endpoint to provide them to the Authorizer?

The event being passed to the Authorizer contains a methodArn, which is in the format:
arn:aws:execute-api:<Region id>:<Account id>:<API id>/<Stage>/<Method>/<Resource path>
This would give you the Method and Resource Path that you need. It also would give you an identifier of the API, but not the name of the API itself.
The API id, can be used to get the API name by using the AWS SDK. See here.
This should give you everything you need to construct your endpointPermissions value.

I got a solution to my problem without having to parse the ARN, but it's pretty unconventional:
In the method request of a resource create URL query string parameters with the permission names and set the checkbox for 'required'
When the request is called from the client(Postman) these mandatory parameters have to be provided as keys, they are endpoint-specific. The values do not matter because only the keys will be used at evaluation.
The event received by the authorizer now contains the queryStringParameters which can be evaluated for further use.

Related

Added claims from AWS custom lambda Authorizer, how to access those claims in ASP.NET Core 6 Web API?

I have created AWS custom lambda Authorizer, which is validating token and add claims in APIGatewayCustomAuthorizerResponse with Context property.
private APIGatewayCustomAuthorizerResponse AuthorizedResponse(TokenIntrospectionResponse result) // result with claims after validating token
{
return new APIGatewayCustomAuthorizerResponse()
{
PrincipalID = "uniqueid",
PolicyDocument = new APIGatewayCustomAuthorizerPolicy()
{
Statement = new List<APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement>
{
new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement()
{
Effect = "Allow",
Resource = new HashSet<string> { "*" },
Action = new HashSet<string> { "execute-api:Invoke" }
}
}
},
Context = PrepareRequestContextFromClaims(result.Claims) //APIGatewayCustomAuthorizerContextOutput
};
}
private APIGatewayCustomAuthorizerContextOutput PrepareRequestContextFromClaims(IEnumerable<System.Security.Claims.Claim> claims)
{
APIGatewayCustomAuthorizerContextOutput contextOutput = new APIGatewayCustomAuthorizerContextOutput();
var claimsGroupsByType = claims.GroupBy(x => x.Type);
foreach (var claimsGroup in claimsGroupsByType)
{
var type = claimsGroup.Key;
var valuesList = claimsGroup.Select(x => x.Value);
var values = string.Join(',', valuesList);
contextOutput[type] = values;
}
return contextOutput;
}
Added this lambda authorizer with API GW method request.
For integration request, I have added HTTP Proxy request, which is an ASP.NET Core 6 Web API.
I am trying to access claims from the headers, that were added by authorizer in Web API routes, but not getting any claims.
_httpContext.HttpContext.Request.Headers
// not getting with headers
_httpContext.HttpContext.Items["LAMBDA_REQUEST_OBJECT"] as APIGatewayProxyRequest
// not getting with this as well
Is there any way to achieve this?
Needs to configure claim key value with API Gateway's Method & Integration request.
For example, if custom lambda authorizer validates token and add claim 'role' in Context of APIGatewayCustomAuthorizerResponse => we have to add optional role in headers with 'Method Request' and also need to add header with 'Integration request' as (Name : role, Mapped from : context.authorizer.role).
then after we will get 'role' from headers using _httpContext.HttpContext.Request.Headers['role'] with .Net Core 6 Web API.

How to retrieve cognito identification data in Appsync Lambda Resolver (Using cdk)

I have an appsync lambda resolver which will query a postgresql database. Appsync requests are authorized using API keys for unauthorized users and cognito user pools for authorized users. I would like to retrieve identification data from cognito within my lambda resolver when an authenticated user makes a request, but I can't figure out how to do so. To begin, here is my setup for appsync and the lambda resolver:
this.api = new appsync.GraphqlApi(this, "API-NAME", {
name: "API-NAME",
schema: appsync.Schema.fromAsset("graphql/schema.graphql"),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.API_KEY,
apiKeyConfig: {
expires: cdk.Expiration.after(cdk.Duration.days(365)),
},
},
additionalAuthorizationModes: [
{
authorizationType: appsync.AuthorizationType.USER_POOL,
userPoolConfig: {
userPool: props.userPool,
},
},
],
},
});
const lambdaDs = this.api.addLambdaDataSource(
"lambdaDatasource",
props.LambdaConnectingGraphqlToDatabase
);
lambdaDs.createResolver({
typeName: "Query",
fieldName: "listUsers",
});
// etc. etc.
Within my lambda resolver, context.identity is undefined even when an authenticated user makes a request. I have tried using a request mapping template within the lambdaDs.createResolver(), but I couldn't figure out how to make this work, or if this is the correct method.
How do I see the authentication data within my lambda resolver? Thank you.
You can provide the identity information to your lambda via the resolver mapping template, see https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html
The context.identity section is the relevant one.
There is a section with fields available for the AMAZON_COGNITO_USER_POOLS authorization.
However, note that for API_KEY, context.identity information is not populated.
You can however differentiate between the two scenarios since you will have identity information for Cognito scenario in your lambda, and will not have any identity information for API key scenario (hence you can assume it is request from unauthorized user with API key).

Can I use AWS API Gateway as a reverse proxy for a S3 website?

I have a serverless website on AWS S3. But S3 have a limitation that I want to overcome: it don't allow me to have friendly URLs.
For example, I would like to replace URL:
www.mywebsite.com/user.html?login=daniel
With this URL friendly:
www.mywebsite.com/user/daniel
So, I would like to know if I can use Lambda together with API Gateway to achieve this.
My idea is:
API Gateway ---> Lambda function ---> fetch S3 resource
The API Gateway will get ANY request, and pass information to a Lambda funcion, that will process some logic using the request URL (including maybe some database query) and then fetch the resource from S3.
I know AWS API Gateway main purpose is to be a gateway to REST APIs, but can we also use it as a proxy to an entire website?
The good option can be to use CloudFront as a reverse proxy, you can use Viewer/Origin response request to trigger lambda and fetch the resource from S3.
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-examples.html
https://aws.amazon.com/blogs/networking-and-content-delivery/amazon-s3-amazon-cloudfront-a-match-made-in-the-cloud/
It is possible to use API Gateway as a reverse proxy for a S3 website.
I was able to do that following steps below:
In AWS API Gateway, create a "proxy resource" with resource path = "{proxy+}"
Go to AWS Certificate Manager and request a wildcard certificate for your website (*.mywebsite.com)
AWS will tell you to create a CNAME record in you domain registrar, to verify that you own that domain
After your certificate is validated, go to AWS API Gateway and create a Custom Domain Name (click on "Custom Domain Names" and then "Create Custom Domain Name"). In "domain name" type your domain (www.mywebsite.com) and select the ACM Certificate that you just created (step 1 above). Create a "Base Path Mapping" with path = "/" and in "destination" select your API and stage.
After that, you will need to add another CNAME record, with the CloudFront "Target Domain Name" that was generated for that Custom Domain Name.
In the Lambda, we can route the requests:
'use strict';
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
const myBucket = 'myBucket';
exports.handler = async (event) => {
var responseBody = "";
if (event.path=="/") {
responseBody = "<h1>My Landing Page</h1>";
responseBody += "<a href='/xpto'>link to another page</a>";
return buildResponse(200, responseBody);
}
if (event.path=="/xpto") {
responseBody = "<h1>Another Page</h1>";
responseBody += "<a href='/'>home</a>";
return buildResponse(200, responseBody);
}
if (event.path=="/my-s3-resource") {
var params = {
Bucket: myBucket,
Key: 'path/to/my-s3-resource.html',
};
const data = await s3.getObject(params).promise();
return buildResponse(200, data.Body.toString('utf-8'));
}
return buildResponse(404, '404 Error');
};
function buildResponse(statusCode, responseBody) {
var response = {
"isBase64Encoded": false,
"statusCode": statusCode,
"headers": {
"Content-Type" : "text/html; charset=utf-8"
},
"body": responseBody,
};
return response;
}
A good bet would be to use CloudFront and Lambda#Edge.
Lambda#Edge allows you to run Lambda function in the edge location of the CloudFront CDN network.
CloudFront gives you the option to hook into various events during its lifecycle and apply logic.
This article looks like it might be describing something similar to what you're talking about.
https://aws.amazon.com/blogs/networking-and-content-delivery/implementing-default-directory-indexes-in-amazon-s3-backed-amazon-cloudfront-origins-using-lambdaedge/

How to use only `ctx.identity.cognitoIdentityId` to get related Cognito User Pool user data in AppSync Resolver?

Earlier, when we started our project only with Cognito User Pool I created a lot of resolvers with validation by Cognito User Pool data, for example:
#if( $ctx.identity.claims["custom:role"] == "admin" )
...some code...(get data, invoke lambda, e.t.c.)
#else
$utils.unauthorized()
#end
But later we needed other authorization providers (Facebook, Google e.t.c.). Therefore, we migrated to cognitoIdentityId, but there was a problem obtaining user data from the Cognito User Pool in the AppSync resolvers.
In AWS Lambda I found Cognito User Pool id by the cognitoIdentityAuthProvider and can get Cognito User Attributes as UserAttributes see code below:
...
...
const cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider({
apiVersion: '2016-04-18',
});
const getCognitoUserPoolId = (authProvider) => {
const parts = authProvider.split(':');
return parts[parts.length - 1].slice(0, -1);
};
// cognitoIdentityAuthProvider, which we pass as an parameter($ctx.identity.cognitoIdentityAuthProvider) from the AppSync resolver
const SUB = getCognitoUserPoolId(cognitoIdentityAuthProvider);
const params = {
UserPoolId: COGNITO_USER_POOL_ID,
Username: SUB,
};
try {
const { UserAttributes } = await cognitoidentityserviceprovider.adminGetUser(params).promise();
...
...
} catch (error) {
return error;
}
The question is how to get data from Cognito User Pool using cognitoIdentityId in AppSync resolvers? Or are there any other options? Hope I do not have to create a separate lambda for each resolver?
I assume you are using AWS_IAM as the authorization type on your GraphQL API and you are federating a cognito user pool user through Cognito Federated Identities to obtain temporary AWS credentials that you use to call your GraphQL API.
At the moment, the federating user information is not available in the $context.identity object. The workaround for this is what you posted to retrieve it using a lambda and use it further in your resolver by using pipeline resolvers for example.
I am on the AppSync team and we have heard this feature request in the past so I will +1 it for you on your behalf.

Access permissions on AWS API Gateway

I'm building an application where some data within DynamoDb can be accessed by users over a Rest API.
What I have in mind is:
User accesses API Gateway, authenticated by a Cognito user pool;
API Gateway invokes a Lambda function (written in Python);
Lambda function accesses DynamoDB and returns data.
I'd like to be able to restrict the level of access to DynamoDb according to the user. Initially I thought that the Lambda function could inherit its permissions from the user, but this doesn't work because it needs an execution role.
What is the best way of achieving this? For example, can I pass user information to the Lambda function, which in turn can assume this role before accessing DynamoDb? If so a code example would be appreciated.
Take a look at SpaceFinder - Serverless Auth Reference App and Use API Gateway Lambda Authorizers
With Cognito you can use RBAC:
Amazon Cognito identity pools assign your authenticated users a set of
temporary, limited privilege credentials to access your AWS resources.
The permissions for each user are controlled through IAM roles that
you create. You can define rules to choose the role for each user
based on claims in the user's ID token. You can define a default role
for authenticated users. You can also define a separate IAM role with
limited permissions for guest users who are not authenticated.
so you can create specific roles for each user, although it would be better to use groups
With Lambda authorisers you create your own policy. An example is in awslabs.
In addition to blueCat's answer, I briefly tried giving my Lambda function sts:AssumeRole permissions, and then allowing it to assume the role of the Cognito user that invoked it via the API. I can then use this to get a new set of credentials and carry out some activity with the Cognito user's permissions. Roughly the code inside the lambda is:
def lambda_handler(event, context):
sts_client = boto3.client('sts')
role = event['requestContext']['authorizer']['claims']['cognito:roles']
cognito_role = sts_client.assume_role(
RoleArn=role,
RoleSessionName='lambda-session',
DurationSeconds=3600
)
credentials = cognito_role['Credentials']
sess = boto3.session.Session(
aws_access_key_id=credentials['AccessKeyId'],
aws_secret_access_key=credentials['SecretAccessKey'],
aws_session_token=credentials['SessionToken']
)
# Do something as the assumed user, e.g. access S3
s3_client = sess.client('s3')
# Do stuff here...
Although this works I found that there was roughly 0.5s overhead to assume the role and get the S3 client, and I can't re-use this session between invocations of the function because it is user-specific. As such this method didn't really suit my application.
I've decided instead to give my Lambda full access to the relevant DynamoDb tables, and use the Cognito user groups plus a Lambda authorizer to restrict the parts of the API that individual users are able to call.
I dealt with this issue also but I implemented my solution with Node.js and I figured that although your question is for a Python implementation, then maybe someone would stumble upon this question looking for an answer in JS and I figured this could help out the next person who comes along.
It sounds like you're trying to come up with an effective Authorization strategy after the user has Authenticated their credentials against your Cognito User Pool using custom attributes.
I created a library that I use to export a few functions that allow me to capture the UserPoolId and the Username for the authenticated user so that I can capture the custom:<attribute> I need within my lambda so that the conditions I have implemented can then consume the API to the remaining AWS Services I need to provide authorization to for each user that is authenticated by my app.
Here is My library:
import AWS from "aws-sdk";
// ensure correct AWS region is set
AWS.config.update({
region: "us-east-2"
});
// function will parse the user pool id from a string
export function parseUserPoolId(str) {
let regex = /[^[/]+(?=,)/g;
let match = regex.exec(str)[0].toString();
console.log("Here is the user pool id: ", match);
return match.toString();
}
// function will parse the username from a string
export function parseUserName(str) {
let regex = /[a-z,A-Z,0-9,-]+(?![^:]*:)/g;
let match = regex.exec(str)[0].toString();
console.log("Here is the username: ", match);
return match.toString();
}
// function retries UserAttributes array from cognito
export function getCustomUserAttributes(upid, un) {
// instantiate the cognito IdP
const cognito = new AWS.CognitoIdentityServiceProvider({
apiVersion: "2016-04-18"
});
const params = {
UserPoolId: upid,
Username: un
};
console.log("UserPoolId....: ", params.UserPoolId);
console.log("Username....: ", params.Username);
try {
const getUser = cognito.adminGetUser(params).promise();
console.log("GET USER....: ", getUser);
// return all of the attributes from cognito
return getUser;
} catch (err) {
console.log("ERROR in getCustomUserAttributes....: ", err.message);
return err;
}
}
With this library implemented it can now be used by any lambda you need to create an authorization strategy for.
Inside of your lambda, you need to import the library above (I have left out the import statements below, you will need to add those so you can access the exported functions), and you can implement their use as such::
export async function main(event, context) {
const upId = parseUserPoolId(
event.requestContext.identity.cognitoAuthenticationProvider
);
// Step 2 --> Get the UserName from the requestContext
const usrnm = parseUserName(
event.requestContext.identity.cognitoAuthenticationProvider
);
// Request body is passed to a json encoded string in
// the 'event.body'
const data = JSON.parse(event.body);
try {
// TODO: Make separate lambda for AUTHORIZATION
let res = await getCustomUserAttributes(upId, usrnm);
console.log("THIS IS THE custom:primaryAccountId: ", res.UserAttributes[4].Value);
console.log("THIS IS THE custom:ROLE: ", res.UserAttributes[3].Value);
console.log("THIS IS THE custom:userName: ", res.UserAttributes[1].Value);
const primaryAccountId = res.UserAttributes[4].Value;
} catch (err) {
// eslint-disable-next-line
console.log("This call failed to getattributes");
return failure({
status: false
});
}
}
The response from Cognito will provide an array with the custom attributes you need. Console.log the response from Cognito with console.log("THIS IS THE Cognito response: ", res.UserAttributes); and check the index numbers for the attributes you want in your CloudWatch logs and adjust the index needed with:
res.UserAttributes[n]
Now you have an authorization mechanism that you can use with different conditions within your lambda to permit the user to POST to DynamoDB, or use any other AWS Services from your app with the correct authorization for each authenticated user.