moleculerjs / moleculer-web

:earth_africa: Official API Gateway service for Moleculer framework
http://moleculer.services/docs/moleculer-web.html
MIT License
294 stars 119 forks source link

Losing req and res objects when running inside Docker container #179

Closed rishighan closed 4 years ago

rishighan commented 4 years ago

I am implementing passport-local authentication in my user-service micro service. Since all examples for Passport assume tight coupling with express, I am having a tough time implementing the login functionality.

Specifically, Passport makes its logIn method available on the req object. Now this object is an express wrapper around the node.js request I don't know how to make these available in my actions.

Usually the examples assume an express server:

router.post('/login', (req, res, next) => {
    passport.authenticate('local', (err, user, info) => {
        if (err) {
            winston.log('error', 'Passport login strategy failure', {errorObj: err});
            return next(err);
        }
        if (!user) {
            winston.log('info', 'Invalid credentials', {errorObj: user.username});
            return res.status(401).json({
                err: info
            });
        }
        req.logIn(user, err, () => {
            if (err) {
                winston.log('error', 'User login failure', {errorObj: err});
                return res.status(500).json({
                    err: 'Could not log in user'
                });
            }
            winston.log('info', 'User login successful', {username: user.username});
            res.status(200).json({
                status: 'Login successful!'
            });
        });
    })(req, res, next);

So far, I have this in my api.service.js

module.exports = {
    name: "api",
    mixins: [ApiGateway],
    settings: {
        port: process.env.PORT || 3456,
        ip: "0.0.0.0",
        use: [
            passport.initialize(),
            passport.session(),
        ],
        routes: [
            {
                path: "/users",
                whitelist: ["**"],
                use: [passport.authenticate("local")],
                mergeParams: true,
                authentication: false,
                authorization: false,
                autoAliases: true,

                aliases: {
                    "POST health": "$node.health",
                    "POST login": "user.login",
                },

                callingOptions: {},
                bodyParsers: {
                    json: {
                        strict: false,
                        limit: "1MB",
                    },
                    urlencoded: {
                        extended: true,
                        limit: "1MB",
                    },
                },

                // Mapping policy setting. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Mapping-policy
                mappingPolicy: "all", // Available values: "all", "restrict"

                // Enable/disable logging
                logging: true,
            },
        ],

        log4XXResponses: false,
        logRequestParams: null,
        logResponseData: null,
    },

    methods: {},
    created() {
        const app = express();
        app.use(this.express());
        this.app = app;
    },

    started() {
        this.app.listen(Number(this.settings.port), (err) => {
            if (err) return this.broker.fatal(err);
            this.logger.info(
                `WWW server started on port ${this.settings.port}`
            );
        });
    },

    stopped() {
        if (this.app.listening) {
            this.app.close((err) => {
                if (err)
                    return this.logger.error("WWW server close error!", err);
                this.logger.info("WWW server stopped!");
            });
        }
    },
};

and my service schema is:

"use strict";

const DBService = require("../mixins/db.mixin");
const User = require("../models/user.model");
const passport = require("passport");

module.exports = {
    name: "user",
    mixins: [DBService("userschemas", User)],
    settings: {},
    dependencies: [],
    actions: {
        register: {
            params: { username: "string" },
            handler(broker) {
                User.register(
                    new User({ username: broker.params.username }),
                    broker.params.password,
                    (err, account) => {
                        if (err) {
                            return { error: new Error(err, account) };
                        }
                        passport.authenticate("local")(null, null, () => {
                            return { status: "User successfully registered." };
                        });
                    }
                );
            },
        },
        login: {
            handler(ctx) {
                // NOT SURE HOW TO GET THIS TO WORK
                passport.authenticate("local", (error, user, info) => {
                    console.log("here");
                });

            },
        },

    },
};

I found enough documentation around to implement user registration. But how do I get the req, res and next objects to be available in my service action?

  1. I want to simply call the req.logIn to login the user,
  2. Upon successful authentication, send a res.status(200).json({info: 'logged in'})
  3. Upon failure, send a res.status(401)
rishighan commented 4 years ago

I made some progress and now have my login action working temporarily:

login: {
            handler(ctx) {
                return new Promise((resolve, reject) => {
                    passport.authenticate("local", (err, user, info) => {
                        if (err) {
                            reject(new Error(err));
                        }

                        if (!user) {
                            ctx.meta.$statusCode = 401;
                            reject({ status: "Invalid credentials" });
                        }

                        ctx.options.parentCtx.params.req.logIn(
                            user,
                            err,
                            () => {
                                if (err) {
                                    ctx.meta.$statusCode = 500;
                                    reject({ status: "Could not login user" });
                                }
                                ctx.meta.$statusCode = 200;
                                resolve({ status: "Login successful" });
                            }
                        );
                    })(
                        ctx.options.parentCtx.params.req,
                        ctx.options.parentCtx.params.res,
                        () => {
                            console.log("Inside user.login's callback");
                        }
                    );
                });
            },
        },

Couple of questions:

  1. Is that the right way to reference req ?
  2. I have noticed that when I run this micro service dockerized, I get undefined errors when referencing req
rishighan commented 4 years ago

I just found out that when I run the service dockerized, the context param of the action handler has a different signature:

Context< {
  id: 'fd5f7c87-51db-4e5c-8297-6f1ade4750a9',
  nodeID: 'abf3540dd285-18',
  action: { name: 'user.status' },
  service: { name: 'user', version: undefined, fullName: 'user' },
  options: { timeout: 10000, retries: null },
  parentID: '621aafc2-a3a4-419b-a630-7a0ac32bc073',
  caller: 'api',
  level: 2,
  params: {},
  meta: {},
  requestID: '621aafc2-a3a4-419b-a630-7a0ac32bc073',
  tracing: true,
  span: Span {
    name: "action 'user.status'",
    type: 'action',
    id: 'fd5f7c87-51db-4e5c-8297-6f1ade4750a9',
    traceID: '621aafc2-a3a4-419b-a630-7a0ac32bc073',
    parentID: '621aafc2-a3a4-419b-a630-7a0ac32bc073',
    service: { name: 'user', version: undefined, fullName: 'user' },
    priority: 5,
    sampled: true,
    startTime: 1588806411889.3867,
    finishTime: null,
    duration: null,
    error: null,
    logs: [],
    tags: {
      callingLevel: 2,
      action: [Object],
      remoteCall: true,
      callerNodeID: 'abf3540dd285-18',
      nodeID: 'f4fc833ec47f-18',
      options: [Object],
      params: {}
    }
  },
  needAck: null,
  ackID: null,
  eventName: null,
  eventType: null,
  eventGroups: null,
  cachedResult: false
} >

Why aren't there req and res objects?

icebob commented 4 years ago

The req & res instances are not serializable. You can't send them through the transporter. You should use them only in API gateway service locally.

rishighan commented 4 years ago

So put the action as a custom function in api.service.js like this:

"POST login" (req, res) {
    return new Promise((resolve, reject) => {
        passport.authenticate("local", (err, user, info) => {
            if (err) {
                reject(new Error(err));
            }

            if (!user) {
                // ctx.meta.$statusCode = 401;
                reject({
                    status: "Invalid credentials"
                });
            }

            req.logIn(
                user,
                err,
                () => {
                    if (err) {
                        // ctx.meta.$statusCode = 500;
                        reject({
                            status: "Could not login user"
                        });
                    }
                    // ctx.meta.$statusCode = 200;
                    resolve({
                        status: "Login successful"
                    });
                }
            );
        })(
            req,
            res,
            () => {
                console.log("Inside user.login's callback");
            }
        );
    });
},

When I hit the endpoint, the request times out. Not sure why...

icebob commented 4 years ago

If you use custom alias, you should handle the response completely, e.g calling res.end().

E.g.: https://github.com/icebob/kantab/blob/6ec7fdab837598f36dc5302bd4f202a07baf3a43/backend/mixins/passport.mixin.js#L49-L66

rishighan commented 4 years ago

Thanks @icebob! You can mark this as a question, since the req and res objects aren't serializable by design.

mingfang commented 2 years ago

Is is possible to add the addition http request data as action handler params ? I need the request headers, cookies, method, baseUrl, and originalUrl. These fields should be serializable through the transporter.

icebob commented 2 years ago

you should expose these data in onBeforeCall hook into ctx.meta which will be serialized.