Vendia Serverless Express does not pass custom event headers to express request headers - node.js

I have an Express server running on Lambda. There is a custom lambda authorizer attached to his lambda which passes auth headers via events.
My goal is to extract these request context values and attach them to the event headers of the lambda and then pass it on to the Express request headers which can then be used across my route implementations
lambda.ts
'use strict';
import { APIGatewayProxyHandler } from "aws-lambda";
import serverlessExpress from "#vendia/serverless-express";
import app from "./express.app";
import MongoDbConnectionService from "./services/mongodb-connection-service";
let connection = null;
let serverlessExpressInstance = null;
export const handler: APIGatewayProxyHandler = async (event, context, callback) => {
context.callbackWaitsForEmptyEventLoop = false;
const authHeaders = (event.requestContext || {}).authorizer || {};
if(connection == null) {
connection = await MongoDbConnectionService.connect();
}
if(serverlessExpressInstance) {
return serverlessExpressInstance(event, context, callback);
}
['principalId'].forEach((headerKey) => {
if (authHeaders.hasOwnProperty(headerKey)) {
event.headers[headerKey.toLowerCase()] = authHeaders[headerKey];
}
});
console.log("Event headers: ", event.headers);
serverlessExpressInstance = serverlessExpress({ app });
return serverlessExpressInstance(event, context, callback);
};
As you can see that I am extracting "principalId" from the event.requestContext and adding it to the event.headers.
When I log the event.headers it does show that "principalid" is included in it.
express server
'use strict';
import cors from 'cors';
import express from 'express';
import 'reflect-metadata';
import { ExpressRouter } from './routes/routes';
import path from 'path';
class ExpressApplication {
public app: express.Express;
private readonly router: express.Router;
constructor() {
const expressRouter = new ExpressRouter();
this.app = express();
this.router = express.Router();
this.app.use(cors());
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
expressRouter.setRoutes(this.app, this.router);
}
}
const app = new ExpressApplication();
export default app.app;
route
app.post("/calendar-integration", requestValidator(CalIntegrationValidationModel), (req: Request, res: Response) => {
console.log(req.headers);
res.send("Testing this shit");
});
In the above console.log(req.headers) the principalid header is missing. I am unable to figure out what I might be doing wrong, this has been working for me when I was using aws-serverless-express before it got depreciated in favor of #vendia/serverless-express.

Seems like we need to pass the custom headers to event headers via multiValueHeaders.
So the code should be
['principalId'].forEach((headerKey) => {
if (authHeaders.hasOwnProperty(headerKey)) {
event.multiValueHeaders[headerKey.toLowerCase()] = authHeaders[headerKey];
}
});

Related

When to load .env variables in NodeJS app?

I am coding a simple NodeJS Express REST API, using TypeScript. I have some environment variables that I load with dotenv.
I access my .env variables at two different stages in my code: index.ts, which is my start file, and in a MyControllerClass.ts file. To access these variables, the code is process.env.MY_ENV_VAR. To load them for the application, the code is dotenv.config().
As my index.ts file seems to be the root of my program (I configure my app in it), I use dotenv.config() to load my .env file for the rest of the program. However, in my MyControllerClass.ts file, in the constructor, if I do console.log(process.env.MY_ENV_VAR), I get "undefined". I could workaround this by adding a dotenv.config() in my constructor (it works) but it's nonsense to me to have it here.
How do I use dotenv.config() once and for all in my program, in a readable manner (like in an appropriate .ts file)? and more generally: what is a NodeJS Express loading cycle?
Here is a sample of the file structure of my code
src
├── index.ts
├── Authentication
│ └── authentication.router.ts
│ └── authentication.controller.ts
Here is the code of index.js
/**
* Required External Modules
*/
import * as dotenv from "dotenv";
import express from "express";
import cors from "cors";
import helmet from "helmet";
import { authenticationRouter } from "./authentication/authentication.router"
dotenv.config();
/**
* App Variables
*/
if(!process.env.PORT) {
process.exit(1);
}
const PORT: number = parseInt(process.env.PORT as string, 10);
const app = express();
/**
* App Configuration
*/
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(authenticationRouter);
app.use("api/authenticate/", authenticationRouter);
/**
* Server Activation
*/
app.listen(PORT, () => {
console.log(`Listening on port ${PORT}`);
});
Here is the code of authentication.router.ts
import express, { Request, Response } from "express";
import { AuthenticatorController } from "./authentication.controller";
export const authenticationRouter = express.Router();
const authenticatorController = AuthenticatorController.getInstance();
authenticationRouter.post("/api/authenticate", async (req: Request, res: Response) => {
try {
if (await authenticatorController.authenticate(req.body.login, req.body.password)) {
res.send({"status": "ok"})
} else
res.send({"status": "Error"})
} catch (e) {
console.debug(e)
res.send({"status": "500"});
}
});
Here is the code of authentication.controller.ts
import { ClientSecretCredential } from "#azure/identity";
import { SecretClient } from "#azure/keyvault-secrets";
import { Authenticator } from "./api/Authenticator";
import * as dotenv from "dotenv";
dotenv.config();
export class AuthenticatorController implements Authenticator {
private static singleInstance: AuthenticatorController | null = null;
private azureSecretCredential= new ClientSecretCredential(
process.env.AZURE_TENANT_ID as string,
process.env.AZURE_CLIENT_ID as string,
process.env.AZURE_CLIENT_SECRET as string);
private azureSecretClient = new SecretClient(
process.env.KEY_VAULT_URL as string,
this.azureSecretCredential);
private constructor () {}
public static getInstance(): AuthenticatorController {
if (this.singleInstance === null) {
this.singleInstance = new AuthenticatorController();
}
return this.singleInstance;
}
public async authenticate(login: string, password: string): Promise<Boolean> {
let isAuthenticated = false;
try {
const secret = await this.azureSecretClient.getSecret(login)
if (secret.name === login) {
if (secret.value === password) {
isAuthenticated = true;
}
}
} catch (e) {
console.debug(e);
}
return isAuthenticated;
}
}
You only call dotenv.config() once:
As early as possible in your application, require and configure
dotenv.
require('dotenv').config()
Therefore index.ts seems to be correct, process.env should then hold your parsed values. Maybe you can use something like this to make sure, data is parsed correctly:
const result = dotenv.config();
if (result.error) {
throw result.error;
}
console.log(result.parsed);
Edit:
You can try the following. I changed your exports a bit, because there is no need for a singleton within your controller.
authentication.router.ts:
// Imports (no dotenv; no dotenv.config())
// [...]
// Import controller
import { authenticatorController } from "./authentication.controller";
export const authenticationRouter = express.Router();
// Adding routes
// [...]
authentication.controller.ts:
// Imports (no dotenv; no dotenv.config())
// [...]
class AuthenticatorController implements Authenticator {
// [...]
}
export const authenticatorController = new AuthenticatorController();
index.ts:
// Imports (dotenv)
// [...]
const { error, parsed } = dotenv.config();
if (error) {
throw error;
}
console.log(parsed);
// [...]
app.use("api/authenticate/", authenticationRouter);
// [...]

Nestjs on AWS Lambda (Serverless Framework) | How to access the event parameter?

I'm hosting a Nestjs application on AWS Lambda (using the Serverless Framework).
Please note that the implementation is behind AWS API Gateway.
Question: How can I access to event parameter in my Nest controller?
This is how I bootstrap the NestJS server:
import { APIGatewayProxyHandler } from 'aws-lambda';
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import { Server } from 'http';
import { ExpressAdapter } from '#nestjs/platform-express';
import * as awsServerlessExpress from 'aws-serverless-express';
import * as express from 'express';
let cachedServer: Server;
const bootstrapServer = async (): Promise<Server> => {
const expressApp = express();
const adapter = new ExpressAdapter(expressApp);
const app = await NestFactory.create(AppModule, adapter);
app.enableCors();
await app.init();
return awsServerlessExpress.createServer(expressApp);
}
export const handler: APIGatewayProxyHandler = async (event, context) => {
if (!cachedServer) {
cachedServer = await bootstrapServer()
}
return awsServerlessExpress.proxy(cachedServer, event, context, 'PROMISE')
.promise;
};
Here is a function in one controller:
#Get()
getUsers(event) { // <-- HOW TO ACCESS event HERE?? This event is undefined.
return {
statusCode: 200,
body: "This function works and returns this JSON as expected."
}
I'm struggling to understand how I can access the event paramenter, which is easily accessible in a "normal" node 12.x Lambda function:
module.exports.hello = async (event) => {
return {
statusCode: 200,
body: 'In a normal Lambda, the event is easily accessible, but in NestJS its (apparently) not.'
};
};
Solution:
Add AwsExpressServerlessMiddleware to your setup during bootstrap:
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware')
app.use(awsServerlessExpressMiddleware.eventContext())
Note: The app.use should be before app.init()
Now the event and context object can be accessed:
var event = req.apiGateway.event;
var context = req.apiGateway.context;
Credits: This answer on SO

Using socket.io with React and Google App Engine

I've created a Node(express)/React app that uses socket.io and Redux's store as follows:
import io from "socket.io-client";
import * as types from "../actions/types";
import { cancelReview, startReview } from "./actions";
const socket = io("http://localhost:8080", {
transports: ["websocket"]
});
export const init = store => {
socket.on("connect", () => {
console.log("websocket connection successful...");
socket.on("cancelReview", (id, name) => {
cancelReview(store, id, name);
});
socket.on("startReview", (id, name) => {
startReview(store, id, name);
});
});
};
This function is then called from store.js as follows:
import { createStore, applyMiddleware } from "redux";
import { composeWithDevTools } from "redux-devtools-extension/developmentOnly";
import thunk from "redux-thunk";
import rootReducer from "./reducers";
import { init } from "./socket/socket";
// Initial state
const initialState = {};
// Middleware
const middleware = [thunk];
const store = createStore(
rootReducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
init(store);
export default store;
Everything works fine on my local machine, but I'm now realizing after doing some research that this will not work on Google's app engine because instead of http://localhost:8080 I need to get the actual IP address from Google's metadata server and pass in EXTERNAL_IP + ":65080". So I'm able to get the external IP in my express app as follows:
const METADATA_NETWORK_INTERFACE_URL =
"http://metadata/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip";
function getExternalIp(cb) {
const request = axios.create({
baseURL: METADATA_NETWORK_INTERFACE_URL,
headers: { "Metadata-Flavor": "Google" }
});
request
.get("/", (req, res) => {
return cb(res.data);
})
.catch(err => {
console.log("Error while talking to metadata server, assuming localhost");
return cb("localhost");
});
}
However, if I pass this value into my render function as seen below, React creates a prop to pass into components (as far as I understand from the info I could find):
app.get("*", (req, res) => {
getExternalIp(extIp => {
res.render(path.resolve(__dirname, "client", "build", "index.html"), {
externalIp: extIp
});
});
I am not able to access this value via the window global. So my question is, how do I access this external IP from my store initialization, since it is not an actual React component?
Thanks in advance.

Using Service in Express Router

I am pretty new in the NodeJS but I would like to learn something new. I came from .NET fancy dependency injection, inversion of controll, microservice shiny world so I am trying write some service in TypeScript based on my previous experiences.
I am using express and express router to create some api. I have some methods in router which handles api calls and I want to use some kind of service object for data retrieving and manipulation.
I inject the service into the router using constructor injection but if I want to use my service it throws an error:
TypeError: Cannot read property 'layoutService' of undefined
I understood that the methods were called withouth context so I added .bind(this) to the each method regsitration and it works, but I dont know if it is the best way how to do it.
Does anyone have a better idea?
simplified server.ts
import express, { Router } from "express";
// inversion of controll
import container from "./ioc";
import { TYPE } from "./constants";
import IMyService from "./abstract/IMyService";
// import routers
import MyRouter from "./api/MyRouter";
app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const router: Router = express.Router();
const myRouter: MyRouter = new MyRouter(container.get<IMyService>(TYPE.IMyService));
app.use("/", router);
app.use("/api/v1/layouts", layoutRouter.router);
MyRouter.ts
import IMyService from "./abstract/IMyService";
import { Router, Request, Response } from "express";
import { inject } from "inversify";
import { TYPE } from "../constants";
export default class MyRouter {
public readonly router: Router;
private readonly myService: IMyService;
constructor(
#inject(TYPE.IMyService) myService: IMyService
) {
this.myService = myService;
this.router = Router();
this.routes();
}
public GetAll(req: Request, res: Response): void {
this.myService.getAll()
.then(data => {
const status: number = res.statusCode;
res.json({ status, data });
})
.catch(err => {
const status: number = res.statusCode;
res.json({ status, err });
});
}
public GetOne(req: Request, res: Response): void {
const id: string = req.params.id;
this.myService.getOne(new ObjectID(id))
.then(data => {
const status: number = res.statusCode;
res.json({ status, data });
})
.catch(err => {
const status: number = res.statusCode;
res.json({ status, err });
});
}
routes(): void {
this.router
.get("/", this.GetAll)
.get("/:id", this.GetOne);
}
}
If you define your function with the arrow syntax (ES6), it will "bind" the context to it automatically and you won't need to bind them. But it will depends on your use case (ou might need to bind a different context)

localhost REST API request error ionic2 angular2

I am making a get/post request to my locally hosted REST API server in an Ionic 2 app. The errow below shows up afer a couple of seconds.
3 387557 group EXCEPTION: Response with status: 0 for URL: null
4 387558 error EXCEPTION: Response with status: 0 for URL: null
5 387558 groupEnd
6 387568 error Uncaught Response with status: 0 for URL: null, http://localhost:8100/build/js/app.bundle.js, Line: 88826
I am able to make a successful curl request to the local server. Here is my code for reference.
app.js
var express = require("express");
var mysql = require("mysql");
var bodyParser = require("body-parser");
var SHA256 = require("sha256");
var rest = require("./REST.js");
var app = express();
function REST(){
var self = this;
self.connectMysql();
};
REST.prototype.connectMysql = function() {
var self = this;
var pool = mysql.createPool({
connectionLimit : 100,
host : 'host',
user : 'user',
password : 'password',
database : 'database',
debug : false
});
pool.getConnection(function(err,connection){
if(err) {
self.stop(err);
} else {
self.configureExpress(connection);
}
});
}
REST.prototype.configureExpress = function(connection) {
var self = this;
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
var router = express.Router();
app.use('/api', router);
var rest_router = new rest(router,connection,SHA256);
self.startServer();
}
REST.prototype.startServer = function() {
app.listen(3000, function() {
console.log("All right ! I am alive at Port 3000. OKAY BUDDY");
});
}
REST.prototype.stop = function(err) {
console.log("ISSUE WITH MYSQL n" + err);
process.exit(1);
}
new REST();
REST.js
var mysql = require("mysql");
function REST_ROUTER(router, connection, SHA256) {
var self = this;
self.handleRoutes(router, connection, SHA256);
}
REST_ROUTER.prototype.handleRoutes= function(router,connection,SHA256) {
router.get("/",function(req,res){
res.json({'foo': 'bar'});
});
});
login.js (component)
import {Component} from '#angular/core';
import {NavController} from 'ionic-angular';
import {AuthProvider} from '../../providers/auth/auth';
/*
Generated class for the LoginPage page.
See http://ionicframework.com/docs/v2/components/#navigation for more info on
Ionic pages and navigation.
*/
#Component({
templateUrl: 'build/pages/login/login.html',
providers: [AuthProvider]
})
export class LoginPage {
static get parameters() {
return [[NavController], [AuthProvider]];
}
constructor(nav, AuthProvider) {
this.nav = nav;
this.authProvider = AuthProvider;
this.form = {};
}
login(form) {
this.authProvider.login(form).then(res => {
alert(JSON.stringify(res));
});
}
}
auth.js (provider)
import {Injectable} from '#angular/core';
import {Http, Headers, RequestOptions} from '#angular/http';
import 'rxjs/add/operator/map';
/*
Generated class for the Auth provider.
See https://angular.io/docs/ts/latest/guide/dependency-injection.html
for more info on providers and Angular 2 DI.
*/
#Injectable()
export class AuthProvider {
static get parameters(){
return [[Http]]
}
constructor(http) {
this.url = 'http://localhost:3000/api';
this.http = http;
}
login(form) {
return new Promise(resolve => {
this.http.get(this.getUrl)
.map(res => res.json())
.subscribe(data => {
resolve(data);
});
});
}
}
I had the same problem, and was able to resolve it. I was serving my API on localhost:8000. When ionic makes a request to localhost or 127.0.0.1, I think it is blocked. I instead found my computer's IP address and hosted my webserver on 0.0.0.0:8000 and instead of hitting http://localhost:8000/api/my/endpoint I hit http://mycomputerip:8000/api/my/endpoint, and it worked!
You are trying to request empty URL bacause of typo in auth.js login function:
this.http.get(this.getUrl)
this.getUrl is not defined in your code samples. Easy fix:
this.http.get(this.url)

Resources