AWS-Cognito bot prevention with google reCaptcha - amazon-web-services

My problem is the POST-Request if the user is a bot or human.
It's not possible to send the request form Client-side, otherwise u will get an error on the OPTIONS request:" (response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource)", that's because the request is only possible from Server-Side.
In AWS-Cognito there is a way to use a pre authentication function to check something like that, but I couldn't find a way to get my response element into the function.
So my question is: Is there a way to implement Google recaptcha on AWS Cognito?

You can send this as validationData in the signup request and perform the recaptcha verify logic in a trigger SNS Lambda
Here is a snippet using the AWS Amplify library, excuse the typescript:
Client
class AuthService {
...
public signUp(
emailAddress: string,
phoneNumber: string,
password: string,
recaptchaToken: string
): Observable<ISignUpResult> {
const recaptchaTokenAttributeData: ICognitoUserAttributeData = {
Name: 'recaptchaToken',
Value: recaptchaToken
};
const signupParams: SignUpParams = {
username: emailAddress,
password,
attributes: {
'email': emailAddress,
'phone_number': phoneNumber
},
validationData: [
new CognitoUserAttribute(recaptchaTokenAttributeData)
]
};
return fromPromise(Auth.signUp(signupParams));
}
...
}
Cognito trigger on PreSignUp SNS Lambda code
export async function validateHuman(
event: CognitoUserPoolTriggerEvent,
context: Context,
callback: Callback
): Promise<CognitoUserPoolTriggerHandler> {
try {
const recaptchaToken: string = event.request.validationData.recaptchaToken;
console.log(recaptchaToken);
const isHuman: boolean = await googleRecaptcha.verify({response: recaptchaToken}, (error: Error) => {
if (error) {
console.error(error);
return false;
}
return true;
});
if (!isHuman) {
throw new Error('Not human');
}
callback(null, event);
return;
} catch (error) {
console.error(error);
callback(null, new Response(INTERNAL_SERVER_ERROR, {message: 'Something went wrong'}));
return;
}
}

Related

Twitter_api_v2 reply to a tweet example

Could anyone Show me an example of your finished Parameters and Endpoint for a Twitter Reply maybe with a Screenshot? Because i dont understand exactly what to type in my Params and, do I got to change anything in the Pre-request Script?
Kind regards Alex
For the Params for https://api.twitter.com/2/tweets I tried:
Key : in_reply_to
Value : tweet_id
And the Result was "errors"
"message": "The query Parameters [in_reply_to] is not one of [expantions,tweet.fields,media.fields,poll.fields,place.fields,user.fields]"
"title":"Invalid Request"
"detail": "One or more Parameters to your request Was invalid.",
"type":"https://api.twitter.com/2/problems/invalid-request"
From the twitter's documentation
query parameter ids is required. You missed that parameter.
I will get tweet this demo
https://twitter.com/pascal_bornet/status/1604754709000200193
BY Postman
Full code by node.js
#1 Get an access token by API Key and API secret
#2 Get text by access token
Credential in config.json
{
"API_KEY" : "7hK your API Key GND",
"API_KEY_SECRET" : "Zr4 your API Key secret 0qX0"
}
Save as get-tweet.js
const axios = require('axios')
const config = require('./config.json');
const getAccessToken = async () => {
try {
const resp = await axios.post(
'https://api.twitter.com/oauth2/token',
'',
{
params: {
'grant_type': 'client_credentials'
},
auth: {
username: config.API_KEY,
password: config.API_KEY_SECRET
}
}
);
// console.log(resp.data);
return Promise.resolve(resp.data.access_token);
} catch (err) {
// Handle Error Here
console.error(err);
return Promise.reject(err);
}
};
const getTweetText = async (token, tweet_id) => {
try {
const resp = await axios.get(
`https://api.twitter.com/2/tweets?ids=${tweet_id}`,
{
headers: {
'Authorization': 'Bearer '+ token,
}
}
);
return Promise.resolve(resp.data);
} catch (err) {
// Handle Error Here
console.error(err);
return Promise.reject(err);
}
};
getAccessToken()
.then((token) => {
console.log(token);
getTweetText(token, '1604754709000200193')
.then((result) => {
console.log(result.data[0].text);
})
})
Get Result
$ node get-tweet.js
AAAAAksadf--very long access token in here ----JlIMJIIse
Is this the future of Christmas shopping?
Credit: Nike
#innovation #AR # VR #AugmentedReality https://~~~

Expecting to get "non_field_errors: Unable to log in with provided credentials", but not getting it

Expectation: when wrong login credentials are provided, "non_field_errors: Unable to log in with provided credentials" is returned, such as below (screenshot from a tutorial which I'm following verbatim)
Reality: instead I'm getting the error below.
This gets printed to the console:
POST http://127.0.0.1:8000/api/v1/token/login 400 (Bad Request)
Interestingly I get this same error when I try to create users with passwords that are too short. I'm not having any issues with axios or the server when I provide the right credentials for log in, or use passwords of sufficient length when creating new users. When trying to catch errors such as these that I'm failing to get the expected result.
My code for catching the error is the same as in the tutorial:
methods: {
submitForm() {
axios.defaults.headers.common['Authorization'] = ''
localStorage.removeItem('token')
const formData = {
username: this.username,
password: this.password
}
axios
.post('/api/v1/token/login', formData)
.then(response => {
const token = response.data.auth_token
this.$store.commit('setToken', token)
axios.defaults.headers.common['Authorization'] = 'Token ' + token
localStorage.setItem('token', token)
this.$router.push('/dashboard/my-account')
})
.catch(error => {
if (error.response) {
for (const property in error.response) {
this.errors.push(`${property}: ${error.response.data[property]}`)
}
} else if (error.message) {
this.errors.push('Something went wrong. Please try again!')
}
})
}
}
Is there something in the server settings that I should change?
I'm using Django, rest framework, and djoser.
Don't know if you're using a custom exception handler in Django rest framework but it looks like the issue could be from the way you're handling the error in your frontend application.
You can handle the errors like this.
methods: {
submitForm() {
axios.defaults.headers.common['Authorization'] = ''
localStorage.removeItem('token')
const formData = {
username: this.username,
password: this.password
}
axios
.post('/api/v1/token/login', formData)
.then(response => {
const token = response.data.auth_token
this.$store.commit('setToken', token)
axios.defaults.headers.common['Authorization'] = 'Token ' + token
localStorage.setItem('token', token)
this.$router.push('/dashboard/my-account')
})
.catch(error => {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.log(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.log('Error', error.message);
}
console.log(error.config);
})
}
Can be found here

How can I use Postman to work with AWS Cognito?

I'll start by saying that I have everything working, I'm just not sure how to put all the pieces together so that I can use Postman to test/work with my API.
I am using NestJS as my backend, and am using AWS Cognito to provide an authentication mechanism.
I'm using adminCreateUser to register users as I want the new user to be forced to reset their password the first time they attempt to log in. I'm successfully getting an email with an email & temporary password. I know the next step is to incorporate completeNewPasswordChallenge within my authentication method, but that's where I'm struggling.
Using postman, sending a POST request to my /signup route ressolves to this method in my cognitoService
private async adminCreateUser(
createUserDto: CreateUserDto,
): Promise<AdminCreateUserResponse> {
const { givenName, familyName, email, phone, role } = createUserDto;
const poolData = {
UserPoolId: this.userPool.getUserPoolId(),
ClientId: this.userPool.getClientId(),
};
const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
try {
const cognito = new AWS.CognitoIdentityServiceProvider();
return await new Promise(function (resolve, reject) {
cognito.adminCreateUser(
{
UserPoolId: userPool.getUserPoolId(),
Username: email,
UserAttributes: [
{
Name: 'given_name',
Value: foo,
},
...
],
DesiredDeliveryMediums: ['EMAIL'],
},
function (error, adminCreateUserResponse) {
if (error) {
reject(error);
}
resolve(adminCreateUserResponse);
},
);
});
} catch (error) {
throw new BadRequestException(error.message);
}
}
From that I am successfully getting an email:
Your username is email#example.com and temporary password is YWE#8CEu
From there I have another POST route of /signin that resolves to this method:
async authenticateUser(emailPasswordDto: EmailPasswordDto) {
const { email, password } = emailPasswordDto;
const authenticationDetails =
new AmazonCognitoIdentity.AuthenticationDetails({
Username: email,
Password: password,
});
const userData = {
Username: email,
Pool: this.userPool,
};
this.cognitoUser = new CognitoUser(userData);
return new Promise((resolve, reject) => {
this.cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: function (cognitoUserSession) {
//
},
onFailure: function (err) {
//
},
mfaRequired: function (codeDeliveryDetails) {
//
},
newPasswordRequired: (userAttributes, requiredAttributes) => {
// I'm getting here & don't know how to provide a new password.
},
});
});
}
If I log out userAttributes, this is what I'm seeing which looks good:
{
"email_verified": "false",
"phone_number_verified": "false",
"phone_number": "+12345678900",
"given_name": "First",
"family_name": "Last",
"email": "example#email.com"
}
I have another method in my service, but I'm not sure how to get from A to B.
For example, I don't know how to get to this method from within the previous callback.
async completeNewPasswordChallenge(
completePasswordChallengeDto: CompletePasswordChallengeDto,
) {
const { email, newPassword } = completePasswordChallengeDto;
// get the user
// get the user's attributes
// delete userAttributes.email_verified
// ??
this.cognitoUser.completeNewPasswordChallenge(newPassword, null, null);
}
How can I use Postman to work through the entire auth process? I thought perhaps I could just resolve to forgotPassword, but I think I need to have the email verified before cognito will send that email.

AWS Cognito: email unverified on main account after AdminLinkProviderForUser

I am implementing linking of user accounts in cognito that have the same email. So if someone signs up e.g. with Google and the email is already in cognito, I will link this new account to existing with AdminLinkProviderForUser. I have basically been following this answer here: https://stackoverflow.com/a/59642140/13432045. Linking is working as expected but afterwards email_verified is switched to false (it was verified before). Is this an expected behavior? If yes, then my question is why? If no, then my question is what am I doing wrong? Here is my pre sign up lambda:
const {
CognitoIdentityProviderClient,
AdminLinkProviderForUserCommand,
ListUsersCommand,
AdminUpdateUserAttributesCommand,
} = require("#aws-sdk/client-cognito-identity-provider");
exports.handler = async (event, context, callback) => {
if (event.triggerSource === "PreSignUp_ExternalProvider") {
const client = new CognitoIdentityProviderClient({
region: event.region,
});
const listUsersCommand = new ListUsersCommand({
UserPoolId: event.userPoolId,
Filter: `email = "${event.request.userAttributes.email}"`,
});
try {
const data = await client.send(listUsersCommand);
if (data.Users && data.Users.length) {
const [providerName, providerUserId] = event.userName.split("_"); // event userName example: "Facebook_12324325436"
const provider = ["Google", "Facebook", "SignInWithApple"].find(
(p) => p.toUpperCase() === providerName.toUpperCase()
);
const linkProviderCommand = new AdminLinkProviderForUserCommand({
DestinationUser: {
ProviderAttributeValue: data.Users[0].Username,
ProviderName: "Cognito",
},
SourceUser: {
ProviderAttributeName: "Cognito_Subject",
ProviderAttributeValue: providerUserId,
ProviderName: provider,
},
UserPoolId: event.userPoolId,
});
await client.send(linkProviderCommand);
/* fix #1 - this did not help */
// const emailVerified = data.Users[0].Attributes.find(
// (a) => a.Name === "email_verified"
// );
// if (emailVerified && emailVerified.Value) {
// console.log("updating");
// const updateAttributesCommand = new AdminUpdateUserAttributesCommand({
// UserAttributes: [
// {
// Name: "email_verified",
// Value: "true",
// },
// ],
// UserPoolId: event.userPoolId,
// Username: data.Users[0].Username,
// });
// await client.send(updateAttributesCommand);
// }
/* fix #2 - have no impact on the outcome */
// event.response.autoConfirmUser = true;
// event.response.autoVerifyEmail = true;
}
} catch (error) {
console.error(error);
}
}
callback(null, event);
};
As you can see, I tried passing autoConfirmUser and autoVerifyEmail which had no impact. And I also tried to manually update email_verified after calling AdminLinkProviderForUser which also did not help. So I think email_verified is set to false only after the lambda is finished.
I've actually find a solution for this issue! It's tricky, but it works quite well.
The main problem i was having is the fact that pre/post auth lambdas do not trigger if the user is logging in with the connected provider account. Nor does the post confirmation lambda triggers after the signUp link. On top of that, if you manually try to alter the provider's user inside cognito, you still end up with the email_verified = false.
So whats the solution here?
Well, it's actually two in one.
1. Attribute Mapping
Some of the providers supported by Cognito already have an 'email verified' attribute we can directly map in the Attribute Mapping section. That is the case with Google. Simply map it to Cognito's attribute and you are done!
2. PreToken trigger
For the other providers, mainly Facebook (i didnt use any provider other than Facebook and Google, but it should all work the same), that don't natively have the email verified attribute, the only way i could find to properly enforce it to be verified was through the pre-token trigger. The logic goes: once the lambda is invoked, verify if the user getting authenticated have a provider linked to it and, at the same time, have its email_verified options to false. If you meet this condition, update the user with the adminUpdateUserAttributes method before returning to Cognito. That way, it doesnt matter if any subsequent provider login flips the attribute back to false, this lambda ensures it's flipped back on.
If you want a sample code for the solution, here's mine pre-token handler:
const AWS = require('aws-sdk');
class PreTokenHandler {
constructor({ cognitoService }) {
this.cognitoService = cognitoService;
}
async main(event, _context, callback) {
const {
userPoolId,
userName: Username,
request: { userAttributes: { identities, email_verified } }
} = event;
const emailVerified = ['true', true].some(value => value === email_verified);
if (!identities?.length || emailVerified) return callback(null, event);
await this.cognitoService.adminUpdateUserAttributes({
UserPoolId: userPoolId,
Username,
UserAttributes: [{ Name: 'email_verified', Value: 'true' }]
}).promise();
callback(null, event);
}
}
const cognitoService = new AWS.CognitoIdentityServiceProvider({ apiVersion: '2016-04-18' });
const handler = new PreTokenHandler({ cognitoService });
module.exports = handler.main.bind(handler);
I've added the 'true' x true check to ensure i'm safe on possibly inconsistencies on the attribute value, although it's just being extra safe and probably not necessary.
My answer is the same as #fabio, here's how you do it in aws-sdk-js-v3:
const {
CognitoIdentityProviderClient,
AdminUpdateUserAttributesCommand,
} = require('#aws-sdk/client-cognito-identity-provider')
const client = new CognitoIdentityProviderClient({
region: process.env.REGION,
})
exports.handler = async(event, context, callback) => {
try {
const {
userPoolId,
userName,
request: {
userAttributes: { email_verified }
}
} = event
if (!email_verified) {
const param = {
UserPoolId: userPoolId,
Username: userName,
UserAttributes: [{
Name: 'email_verified',
Value: 'true',
}],
}
await client.send(new AdminUpdateUserAttributesCommand(param))
}
callback(null, event)
}
catch (err) {
console.error(err)
}
}

How can I tell a AWS Lambda Function to redirect based off the environment?

So currently I'm using AWS Lambdas as triggers for my Cognito Passwordless authentication. For the create_auth_challenge trigger I have an AWS Lambda function that sends a link to the user to redirect them somewhere based on the environment. The only problem is that I'm not sure how to dynamically tell the function which environment the auth request is coming from.
AWS.config.update({ region: 'us-west-2' });
const SES = new AWS.SES();
exports.handler = async (event,context) => {
console.log("HERE: ", event,context);
let secretLoginCode;
if (!event.request.session || !event.request.session.length) {
// Generate a new secret login code and send it to the user
secretLoginCode = Date.now().toString().slice(-4);
try {
if ('email' in event.request.userAttributes) {
const emailResult = await SES.sendEmail({
Destination: { ToAddresses: [event.request.userAttributes.email] },
Message: {
Body: {
Html: {
Charset: 'UTF-8',
Data: `<html><body><p>This is your secret login code:</p>
<h3>Your magic link: ${INSERT ENVIRONMENT HERE}/api/auth/cognito/verify?email=${event.request.userAttributes.email}&code=${secretLoginCode}</h3></body></html>`
},
Text: {
Charset: 'UTF-8',
Data: `Your magic link: ${INSERT ENVIRONMENT HERE}/api/auth/cognito/verify?email=${event.request.userAttributes.email}&code=${secretLoginCode}`
}
},
Subject: {
Charset: 'UTF-8',
Data: 'Your magic link'
}
},
Source: 'Company <no-reply#company.com>'
}).promise();
}
} catch (error) {
console.log(error)
}
} else {
// re-use code generated in previous challenge
const previousChallenge = event.request.session.slice(-1)[0];
secretLoginCode = previousChallenge.challengeMetadata.match(/CODE-(\d*)/)[1];
}
// Add the secret login code to the private challenge parameters
// so it can be verified by the "Verify Auth Challenge Response" trigger
event.response.privateChallengeParameters = { secretLoginCode };
// Add the secret login code to the session so it is available
// in a next invocation of the "Create Auth Challenge" trigger
event.response.challengeMetadata = `CODE-${secretLoginCode}`;
return event;
};```
This is a magic link authentication by the way.