Authentication on Server side routes in Meteor

后端 未结 5 690
甜味超标
甜味超标 2020-12-05 06:11

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

相关标签:
5条回答
  • 2020-12-05 06:41

    In addition to using url tokens as the other answer you could also use cookies:

    Add in some packages that allow you to set cookies and read them server side:

    meteor add mrt:cookies thepumpinglemma:cookies
    

    Then you could have something that syncs the cookies up with your login status

    Client Side

    Tracker.autorun(function() {
         //Update the cookie whenever they log in or out
         Cookie.set("meteor_user_id", Meteor.userId());
         Cookie.set("meteor_token", localStorage.getItem("Meteor.loginToken"));
    });
    

    Server Side

    On the server side you just need to check this cookie is valid (with iron router)

    Router.route('/somepath/:fileid', function() {
    
       //Check the values in the cookies
       var cookies = new Cookies( this.request ),
           userId = cookies.get("meteor_user_id") || "",
           token = cookies.get("meteor_token") || "";
    
       //Check a valid user with this token exists
       var user = Meteor.users.findOne({
           _id: userId,
           'services.resume.loginTokens.hashedToken' : Accounts._hashLoginToken(token)
       });
    
       //If they're not logged in tell them
       if(!user) return this.response.end("Not allowed");
    
       //Theyre logged in!
       this.response.end("You're logged in!");
    
    }, {where:'server'});
    
    0 讨论(0)
  • 2020-12-05 06:41

    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!

    0 讨论(0)
  • 2020-12-05 06:45

    Since Meteor doesn't use session cookies, client must explicitly include some sort of user identification when making a HTTP request to a server route.

    The easiest way to do it is to pass userId in the query string of the URL. Obviously, you also need to add a security token that will prove that the user is really who the claim they are. Obtaining this token can be done via a Meteor method.

    Meteor by itself doesn't provide such mechanism, so you need some custom implementation. I wrote a Meteor package called mhagmajer:server-route which was thoroughly tested. You can learn more about it here: https://blog.hagmajer.com/server-side-routing-with-authentication-in-meteor-6625ed832a94

    0 讨论(0)
  • 2020-12-05 06:49

    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

    0 讨论(0)
  • 2020-12-05 06:53

    Because server-side routes act as simple REST endpoints, they don't have access to user authentication data (e.g. they can't call Meteor.user()). Therefore you need to devise an alternative authentication scheme. The most straightforward way to accomplish this is with some form of key exchange as discussed here and here.

    Example implementation:

    server/app.js

    // whenever the user logs in, update her apiKey
    Accounts.onLogin(function(info) {
      // generate a new apiKey
      var apiKey = Random.id();
      // add the apiKey to the user's document
      Meteor.users.update(info.user._id, {$set: {apiKey: apiKey}});
    });
    
    // auto-publish the current user's apiKey
    Meteor.publish(null, function() {
      return Meteor.users.find(this.userId, {fields: {apiKey: 1}});
    });
    

    lib/routes.js

    // example route using the apiKey
    Router.route('/secret/:apiKey', {name: 'secret', where: 'server'})
      .get(function() {
        // fetch the user with this key
        // note you may want to add an index on apiKey so this is fast
        var user = Meteor.users.findOne({apiKey: this.params.apiKey});
    
        if (user) {
          // we have authenticated the user - do something useful here
          this.response.statusCode = 200;
          return this.response.end('ok');
        } else {
          // the key is invalid or not provided so return an error
          this.response.statusCode = 403;
          return this.response.end('not allowed');
        }
      });
    

    client/app.html

    <template name="myTemplate">
        {{#with currentUser}}
          <a href="{{pathFor route='secret'}}">secret</a>
        {{/with}}
    </template>
    

    Notes

    • Make /secret only accessible via HTTPS.

    • While it's very likely that the user requesting /secret is currently connected, there is no guarantee that she is. The user could have logged in, copied her key, closed the tab, and initiated the request sometime later.

    • This is a simple means of user authentication. I would explore more sophisticated mechanisms (see the links above) if the server-route reveals high-value data (SSNs, credit cards, etc.).

    • See this question for more details on sending static content from the server.

    0 讨论(0)
提交回复
热议问题