Steam authentication with API Gateway and lambda - node.js

I'm trying to create the authentication of my website using
https://github.com/LeeviHalme/node-steam-openid.
Steam OpenID: https://partner.steamgames.com/doc/features/auth
I have an API Gateway with these two endpoints:
/login
// the steamAuth file is the same module as node-steam-openid but for ts
import { SteamAuth } from "../utils/steamAuth";
export const login = async () => {
const client = new SteamAuth(
'http://localhost:3000',
`${process.env.API_URL}/consume`,
process.env.STEAM_API_KEY,
);
try {
const redirectUrl = await client.getRedirectUrl();
return {
statusCode: 302,
headers: { Location: redirectUrl }
};
} catch (e) {
console.log(e);
return {
statusCode: 500,
message: 'Internal server error'
};
}
}
/consume
import { APIGatewayEvent } from 'aws-lambda';
import { SteamAuth } from "../utils/steamAuth";
export const consume = async (event: APIGatewayEvent) => {
const client = new SteamAuth(
'http://localhost:3000',
`${process.env.API_URL}/consume`,
process.env.STEAM_API_KEY,
);
console.log(event);
try {
const user = await client.authenticate(event);
console.log('success', user);
} catch (e) {
console.log('error', e);
}
return {
statusCode: 302,
headers: { Location: 'http://localhost:3000/' },
};
}
The thing is I get this error in /consume endpoint
error TypeError: Cannot read property 'toUpperCase' of undefined
at Object.openid.verifyAssertion (/var/task/node_modules/openid/openid.js:905:28)
at openid.RelyingParty.verifyAssertion (/var/task/node_modules/openid/openid.js:68:10)
at /var/task/src/utils/steamAuth.js:60:31
at new Promise (<anonymous>)
at SteamAuth.authenticate (/var/task/src/utils/steamAuth.js:59:16)
at Runtime.consume [as handler] (/var/task/src/lambda/consume.js:9:35)
at Runtime.handleOnceNonStreaming (/var/runtime/Runtime.js:73:25)
I believe this error occurs because the verifyAssertion is waiting for an express request while it is provided an API Gateway one.
Link to the code with the mentioned function is here
Should I use another module to do the authentication as I don't really want to modify the source code of the module? I didn't find anything at the moment
Thanks!

I found a workaround using express in lambda. As expected, the openid module used by node-steam-openid is expecting an express request and not a lambda event.
import { SteamAuth } from "../utils/steamAuth";
const express = require('express');
const serverless = require('serverless-http');
const app = express();
app.get('/verify', async (req: any, res: any) => {
const client = new SteamAuth(
process.env.HOSTNAME,
`${process.env.API_URL}/verify`,
process.env.STEAM_API_KEY,
);
try {
const user: any = await client.authenticate(req);
} catch (e) {
throw new Error(e.message);
}
});
module.exports.verify = serverless(app);

Related

how to set headers in axios patch request in react js

Can someone tell me what mistake I am making or tell me how to set the header in axios patch request. when I am running the API through postman, everything is working fine but when I connect it with the front end, an error comes up saying that the JWT is not provided on the backend
here is the frond end code :
import React, { useEffect } from 'react';
import { useParams } from 'react-router';
import axios from 'axios';
const Loader = () => {
const parmas = useParams();
const { id } = parmas;
console.log(id);
useEffect(() => {
const fetchBags = async () => {
try {
const res = await axios.patch('http://localhost:4001/public/verify', {
headers: {
'Content-Type': 'application/json',
Token: id,
},
});
console.log(res);
console.log('CBM', { res });
} catch (error) {
console.log(error);
}
};
fetchBags();
}, []);
return <div>this is loader</div>;
};
export default Loader;
below is my backend code:
export const verifyUser = async (data) => {
const token1 = data.header("Token");
try {
const verified = jwt.verify(token1, getTokenSecret());
console.log(verified)
await userModel.verifyUser(verified);
return {
message: "success",
};
} catch (error) {
console.log(`Auth Service > verifyUser > ${error.toString()}`);
throw error;
}
};
this error is comming:
Error
From docs
axios.patch(url[, data[, config]])
As you can see you pass config in 3rd argument not 2nd.
const res = await axios.patch(
'http://localhost:4001/public/verify',
{}, // data (2nd argument)
{
headers: {
'Content-Type': 'application/json',
Token: id,
},
} // config (3rd argument)
)

My cloud functions app failing to deploy now that i've added cloud tasks?

I want to schedule some cloud tasks to run at some specific times, however adding cloud tasks to my cloud functions project is now resulting in failure to deploy, with no errors anywhere for me to see?
Here is my project (uses express):
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import express = require('express');
import cors = require('cors');
import helmet = require('helmet');
import favicon = require('serve-favicon');
import sgMail = require('#sendgrid/mail');
import { RuntimeOptions } from 'firebase-functions';
import { CloudTasksClient } from '#google-cloud/tasks';
admin.initializeApp({
credential: admin.credential.applicationDefault(),
});
export const db = admin.firestore();
sgMail.setApiKey(functions.config().sendgrid.key);
const createTestTask = async (text: string) => {
try {
const client = new CloudTasksClient();
const project = 'my project ID is here';
const queue = 'my-test-queue';
const location = 'europe-west2';
const parent = client.queuePath(project, location, queue);
const url =
'my cloud function url is here';
const payload = JSON.stringify({
user: 'Test',
mode: 'secret mode',
text,
});
const body = Buffer.from(payload).toString('base64');
const task = {
httpRequest: {
httpMethod: 'POST',
url: url,
dispatchDeadline: 30 * 60, // 30 minutes
body: body,
headers: { 'Content-type': 'application/json' },
},
};
// Send create task request.
console.log('Sending task:');
// eslint-disable-next-line #typescript-eslint/ban-ts-comment
// #ts-ignore
const [response] = await client.createTask({ parent, task });
console.log(`Created task ${response.name}`);
} catch (e) {
functions.logger.error(e);
}
};
// #####################################################################
app.post('/tasks', async (req, res) => {
try {
const { text } = req.body;
createTestTask(text);
res.status(201).send({
success: true,
message: 'Success',
});
} catch (e) {
functions.logger.error(e);
}
});
// #####################################################################
app.post('/tasks/test', async (req, res) => {
try {
const { text } = req.body;
await db.collection('tasks').doc().create({
text,
});
res.status(201).send({
success: true,
message: 'Success',
});
} catch (e) {
functions.logger.error(e);
}
});
// #####################################################################
exports.api = regionalFunctions.runWith(expressOpts).https.onRequest(app);
The errors I get when I deploy are:
Functions deploy had errors with the following functions:
api(europe-west2)
Error: There was an error deploying functions:
- Error Failed to update function api in region europe-west2
The project deploys fine without the cloud tasks code in here and just normal express routes, does anyone know why or where I can find out what exactly is wrong here?
As per the documentation, you can set/change the default region of the cloud functions from us-central1 through the below code example (I don’t know why you had put
regionalFunctions.runWith(expressOpts).https.onRequest(app);
It should be :
const functions = require('firebase-functions');
exports.api = functions
.runWith(expressOpts)
.region('europe-west2')
.https.onRequest(async(req, res) => {
try {
await createMessage();
res.status(200).send("OK");
}
catch (error) {
// Handle the error
console.log(error);
res.status(500).send(error);
};
I think the function deployment failed because it cannot find the #google-cloud/tasks in package.json in your function directory and there's no problem about region concern. It can't be deployed to any region if there's a missing dependency in your function.
To fix it and add the #google-cloud/tasks dependency in your function/package.json, run the following command in your function directory:
npm install #google-cloud/tasks

How to thrown error in catch block using sinon stub

I am using mocha and sinon for test the node services, In controller I have getToken npm module for getting the token with name and value as parameters and in spec file I trying to send empty name as parameter using withargs but the response getting success excepted result is token creating fail please help on this issue.
controller.ts
import {getToken} from './tokenUtil';
export async function userInfo(req:Request,res:Response){
try{
let token = await getToken(name,value);
}
catch(error){
res.send({status:'Failue',message:error});
return
}
res.send({status:'success',message:'token creation success'})
}
tokenUtil.ts
export async function getToken(name,value){
// token code here
}
token.spce.ts
import * as sinon from 'sinon';
import * as proxyquire from 'proxyquire';
describe('get token',()=>{
let req:any;
let res:any;
beforeEach(()=>{
res={
send:sinon.stub();
}
it('getting error when given empty name',async()=>{
let tokenStub = sinon.stub().withArgs('',tokenValue).returns(undefined);
let tokenctl=proxyquire('./controller',{
'./tokenUtil':tokenStub
})
await tokenctl.userInfo(req,res);
sinon.assert.calledWithExactly(res.send,{status:'Failue',message:'token creating fail'})
})
})
})
You are testing the controller.ts module, so the test file name should be controller.spec.ts or controller.test.ts.
Since the ./tokenUtil use named exports, so the tokenStub should be an object.
You should use sinon.stub().rejects() to create a promise stub with rejected value.
E.g.
controller.ts:
import { getToken } from './tokenUtil';
import { Request, Response } from 'express';
export async function userInfo(req: Request, res: Response) {
const { name, value } = req.body;
try {
let token = await getToken(name, value);
res.send({ status: 'success', message: 'token creation success' });
} catch (error) {
res.send({ status: 'Failue', message: error });
}
}
tokenUtil.ts:
export async function getToken(name, value) {
// token code here
}
controller.test.ts:
import sinon from 'sinon';
import proxyquire from 'proxyquire';
describe('get token', () => {
let req: any;
let res: any;
beforeEach(() => {
res = {
send: sinon.stub(),
};
});
it('should create token success', async () => {
req = { body: { value: '123', name: 'teresa teng' } };
let tokenStub = {
getToken: sinon.stub().withArgs(req.body.name, req.body.value).resolves(),
};
let tokenctl = proxyquire('./controller', {
'./tokenUtil': tokenStub,
});
await tokenctl.userInfo(req, res);
sinon.assert.calledWithExactly(res.send, { status: 'success', message: 'token creation success' });
});
it('should handle error when given empty name', async () => {
const tokenValue = '123';
req = { body: { value: tokenValue, name: '' } };
const error = new Error('token creating fail');
let tokenStub = {
getToken: sinon.stub().withArgs('', tokenValue).rejects(error),
};
let tokenctl = proxyquire('./controller', {
'./tokenUtil': tokenStub,
});
await tokenctl.userInfo(req, res);
sinon.assert.calledWithExactly(res.send, { status: 'Failue', message: error });
});
});
Test result:
get token
✓ should create token success (101ms)
✓ should handle error when given empty name
2 passing (112ms)

Shopify Apps with NodeJS problem "Error: Failed to parse session token '******' jwt expired"

Greetings I have a problem every time when I want to make an Admin REST API call to Shopify I get this problem "Error: Failed to parse session token '****' jwt expired" I see some code examples on the net I have my own custom session storage for accessToken and shop but every time when I try to call my own route from front-end and get more details about the shop I get this problem here is code example can anyone help me?
server.js
import "#babel/polyfill";
import dotenv from "dotenv";
import "isomorphic-fetch";
import createShopifyAuth, { verifyRequest } from "#shopify/koa-shopify-auth";
import Shopify, { ApiVersion } from "#shopify/shopify-api";
import Koa from "koa";
import next from "next";
import Router from "koa-router";
const helmet = require("koa-helmet");
const compress = require("koa-compress");
const cors = require("koa-cors");
const logger = require("koa-logger");
const bodyParser = require("koa-bodyparser");
import axios from "axios";
import { storeCallback, loadCallback, deleteCallback } from "./custom-session";
const sequelize = require("./database/database");
const { Shopify_custom_session_storage } = require("./../models/sequelizeModels");
// import apiRouter from "./../routers/apiRouter";
dotenv.config();
const port = parseInt(process.env.PORT, 10) || 8081;
const dev = process.env.NODE_ENV !== "production";
const app = next({
dev,
});
const handle = app.getRequestHandler();
Shopify.Context.initialize({
API_KEY: process.env.SHOPIFY_API_KEY,
API_SECRET_KEY: process.env.SHOPIFY_API_SECRET,
SCOPES: process.env.SCOPES.split(","),
HOST_NAME: process.env.HOST.replace(/https:\/\/|\/$/g, ""),
API_VERSION: ApiVersion.October20,
IS_EMBEDDED_APP: true,
// This should be replaced with your preferred storage strategy
SESSION_STORAGE: new Shopify.Session.CustomSessionStorage(storeCallback, loadCallback, deleteCallback)
});
sequelize.sync()
.then(() => {
app.prepare().then(async () => {
const server = new Koa();
const router = new Router();
server.keys = [Shopify.Context.API_SECRET_KEY];
server.use(
createShopifyAuth({
async afterAuth(ctx) {
// Access token and shop available in ctx.state.shopify
const { shop, accessToken, scope } = ctx.state.shopify;
const host = ctx.query.host;
// Getting users data from database and saving it to variable //
try {
await Shopify_custom_session_storage.findAll({
raw: true,
where:{
shop: shop
},
limit:1
});
} catch(err) {
console.log(err);
throw err;
}
// End of Getting users data from database and saving it to variable //
const response = await Shopify.Webhooks.Registry.register({
shop,
accessToken,
path: "/webhooks",
topic: "APP_UNINSTALLED",
webhookHandler: async (topic, shop, body) =>{
return Shopify_custom_session_storage.destroy({
where: {
shop: shop
}
})
.then(result => {
return true;
})
.catch(err => {
if(err) throw err;
return false;
});
}
});
if (!response.success) {
console.log(
`Failed to register APP_UNINSTALLED webhook: ${response.result}`
);
}
// Redirect to app with shop parameter upon auth
ctx.redirect(`/?shop=${shop}&host=${host}`);
},
})
);
const handleRequest = async (ctx) => {
await handle(ctx.req, ctx.res);
ctx.respond = false;
ctx.res.statusCode = 200;
};
router.post("/webhooks", async (ctx) => {
try {
await Shopify.Webhooks.Registry.process(ctx.req, ctx.res);
console.log(`Webhook processed, returned status code 200`);
} catch (error) {
console.log(`Failed to process webhook: ${error}`);
}
});
router.post("/graphql", verifyRequest({ returnHeader: true }), async (ctx, next) => {
await Shopify.Utils.graphqlProxy(ctx.req, ctx.res);
}
);
// Our Routes //
router.get("/getProducts", verifyRequest({ returnHeader: true }), async (ctx) => {
try{
const session = await Shopify.Utils.loadCurrentSession(ctx.req, ctx.res);
const client = new Shopify.Clients.Rest(session.shop, session.accessToken);
console.log(session);
}catch(err) {
console.log(err);
throw new Error(err);
}
});
// End of Our Routes //
router.get("(/_next/static/.*)", handleRequest); // Static content is clear
router.get("/_next/webpack-hmr", handleRequest); // Webpack content is clear
router.get("(.*)", async (ctx) => {
const shop = ctx.query.shop;
try {
let user = await Shopify_custom_session_storage.findAll({
raw: true,
where:{
shop: shop
},
limit:1
});
// This shop hasn't been seen yet, go through OAuth to create a session
if (user[0].shop == undefined) {
ctx.redirect(`/auth?shop=${shop}`);
} else {
await handleRequest(ctx);
}
} catch(err) {
console.log(err);
throw err;
}
});
server.use(router.allowedMethods());
server.use(router.routes());
// Setting our installed dependecies //
server.use(bodyParser());
server.use(helmet());
server.use(cors());
server.use(compress());
server.use(logger());
// End of Setting our installed dependecies //
server.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`);
});
});
})
.catch((err) => {
if(err) throw err;
return process.exit(1);
})
_app.js
import ApolloClient from "apollo-boost";
import { ApolloProvider } from "react-apollo";
import App from "next/app";
import { AppProvider } from "#shopify/polaris";
import { Provider, useAppBridge } from "#shopify/app-bridge-react";
import { authenticatedFetch, getSessionToken } from "#shopify/app-bridge-utils";
import { Redirect } from "#shopify/app-bridge/actions";
import "#shopify/polaris/dist/styles.css";
import translations from "#shopify/polaris/locales/en.json";
import axios from 'axios';
function userLoggedInFetch(app) {
const fetchFunction = authenticatedFetch(app);
return async (uri, options) => {
const response = await fetchFunction(uri, options);
if (
response.headers.get("X-Shopify-API-Request-Failure-Reauthorize") === "1"
) {
const authUrlHeader = response.headers.get(
"X-Shopify-API-Request-Failure-Reauthorize-Url"
);
const redirect = Redirect.create(app);
redirect.dispatch(Redirect.Action.APP, authUrlHeader || `/auth`);
return null;
}
return response;
};
}
function MyProvider(props) {
const app = useAppBridge();
const client = new ApolloClient({
fetch: userLoggedInFetch(app),
fetchOptions: {
credentials: "include",
},
});
const axios_instance = axios.create();
// Intercept all requests on this Axios instance
axios_instance.interceptors.request.use(function (config) {
return getSessionToken(app) // requires a Shopify App Bridge instance
.then((token) => {
// Append your request headers with an authenticated token
config.headers["Authorization"] = `Bearer ${token}`;
return config;
});
});
const Component = props.Component;
return (
<ApolloProvider client={client}>
<Component {...props} axios_instance={axios_instance}/>
</ApolloProvider>
);
}
class MyApp extends App {
render() {
const { Component, pageProps, host } = this.props;
return (
<AppProvider i18n={translations}>
<Provider
config={{
apiKey: API_KEY,
host: host,
forceRedirect: true,
}}
>
<MyProvider Component={Component} {...pageProps} />
</Provider>
</AppProvider>
);
}
}
MyApp.getInitialProps = async ({ ctx }) => {
return {
host: ctx.query.host,
};
};
export default MyApp;
index.js
import { Heading, Page, Button } from "#shopify/polaris";
function Index(props){
async function getProducts(){
const res = await props.axios_instance.get("/products");
return res;
}
async function handleClick() {
const result = await getProducts();
console.log(result);
}
return (
<Page>
<Heading>Shopify app with Node and React </Heading>
<Button onClick={handleClick}>Get Products</Button>
</Page>
);
}
export default Index;
I found the solution for "Error: Failed to parse session token '******' jwt expired" the problem was Computer Time was not synchronized, check the computer time and synchronized it, for my example, I'm on Kali Linux and I search it how to synchronize time on Kali Linux and follow that tutorial when you finally synchronize your time restart your application server and try again. That's it so dump I lost 4 days on this.

How do I send a HEAD request for a static sendFile in fastify?

When I try to send a HEAD request for sendFile I get the following error:
app.head(filePath, { logLevel: LOG_LEVEL }, async (request, reply) => {
console.log('head');
try {
const { '*': uriPath } = request.params;
const isFile = !!uriPath.match(/\.[a-zA-Z0-9]{1,5}(\?.*)?/);
if (isFile) {
setCacheControl(reply, FILE_CACHE_CONTROL_MAX_AGE);
reply.sendFile(uriPath);
} else {
const indexPath = 'index.html';
const indexStr = await fs.readFile(path.join(serveRoot, indexPath), {
encoding: 'utf-8',
});
const indexPayload = await injectEnv(indexStr);
setCacheControl(reply, INDEX_CACHE_CONTROL_MAX_AGE);
reply.type('text/html');
reply.send(indexPayload);
}
} catch (e) {
console.error(e);
}
});
web_1 | {"level":50,"time":1580244056047,"pid":1,"hostname":"3ee631923a16","reqId":5,"err":{"type":"FastifyError","message":"FST_ERR_PROMISE_NOT_FULLFILLED: Promise may not be fulfilled with 'undefined' when statusCode is not 204","stack":"FastifyError [FST_ERR_PROMISE_NOT_FULLFILLED]: FST_ERR_PROMISE_NOT_FULLFILLED: Promise may not be fulfilled with 'undefined' when statusCode is not 204\n at /usr/src/server/node_modules/fastify/lib/wrapThenable.js:34:30\n at processTicksAndRejections (internal/process/task_queues.js:85:5)","name":"FastifyError [FST_ERR_PROMISE_NOT_FULLFILLED]","code":"FST_ERR_PROMISE_NOT_FULLFILLED","statusCode":500},"msg":"Promise may not be fulfilled with 'undefined' when statusCode is not 204","v":1}
The way that express handles this is by simply passing through HEAD requests to the GET method, and then letting send (the underlying package that sends responses for both fastify and express) handle it here by not sending the output but sending the headers.
But fastify seems to incorrectly mark this as an error here
Here a working example:
const fs = require('fs').promises
const path = require('path')
const app = require('fastify')({ logger: true })
app.head('/', async (request, reply) => {
request.log.debug('head')
try {
const indexStr = await fs.readFile(path.join(__dirname, 'index.html'), { encoding: 'utf-8' })
reply.type('text/html')
return indexStr
} catch (e) {
request.log.error(e)
return e
}
})
app.listen(3000)
// curl --location --head 'http://localhost:3000/'
When an error is thrown and caught the promise is fulfilled with undefined that is causing the error you linked in the source code.
Moreover, when you use async functions as handlers, you should return what you want to send in the body or use return reply.send(content)
Anyway, consider to don't use HEAD methot because the standard says:
A response to a HEAD method should not have a body. If so, it must be ignored. Even so, entity headers describing the content of the body, like Content-Length may be included in the response. They don't relate to the body of the HEAD response, which should be empty, but to the body which a similar request using the GET method would have returned as a response.
So your body will be empty:
HTTP/1.1 200 OK
content-type: text/html
content-length: 41
Date: Wed, ---
Connection: keep-alive
Solved it. The key is to return the binary stream for filesreturn fs.readFile(uriPath); and the string for string responses return indexPayload;
import path from 'path';
import fastify from 'fastify';
import staticServe from 'fastify-static';
import { promises as fs } from 'fs';
(async () => {
const app = fastify({
logger: !!LOG_LEVEL,
});
const filePath = `${ROOT_PATH}*`;
const serveRoot = path.resolve(SERVE_PATH);
app.register(staticServe, {
root: serveRoot,
serve: false,
});
// #ts-ignore
const getHandler = async (request, reply) => {
try {
const { '*': uriPath } = request.params;
const isFile = !!uriPath.match(/\.[a-zA-Z0-9]{1,5}(\?.*)?/);
if (isFile) {
setCacheControl(reply, FILE_CACHE_CONTROL_MAX_AGE);
reply.sendFile(uriPath);
return fs.readFile(uriPath);
} else {
const indexPath = 'index.html';
const indexStr = await fs.readFile(path.join(serveRoot, indexPath), {
encoding: 'utf-8',
});
const indexPayload = await injectEnv(indexStr);
setCacheControl(reply, INDEX_CACHE_CONTROL_MAX_AGE);
reply.type('text/html');
return indexPayload;
}
} catch (e) {
request.log.error(e);
return e;
}
};
app.get(filePath, { logLevel: LOG_LEVEL }, getHandler);
// More info here on how it works
// https://github.com/fastify/fastify/issues/2061
app.head(filePath, { logLevel: LOG_LEVEL }, getHandler);
app.listen(Number.parseInt(PORT, 10), '0.0.0.0', (err: Error) => {
if (err) {
throw err;
}
});
})();

Resources