Reputation: 1359
I want to build a rest api using Aws Rest Api Gateway. This will be the new version of an already existing production api (hosted on private servers).
In the current version of the api we use oauth2 with grant type password for authentication. This means that a client sends their username and pass to a ".../access_token" endpoint from where it gets their token. With this token they can then call the other endpoints.
In 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 it's machine to machine and client secret will get 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 am I missing?
Upvotes: 0
Views: 598
Reputation: 31
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;
}
Upvotes: 0
Reputation: 104168
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": "[email protected]",
"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.
Upvotes: 2