Validating Shopify webhook using nodejs and express - shopify

There are several SO posts, but years old, and there is no documentation that I can find on how to use nodejs and express to validate a webhook from Shopify for nodejs in 2023. My issue, I know, is in getting the request body in the same format that Shopify used to create their hmac.
How do you correctly get the req body to create the local hash to compare against the hmac from Shopify?
import express from 'express'
import cors from 'cors'
import * as crypto from 'crypto'
import bodyParser from 'body-parser'
const app = express()
app.use(cors({ origin: true }))
app.post('/order/create', bodyParser.text({ type: 'application/json' }), async (req, res) => {
try {
const hmac = req.header('X-Shopify-Hmac-Sha256')
const topic = req.header('X-Shopify-Topic')
const shop = req.header('X-Shopify-Shop-Domain')
const secret = await shopifySharedSecret()
if (!secret) {
throw Error('Check logs.')
}
const hash = crypto.createHmac('sha256', secret).update(req.body).digest('hex')
if (hash !== hmac) {
throw Error('hmac validation failed')
}
res.send({
processed: true,
})
} catch (error) {
res.send({})
}
})

I believe the issue is that you use digest('hex') instead of digest('base64').
The Shopify docs says :
Each webhook request includes a base64-encoded X-Shopify-Hmac-SHA256
header
So you need to use the same encoding.

Needed to add an augment to access req.rawBody.
import express from 'express'
import cors from 'cors'
import * as crypto from 'crypto'
declare module 'http' {
interface IncomingMessage {
rawBody: unknown
}
}
app.post('/ingest', async (req, res) => {
try {
const hmac = req.header('X-Shopify-Hmac-Sha256')
const topic = req.header('X-Shopify-Topic')
const shop = req.header('X-Shopify-Shop-Domain')
if (!hmac || !topic || !shop) {
res.send({})
return
}
const secret = await shopifySharedSecret()
if (!secret) {
throw Error('Check logs.')
}
const hash = crypto.createHmac('sha256', secret).update(String(req.rawBody)).digest('base64')
if (hash !== hmac) {
logger.info('hmac validation failed')
res.send({})
return
}
// Now can safely use webhook's req.body, which is json.
res.send({})
} catch (error) {
res.status(403).send({})
}
})

Related

How to consume Next.JS Rest Endpoints secured with Amplify from a React Native app

Background:
My current stack is a Next server to use as an admin portal and REST API for a Mobile App running with Expo - React Native. The Next Server is currently hosted as a Lambda#Edge.
I have secured both the Next server and the React Native app with AWS Amplify's withAuthenticator wrapper. (I also tried specific auth packages like Next-Auth and Expo's auth package)
Problem:
However, I can't figure out how to add the Auth info (Access_token) to my REST Requests from Mobile app -> Next Server
I tried adding the tokens as bearer headers to the API without luck after that I was fairly sure it all has to be set up and sent via cookies.
BUT I am stuck on how to actually implement these cookies properly. I was hoping the endpoints:[] config could be used to set up my own domain to post to and handle the cookies. Reading the request on the server showed that it contained no Auth info when posted with this method.
Likewise using RTK Query (Preferably I add all the Auth to this instead of Amplify's API setup) I don't have the correct info to make an Authorized api request
Here are some snippets of the working page Authentication for both apps
API Endpoint /api/version:
import type { NextApiRequest, NextApiResponse } from 'next'
import { withSSRContext } from 'aws-amplify'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data | Error>,
) {
const { Auth } = withSSRContext({req})
try {
const user = await Auth.currentAuthenticatedUser()
return res.status(200).json({
version: '1.0.0',
user: user.username,
})
} catch (err) {
console.log(err)
return res.status(200).json({
message: 'Unauthenticated',
})
}
}
Mobile App Config:
import {
useAuthenticator,
withAuthenticator,
} from '#aws-amplify/ui-react-native'
import { Amplify, Auth } from 'aws-amplify'
import awsconfig from './aws-exports'
Amplify.configure({
...awsconfig,
API: {
endpoints: [
{
name: 'MyApi',
endpoint: 'http://NextIP:NextPort/api/',
},
],
},
})
Auth.configure(awsconfig)
export default withAuthenticator(App)
Mobile Screen:
import { API } from 'aws-amplify'
function getData() {
const apiName = 'MyApi'
const path = '/version'
const myInit = {
headers: {}, // OPTIONAL
}
return API.get(apiName, path, myInit)
}
export default function ModalScreen() {
// Get token / Cookie for auth
// const { data, isLoading, error } = useGetApiVersionQuery(null) // RTK Query
getData() // Amplify
.then(response => {
console.log(response)
})
.catch(error => {
console.log(error.response)
})
return ( <></>)}
I found a solution, however, could not get the Next-Auth middleware to fire when the token was sent using the Bearer token in headers. Which is my ideal way of handling the routes.
I wrapped the getToken({req}) call so that if there is no JWT Web token it would try encode the token separately
Lastly ChatGpt somehow got me onto the package aws-jwt-verify which has everything you need to verify a token generated by aws-amplify/auth, in my case from react-native.
components/utils/auth.utils.ts
import { NextApiRequest } from 'next'
import { CognitoJwtVerifier } from 'aws-jwt-verify'
import { getToken } from 'next-auth/jwt'
// Verifier that expects valid token:
const verifier = CognitoJwtVerifier.create({
userPoolId: process.env.COGNITO_USERPOOL_ID ?? '',
tokenUse: 'id',
clientId: process.env.COGNITO_CLIENT_ID ?? '',
issuer: process.env.COGNITO_ISSUER ?? '',
})
export async function getMobileToken(req: NextApiRequest) {
let token = null
try {
token = await getToken({ req })
} catch (error) {
console.log('Could not get JWT Web Token')
}
try {
if (!token)
token = await getToken({
req,
async decode({ token }) {
if (!token) return null
const decoded = await verifier.verify(token)
return decoded
},
})
} catch (error) {
return null
}
console.log('Mobile Token:', token)
return token
}

Nuxt - is it possible to check if a user is logged in from SSR?

I created a Nuxt app that uses Django on the backend, i'm using the standard Django Session Authentication, so when i log in from Nuxt, a session cookie is set in my browser.
I've been trying for days to find a way to restrict some pages to authenticated users only, but i don't seem to find any working approach to do that. I need to check if the user is logged in before the page is loaded, so i tried to use a middleware but middleware won't work at all because the middleware is executed from server side (not client side) so there won't be any cookie in the request.
At this point, is there any other way to do this from SSR? Here is my request:
export default async function (context) {
axios.defaults.withCredentials = true;
return axios({
method: 'get',
url: 'http://127.0.0.1:8000/checkAuth',
withCredentials: true,
}).then(function (response) {
//Check if user is authenticated - response is always False
}).catch(function (error) {
//Handle error
});
}
If you are running Nuxt in SSR mode as server, you can access the cookie headers to find out if the user has a certain cookie. Packages like cookieparser (NPM) can easily do that for you.
But as you already found out, you can't do that in a middleware. What you could use instead is the nuxtServerInit action in your store (Docs). This action is run on the server and before any middleware gets executed. In there you can use cookieparser to get the user's cookies, authenticate them and save the any information you need in the store.
Later you can access the store in your middleware and for example redirect the user.
actually you can get cookies in a middleware.... Ill put my example, but the answer above is more correct .
middleware/auth.js
import * as cookiesUtils from '~/utils/cookies'
export default function ({ route, req, redirect }) {
const isClient = process.client
const isServer = process.server
const getItem = (item) => {
// On server
if (isServer) {
const cookies = cookiesUtils.getcookiesInServer(req)
return cookies[item] || false
}
// On client
if (isClient) {
return cookiesUtils.getcookiesInClient(item)
}
}
const token = getItem('token')
const { timeAuthorized } = cookiesUtils.authorizeProps(token)
const setRedirect = (routeName, query) => {
return redirect({
name: routeName,
query: query
? {
redirect: route.fullPath
}
: null
})
}
// strange bug.. nuxt cant redirect '/' to '/login'
if (route.path === '/') {
setRedirect('users')
}
if (!route.path.match(/\/login\/*/g) && !timeAuthorized) {
setRedirect('login', true)
}
}
utils/cookies.js
import Cookie from 'js-cookie'
import jwtDecoded from 'jwt-decode'
/*
TOKEN
*/
// Get server cookie
export const getcookiesInServer = (req) => {
const serviceCookie = {}
if (req && req.headers.cookie) {
req.headers.cookie.split(';').forEach((val) => {
const parts = val.split('=')
serviceCookie[parts[0].trim()] = (parts[1] || '').trim()
})
}
return serviceCookie
}
// Get the client cookie
export const getcookiesInClient = (key) => {
return Cookie.get(key) || false
}
export const setcookiesToken = (token) => {
Cookie.set('token', token)
}
export const removecookiesToken = () => {
Cookie.remove('token')
}
export const authorizeProps = (token) => {
const decodeToken = token && jwtDecoded(token)
const timeAuthorized = (decodeToken.exp > Date.now() / 1000) || false
return {
timeAuthorized
}
}

Express-jwt is not returning any response

I'm trying to create a Login functionality using express-jwt, and using the middleware function in my app.js file. But whenever I'm trying to send a get request using the postman, it sending request for infinite of time and never returns back any error or success message.
I'm using dynamoDB as database.
here's my Login.js file
const AWS = require("aws-sdk");
const express = require("express");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
require("dotenv").config();
AWS.config.update({ region: "us-east-2" });
const docClient = new AWS.DynamoDB.DocumentClient();
const router = express.Router();
router.post("/login", (req, res) => {
user_type = "customer";
const email = req.body.email;
docClient.get(
{
TableName: "users",
Key: {
user_type,
email,
},
},
(err, data) => {
if (err) {
res.send("Invalid username or password");
} else {
if (data && bcrypt.compareSync(req.body.password, data.Item.password)) {
const token = jwt.sign(
{
email: data.Item.email,
},
process.env.SECRET,
{ expiresIn: "1d" }
);
res.status(200).send({ user: data.Item.email, token: token });
} else {
res.status(400).send("Password is wrong");
}
}
}
);
});
module.exports = router;
Here's my jwt.js file:
const expressJwt = require("express-jwt");
require("dotenv").config();
function authJwt() {
const secret = process.env.SECRET;
return expressJwt({
secret,
algorithms: ["HS256"],
});
}
module.exports = authJwt;
And I'm trying to use the expressJwt like this in my app.js file:
app.use(authJwt); //If I'm not using this, then the code works fine without API protection
Can Anyone tell me what's wrong with my code?
Any help from your side is appreciated.
Remove function from your jwt.js ,it should look like this
const expressJwt = require('express-jwt');
const secret = process.env.secret
const authJwt = expressJwt({
secret,
algorithms:['HS256']
})
module.exports = authJwt;

Node/Express server CORS issue with a different server port

I have a node/graphql server running on sitename.com:3333
I've created another server that I'm running on sitename.com:3334
I'm able to make requests to the server at sitename.com:3333 from sitename.com as well as subdomain.sitename.com
But if I try to connect to sitename.com:3334 (just a different port) from subdomain.sitename.com it gives me a cors error:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://sitename.com:3334/graphql. (Reason: CORS request did not succeed)
I've opened the ports in the firewall and setup ssl on the server and client.
Please help!
Client code is below:
import { ApolloClient } from 'apollo-client'
import { withClientState } from 'apollo-link-state'
import { HttpLink } from 'apollo-link-http'
import { Agent } from 'https'
import fs from 'fs'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { setContext } from 'apollo-link-context'
import { onError } from 'apollo-link-error'
import { ApolloLink } from 'apollo-link'
import decode from 'jwt-decode'
import history from '../history'
import Cookies from 'universal-cookie'
import {
APP,
AUTH,
CLIENT_AUTH_REQUEST_TYPE,
CLIENT_AUTHENTICATION_METHOD,
JWT,
VERSION
} from '../environment'
import https from 'https'
import { defaults, resolvers } from '../api'
import { createUploadLink } from 'apollo-upload-client'
const { CONSTANTS: { UNAUTHORIZED, FORBIDDEN } = {} } = APP
const cookies = new Cookies()
const opts = {
credentials: 'same-origin',
headers: {
'frontend-version': VERSION,
[AUTH.STRATEGIES.CLIENT.AUTH_HEADER]: CLIENT_AUTH_REQUEST_TYPE
}
}
const useLocalStorage = CLIENT_AUTHENTICATION_METHOD.LOCAL_STORAGE
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0
// const apolloCache = new InMemoryCache();
const apolloCache = new InMemoryCache({
// dataIdFromObject: e => `${e.__typename}_${e.id}` || null // eslint-
disable-line no-underscore-dangle
})
// const watchedMutationLink = new WatchedMutationLink(apolloCache,
watchedMutations);
const stateLink = withClientState({
cache: apolloCache,
defaults,
resolvers
})
const uploadLink = createUploadLink({
// uri: 'http://localhost:3333/graphql',
uri: 'https://demo.MYSITE.in:3334/graphql',
fetchOptions: {
agent: new https.Agent()
}
})
const httpLink = new HttpLink({
uri: 'https://demo.MYSITE.in:3334/graphql',
...opts
})
const TOKEN_NAME = 'x-connector-token'
const authLink = new ApolloLink((operation, forward) => {
operation.setContext(({ headers = {} }) => {
const token = cookies.get('token')
if (token) {
headers = { ...headers, 'x-connector-token': token }
}
return { headers }
})
return forward(operation)
})
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors && graphQLErrors.filter(e => e).length > 0) {
graphQLErrors.map(({ message = '', status = 200 }) => {
if (UNAUTHORIZED === message || status === 401) {
if (
history &&
history.location &&
history.location.pathname !== '/login'
) {
history.push('/login')
}
}
if (FORBIDDEN === message || status === 403) {
history.push(`/error-page/403`)
}
return null
})
}
if (networkError && networkError.statusCode === 401) {
// eslint-disable-next-line
history.push('/login')
}
if (networkError && networkError.statusCode === 403) {
// Do something
}
if (networkError && networkError.statusCode === 400) {
// Do something
}
if (networkError && networkError.statusCode >= 500) {
// eslint-disable-next-line
history.push(`/error-page/${networkError.statusCode}`)
}
})
let links = [errorLink, stateLink, httpLink]
links = [
errorLink,
stateLink,
// afterwareLink,
// authMiddlewareLink,
authLink,
// watchedMutationLink,
// httpLink,
uploadLink
]
const link = ApolloLink.from(links)
export default new ApolloClient({
link,
cache: apolloCache,
connectToDevTools: true,
// opts: {
// agent
// },
fetchOptions: {
agent: new https.Agent()
// rejectUnauthorized: false
},
defaultOptions: {
query: {
errorPolicy: 'all'
}
},
onError: ({ networkError, graphQLErrors }) => {}
})
Server Code:
const app = express();
// tried this too
const corsOptions = {
origin: 'https://demo.MYSITE.in',
}
// also tried app.use(cors)
app.use(cors({
'allowedHeaders': ['Content-Type'],
'origin': '*',
'preflightContinue': true
}));
app.use(helmet());
// app.use(cors());
The browser will not make requests to a server of different origin than the origin that the web page itself came from (and a different port constitutes a different origin) unless you specifically enable that request for that new origin on your server. This is a garden variety CORs issue of which there are millions of posts and articles on how to handle. Since you show NONE of your code in your question, we can't recommend a specific code fix to your code.
Your server needs to support the specific CORS request you are trying to do. If you're using Express, then the CORS module does a lot of the work for you if properly implemented. CORS is there for your site's protection so Javascript in other people's web pages run from a browser can't arbitrarily use your APIs so be careful in exactly what you open up to CORS requests.
And, since this seems like a new issue to you, I would strongly suggest you read and learn about what CORs is and how it works.
Also, note that there are "Simple" CORS requests and "Pre-flighted Requests" (non-simple requests) and more work is required to enable Pre-flighted requests. The browser decides whether a given cross origin request is simple or requires pre-flight based on the exact parameters of the request and your server has to do more things to enable pre-flighted requests.

How can I verify token google recaptcha 3 on adonis js?

I using vue as my front end. I send token from my front end like this :
let payload = {
token: tokenCaptcha
}
axios.post(`http://127.0.0.1:3333/api/v1/category`, payload)
.then(response => {
return response.data
}).catch(
error => {
console.log(error)
})
The token will used to verify on the backend. My backend using adonis.js
The script of controller like this :
'use strict'
class CategoryController {
async store ({ request, response }) {
return request.input('token')
}
}
module.exports = CategoryController
My routes like this :
Route.group(()=>{
Route.post('category', 'CategoryController.store')
}).prefix('api/v1')
How can I verify the token on adonis.js(backend)?
I had search reference. But I don't find it
You need to use axios. Something like:
const axios = use('axios')
const Env = use('Env')
const querystring = use('querystring')
async store({ request, response }) {
const data = request.only(['token'])
try {
const data_request = await axios.post('https://www.google.com/recaptcha/api/siteverify', querystring.stringify({ secret: Env.get('RECAPTCHA_PRIVATE_KEY'), response: data['token'], remoteip: '172.217.23.110' }))
if (!data_request.data.success) {
//If the recaptcha check fails
...
}
} catch (error) {
...
}
}
Google documentation - Verifying the user's response
This code is made for v2. But the verification is the same : https://developers.google.com/recaptcha/docs/v3#site_verify_response