Custom authorisation logic


#1

Hi!

How can I implement custom authorisation algorithm with MeteorJS?

When user types in his login and password in my application I send this credentials to external API (not google/facebook/etc it is private service) and that API responses me if this authorisation attempt was “ok” or “failed”. If it was “ok” then I use users login to sign any data that user creates or updates in my application but I do not want to store any data about this user on my side.

Please help me to find a way to do that.
I am really like meteors reactivity but I stuck on this point.
Not storing any user data is critical for my application.


#2

#3

@rjdavid thank you for your advise. I read this article 4 or 5 times already but still got issues.

I understood that I need to use packages accounts-base and accounts-password. Installed it via meteor add accounts-base accounts-password. On the client side I calling Meteor.loginWithPassword(login, password); in “submit” method of my auth form.

What I can’t understand is where do I need to call AccountsServer.validateLoginAttempt(myCheckFn) on the server side?

I tried to put it in “server” folder in file called “accounts.js” assuming that MeteorJS will automatically load it on startup. But it did not. Also I tried to import this “accounts.js” in “main.js” of “server” folder but still no luck.

When I fill the authorisation form in my app and hit submit button I see that Meteor.loginWithPassword(login, password); is firing and it gives me an error “403 user not found…” that makes me think that my “myCheckFn” isn’t firing because I don’t see any console.log that “myCheckFn” has.

What am I doing wrong?


#4

Ok I found out that Accounts.validateLoginAttempt(myCheckFn) must be called inside Meteor.startup(function() { ... }). Now I see my console.log :slight_smile:

But now I see that object passed to my callback has property allowed: false and appropriate error.

So now I have two questions:

  1. how can I disable all other checker-functions except my own;
  2. how can I tell Meteor.loginWithPassword(login, password) not to encrypt password (I need it in plain mode)?

#5

In my implementation, I only used account-base and did not use account-password and then I rolled my own password check implementation


#6

And how did you told MeteorJS to remember that this session belongs to particular user after your custom password check returned success?

I want this session to be alive across page reloads.

And how did you performed logout after that?

What methods did you use?


#7

I did not do anything else. Meteor automatically authorizes the current user for the entire session.

In the client, I am using something like:

Accounts.callLoginMethod({
    methodArguments: [{type: 'mobile-login', username, password}],
    validateResult: this.handleRedirect,
    userCallback: this.handleError
});

Then I have something like this in the server:

Accounts.registerLoginHandler('mobile', options => {
    if (options.type === 'mobile-login') {
        // handle login check here
    }
});

#8

I’m very confused.

Client:

Meteor.foobar = function(username, password) {
  Accounts.callLoginMethod({
    methodArguments: [{username, password}],
    validateResult: this.handleRedirect,
    userCallback: this.handleError
  });
};

Server:

Accounts.registerLoginHandler('mobile', function(options) {
  console.log("in mobile", options);
});

Typing in browser console:

Meteor.foobar("login", "password");

See in terminal (where meteor runs):

I20181010-10:49:31.589(4)? Exception while invoking method 'login' { Error: Match error: Unknown key in field username
I20181010-10:49:31.589(4)?     at check (packages/check/match.js:36:17)
I20181010-10:49:31.590(4)?     at MethodInvocation.<anonymous> (packages/accounts-password/password_server.js:290:3)
I20181010-10:49:31.590(4)?     at packages/accounts-base/accounts_server.js:483:32
I20181010-10:49:31.590(4)?     at tryLoginMethod (packages/accounts-base/accounts_server.js:259:14)
I20181010-10:49:31.590(4)?     at AccountsServer.Ap._runLoginHandlers (packages/accounts-base/accounts_server.js:480:18)
I20181010-10:49:31.590(4)?     at MethodInvocation.methods.login (packages/accounts-base/accounts_server.js:543:27)
I20181010-10:49:31.590(4)?     at maybeAuditArgumentChecks (packages/ddp-server/livedata_server.js:1767:12)
I20181010-10:49:31.590(4)?     at DDP._CurrentMethodInvocation.withValue (packages/ddp-server/livedata_server.js:719:19)
I20181010-10:49:31.590(4)?     at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1304:12)
I20181010-10:49:31.590(4)?     at DDPServer._CurrentWriteFence.withValue (packages/ddp-server/livedata_server.js:717:46)
I20181010-10:49:31.591(4)?     at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1304:12)
I20181010-10:49:31.591(4)?     at Promise (packages/ddp-server/livedata_server.js:715:46)
I20181010-10:49:31.591(4)?     at new Promise (<anonymous>)
I20181010-10:49:31.591(4)?     at Session.method (packages/ddp-server/livedata_server.js:689:23)
I20181010-10:49:31.591(4)?     at packages/ddp-server/livedata_server.js:559:43
I20181010-10:49:31.591(4)?   message: 'Match error: Unknown key in field username',
I20181010-10:49:31.591(4)?   path: 'username',
I20181010-10:49:31.591(4)?   sanitizedError: 
I20181010-10:49:31.591(4)?    { Error: Match failed [400]
I20181010-10:49:31.592(4)?     at errorClass.<anonymous> (packages/check/match.js:91:27)
I20181010-10:49:31.592(4)?     at new errorClass (packages/meteor.js:725:17)
I20181010-10:49:31.592(4)?     at check (packages/check/match.js:36:17)
I20181010-10:49:31.592(4)?     at MethodInvocation.<anonymous> (packages/accounts-password/password_server.js:290:3)
I20181010-10:49:31.592(4)?     at packages/accounts-base/accounts_server.js:483:32
I20181010-10:49:31.592(4)?     at tryLoginMethod (packages/accounts-base/accounts_server.js:259:14)
I20181010-10:49:31.592(4)?     at AccountsServer.Ap._runLoginHandlers (packages/accounts-base/accounts_server.js:480:18)
I20181010-10:49:31.593(4)?     at MethodInvocation.methods.login (packages/accounts-base/accounts_server.js:543:27)
I20181010-10:49:31.593(4)?     at maybeAuditArgumentChecks (packages/ddp-server/livedata_server.js:1767:12)
I20181010-10:49:31.593(4)?     at DDP._CurrentMethodInvocation.withValue (packages/ddp-server/livedata_server.js:719:19)
I20181010-10:49:31.593(4)?     at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1304:12)
I20181010-10:49:31.593(4)?     at DDPServer._CurrentWriteFence.withValue (packages/ddp-server/livedata_server.js:717:46)
I20181010-10:49:31.593(4)?     at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1304:12)
I20181010-10:49:31.593(4)?     at Promise (packages/ddp-server/livedata_server.js:715:46)
I20181010-10:49:31.593(4)?     at new Promise (<anonymous>)
I20181010-10:49:31.593(4)?     at Session.method (packages/ddp-server/livedata_server.js:689:23)
I20181010-10:49:31.594(4)?      isClientSafe: true,
I20181010-10:49:31.594(4)?      error: 400,
I20181010-10:49:31.594(4)?      reason: 'Match failed',
I20181010-10:49:31.594(4)?      details: undefined,
I20181010-10:49:31.594(4)?      message: 'Match failed [400]',
I20181010-10:49:31.594(4)?      errorType: 'Meteor.Error' },
I20181010-10:49:31.594(4)?   errorType: 'Match.Error' }
I20181010-10:49:31.595(4)? Sanitized and reported to the client as: { Error: Match failed [400]
I20181010-10:49:31.595(4)?     at errorClass.<anonymous> (packages/check/match.js:91:27)
I20181010-10:49:31.595(4)?     at new errorClass (packages/meteor.js:725:17)
I20181010-10:49:31.595(4)?     at check (packages/check/match.js:36:17)
I20181010-10:49:31.595(4)?     at MethodInvocation.<anonymous> (packages/accounts-password/password_server.js:290:3)
I20181010-10:49:31.595(4)?     at packages/accounts-base/accounts_server.js:483:32
I20181010-10:49:31.595(4)?     at tryLoginMethod (packages/accounts-base/accounts_server.js:259:14)
I20181010-10:49:31.595(4)?     at AccountsServer.Ap._runLoginHandlers (packages/accounts-base/accounts_server.js:480:18)
I20181010-10:49:31.596(4)?     at MethodInvocation.methods.login (packages/accounts-base/accounts_server.js:543:27)
I20181010-10:49:31.596(4)?     at maybeAuditArgumentChecks (packages/ddp-server/livedata_server.js:1767:12)
I20181010-10:49:31.596(4)?     at DDP._CurrentMethodInvocation.withValue (packages/ddp-server/livedata_server.js:719:19)
I20181010-10:49:31.596(4)?     at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1304:12)
I20181010-10:49:31.596(4)?     at DDPServer._CurrentWriteFence.withValue (packages/ddp-server/livedata_server.js:717:46)
I20181010-10:49:31.596(4)?     at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1304:12)
I20181010-10:49:31.596(4)?     at Promise (packages/ddp-server/livedata_server.js:715:46)
I20181010-10:49:31.596(4)?     at new Promise (<anonymous>)
I20181010-10:49:31.596(4)?     at Session.method (packages/ddp-server/livedata_server.js:689:23)
I20181010-10:49:31.596(4)?   isClientSafe: true,
I20181010-10:49:31.596(4)?   error: 400,
I20181010-10:49:31.597(4)?   reason: 'Match failed',
I20181010-10:49:31.597(4)?   details: undefined,
I20181010-10:49:31.597(4)?   message: 'Match failed [400]',
I20181010-10:49:31.597(4)?   errorType: 'Meteor.Error' }

#9

Got rid of error in terminal by removing accounts-password package.

Now I see in terminal what I expected to see:

I20181010-10:56:54.459(4)? registerLoginHandler { username: "login", password: "password" }

Keep exploring further.


#10

Could please anybody provide me with full example of my task? I still have no luck on this.

I need an a-to-z tutorial for how to create a simplest meteor app which authorizes user by making an http request to external custom API.

The request is very simple: POST http://my-local-auth-gate/sessions { “login”: “foo”, “password”: “bar” }.

Response is like { “token”: “some-hash” }

And then I need to store info about token and users login in the session until user logs out.

I can’t find tutorials that covers my question and official documentation doesn’t helps. I don’t know which way of implementing this functionality is correct and which is not.


#11

on the server, in meteor startup, run this function:

Accounts.registerLoginHandler('NEW_LOGIN_METHOD', (options) => {
    const { somePassword, someUsername } = options;
    if (!somePassword || !someUsername) {
      return undefined; // don't handle
    }
    
   // run some logics here to validate login

    const serviceName = 'NEW_LOGIN_METHOD';
    const serviceData = 'newLoginMethodData';
    const serviceOptions = {
      profile: {
        avatar: 'someAvatarUrl',
        otherField: 'otherData',
      },
    };
    serviceData.id = 'someUserId'; // unique
    const result = Accounts.updateOrCreateUserFromExternalService(
      serviceName,
      serviceData,
      serviceOptions,
    );
    if (!result || !result.userId) {
      throw new Meteor.Error('someError', 'Some message');
    }
    // update username if you have one
    Accounts.setUsername(result.userId, 'someNewUsername');
    // add email (not verified)
    Accounts.addEmail(result.userId, 'SomeUserEmail');
    // update password
    Accounts.setPassword(result.userId, 'SomePassword', { logout: false });

    return result;
  });

On the client side, once user enter some kind of login information, you send it to the server by calling:

Accounts.callLoginMethod({
      // methodName: 'NEW_LOGIN_METHOD',
      methodArguments: [{
        someUsername: 'someValue',
        somePassword: 'someOtherValue',
      }],
      validateResult: (result) => {
        // Custom validation of login on client side can go here
        // console.log(result);
      },
      userCallback: (error, loginDetails) => {
        // console.log(error, loginDetails);
        if (error) {
          showError(error.message);
        } else {
          // wow, it works, send user to redirect url or something
        }
      },
    });

#12

Thank you very much!

How I can implement an asynchronous password check in external api? Can I use async/await construction or what?

Also I have to send password that user inputs in plain text mode.

And the last question - can I somehow handle session for user without creating or updating user in mongo or not?


#13

You have to do the external api check from the server. And it has to be synchronous.

The password you will receive inside Accounts.registerLoginHandler() is in plain text

I am not familiar if there are hooks inside Meteor for this. I wanted to say “you can” but you most probably will need to hack into Meteor


#14

If I understand you correct, I have to do this check inside callback (second argument of Accounts.registerLoginHandler method), but how if it have to return a value that affects authorization process? I could return a promise there but I think it will not work. How can I make http request in synchronous manner?


#15

Yes

Wait for the return of the http call and throw a Meteor error if the authorization fails

I have not tried it yet but you should be able to async/await http requests


#16

You can make a synchronous-ish http request, just use Meteor’s HTTP helper.
Meteor uses fibers (thread-like co-routines) to make async calls behave in a synchronous fashion without blocking the event loop

  // Thanks to fibers, this appears to run synchronously
  // any error (including status code 4xx - 5xx) will immediately throw
  const response = HTTP.post("http://my-local-auth-gate/sessions", {
    data: { login: user, password: password },
  });

I had a look through the accounts packages and we can simplify from the solutions posted so far:

import { HTTP } from "meteor/http";

Accounts.registerLoginHandler("custom", options => {
  const { custom, password, user } = options;
  // use an extra login method argument to check if this loginHandler should be used
  if (!custom) return undefined; // return undefined to allow Meteor to try other login handlers

  const response = HTTP.post("http://my-local-auth-gate/sessions", {
    // data is automatically JSON.stringified and sent with correct content-type
    data: { login: user, password: password },
  });

  const serviceName = "custom";
  const serviceData = {
    // id is a manditory key in serviceData
    id: user,
    // response.data has the parsed result of the request
    // Not sure if you want to store the token but let's do it anyway
    token: response.data.token,
  };
  // This creates or updates an internal user record that Meteor uses to track
  // your login status. The function returns an object with the same schema
  // for the expected return type of this funciotn, so we just return directly
  return Accounts.updateOrCreateUserFromExternalService(
    serviceName,
    serviceData
    // you can pass options which will be added to the user record
  );
});

And on the client, you can create your own login handler

Meteor.loginWithCustom = (username, password, callback) => {
  Accounts.callLoginMethod({
    methodArguments: [
      {
        // add this to target your login handler
        custom: true,
        user: username,
        password: password,
      },
    ],
    userCallback: (error, result) => {
      if (error) {
        if (callback) {
          return callback(error);
        }
        throw error;
      }
      if (callback) callback();
    },
  });
};