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;
}
Related
var normalAuth = async (username, password) => {
try{
var user = await Auth.signIn(username, password)
console.log(user)
if(user.challengeName === "NEW_PASSWORD_REQUIRED"){
console.log("assigning new password!")
user = await Auth.completeNewPassword(user, "password") // give the user a new password. this would NOT be included in any kind of production code, its only here as a work around
console.log(user)
} // work around. users created in the console are only given temporary passwords, and as such have no authorization
// setting a new password will fix this
console.log(await Auth.currentCredentials()) // THIS THROWS AN ERROR?!?!
setAuthState(false) // auth is no longer in progress
}
catch(error){
console.error(error)
}
}
I'm trying to grab the tokens from my Amplify user after authenticating but I'm receiving the error
No Cognito Identity pool provided for unauthenticated access
This is pretty strange because I'm not using an identity pool at all. My baseline assumption was that I shouldn't need to provide an identity pool ID at all in my amplify config.
config looks like this
var amplifyConfig = {
Auth: {
mandatorySignIn: false,
region: "us-east-1",
userPoolId: "mypoolid",
userPoolWebClientId: "myclientid"
}
}
The really weird part is that the tokens are actually there in user after I authenticate. It's only when I try and retrieve them this way specifically that an error is thrown.
Any idea what's going on here?
I'm trying to implement social login using Microsoft account in AWS Cognito User Pools.
I followed documentation and the solution mentioned in this thread:
https://forums.aws.amazon.com/thread.jspa?threadID=287376&tstart=0
My problem is with setting the issuer to allow multiple tenants.
This issuer works only for private accounts:
https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0
This issuer works only for accounts in our directory (tenant):
https://login.microsoftonline.com/AZURE_ACTIVE_DIRECTORY/v2.0
This issuer does not work at all. I get bad issuer error or bad request after sign in with Microsoft:
https://login.microsoftonline.com/common/v2.0
I need to have one oidc provider that will work for any Microsoft account (all tenants) is that even possible?
If I set issuer tenant to common in the AWS Cognito oidc config, then this starts the correct Microsoft flow, but I assume the check for issuer in Cognito fails because Microsoft always returns the specific tenant id inside the jwt token as part of the issuer.
Additional info from microsoft documentation I have checked:
https://learn.microsoft.com/de-de/azure/active-directory/develop/v2-protocols-oidc
https://learn.microsoft.com/de-de/azure/active-directory/develop/id-tokens
I am a colleague of Dragan and after a lot of trying we have found a solution in our team that actually works. Just to notice that we actually had access to premium AWS and Microsoft support, but they couldn't help us. The AWS Cognito Team is aware of the issue, but seems like it has no priority - since nearly a year there hasn't been any fix.
Explanation of the flow
We authenticate against microsoft using their javascript library msal in the frontend (no Cognito involved). We receive a JWT token and use this one to create a normal Cognito user in the user pool. The e-mail is read from the microsoft token and the password is autogenerated with a secure random (as long as possible). Additionally we send the microsoft token as custom user attribute. In PreSignUp Lambda we auto activate the user if the microsoft token is valid, so no password verify e-mail is sent to the user. Back in the frontend we use the amplify custom auth challenge signIn with the e-mail we have cached in the frontend. Now we go through DefineAuthChallenge and then CreateAuthChallenge. CreateAuthChallenge doesn't do anything as the microsoft token is our challenge and doesn't need to be created. Back in the frontend we call CustomChallenge containing sessionKey and microsoft token. We are now in VerifyChallenge Lambda where we verify the microsoft token itself using open source JWT libraries. The flow goes back through DefineAuthChallenge where we only allow one try. Finally the user receives the Cognito tokens from Cognito.
The following snippets are the full code snippets for the Lambdas. I had to remove some specific stuff from our project so hopefully didn't break anything while doing so. All files are the index.js and no additional files are needed for the Lambdas. You could for sure outsource some duplicated code, which we haven't done yet. The FE code is not included here.
PreSignUp Lambda
const jwksClient = require('jwks-rsa');
const jwt = require('jsonwebtoken');
const client = jwksClient({
jwksUri: 'https://login.microsoftonline.com/common/discovery/v2.0/keys'
});
const options = {
algorithms: ['RS256']
};
function getKey(header, callback) {
client.getSigningKey(header.kid, function (err, key) {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
const verifyMicrosoftToken = async (jwt, token, key) => {
if (!token) return {};
return new Promise((resolve, reject) =>
jwt.verify(token, key, options, (err, decoded) => err ? reject({}) :
resolve(decoded))
);
};
exports.handler = async (event) => {
const email = event.request.userAttributes.email.toLowerCase();
//verify microsoft and auto enable user
if (event.request.userAttributes['custom:msalIdtoken']) {
const token = await verifyMicrosoftToken(
jwt, event.request.userAttributes['custom:msalIdtoken'], getKey
);
const emailFromToken = token.email !== undefined ? token.email : token.preferred_username;
if (token && emailFromToken.toLowerCase() === email) {
event.response.autoConfirmUser = true;
event.response.autoVerifyEmail = true;
}
}
return event;
};
DefineAuthChallenge Lambda
exports.handler = (event, context, callback) => {
if (event.request.session &&
event.request.session.length > 0 &&
event.request.session.slice(-1)[0].challengeName === 'CUSTOM_CHALLENGE' &&
event.request.session.slice(-1)[0].challengeResult === true){
console.log("Session: ", event.request.session);
event.response.issueTokens = true;
event.response.failAuthentication = false;
} else {
event.response.failAuthentication = false;
event.response.issueTokens = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';
}
// Return to Amazon Cognito
callback(null, event);
};
CreateChallenge Lambda
exports.handler = (event, context, callback) => {
if (event.request.challengeName === 'CUSTOM_CHALLENGE') {
event.response.publicChallengeParameters = {};
event.response.publicChallengeParameters.dummy = 'dummy';
event.response.privateChallengeParameters = {};
event.response.privateChallengeParameters.dummy = 'dummy';
event.response.challengeMetadata = 'MICROSOFT_JWT_CHALLENGE';
}
callback(null, event);
};
VerifyAuthChallenge Lambda
const AWS = require('aws-sdk');
const jwksClient = require('jwks-rsa');
const jwt = require('jsonwebtoken');
const client = jwksClient({
jwksUri: 'https://login.microsoftonline.com/common/discovery/v2.0/keys'
});
const options = {
algorithms: ['RS256']
};
function getKey(header, callback){
client.getSigningKey(header.kid, function(err, key) {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
exports.handler = (event, context, callback) => {
if(event.request.challengeAnswer){
jwt.verify(event.request.challengeAnswer, getKey, options, function(err, decoded) {
if(decoded){
const email = decoded.email !== undefined ? decoded.email : decoded.preferred_username;
if (email.toLowerCase() === event.request.userAttributes['email'].toLowerCase()) {
event.response.answerCorrect = true;
// it is necessary to add this group to user so in BE we can resolve microsoft provider
const cognitoIdentityServiceProvider = new AWS.CognitoIdentityServiceProvider();
var params = {
GroupName: "CUSTOM_MICROSOFT_AUTH",
UserPoolId: event.userPoolId,
Username: event.userName
};
cognitoIdentityServiceProvider.adminAddUserToGroup(params, function (err) {
if (err) {
console.log("Group cannot be added to the user: " + event.userName, err);
}
callback(null, event);
});
}
}
if(err){
console.log(err);
}
});
}else{
event.response.answerCorrect = false;
callback(null, event);
}
};
Frontend (Angular component)
ngOnInit() {
// after microsoft successful sign in we need to continue to cognito authentication
this.authMsalService.handleRedirectCallback((authError, response) => {
if (authError) {
this.showLoginError = true;
return;
}
this.signUpOrSignInWithMicrosoftToken(response.idToken.rawIdToken);
});
}
onSignInWithProvider(provider: string) {
this.cognitoService.clearAuthData();
if (provider === SINGLE_SIGN_ON_PROVIDER.MICROSOFT) {
this.authMsalService.loginRedirect({
scopes: ['user.read', 'email'],
});
} else {
const options: FederatedSignInOptions = {provider: CognitoHostedUIIdentityProvider[GeneralUtils.capitalize(provider)]};
this.socialSignIn(options);
}
}
private socialSignIn(options: any): void {
Auth.federatedSignIn(options).catch(() => {
this.showLoginError = true;
this.uiBlockerService.setIsUiBlocked(false);
});
}
private signUpOrSignInWithMicrosoftToken(microsoftIdToken: string) {
this.uiBlockerService.setIsUiBlocked(true);
const attributes = {};
const userName: string = this.authMsalService.getAccount().userName.toLowerCase();
attributes['email'] = userName;
attributes['custom:msalIdtoken'] = microsoftIdToken;
if (this.authMsalService.getAccount().idToken['family_name']) {
attributes['family_name'] = this.authMsalService.getAccount().idToken['family_name'];
}
if (this.authMsalService.getAccount().idToken['given_name']) {
attributes['given_name'] = this.authMsalService.getAccount().idToken['given_name'];
}
Auth.signUp({
username: userName,
password: SSOUtils.getSecureRandomString(20),
attributes: attributes
}).then(user => {
// register
// after successfully signup we need to continue with authentication so user is signed in automatically
this.authenticateWithMicrosoftToken(microsoftIdToken);
}).catch(error => {
// login
// if user is already registered we continue with sign in
if (error.code === 'UsernameExistsException') {
this.authenticateWithMicrosoftToken(microsoftIdToken);
}
this.uiBlockerService.setIsUiBlocked(false);
});
}
private authenticateWithMicrosoftToken(microsoftIdToken: string) {
const userName: string = this.authMsalService.getAccount().userName.toLowerCase();
Auth.signIn(userName).then(cognitoUser => {
// after sign in is started we need to continue with authentication and we sent microsft token
Auth.sendCustomChallengeAnswer(cognitoUser, microsoftIdToken);
});
}
Here are some links we used
https://aws.amazon.com/blogs/mobile/implementing-passwordless-email-authentication-with-amazon-cognito/
https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-challenge.html
https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-overview
PostScript
If you find any security relevant issue in this code, please contact me privately and our company will show some appreciation ($) depending on severity.
Root cause of the issue:
When we integrate Microsoft login via OIDC, we have a couple of options based on our requirement.
In the case where only the users with work or school accounts from Azure AD can sign in to the application then, we have to refer to https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration
Also in the case where any user who has a Microsoft account (work or school Azure AD accounts, OR personal - outlook, live, etc) can sign in to the application then, we have to refer to
https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
In those metadata files, we can see that the issuer is https://login.microsoftonline.com/{tenantid}/v2.0.
So basically, depending on the end user’s Azure AD tenant, the id_token issued by Azure AD will have a different value for issuer (iss) claim.
Which means the iss claim is dynamically change for each user.
Right now, that dynamic behavior isn’t being supported by Cognito.
In Cognito, under the OIDC Identity provider configurations, we have to specify the issuer manually and we can only specify one.
So Cognito can not properly validate the id_token issued by Azure AD. and it returns an error saying Bad id_token issuer.
Another Workaround:
There are identity providers which support this dynamic iss claim behavior of Azure AD. (Auth0, Azure AD B2C, etc). So we can select one of them and configure that to communicate with Microsoft (Azure AD) via OIDC. Then add that IDP as an OIDC identity provider in Cognito. Basically we place that IDP in between Cognito and Microsoft (Azure AD).
I avoided this (tenancy/issuer) problem by avoiding usage of the userpool, and directly interacting with the azure endpoints https://login.microsoftonline.com/common/oauth2/v2.0/authorize etc..
I still have to use the identitypool, to map to IAM role.
Understandably, this is more work than having the userpool handle token stuff, but this is the only way I found it to work with all azure ad accounts.
I have created an ASP .Net Core application that authenticates against Cognito.
My Authentication Controller looks like:
public class AuthenticationController : Controller
{
[HttpPost]
[Route("api/signin")]
public async Task<ActionResult<string>> SignIn(User user)
{
var cognito = new AmazonCognitoIdentityProviderClient(RegionEndpoint.APSoutheast2);
var request = new AdminInitiateAuthRequest
{
UserPoolId = "ap-southeast-2_mypoolid",
ClientId = "myclientid",
AuthFlow = AuthFlowType.ADMIN_USER_PASSWORD_AUTH
};
request.AuthParameters.Add("USERNAME", user.Username);
request.AuthParameters.Add("PASSWORD", user.Password);
var response = await cognito.AdminInitiateAuthAsync(request);
return Ok(response.AuthenticationResult);
}
}
Startup.ConfigureServices looks like:
services.AddAuthentication("Bearer")
.AddJwtBearer(options =>
{
options.Audience = "client key";
options.Authority = "https://cognito-idp.ap-southeast-2.amazonaws.com/ap-southeast-poolid";
});
I have included this in case it is something I have done in the above.
My Bearer tokens work fine. I auth against Cognito and get my access/id/refresh tokens.
Being new to Cognito and AWS in general and being curious, I ran my tokens at https://jwt.io/ and found that they contained my poolId and clientId. I was under the impression these are to be hidden away with the utmost security.
Is this normal or is it something I have done. I feel that maybe this shouldn't be exposed so easily?
Yes its normal, Only the secret should not be exposed.
I tried with the aws-sdk-react-native module:
https://github.com/awslabs/aws-sdk-react-native
The configuration took some time, but thanks to this links I could i.e. list the topics:
https://github.com/awslabs/aws-sdk-react-native/issues/35
https://github.com/awslabs/aws-sdk-react-native/blob/master/SNS/IntegrationTests/SNSTests.js
The test includes a sample how to subscribe to get emails but not how to get notifications in the app. I don't know how to get the platformEndpoint, the PlatformApplicationArn and the deviceToken.
endPoint = sns.createPlatformEndpoint({
PlatformApplicationArn: '{APPLICATION_ARN}',
Token: '{DEVICE_TOKEN}'
})
...
var subscribeRequest= {
"Protocol":"application",
"TopicArn":topicARN,
"Endpoint":endPoint
}
try{
await AWSSNS.Subscribe(subscribeRequest);
}catch(e){
console.error(e);
shouldResolve = false;
return shouldResolve;
}
Are there any samples for this?
I'm also looking for an authentication sample.
Would it be easier to use firebase?
Thanks
I have used GCM over SNS to send notifications. Here are the steps I went through assuming you already set up GCM and added required libraries from AWS React Native SDK:
First create a SNS app from AWS:
Then you need to create Federated Identity through Cognito service of AWS. This is required for sending your device token from mobile app to AWS SNS app. Choose Manage Federated Identities
Then create your pool, don't forget to check Enable Access to unauthenticated identities
When you create the pool you will need to create IAM Roles for unauthenticated and authenticated roles of that pool. AWS will help you create new roles for that but you need to go to IAM Roles menu and attach AmazonSNSFullAccess to created roles, otherwise from the mobile app you won't able to send device token.
After doing these steps you will able send your device token using Amazon's React Native SDK. I have written a helper class for sending token to AWS SNS as suggested here:
class AWSUtility {
constructor() {
const region = "us-west-1"; //change it with your region
const IDENTITY_POOL_ID = "pool id created from Federated Identities"
AWSCognitoCredentials.initWithOptions({region, identity_pool_id: IDENTITY_POOL_ID});
AWSSNS.initWithOptions({region});
}
addTokenToAWSSNS(token, snsEndpointARN) {
const applicationArn = "change with SNS application Amazon resource name";
return Promise.try(() => {
if (!snsEndpointARN) {
return this.createPlatformEndpoint(token, applicationArn);
} else {
return AWSSNS.GetEndpointAttributes({EndpointArn: snsEndpointARN})
.then((result) => {
const {Attributes = {}} = result;
const {Token, Enabled} = Attributes;
const updateNeeded = Token !== token || Enabled !== 'true';
if (updateNeeded) {
return this.updateEndpoint(token).then(() => result.EndpointArn);
}
return snsEndpointARN;
})
.catch(() => {
this.createPlatformEndpoint(token, applicationArn)
});
}
});
}
updateEndpoint(snsEndpointARN, token) {
//AWS is returning error saying that it requires 6 params to update endpoint, if anyone has any idea about it let me know please
return AWSSNS.SetEndpointAttributes({EndpointArn: snsEndpointARN, Attributes: {Token: token, Enabled: true}});
}
createPlatformEndpoint(token, applicationArn) {
return AWSSNS.CreatePlatformEndpoint({Token: token, PlatformApplicationArn: applicationArn})
.then(result => result.EndpointArn)
.catch((error = {}) => {
console.log(error);
});
}
}
export default new AWSUtility();
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