How to get current user username in AWS Lambda? - amazon-web-services

I use AWS Lambda + Cognito (User Pool + Federated Identity) + API Gateway. Users authenticate in WEB application with amazon-cognito-identity-js and invokes API with aws-api-gateway-client. API Gateway methods have AWS_IAM authorizer. How to get username (from User Pool) in Lambda function?

You can use event.identity.username
exports.handler = async (event, _, callback) => {
try {
console.log('event', event);
console.log('event.identity.username', event.identity.username);
const userId = event.identity.username;
console.log('userId', userId);
callback(null, true);
} catch(e) {
console.log(e);
callback(e);
}
};

Modify the request sent to your Lambda function using aws-api-gateway-client to pass the JWT ID Token in the request header.
You may need to ensure your API gateway is configured to forward headers.
apigClient.invokeApi(
params,
pathTemplate,
method,
{ { headers: { IDToken } } },
body);
The ID Token should be used here as its payload contains cognito:username field
The ID Token is gotten after authentication using amazon-cognito-identity-js.
You can parse this field from the header of the request in your lambda handler function.
Verify its signature before trusting the contents of its payload.
import { util } from 'aws-sdk/global';
exports.handler = function(event, context) {
// Parse ID Token from request header
const headers = event.headers;
const idToken = headers.IDToken;
...
};

Related

How to create a NodeJS Authorization middleware on a serverless framework?

I'd like to create a middleware that checks the authorization header, decodes the token and sends the decoded data to the actual function, just like you would by adding userData to the request and using next() on an Express server, so the actual function gets back the decoded data on the req and it can then check what content to display to the user (if any).
I'm using Lambda functions on a serverless framework.
This was the function on my Express NodeJS local server:
const authorizerFunc = async (req, res, next) => {
let token;
try {
if (
req.headers.authorization &&
req.headers.authorization.split(" ")[0] === "Bearer"
) {
token = req.headers.authorization.split(" ")[1];
}
if (!token) {
req.userData = { userId: "", username: "" };
next();
return;
}
const decodedToken = jwt.verify(token, process.env.JWT_SECRET_KEY);
console.log("DECODED TOKEN", decodedToken);
req.userData = {
userId: decodedToken.userId,
username: decodedToken.username,
email: decodedToken.email,
};
next();
} catch (err) {
req.userData = { userId: "", username: "" };
next();
return;
}
};
The question is, how do I create a Lambda function that does this and sends the decoded data to the real function?
Edit: is it bad if I decode the auth token directly in the functions at the very beginning? I don't think it would add huge complexity to them.
Well, I don't have an actuall example for the serverless framework, but i can tell what you should do.
Create an Lambda Function to act as a Amazon API Gateway Lambda authorizer - you can see the documentation here - https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html
make sure you do the validation logic what you have defined, and also return the context object in the response - which you can define your user data
add the Amazon API Gateway Lambda authorizer to the API Gateway - https://docs.aws.amazon.com/apigateway/latest/developerguide/configure-api-gateway-lambda-authorization-with-console.html
If the authorization successful your rest api lambda can access the context object with the user data, which you customize in step 2

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;
}

How to parse JWT Token after user Sign In AWS Cognito

I already deploy a static web site use AWS S3 and use AWS cognito to handle User Sign in.
The web Site is https://www.tianboqing.com
The HTML page have a Button,When user Click the button,the url will redirect to cognito sign in url.
'https://tianboqing.auth.us-east-1.amazoncognito.com/login?
client_id=4fgb77l991egfiubejfqep3e6e&response_type=token&scope=aws.cognito.signin.user.admin&redirect_uri=https://www.tianboqing.com';
const handleClick = (event) => {
window.location.href =
'https://tianboqing.auth.us-east-1.amazoncognito.com/login?...
};
document.querySelector('button').addEventListener('click', handleClick);
When User sign In,The Cognito will return a new url like this:
https://www.tianboqing.com/#access_token=eyJraWQiOiJrNFdXeWpRZXZiSlwvN3JNRUlVMzFHS0p4YmtBZHpXMExwN0xMT0tiS1BHRT0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI2ZmNiNTZhOS1kNDhmLTQyYzItYWUyMi1hMzk0NTllNzc5ZDIiLCJ0b2tlbl91c2UiOiJhY2Nlc3MiLCJzY29wZSI6ImF3cy5jb2duaXRvLnNpZ25pbi51c2VyLmFkbWluIiwiYXV0aF90aW1lIjoxNjM1MzAyMDIxLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAudXMtZWFzdC0xLmFtYXpvbmF3cy5jb21cL3VzLWVhc3QtMV9Zcm9BY0h5R20iLCJleHAiOjE2MzUzMDU2MjEsImlhdCI6MTYzNTMwMjAyMSwidmVyc2lvbiI6MiwianRpIjoiM2EyMjg0YjItNGM2MS00ZDcyLTk4NGYtNzIzNTExMDc3YmZlIiwiY2xpZW50X2lkIjoiNGZnYjc3bDk5MWVnZml1YmVqZnFlcDNlNmUiLCJ1c2VybmFtZSI6IjZmY2I1NmE5LWQ0OGYtNDJjMi1hZTIyLWEzOTQ1OWU3NzlkMiJ9.xKToMEhhSCqSy9ip0ikmdezY9XRz0GIlIdESSThEuLabWL4rFTw1XRQi4Z9OeDLZcmHyemZt0A1o1OvqZLFyrEHLmRAoyg5SIGD2Ic6tExS1PAmmX8Fe3uF7f851DtKMeapxsaNYyaLE3v-_vkJkDwRvUNoz8nOMUCYB3JKJxGPBlMz1yfn-3CXejepKLYeYYDOUaCPmyErfCy84_eQ-ZoEZFd3bH4vZXDNJKFj6W5_C4IZHuIAJveep3dYVq9cLWy3m8BWOAKWVxk6jTt1w0xI5og5jJiEIPn8Ok10WL1s4eEzAN04AYj6e05uzw4Ka_ip4y7VCdnndnTuWAx_5wQ&expires_in=3600&token_type=Bearer
Which contain the access_token to parse.
My question is,How the code can know the new url which contain access_token is return.
Do I need front route or async await or something?
const handleClick = (event) => {
window.location.href =
'https://tianboqing.auth.us-east-1.amazoncognito.com/login?client_id=...';
};
How to get the user token so I can use it to post to dynamodb?
I will suggest to use API Gateway, where you can add Cognito Authorizer for each and every API you deploy to interact with DynamoDB.
Here is the official documentation you can go through to setup your API-Gateway with Cognito, that you want to use to save records in DynamoDB.
You can grab that access token from the Cognito return page and save it as Session Storage in the front-end using JavaScript.
window.sessionStorage.setItem("accessToken", YourAccessToken);
Use the above stored accessToken as Authorization Header when you call that API via AJAX.
var tok = window.sessionStorage.getItem("accessToken")
var xhr = new XMLHttpRequest();
xhr.open("POST", "URL");
xhr.setRequestHeader("Authorization", tok);
xhr.send();
Instead of url forwarding, you can use AWS-Cognito javascript SDK and use Javascript (via AJAX) to authorize a user.
var cognitoUser = new AmazonCognitoIdentity.CognitoUser({
Username: username,
Pool: userPool
});
var authenticationData = new AmazonCognitoIdentity.AuthenticationDetails({
Username: username,
Password: password
});
cognitoUser.authenticateUser(authenticationData, {
onSuccess: function (result) {
// your code here on success
},
onFailure: function (err) {
// your code here if error occurs
}
});

List Users in a Group Cognito - Amplify - Lambda - API REST

I am struggle with AWS amplify to get the users list of a specific cognito group.
I always get this issue : localhost/:1 Access to XMLHttpRequest at 'https://ixa37ulou3.execute-api.eu-central-1.amazonaws.com/dev/users?groupName=xxx' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.
I tried a lot of things, see my last try below :
First Step: creatation of my Lambda function using "amplify add function":
const AWS = require('aws-sdk');
const cognito = new AWS.CognitoIdentityServiceProvider({region:"eu-central-1"});
exports.handler = async (event) => {
const params = {
GroupName: event.groupName,
UserPoolId: 'eu-central-1_xqIZx0wkT',
};
const cognitoResponse = await cognito.listUsersInGroup(params).promise()
const response = {
statusCode:200,
headers:{
"Access-Control-Allow-Origin":"*",
"Access-Control-Allow-Headers":"Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent",
"Access-Control-Allow-Methods": "*",
"Access-Control-Allow-Credentials": true
},
body: JSON.stringify(cognitoResponse),
};
return response;
};
Second Step: Creation of my REST Api using "amplify add api" (pathname: /users)
Third Step: create in API Gateway a new authorizer "using cognito type and link to my user pool, and for the token source : Authorization. And for /users - ANY - Method Request => i added my authorizer in Authorization field.
Fourth Step: creation of my Calling API Function:
async function callApi(){
const user = await Auth.currentAuthenticatedUser()
const token = user.signInUserSession.idToken.jwtToken
console.log({token})
const requestInfo = {
headers:{
Authorization: token
},
queryStringParameters:{
groupName:"MyuserGroup"
}
}
const data = await API.get('apilistusers','/users',requestInfo)
console.log({data})
}
I would very much appreciate any help on this topic, thank you very much.
the current version of amplify has an option to create a lambda function for administrating cognito users, see "amplify add auth" and this page https://docs.amplify.aws/cli/auth/admin/

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