Lambda Authorizer fails with invalid token after generating using apigClientFactory - amazon-web-services

This is within a VUE project using VUEX.
The aim is to be able to control access to the API Gateway for both authenticated users and unauthenticated users.
In my Identity pool I can see that both authenticated and unauthenticated users are being picked up but I cannot get the Authorizer to accept the token that the SDK (aws-api-gateway-client) generates.
First I am getting accessKeyId etc
let AWS = require("aws-sdk");
AWS.config.region = env.aws.aws_cognito_region;
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId:
env.aws.aws_cognito_region +
":" +
env.aws.aws_cognito_identity_pool_id,
Logins: {
["cognito-idp." +
env.aws.aws_cognito_region +
".amazonaws.com/" +
env.aws
.aws_user_pools_id]: rootState.UserStore.authSession
.getIdToken()
.getJwtToken(),
},
});
AWS.config.credentials.get(function() {
commit("SET_ACCESS_CREDS", {
updatedAt: new Date(),
accessKeyId: AWS.config.credentials.accessKeyId,
secretAccessKey: AWS.config.credentials.secretAccessKey,
sessionToken: AWS.config.credentials.sessionToken,
});
});
These credentials looks good. I can output them and am happy they don't look broken.
Then I try to use these to access the api gateway using apigClientFactory.newClient
var apigClientFactory = require("aws-api-gateway-client").default;
var client = apigClientFactory.newClient({
accessKey: state.AccessCredentials.accessKeyId,
secretKey: state.AccessCredentials.secretAccessKey,
sessionToken: state.AccessCredentials.sessionToken,
region: env.aws.aws_cognito_region,
invokeUrl: process.env.VUE_APP_APIBASEURL,
});
console.log(client);
client.invokeApi({}, "/testauth", "GET").then(function(data) {
console.log(data);
});
My understanding here is that the SDK takes those values and generates what it needs to send to the API. I've read about it's algorithm, I don't understand it entirely but it implies it creates the access token and wraps it into a request.
The route testauth has the authorizer assigned.
The code for the authorizer is from the AWS doc
// A simple token-based authorizer example to demonstrate how to use an authorization token
// to allow or deny a request. In this example, the caller named 'user' is allowed to invoke
// a request if the client-supplied token value is 'allow'. The caller is not allowed to invoke
// the request if the token value is 'deny'. If the token value is 'unauthorized' or an empty
// string, the authorizer function returns an HTTP 401 status code. For any other token value,
// the authorizer returns an HTTP 500 status code.
// Note that token values are case-sensitive.
exports.handler = function(event, context, callback) {
var token = event.authorizationToken;
switch (token) {
case 'allow':
callback(null, generatePolicy('user', 'Allow', event.methodArn));
break;
case 'deny':
callback(null, generatePolicy('user', 'Deny', event.methodArn));
break;
case 'unauthorized':
callback("Unauthorized"); // Return a 401 Unauthorized response
break;
default:
callback("Error: Invalid token"); // Return a 500 Invalid token response
}
};
// Help function to generate an IAM policy
var generatePolicy = function(principalId, effect, resource) {
var authResponse = {};
authResponse.principalId = principalId;
if (effect && resource) {
var policyDocument = {};
policyDocument.Version = '2012-10-17';
policyDocument.Statement = [];
var statementOne = {};
statementOne.Action = 'execute-api:Invoke';
statementOne.Effect = effect;
statementOne.Resource = resource;
policyDocument.Statement[0] = statementOne;
authResponse.policyDocument = policyDocument;
}
// Optional output with custom properties of the String, Number or Boolean type.
authResponse.context = {
"stringKey": "stringval",
"numberKey": 123,
"booleanKey": true
};
return authResponse;
}
The cloudwatch logs for the authorizer say "Invalid token".
I don't see how it could be anything but that because all it's doing it just seems to be asking if the token says "allow" which seems a bit strange.
Am I by chance just using the wrong function to generate the token in this case?
It's surprising there isn't a comprehensive documentation on this type of requirement as it seems pretty standard.

In the end I am just validating the JWT token from Cognito which I got from here:
https://blog.mantalus.com/posts/apigw-custom-authorizer-cognito/
I do not use the API SDK

Related

Autetificate users in AWS Api Gateway with Cognito

I want to build a rest api using Aws Rest Api Gateway. This will be the new version of a already in production api (hosted on private servers).
On the current version of the api we use oauth2 with grant type password for authentication. This means that a client send his username and pass to a ".../access_token" endpoint from where it gets his token. With this token he can then call the other endpoints.
On the new api version I'm using the AWS Api Gateway with Authorizer. I want to provide acces to resources based on the username & passwords fields
I've created a user pool and added my users there. How do i get authenticated using only api endpoints?
I cannot use Oauth "client credentials" flow since is machine to machine and client secret will have to be exposed.
On Authorization code or Implicit grant i have to ask the user to login on AWS / custom ui and get redirected. So i cannot use these in an Api.
What i'm missing ?
I understand that you need to authenticate your users without using a browser. An idea would be to create a login endpoint, where users will give their username and password and get back a token. You should implement this endpoint yourself. From this question:
aws cognito-idp admin-initiate-auth --region {your-aws-region} --cli-input-json file://auth.json
Where auth.json is:
{
"UserPoolId": "{your-user-pool-id}",
"ClientId": "{your-client-id}",
"AuthFlow": "ADMIN_NO_SRP_AUTH",
"AuthParameters": {
"USERNAME": "admin#example.com",
"PASSWORD": "password123"
}
}
This will give access, id and refresh tokens (the same way as the authorization grant type) to your users. They should be able to use the access token to access resources and the refresh token against the Token endpoint to renew access tokens.
This isn't the common way to authenticate an API and may have some security implications.
I solved this issue by creating custom Lambda in NodeJS 16x with exposed URL, that does Basic Authentication on the Cognito side with stored app client id, user pool id, secret. I attach the code here, but you still need to create lambda layer with Cognito SDK, configure IAM yourself.
const AWS = require('aws-sdk');
const {
CognitoIdentityProviderClient,
AdminInitiateAuthCommand,
} = require("/opt/nodejs/node16/node_modules/#aws-sdk/client-cognito-identity-provider");
const client = new CognitoIdentityProviderClient({ region: "eu-central-1" });
exports.handler = async (event, context, callback) => {
let username = event.queryStringParameters.username;
let password = event.queryStringParameters.password;
let app_client_id = process.env.app_client_id;
let app_client_secret = process.env.app_client_secret;
let user_pool_id = process.env.user_pool_id;
let hash = await getHash(username, app_client_id, app_client_secret);
let auth = {
"UserPoolId": user_pool_id,
"ClientId": app_client_id,
"AuthFlow": "ADMIN_NO_SRP_AUTH",
"AuthParameters": {
"USERNAME": username,
"PASSWORD": password,
"SECRET_HASH": hash
}
};
let cognito_response = await requestToken(auth);
var lambda_response;
if (cognito_response.startsWith("Error:")){
lambda_response = {
statusCode: 401,
body: JSON.stringify(cognito_response) + "\n input: username = " + username + " password = " + password,
};
}
else {
lambda_response = {
statusCode: 200,
body: JSON.stringify("AccessToken = " + cognito_response),
};
}
return lambda_response;
};
async function getHash(username, app_client_id, app_client_secret){
const { createHmac } = await import('node:crypto');
let msg = new TextEncoder().encode(username+app_client_id);
let key = new TextEncoder().encode(app_client_secret);
const hash = createHmac('sha256', key) // TODO should be separate function
.update(msg)
.digest('base64');
return hash;
}
async function requestToken(auth) {
const command = new AdminInitiateAuthCommand(auth);
var authResponse;
try {
authResponse = await client.send(command);
} catch (error) {
return "Error: " + error;
}
return authResponse.AuthenticationResult.AccessToken;
}

aws-sdk send request with IAM Auth

I would like to make my Lambda function comunicate with a API Gateway...
However what I want is that the API Gateway checks the request with AWS_IAM... therefore the lambda function should in some way "sign" the request with a specific IAM token i suppose...
I was reading this https://docs.aws.amazon.com/general/latest/gr/signing_aws_api_requests.html, but I'm not able to find any example on how to sign the request with a specific IAM User
(I've already created a User in IAM with AmazonAPIGatewayInvokeFullAccess and I have both access key and secret key, so I suppose I really need only the "how to sign the request")
We need to build regular nodejs request options with three additional parameters
service: "execute-api" for Api Gateway
region: "us-east-1" AWS Region.
body: postData , typically we pass body req.write, we also need it in options because it is needed for signing.
and finally the aws4.sign(...) is passed to request.
All .sign method does is adds 4 additional headers X-Amz-Content-Sha256, X-Amz-Security-Token, X-Amz-Date and Authorization
var aws4 = require("aws4");
var https = require("https");
const requestBody = { name: "test" };
var postData = JSON.stringify(requestBody);
var options = {
method: "POST",
hostname: "abcdefgh.execute-api.us-east-1.amazonaws.com",
path: "/qa",
headers: {
"Content-Type": "application/json",
},
service: "execute-api",
region: "us-east-1",
body: postData,
maxRedirects: 20,
};
const signedRequest = aws4.sign(options, {
secretAccessKey: "abcadefghijknlmnopstabcadefghijknlmnopst",
accessKeyId: "ABCDEFGHIJKLMNOPQRST",
sessionToken: "this is optional ==",
});
console.log(signedRequest);
var req = https.request(signedRequest, function (res) {
var chunks = [];
res.on("data", function (chunk) {
chunks.push(chunk);
});
res.on("end", function (chunk) {
var body = Buffer.concat(chunks);
console.log(body.toString());
});
res.on("error", function (error) {
console.error(error);
});
});
req.write(postData);
req.end();
since this call is made from lambda whole object with keys can be skipped and simply call aws4.sign(options), it should use from environment variables.

Using instagram as an 'authorizer' for API Gateway - AWS Cognito

I am using Congnito User Pool to perform API Gateway authorization which is working fine. Now, I am trying to add Instagram as one of the login provider, for that I created Custom Authentication Provider in Federated Identities and using code below:
var cognitoidentity = new AWS.CognitoIdentity({ apiVersion: '2014-06-30' });
var params = {
IdentityPoolId: 'us-east-1:7d99e750-.....',
Logins: {
'login.instagram': 'Access-Token-Returned-By-Instagram',
},
TokenDuration: 60
};
cognitoidentity.getOpenIdTokenForDeveloperIdentity(params, function (err, data) {
if (err) {
console.log(err, err.stack);
} else {
console.log(data);
var idParams = {
IdentityId: data['IdentityId'],
Logins: {
'cognito-identity.amazonaws.com': data['Token']
}
};
cognitoidentity.getCredentialsForIdentity(idParams, function (err2, data2) {
if (err2) console.log(err2, err2.stack); // an error occurred
else console.log(data2); // successful response
});
}
});
I am able to get accessToken and sessionToken, however, I am still unable to find a way to get idToken and accessToken which is required by API Gateway to authorize the incoming request.
I tried looking into SDK as well as AWS forum, but I am still unable to find a way to use custom federated identity provider to authorize API Gateway which use a cognito user pool.
I am going to take a whirl at this....
Do you have a cognito user?
No
import { CognitoUser, CognitoUserPool, AuthenticationDetails } from "amazon-cognito-identity-js";
let cognitoUser;
const userPool = new CognitoUserPool({
UserPoolId: config.USER_POOL.pool_Id, //your userpool id
ClientId: config.appClientId, //your appClient
});
const userData = {
Username: 'user name',
Pool: userPool
};
cognitoUser = new CognitoUser(userData);
/* Should now have a cognitoUser */
Authenticate your cognito user
const authenticationData = {
Username : payload.userName,
Password : payload.password,
};
const authenticationDetails = new AuthenticationDetails(authenticationData);
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: function (result) {
const accessToken = result.getAccessToken().getJwtToken();
/* Use the idToken for Logins Map when Federating User Pools with identity pools or when passing through an Authorization Header to an API Gateway Authorizer*/
const idToken = result.idToken.jwtToken;
/*
Do something with
idToken
accessToken
I would write them to a file or encrypt them and store them on the localStorage OR encrypt and save in REDUX store.
*/
},
onFailure: function(err) {
//handle error
},
});

Change password using AWS.CognitoIdentityServiceProvider

I'm trying to figure out how to use the changePassword function of the AWS.CognitoIdentityServiceProvider.
I need to pass the following as params:
{
PreviousPassword: 'STRING_VALUE', /* required */
ProposedPassword: 'STRING_VALUE', /* required */
AccessToken: 'STRING_VALUE'
}
I use this inside a Lambda function, so how do I get hold of the access token? I have the cognitoIdentityPoolId and the cognitoIdentityId to use, but I can't understand which this access token is.
Because there is no admin version of changePassword, you must first authenticate as the user you are trying to impact, then you can call the changePassword routine. It took me a long time to figure this out and no other posts seem to cover the case where you are running a NodeJS lambda function with the admin calls and UserPools, where you want to support "admin" changing of a user password. My (currently working) code is below. Note I believe preventing the admin from changing the user password is a deliberate design decision made by AWS, so I am not sure how long the workaround below will continue to be valid...
const cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider();
// Accept a POST with a JSON structure containing the
// refresh token provided during the original user login,
// and an old and new password.
function changeUserPassword(event, context, callback) {
// Extract relevant JSON into a request dict (This is my own utility code)
let requiredFields = ['old_password','new_password','refresh_token'];
let request = Utils.extractJSON(event['body'], requiredFields);
if (request == false) {
Utils.errorResponse("Invalid JSON or missing required fields", context.awsRequestId, callback);
return; // Abort here
}
// This function can NOT be handled by admin APIs, so we need to
// authenticate the user (not the admin) and use that
// authentication instead.
let refreshToken = request['refresh_token']
// Authenticate as the user first, so we can call user version
// of the ChangePassword API
cognitoidentityserviceprovider.adminInitiateAuth({
AuthFlow: 'REFRESH_TOKEN',
ClientId: Config.ClientId,
UserPoolId: Config.UserPoolId,
AuthParameters: {
'REFRESH_TOKEN': refreshToken
},
ContextData: getContextData(event)
}, function(err, data) {
if(err){
Utils.errorResponse(err['message'], context.awsRequestId, callback);
return // Abort here
} else {
// Now authenticated as user, change the password
let accessToken = data['AuthenticationResult']['AccessToken'] // Returned from auth - diff signature than Authorization header
let oldPass = request['old_password']
let newPass = request['new_password']
let params = {
AccessToken: accessToken, /* required */
PreviousPassword: oldPass, /* required */
ProposedPassword: newPass /* required */
}
// At this point, really just a pass through
cognitoidentityserviceprovider.changePassword(params, function(err2, data2) {
if(err2){
let message = {
err_message: err2['message'],
access_token: accessToken
}
Utils.errorResponse(message, context.awsRequestId, callback);
} else {
let response = {
'success': 'OK',
'response_data': data2 // Always seems to be empty
}
callback(response)
}
});
}
});
}
As You are using the AWS Lambda you dont need to worry about the access token you can simply pass the username and password along with the poolID to the cognito function adminSetUserPassword().this function will update the password easily
const updateCognitoPassword = async(user_name, password) => {
try {
var changePasswordParams = {
Password: password,
Permanent: true,
Username: user_name.trim(),
UserPoolId: constants.user_pool_id
};
let data = await cognitoidentityserviceprovider.adminSetUserPassword(changePasswordParams).promise();
return data;
}
catch (err) {
throw new Error(err);
}
};
I would like to extend on some answers above with a solution that can be used inside the lambda function and also shows how to set the authentication required (using an AWS access key and secret access key.
This is a worked example of a "change password" function created as a lambda.
export async function change_password (event, context, callback) {
context.callbackWaitsForEmptyEventLoop = false;
try {
const { aws_cognito_id, newPassword } = JSON.parse(event.body)
const cognitoIdentityService = new AWS.CognitoIdentityServiceProvider({ apiVersion: '2016-04-18', region: '***AWS REGION GOES HERE***' });
const userPoolId = "***COGNITO USER POOL ID GOES HERE***";
const params = {
Password: newPassword,
Permanent: true,
Username: aws_cognito_id,
UserPoolId: userPoolId
};
AWS.config.region = '**AWS REGION**';
cognitoIdentityService.config.update({
accessKeyId: '***AWS ACCESS KEY***',
secretAccessKey: '***AWS SECRET ACCESS KEY***'
})
let result = await cognitoIdentityService.adminSetUserPassword(params).promise();
return generate_response(200, result)
} catch (err) {
return generate_error(500, err.message)
}
}
The identity pool id and identity id are Cognito federated identities concepts, while the ChangePassword API is a user pools one. They are two different services - think of user pools as an identity provider to your identity pool.
The short version is you can get the access token by signing in with a user in your user pool. Doing so returns an access token, id token, and refresh token. That being said, a common theme is to use the admin versions of the various user pool APIs on Lambda side, since you may not have user credentials there.

AWS API Gateway / Cognito Userpools / Lambdas not able to pass caller credentials

I'm working on an AWS API Gateway implementation with a Lambda backend. I use the API Gateway integration with the Cognito Userpools (fairly new) instead of building a custom authorizer using Lambda (which was the recommended way before it was integrated).
I've created a proof of concept (javascript) that authenticates a user with Cognito and then makes a call to the API Gateway with those credentials. So, basically, the end call to the API Gateway is with the JWT token that I received from Cognito (result.idToken.jwtToken) in the Authorization header. This all works and I can validate that only with this token you can access the API.
All working fine, but now I want to get access to the Cognito identity in my Lambda; for instance the identy id or the name or email. I have read how to map all the parameters, but I'm actually just using the standard 'Method Request Passthrough' template in the integration request. I log all the parameters in the lambda and all the 'cognito' parameters are empty.
I've looked through many similar questions and they all propose to enable the 'Invoke with caller credentials' checkbox on the integration request. That makes perfect sense.
However, this checkbox can only be enabled if you are using AWS_IAM as authorization and not if you have selected your cognito UserPool. So it is just not possible to select it and is actually disabled.
Does anybody know what to do in this case? Is this still work in progress, or is there a reason why you can't enable this and get the cognito credentials in your Lambda?
Many thanks.
If you need to log the user information in your backend, you can use $context.authorizer.claims.sub and $context.authorizer.claims.email to get the sub and email for your Cognito user pool.
Here is the documentation about Use Amazon Cognito Your User Pool in API Gateway
For anyone else still struggling to obtain the IdentityId in a Lambda Function invoked via API-Gateway with a Cognito User Pool Authorizer, I finally was able to use the jwtToken passed into the Authorization header to get the IdentityId by using the following code in my JavaScript Lambda Function:
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()
var identity_ID = credentials.identityId
console.log( identity_ID)
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;
}
After a Cognito User has signed in to my application, I can use the Auth directive of aws-amplify and fetch() in my React-Native app to invoke the lambda function shown above by sending a request to my API-Gateway trigger (authenticated with a Cognito User Pool Authorizer) by calling the following code:
import { Auth } from 'aws-amplify';
var APIGatewayEndpointURL = 'https://5lstgsolr2.execute-api.us-west-2.amazonaws.com/default/-'
var response = {}
async function getIdentityId () {
var session = await Auth.currentSession()
var IdToken = await session.getIdToken()
var jwtToken = await IdToken.getJwtToken()
var payload = {}
await fetch(APIGatewayEndpointURL, {method:"POST", body:JSON.stringify(payload), headers:{Authorization:jwtToken}})
.then(async(result) => {
response = await result.json()
console.log(response)
})
}
More info on how to Authenticate using aws-amplify can be found here https://docs.amplify.aws/ui/auth/authenticator/q/framework/react-native/#using-withauthenticator-hoc