Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Authentication on Server side routes in Meteor

What is the best way (most secure and easiest) to authenticate a user for a server side route?

Software/Versions

I'm using the latest Iron Router 1.* and Meteor 1.* and to begin, I'm just using accounts-password.

Reference code

I have a simple server side route that renders a pdf to the screen:

both/routes.js

Router.route('/pdf-server', function() {
  var filePath = process.env.PWD + "/server/.files/users/test.pdf";
  console.log(filePath);
  var fs = Npm.require('fs');
  var data = fs.readFileSync(filePath);
  this.response.write(data);
  this.response.end();
}, {where: 'server'});

As an example, I'd like to do something close to what this SO answer suggested:

On the server:

var Secrets = new Meteor.Collection("secrets"); 

Meteor.methods({
  getSecretKey: function () {
    if (!this.userId)
      // check if the user has privileges
      throw Meteor.Error(403);
    return Secrets.insert({_id: Random.id(), user: this.userId});
  },
});

And then in client code:

testController.events({
  'click button[name=get-pdf]': function () {
      Meteor.call("getSecretKey", function (error, response) {
        if (error) throw error;

        if (response) 
          Router.go('/pdf-server');
      });
  }
});

But even if I somehow got this method working, I'd still be vulnerable to users just putting in a URL like '/pdf-server' unless the route itself somehow checked the Secrets collection right?

In the Route, I could get the request, and somehow get the header information?

Router.route('/pdf-server', function() {
  var req = this.request;
  var res = this.response;
}, {where: 'server'});

And from the client pass a token over the HTTP header, and then in the route check if the token is good from the Collection?

like image 872
Aaron Avatar asked Jan 01 '15 19:01

Aaron


2 Answers

I think I have a secure and easy solution for doing this from within IronRouter.route(). The request must be made with a valid user ID and auth token in the header. I call this function from within Router.route(), which then gives me access to this.user, or responds with a 401 if the authentication fails:

//  Verify the request is being made by an actively logged in user
//  @context: IronRouter.Router.route()
authenticate = ->
  // Get the auth info from header
  userId = this.request.headers['x-user-id']
  loginToken = this.request.headers['x-auth-token']

// Get the user from the database
if userId and loginToken
  user = Meteor.users.findOne {'_id': userId, 'services.resume.loginTokens.token': loginToken}

// Return an error if the login token does not match any belonging to the user
if not user
  respond.call this, {success: false, message: "You must be logged in to do this."}, 401

// Attach the user to the context so they can be accessed at this.user within route
this.user = user


//  Respond to an HTTP request
//  @context: IronRouter.Router.route()
respond = (body, statusCode=200, headers) ->
  this.response.statusCode statusCode
  this.response.setHeader 'Content-Type', 'text/json'
  this.response.writeHead statusCode, headers
  this.response.write JSON.stringify(body)
  this.response.end()

And something like this from the client:

Meteor.startup ->

  HTTP.get "http://yoursite.com/pdf-server",
    headers:
      'X-Auth-Token': Accounts._storedLoginToken()
      'X-User-Id': Meteor.userId()
    (error, result) ->  // This callback triggered once http response received         
      console.log result

This code was heavily inspired by RestStop and RestStop2. It's part of a meteor package for writing REST APIs in Meteor 0.9.0+ (built on top of Iron Router). You can check out the complete source code here:

https://github.com/krose72205/meteor-restivus

like image 196
kahmali Avatar answered Nov 03 '22 01:11

kahmali


I truly believe using HTTP headers are the best solution to this problem because they're simple and don't require messing about with cookies or developing a new authentication scheme.

I loved @kahmali's answer, so I wrote it to work with WebApp and a simple XMLHttpRequest. This has been tested on Meteor 1.6.

Client

import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';

// Skipping ahead to the upload logic
const xhr = new XMLHttpRequest();
const form = new FormData();

// Add files
files.forEach((file) => {
  form.append(file.name,
    // So BusBoy sees as file instead of field, use Blob
    new Blob([file.data], { type: 'text/plain' })); // w/e your mime type is
});

// XHR progress, load, error, and readystatechange event listeners here

// Open Connection
xhr.open('POST', '/path/to/upload', true);

// Meteor authentication details (must happen *after* xhr.open)
xhr.setRequestHeader('X-Auth-Token', Accounts._storedLoginToken());
xhr.setRequestHeader('X-User-Id', Meteor.userId());

// Send
xhr.send(form);

Server

import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import { Roles } from 'meteor/alanning:roles'; // optional
const BusBoy = require('connect-busboy');
const crypto = require('crypto'); // built-in Node library

WebApp.connectHandlers
  .use(BusBoy())
  .use('/path/to/upload', (req, res) => {
    const user = req.headers['x-user-id'];
    // We have to get a base64 digest of the sha256 hashed login token
    // I'm not sure when Meteor changed to hashed tokens, but this is
    // one of the major differences from @kahmali's answer
    const hash = crypto.createHash('sha256');
    hash.update(req.headers['x-auth-token']);

    // Authentication (is user logged-in)
    if (!Meteor.users.findOne({
      _id: user,
      'services.resume.loginTokens.hashedToken': hash.digest('base64'),
    })) {
      // User not logged in; 401 Unauthorized
      res.writeHead(401);
      res.end();
      return;
    }

    // Authorization
    if (!Roles.userIsInRole(user, 'whatever')) {
      // User is not authorized; 403 Forbidden
      res.writeHead(403);
      res.end();
      return;
    }

    if (req.busboy) {
      // Handle file upload
      res.writeHead(201); // eventually
      res.end();
    } else {
      // Something went wrong
      res.writeHead(500); // server error
      res.end();
    }
  });

I hope this helps someone!

like image 39
Micah Henning Avatar answered Nov 02 '22 23:11

Micah Henning