Custom authorisation logic

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.

@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?

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)?

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

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?

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
    }
});

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' }

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.

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.

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
        }
      },
    });
3 Likes

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?

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

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?

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

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();
    },
  });
};

There’s something missing to this. Because if I use this or the various other examples I’ll always end up with this error:

Exception while invoking method 'login' { Error: Match error: Unknown key in field 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,
  };

Your service data must has id field.

Naw, that’s not it. The actual solution:

The proposed function is incompatible with accounts-password. It works as soon as I remove this package and solely use accounts-base.