Re-captcha token verification fails in AWS, but not in vercel - node.js

Below is my Next.js (backend API) code to verify recaptcha token (created from the client side) and send a mail.
import { NextApiRequest, NextApiResponse } from "next";
import NextCors from 'nextjs-cors';
import { recaptchaAxios } from "../../axios/axiosBackend";
import sendGridMail from '#sendgrid/mail';
sendGridMail.setApiKey(process.env.SENDGRID_API_KEY);
interface FormData {
contactName: string;
contactEmail: string;
contactPhone: string;
contactSubject: string;
contactMessage: string;
token: string;
}
export default async (req: NextApiRequest, res: NextApiResponse) => {
await NextCors(req, res, {
// Options
methods: ['GET','POST'],
origin: '*',
optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
});
const formData: FormData = req.body;
console.log("form Data >>>>>>>>>>>>>>",formData)
const human = await validateHuman(formData.token);
if (!human) {
res.status(400);
return res.json({ success: false, errors: ["You are not authenticated"] });
}
const message = {
to: process.env.SENDGRID_MAIL_RECEIVER,
from: process.env.SENDGRID_MAIL_SENDER, // Change to your verified sender
subject: formData.contactSubject,
text: `Name: ${formData.contactName}\n
Contact: ${formData.contactPhone} \n
Email: ${formData.contactEmail} \n
Message: ${formData.contactMessage}`,
html: `Name: ${formData.contactName}
Contact: ${formData.contactPhone}
Email: ${formData.contactEmail}
Message: ${formData.contactMessage}`,
}
try {
await sendGridMail.send(message);
res.status(200);
return res.json({ success: true, errors: [] });
} catch (error) {
console.log(error);
res.status(500);
return res.json({ success: false, errors: ['Error occured while trying to send your details. Please contact your Administrator.']});
}
};
async function validateHuman(token: string): Promise<boolean> {
const secret = process.env.RECAPTCHA_SECRET_KEY;
const response = await recaptchaAxios.post(`/siteverify?secret=${secret}&response=${token}`,{}, {});
const success = response.data['success'];
console.log("server siteverify >>>>>>>>>>>>>",response);
return success;
}
recaptchaAxios has the baseURL as below
const recaptchaAxios = axios.create({
baseURL: `https://www.google.com/recaptcha/api`,
});
I have deployed the same code in vercel as well as using AWS Amplify.
In vercel when called to the above mail API, the Recaptcha token is verified and the mail is sent.
But unfortunately in AWS it gives the error
{ success: false, errors: ["You are not authenticated"] }
I have added all the environment variables in AWS which I have in vercel and the values are the same.
All the domains are added in reCaptch v3 console for the site.
So at this point I am stuck on why in AWS gives the error, but not vercel for the same code base
Is there anything that I am missing in AWS??
Cheers

My first pointer would be to console.log the environment variables on script load, also each time the recaptcha validation is triggered. This way you can be sure the ENV vars are all loaded correctly. You would be suprised to have a small case sensitivity typo, leave you without an important env variable.
Otherwise, I would check if I need to allow outgoing traffic (firewall rules) on AWS amplify, but this is less common, since AWS Amplify spawns a public site.

Issue was in the below code
const secret = process.env.RECAPTCHA_SECRET_KEY;
Even though the RECAPTCHA_SECRET_KEY was available in the environment variables in AWS, it was not accessible.
Fix was to introduce this key in next.config.js file
module.exports = {
images: {
domains: [],
},
env: {
RECAPTCHA_SECRET_KEY: process.env.RECAPTCHA_SECRET_KEY,
},
};
This solved the problem

Related

Server-side user verification with AWS Cognito user pool via MFA verification code (without password)

On the server side using NodeJS + NestJS, TS: 4.7.4, "aws-sdk": "^2.1138.0".
Trying to send a request to AWS Cognito, to obtain a verification code on mobile phone.
It's far away from achieving SMS quota.
An example of my method from the service:
async sendVerificationCode(phoneNumber: string) {
const params = {
AuthFlow: 'USER_SRP_AUTH',
ClientId: process.env.AWS_COGNITO_CLIENT_ID,
// UserPoolId: process.env.AWS_COGNITO_USER_POOL,
AuthParameters: {
USERNAME: phoneNumber,
SRP_A: generateSRPA(),
},
};
console.debug('=========== params: ', params);
try {
const result = await this.cognitoIdentityServiceProvider
.initiateAuth(params)
.promise();
console.log('=========== result: ', result);
return result;
} catch (error) {
if (error instanceof Error) {
console.debug('=========== Error: ', error.message);
throw error;
}
}
}
example of generation SRP_A:
const N_HEX ='EEAF0AB9ADB38DD69C33F80AFA...';
export function generateSRPA() {
const random = randomBytes(32);
const randomHex = random.toString('hex');
const srpA = createHash('sha256').update(randomHex).digest('hex');
return createHash('sha256').update(srpA).update(N_HEX).digest('hex');
}
Now requests are successfully sending to AWS and getting response:
=========== result: {
ChallengeName: 'PASSWORD_VERIFIER',
ChallengeParameters: {
SALT: '4e9b...',
SECRET_BLOCK: '4x1k...',
SRP_B: '161d...',
USERNAME: 'b1d9...',
USER_ID_FOR_SRP: 'b1d9...'
}
}
But I'm not receiving verification code on my phone.
In the same time with the same user pool and same mobile phone all the flow works fine on mobile app which is connected to Cognito.

Would my environement variables be exposed to the browser?

I have coded an e-mail contact form and was wondering if refactoring my code outside of Next.js' api folder could compromise my API key.
Unless mistaken — I am aware that environmental variables aren't exposed to the browser if they:
Aren't prefixed by NEXT_PUBLIC_;
Are used within the api folder of the framework;
Are used within the getStaticProps,getStaticPaths and getServerSideProps server-side functions.
However, what would happen if I "mentioned" my process.env.SENDGRID_API_KEY outside of the api folder in a utilities folder as shown below? Would the browser have any way of accessing it?
root
|
├───pages
│ └───api
| └─────myRoute.ts
├───.env.local
|
├───utilities
│ └───myFunction.ts
│
/utilities/myFunction.ts
const sendgrid = require("#sendgrid/mail");
sendgrid.setApiKey(process.env.SENDGRID_API_KEY); // WILL THIS BE AN ISSUE?
export const sendEmail = async (
name: string,
email: string,
message: string
) => {
const incomingEmail = `
From: ${name}\r\n
Email: ${email}\r\n
Message: ${message}
`;
await sendgrid.send({
to: process.env.RECEIVING_EMAIL, // WILL THIS BE AN ISSUE?
from: process.env.SENDGRID_SENDER_EMAIL, // WILL THIS BE AN ISSUE?
subject: "New message!",
text: incomingEmail,
html: incomingEmail.replace(/\r\n/g, "<br>"),
});
};
pages/api/myRoute.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { sendEmail, acknowledgeReceipt } from "../../utilities/sendGrid"; // Refactors
type Data = {};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
const { lang, name, email, message, token } = req.body;
// Irrelevant validation logic...
try {
// Sending e-mail & acknowledging receipt
await Promise.all([
sendEmail(name, email, message),
acknowledgeReceipt(name, email, lang),
]);
return res.status(200).json({ status: "success", msg: "E-mail sent!" });
} catch (err) {
// Irrelevant
}
}
It looks fine. Server side code like the email sending utility should only be imported into api files, not into frontend component files, which it looks like you are already doing that correctly.

How to validate header in hapi

I want to add a middleware to check the header values. if the expected do not present in the header then I want throw an error on API response. Following code is throwing error and I can see it in the console but I want to send this error to the user.
const server = new Server({
host: config.get('host'),
port: config.get('port' )
});
await server.register(require('#hapi/inert')); // eslint-disable-line #typescript-eslint/no-var-requires
server.register({
plugin: AuthService
})
server.route(
// mapping route paths with context path
routes.map((datum) => {
return {
...datum,
path: `${config.get('basePath')}${datum.path}`
};
})
);
plugin
import { notFound } from '#hapi/boom';
export const AuthService = {
name: 'authService',
version: '1.0.0',
register: async function (server: any, options: any) {
throw notFound(`Unauthorized user not found`);
}
};
Well, I think you are thinking it the express way too much. You should look at the authentication part of Hapi.
You can create a custom strategy and return an error if the header is not present.
Also, you can check how to use a Joi.schema() for all your routes.

Authentication failure after page reload (Vue, Expressjs with Nodejs)

Current situation
I am developing nodejs backend server and vue frontend application, which is run under different port(localhost:3000 and localhost:8080). With purpose to enable CORS connection, I configured devServer proxy from vue.config.js file.
vue.config.js
module.exports = {
devServer: {
proxy: {
'/users': {
target: 'http://127.0.0.1:3000/users',
changeOrigin: true,
pathRewrite: {
'^/users':''
}
},
'/tasks': {
target: 'http://127.0.0.1:3000/tasks',
changeOrigin: true,
pathRewrite: {
'^/tasks': ''
}
}
}
},
outputDir: '../backend/public'
}
and technically used cors.js to enable request to backend server, which was implemented by expressjs.
I am sending the request with vue component to retrieve data from backend server. It works properly from fetching data from server, and my goal is to make the same behavior when I reload page. However, whenever I reload same page, it keep showing 401 http response status set by the backend code written by myself.
enter image description here
Research and Trial til now
Before I go on the attempts I have tried, I should introduce mandatory codes to be operated at first. Somehow this is at least explanations in which vuex actions using axios, axios using backend routers eventually.
tasks.module.js
import axios from "axios"
import authHeader from '../../services/auth-header'
export const tasks = {
state: {
tasks: []
},
getters: {
allTasks: (state) => state.tasks
},
actions: {
async fetchTasks({ commit }) {
const response = await axios.get('http://127.0.0.1:3000/tasks', {headers: authHeader()})
commit('setTasks', response.data)
axios.defaults.headers.common['Authorization'] = authHeader()
},
async addTask({ commit }, description) {
const response = await axios.post('http://127.0.0.1:3000/tasks', { description, completed: false}, {headers: authHeader()})
commit('newTask', response.data)
},
async updateTask({ commit }, updTask) {
const response = await axios.patch('http://127.0.0.1:3000/tasks/'+updTask.id, updTask, {headers: authHeader()})
commit('updateTask', response.data)
}
},
mutations: {
setTasks: (state, tasks) => (state.tasks = tasks),
newTask: (state, task) => state.tasks.unshift(task),
updateTask: (state, updTask) => {
let updates = Object.keys(updTask)
updates.forEach((update) => {
state.task[update] = updTask[update]
})
}
}
}
TaskManager.vue
<template>
<div class="container">
<div class="jumbotron">
<h3>Task Manager</h3>
<AddTask/>
<Tasks/>
</div>
</div>
</template>
<script>
import Tasks from './components/Tasks'
import AddTask from './components/AddTask'
export default {
name:'TaskManager',
components: {
Tasks,
AddTask
}
}
</script>
Tasks.vue
<template>
<div>
<div>
<div class="legend">
<span>Double click to mark as complete</span>
<span>
<span class="incomplete-box"></span> = Incomplete
</span>
<span>
<span class="complete-box"></span> = Complete
</span>
</div>
</div>
<div class="tasks">
<div
#dblclick="onDblClick(task)"
v-for="task in allTasks"
:key="task.id"
class="task"
v-bind:class="{'is-completed':task.completed}">
{{task.description}}
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
name: "Tasks",
methods:{
...mapActions(['fetchTasks', 'updateTask']),
onDblClick(task) {
const updTask = {
id: task._id,
description: task.description,
completed: !task.completed
}
console.log(updTask)
this.updateTask(updTask)
}
},
computed: {
...mapGetters(['allTasks']),
},
created() {
this.fetchTasks()
}
}
Now I need to introduce what I have tried to solve problems
Configuring CORS options
Since this error page didnt show any authorization header which was supposed to set in request header I figured out the way I enabled cors connection and I believe this enables preflight request. Here is what I configured middleware behavior from backend code.
task.js(express router file)
const router = new express.Router()
const auth = require('../middleware/auth')
const cors = require('cors')
const corsOptions = {
origin: 'http://127.0.0.1:3000',
allowedHeaders: 'content-Type, Authorization',
maxAge:3166950
}
router.options(cors(corsOptions))
router.get('/tasks', auth, async (req, res) => {
const match = {}
const sort = {}
if(req.query.completed) {
match.completed = req.query.completed === 'true'
}
if(req.query.sortBy) {
const parts = req.query.sortBy.split('_')
sort[parts[0]] = parts[1] === 'desc' ? -1:1 // bracket notation
}
try {
await req.user.populate({
path: 'tasks',
match,
options: {
limit: parseInt(req.query.limit),
skip: parseInt(req.query.skip),
sort
}
}).execPopulate()
console.log(req.user.tasks)
res.status(200).send(req.user.tasks)
} catch (e) {
res.status(500).send(e)
}
})
module.exports = router
auth.js(middleware)
const jwt = require('jsonwebtoken')
const User = require('../models/user')
const auth = async (req, res, next) => {
try {
const token = req.header('Authorization').replace('Bearer ','')
const decoded = jwt.verify(token, 'thisisnewcourse')
console.log('decoded token passed')
const user = await User.findOne({ _id: decoded._id, 'tokens.token': token})
console.log('user found')
if(!user) {
throw new Error()
}
req.token = token
req.user = user
next()
} catch (error) {
console.log('error caught')
res.status(401).send({error: 'please authenticate'})
}
}
module.exports = auth
Set Authorization header as axios default header after login
auth.module.js(since login works correctly, I am copying only login action part)
actions: {
async login ({ commit }, user){
try {
const response = await axios.post('http://127.0.0.1:3000/users/login', user)
if(response.data.token){
localStorage.setItem('user', JSON.stringify(response.data))
axios.defaults.headers.common['Authorization'] = `Bearer ${response.data.token}`
}
commit('loginSuccess', response.data)
} catch (e) {
console.log(e)
}
}
Middleware chaining on the express route(cors, auth)
I have tried two different middleware on the same backend code(task.js)
router.get('/tasks', [cors(corsOptions), auth], async (req, res) => {
// same as previously attached code
}
Now I believe referring to another post with similar issue will help me out however it's about having CORS enabled, not the issue that the header is not sent via either preflight request or other type of requests.
You haven't included the code for authHeader but I assume it just returns the value of the Authorization header.
This bit looks suspicious to me:
async fetchTasks({ commit }) {
const response = await axios.get('http://127.0.0.1:3000/tasks', {headers: authHeader()})
commit('setTasks', response.data)
axios.defaults.headers.common['Authorization'] = authHeader()
},
The final line seems to be trying to set the Authorization header globally so that it will be included on all subsequent axios requests. That's fine but it seems strange not to do that sooner. You have a similar line inside the login action, which makes sense, but I assume that isn't being called when the page is refreshed.
Then there's this bit:
{headers: authHeader()}
If authHeader returns the value of the Authorization header then this won't work. Instead you need:
{headers: { Authorization: authHeader() }}
Ideally you wouldn't need to set any headers here and instead you'd just set the global header before attempting this request.
While it isn't the direct cause of your problem, you seem to have got your wires crossed about CORS. You've configured a proxy, which means you aren't using CORS. The request you're making is to the same origin, so CORS doesn't apply. You don't need to include CORS response headers if you aren't making a cross-origin request. If you do want to make a cross-origin request then don't use the proxy. You should try to mimic your production environment during development, so if you intend to use CORS in production you should use it during development. Otherwise, stick with the proxy.

MSAL js, AAD B2C Multi Factor Authentication, 400 Bad Request, Request header too long

MSAL js Version: v0.2.4;
Chrome Version: 79.0.3945.88 (Official Build) (64-bit)
From the various post It is understood that due to cookies piled up, we are seeing '400 Bad Request - Request header too long', But it is not happening in all my developer environments.
I would like to know, why it is not with local environments (running from VS Code) but in deployed environments(Azure App Service)
I can update the MSAL package to latest version, but at the same time previously it was working fine in deployed environments but not now, why?
Is there any connection with scope error message (AADB2C90055) with 'Bad Request - Request header too long' ?
AADB2C90055: The scope 'openid profile' must specify resource
Any sort of information will be useful to me or other folks, and thanks in advance
Here is the Code being used in My App,
let userAgentApplication: Msal.UserAgentApplication;
const createAuthorityUrl = (tenantId: string, policy: string) => {
return `https://${tenantId}.b2clogin.com/tfp/${tenantId}.onmicrosoft.com/${policy}`;
};
export const b2cLogin = (config: B2CConfig) => {
const msalAppConfig = {
cacheLocation: 'localStorage',
redirectUri: `${location.protocol}//${location.host}`,
navigateToLoginRequestUrl: false,
storeAuthStateInCookie: true,
validateAuthority: false,
};
const { clientId, tenantId, myb2cSigninPolicy, myb2cPasswordResetPolicy } = config;
return new Promise(resolve => {
let handlingPasswordReset = false;
const app = new Msal.UserAgentApplication(
clientId,
createAuthorityUrl(tenantId, myb2cSigninPolicy),
(errorDesc: string, token: string) => {
if (errorDesc && errorDesc.indexOf('AADB2C90118') > -1) {
// user forgot password
// https://github.com/Azure-Samples/active-directory-b2c-javascript-msal-singlepageapp/issues/9#issuecomment-347556074
handlingPasswordReset = true;
new Msal.UserAgentApplication(
clientId,
createAuthorityUrl(tenantId, myb2cPasswordResetPolicy),
() => null,
msalAppConfig,
).loginRedirect();
}
return resolve(token);
},
msalAppConfig,
);
if (!handlingPasswordReset) {
userAgentApplication = app;
}
// Seems that MSAL's acquireTokenSilent() won't resolve if run within an iframe
if (window.parent !== window) {
return resolve('');
}
if (!userAgentApplication.isCallback(location.hash)) resolve(getAccessToken());
});
};
export const getAccessToken = async (): Promise<string> => {
if (!userAgentApplication) {
throw new Error('getAccessToken attempted before authentication initialized');
}
try {
return await userAgentApplication.acquireTokenSilent(['openid']);
} catch (error) {
console.log(error);
return '';
}
};
The error HTTP 400: Size of header request is too long generally happens because there's too many cookies or cookies that are too big.
reference:
Azure Portal: Bad Request - Request Too Long

Resources