Overriding "Login forbidden" message via Meteor.Error() doesn't work

I’m trying to override the standard “Login forbidden” message of the Meteor Accounts package with a more meaningful message (meaning to tell the user if the password is wrong or the email address).

This is my code:

Accounts.validateLoginAttempt((options) => {
    if (options.allowed === false && options.user !== undefined) {
        if (options.type === 'password') {
            throw new Meteor.Error('invalid-credentials', options.error.reason);

Despite throwing the Meteor.Error() it still shows the “Login forbidden” message.

I’m referring to Accounts (multi-server) | Meteor API Docs and javascript - Override 'Login Forbidden' error message in meteor-useraccounts - Stack Overflow

Thanks in advance!

The vagueness of the default error messages is likely the result of security considerations to prevent attackers fishing for data. If you communicate in your login error that the entered email was correct (by stating that the issue preventing login was specifically an incorrect password), then you have effectively created a tool for attackers to validate if certain email addresses exist in your app. Depending on the app, this might be a problem.

If in your case that’s fine and you still want more explicit error messages, then I’d probably just write out the reason parameter myself in those errors and check the conditions inside the validateLoginAttempt(). Like this:

throw new Meteor.Error('invalid-password', 'Invalid passsword. Please try again.');
1 Like

I am also using Meteor.Error() inside validateLoginAttempt() to create custom errors.

You need to confirm if the custom error line is being thrown. There might be a different error, e.g. different type being returned or user not to be undefined because Meteor can identify the user trying to login even if the authentication failed

Thanks for this explanation as to why this is the default behavior.

Which is exactly what I did in my code (see above). Yet it still doesn’t work as it should

I actually have console.logs in there (took them out when I posted the code) and you’re right that in these case the options.user is undefined. But it triggers the error in the next code block:

// Email address isn't in Meteor.users hence it `userId` is unknown
    if (options.user === undefined) {
        console.log(`161: In Accounts.validateLoginAttempt`);
        remotePromise('frontendServerLog', {userId: undefined,  type: 'Incorrect login', options}).then((res) => res);
        throw new Meteor.Error('invalid-credentials', options.error.reason);

The console.log confirms that it’s executed:
I20220725-21:26:31.257(8)? 161: In Accounts.validateLoginAttempt

Yet, I get the standard error:
Screenshot 2022-07-25 at 21.29.02

If instead of this line you write the following:

throw new Meteor.Error('123', '456');

Then do you see 456 in your GUI instead of ‘Login forbidden’?

Yes, I do:
Screenshot 2022-07-25 at 21.58.42

Is that expected behavior?

This is what I meant in my first comment above. In your original code you are just passing along whatever message you have in options.error.reason. So you’re still relying on the text provided by Meteor itself.

See more details about Meteor errors below. The ‘reason’ parameter, in my example the ‘456’, is what is being shown on client side in your GUI.

1 Like


Sorry, you lost me there.

The reason parameter as in options.error.reason resolves to User not found:
Screenshot 2022-07-25 at 22.32.56

That’s a String. As requested by the Meteor.Error function:

reason String
Optional. A short human-readable summary of the error, like 'Not Found'.

What I don’t understand is why it’s not displaying the text in options.error.reason itself but as soon as I replace it with User not found it works as expected. That’s where I’m lost right now.

I’ve tried this instead:
throw new Meteor.Error('invalid-credentials', 'User not found');

It still shows “Login forbidden”. BTW, the same happens for:
throw new Meteor.Error('123', 'User not found');

I’m puzzled

If the above still shows “Login forbidden” and the below shows 456, then indeed I’m a bit lost myself as well.

throw new Meteor.Error('123', '456');

Not really sure what’s happening then. Maybe it’s something related to the order in which different code is executed in the login flow on server side.

Yes, it does. So that gives me at least some relief, that I’m not the only one who’s lost here.

Maybe someone else knows more. @rjdavid - do you have an idea?

How does your client code look like?

This is the content of the login folder:
Screenshot 2022-07-26 at 11.01.18

File verifyEmail.js:

import { Template } from 'meteor/templating';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { ReactiveDict } from 'meteor/reactive-dict';
import { Modal } from 'meteor/peppelg:bootstrap-3-modal';

import './login.html';

Template.verifyEmailTemplate.onCreated(function () {
    const instance = this;
    instance.state = new ReactiveDict();
    instance.state.set('showError', false);
    instance.state.set('error', null);

Template.verifyEmailTemplate.onRendered(function () {

    showError() {
        return Template.instance().state.get('showError');
    error() {
        return Template.instance().state.get('error');

    'submit .verify-email-form': function (event, templateInstance) {
        const token = event.currentTarget[0].value;
        if (token === '') {
            templateInstance.state.set('showError', true);
            templateInstance.state.set('error', 'Please paste a valid token.');
        // verify the email

Template.verifyEmailTemplate.onDestroyed(function () {

File login.js:

import { Template } from 'meteor/templating';
import { AccountsTemplates } from 'meteor/useraccounts:core';
import { ReactiveDict } from 'meteor/reactive-dict';

import './login.html';

Template.login.onCreated(function () {
    const instance = this;
    instance.state = new ReactiveDict();
    instance.state.set('validEmail', true);

Template.login.onRendered(function () {

    showForgotPwd() {
        return AccountsTemplates.getState() === 'forgotPwd';
    showResendVerification() {
        return AccountsTemplates.getState() === 'resendVerificationEmail';
    validateEmail() {
        if (!Template.instance().state.get('validEmail')) {
            return 'Please enter a valid email address';
        return false;

    'click #at-forgotPwd': function () {
    'click #at-resend-verification-email': function () {
    'blur #at-field-email': function (event, templateInstance) {
        const { value } = event.currentTarget;
        if (value !== '') {
            const regex = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/i;
            if (!regex.test(value) && value !== '' && value !== undefined) {
                templateInstance.state.set('validEmail', false);
                return false;
        templateInstance.state.set('validEmail', true);
        return true;

Template.login.onDestroyed(function () {

I can’t see anything special there. Does this help, @rjdavid ?

I’m not that used to Blaze. But nothing seems out of the ordinary.

The only thing I have in mind is with my original post that your custom Meteor error call is being bypassed.

Thanks for spending time on this. I agree with your assumption but it seems neither me nor others can figure it out. It’s not a big problem (for me), I will leave it as it is, especially after @vooteles explained the security concerns behind it.