Skip to content

Parse Authentication in an existent Express App #2985

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ghost opened this issue Nov 2, 2016 · 13 comments
Closed

Parse Authentication in an existent Express App #2985

ghost opened this issue Nov 2, 2016 · 13 comments

Comments

@ghost
Copy link

ghost commented Nov 2, 2016

I don't know if this is the right place to ask that.

I have an existent Express API and now we started to using Parse in the same server.
Is there a way to authenticate an user in the Parse server from the Express endpoints?

I'm trying to avoid the migration of all the existent API to the cloud functions.

Thanks

@milesrichardson
Copy link

The best way to do this is to use node-parse SDK from within your node app. Call the Parse.User.logIn() method with the credentials supplied by the user. Get the session key and store that in a cookie. Then write an auth middleware to verify that the session key exists in the database and points to a valid user. Optionally, cache the session keys in redis or memcached to avoid DB lookups on every request.

@mi3e
Copy link

mi3e commented Nov 13, 2016

Try adding an express middleware something like this. Just showing the happy path if the token is valid, it'll add the user object to the request and then run next middleware.

app.use('/api/*', (req, res, next) => {
  unirest.get(env.getParseURL() + '/users/me')
    .headers({
      'X-Parse-Application-Id': env.getApplicationId(),
      'x-parse-session-token': req.header("x-parse-session-token")
    })
    .send({})
    .end((userData) => {
      if (userData.status == 200) {
        req.user = Parse.Object.fromJSON(userData.body);
        next();
      }

@milesrichardson
Copy link

What I do is:

  1. On login, use Parse.User.logIn to validate the user. If successful, lookup session token and store the session token in a cookie.
  2. In auth middleware, check for session token in cookie. If there, validate session token via query to parse backend (including the User pointer) to set req.user to the correct user.

Here's the middleware I use:

authMiddleware.requireLogin = function (req, res, next) {
    // debug(req.session);

    debug('AUTH: ' + req.method + ' ' + req.url);

    var redirectUrl = qs.escape(req.originalUrl);

    // debug(req.session);
    if (req.session.token == undefined) {
        debug('No session token');
        return res.redirect('/account/pub/login?redirect=' + redirectUrl);
    } else {
        debug('Query for session token' + req.session.token);
        Parse.Cloud.useMasterKey();

        var sq = new Parse.Query('_Session')
                          .equalTo('sessionToken', req.session.token)
                          .include('user');

        sq.first().then(function(sessionResult) {
            if (sessionResult == undefined) {
                debug("No matching session");
                res.redirect('/account/pub/login');
            } else {
                debug("Got matching session");
                req.user = sessionResult.get('user');
                res.locals.session = req.session;
                res.locals.user = req.user;

                next();
            }
        }, function(err) {
            debug("Error or no matching session: " + err);
            res.redirect('/account/pub/login');
        });
    }
};

And the login code:

router.post('/login', function(req, res) {
    debug('Got post to /login');

    req.checkBody('lg_email', 'Enter a valid email address.').isEmail();
    req.checkBody('lg_password', 'Password required.').notEmpty();

    var errors = req.validationErrors();
    if (errors) {
        debug('ERRORS logging in:');
        debug(errors);

        res.render('pages/account-public/login', { 'errors': errors, 'prev_body': req.body });
        return;
    }

    debug('Got valid login form.');
    debug('Try to login');

    Parse.User.logIn(req.body.lg_email, req.body.lg_password).then(function(user) {
        debug('Successfully logged in!');

        debug(user);
        debug('---');
        debug(Parse.User.current());

        // req.session.user = user;

        req.session.token = user.getSessionToken();

        debug("Session token: " + user.getSessionToken());
        debug("Session token: " + req.session.token);

        debug(req.session);
        req.user = user;

        if (req.body.redirectUrl) {
            res.redirect(req.body.redirectUrl);
        } else {
            res.redirect('/');
        }
    }, function(err) {
        debug('Error logging in.');
        debug('Error: ');
        debug(err);

        if (err.code == 101) {
            errors = [{ param: 'unknown_param',
                        msg: "Invalid email or password.",
                        value: ''
            }];
        } else {
            errors = [{ param: 'unknown_param',
                        msg: error.message,
                        value: ''
            }];
        }

        req.session = null;
        res.render('pages/account-public/login', { 'errors': errors, 'prev_body': req.body });
    });
});

This all depends on the cookie-session middleware (npm install cookie-session) which needs to be applied before any middleware that uses it (e.g. this authentication middleware):

app.use(cookieSession({
    name: 'session',
    secret: "xxxxx",
    maxAge: 15724800000
}));

@bdbaddog
Copy link

@milesrichardson I'm getting:
error: Error generating response. ParseError { code: 209, message: 'This session token is invalid.' } code=209, message=This session token is invalid.

As a result of:

        var sq = new Parse.Query('_Session')
                          .equalTo('sessionToken', req.session.token)
                          .include('user');


        sq.first().then(function(sessionResult) {
          console.log("sessionResult :"+sessionResult);
            if (sessionResult == undefined) {
                console.log("No matching session");
                res.session.redirect_to = req.url;
                return res.redirect('/login');
            } else {
                console.log("Got matching session");
                req.user = sessionResult.get('user');
                res.locals.session = req.session;
                res.locals.user = req.user;

                return next();
            }
        }, function(err) {
            console.log("Error or no matching session: " + err);
            return res.redirect('/login');
        });

Querying for the session token in the API explorer on the dashboard succeeds..
Any thoughts?

@mi3e
Copy link

mi3e commented Nov 23, 2016

Why are you guys home baking the logic to validate a session token using queries against the _Session collection when parse-server already has the build-in token validation via the REST api by doing a GET on parse/users/me? For example are you checking the session expiry? If you query this with the token in the header it'll do all this validation and return the user in the body of the request.. Is it the overhead of doing a http call that's leading you to rewrite this, seems a bit risky imho.

http://blog.parse.com/announcements/validating-session-tokens-with-the-rest-api/

@flovilmart
Copy link
Contributor

We'll provide in the next release an authentication middleware backed by the parse-server auth middleware.

@bdbaddog
Copy link

@flovilmart - Will this be before the parse server shutdown?(oh please oh please) :)

@ghost
Copy link
Author

ghost commented Dec 11, 2016

I came up with this solution, caching the user in Redis and implementing this middleware.

Set the x-session-token as a session header and pass it from ParseUser.getCurrentUser().getSessionToken() value.
And access the req.user object in your routers endpoints.

auth.js

import request from 'request-promise'
import ParseClient from './parse-client'
import Cache from './cache'

export default (config) => {
  let client = ParseClient(config);
  let cache = Cache('auth');

  function auth(req, res, next) {
    // if no session token, bad request
    if(!req.headers['x-session-token']){
      res.status(400).send("Missing sessionToken")
      return
    }
    
    let authToken = req.headers['x-session-token']

    //if user in session, forward the request
    cache.get(authToken)
      .then( user => user == null ? client.getMe(authToken) : user )
      .then( user => cache.set(authToken, user) )
      .then( user => {
        req.user = user;
        next();
      })
      .catch( error => {
        console.error('Fail to authenticate user', error);
        res.sendStatus(401);
      })
  }
  
  return auth
}

cache.js

import redis from 'redis'
import Promise from 'bluebird'
import { config } from './config'

// Define redis promisses
Promise.promisifyAll(redis.RedisClient.prototype);
Promise.promisifyAll(redis.Multi.prototype);

let redisClient = redis.createClient(config.cache.port, config.cache.host, {db: 1});

export default (prefix) => {
  return {
    set: function(_key, value) {
      let key = `${prefix}:${_key}`
      return redisClient.setAsync(key, JSON.stringify(value))
        .then(() => redisClient.persistAsync(key))
        .then(() => redisClient.expireAsync(key, config.cache.ttl))
        .then(() => value)
    },

    get: function(_key) {
      let key = `${prefix}:${_key}`
      return redisClient.getAsync(key)
        .then(value => value == null ? null : JSON.parse(value))
    },

    del: function(_key) {
      let key = `${prefix}:${_key}`
      return redisClient.delAsync(key)
    }
  }
}

and the parse-client.js a simple function to get the user based on the session token.

import request from 'request-promise'
...
getMe: function(authToken) {
      return request({
        uri: config.uri + '/users/me',
        method: 'GET',
        json: true,
        headers: {
          'X-Parse-Application-Id': config.applicationId,
          'X-Parse-REST-API-Key': config.restKey,
          'X-Parse-Session-Token': authToken
        }
      })
    }

and set up the middleware

app.use('/api/*', auth(parseConfig));

@bdbaddog
Copy link

bdbaddog commented Dec 14, 2016 via email

@FaridSafi
Copy link

@flovilmart Hi sir, do you have any news regarding the authentication middleware ?

@cbess
Copy link

cbess commented Apr 4, 2017

For now, I decided to go the single page application route (using Vue.js). I store the session token on the client and upon refresh, I use Parse.User.become(...) from the stored session token, or present login if needed, etc.

@flovilmart
Copy link
Contributor

Closing due to lack of activity, please update to latest parse-server version and open a new issue if the issue persist.

Don't forget to include your current:

  • node version
  • npm version
  • parse-server version
  • any relevant logs (VERBOSE=1 will enable verbose logging)

@georgeplaton7
Copy link

Is there any update on Parse express auth middleware ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants