
If you want do some custom checks when a user logs in, it is possible to override the Mendix login mechanism.
With this, you can do a lot of useful things. For example:
- Implement a custom user blocking mechanism, e.g. block all users that are linked to a certain company.
- Only allow users to log in from certain IP addresses
- Dynamically assign user roles when logging in
- Etc. etc. (leave your comment below for more ideas!)
To override the standard login, we need to do some Java coding. Create a new Java action ReplaceLoginAction, with no parameters. Call this Java action on startup of the application, by adding a startup microflow, and adding a call to this Java action there. Add the following Java code to the Java action:
1 2 3 4 5 6 7 8 |
// BEGIN USER CODE LoginActionListener loginActionListener = new LoginActionListener(); loginActionListener.addReplaceEvent(CustomLoginAction.class.getName()); Core.addUserAction(CustomLoginAction.class); Core.addListener(loginActionListener); return true; // END USER CODE |
You need to added 2 Java class files to your module’s Javasource folder:
LoginActionListener.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import com.mendix.core.Core; import com.mendix.core.action.user.LoginAction; import com.mendix.systemwideinterfaces.core.UserActionListener; public class LoginActionListener extends UserActionListener<LoginAction> { public LoginActionListener() { super(LoginAction.class); } @Override public boolean check(LoginAction action) { Core.addUserAction(CustomLoginAction.class); return true; } } |
And CustomLoginAction.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
public class CustomLoginAction extends UserAction<ISession> { private String userName; private String password; private IMxRuntimeRequest request; private String currentSessionId; public final static String USER_NAME_PARAM = "userName"; public final static String PASSWORD_PARAM = "password"; public CustomLoginAction(Map<String, ? extends Object> params) { super(Core.createSystemContext()); this.userName = (String) params.get(USER_NAME_PARAM); this.password = (String) params.get(PASSWORD_PARAM); this.currentSessionId = (String)params.get("currentSessionId"); this.request = (IMxRuntimeRequest)params.get("request"); } @Override public ISession executeAction() throws Exception { IUser user = Core.getUser(getContext(), this.userName); if (user == null) throw new AuthenticationRuntimeException("Login FAILED: unknown user '" + this.userName + "'."); else if (user.isWebserviceUser()) throw new AuthenticationRuntimeException("Login FAILED: client login attempt for web service user '" + this.userName + "'."); else if (user.isAnonymous()) throw new AuthenticationRuntimeException("Login FAILED: client login attempt for guest user '" + this.userName + "'."); else if (user.isActive() == false) throw new AuthenticationRuntimeException("Login FAILED: user '" + this.userName + "' is not active."); else if (user.isBlocked() == true) throw new AuthenticationRuntimeException("Login FAILED: user '" + this.userName + "' is blocked."); else if (user.getUserRoleNames().isEmpty()) throw new AuthenticationRuntimeException("Login FAILED: user '" + this.userName + "' does not have any user roles."); // You can even call Microflows here!, like this: Map<String, Object> parameters = new HashMap<String, Object>(); parameters.put("User", user.getMendixObject()); //this will call the microflow LoginAllowed that should have a User parameter and returns true if the user is allowed to log in boolean allowed = Core.execute(getContext(), "MyModule.LoginAllowed", parameters); if (!allowed) throw new AuthenticationRuntimeException("Login FAILED: user "+this.userName); if (!Core.authenticate(Core.createSystemContext(), user, this.password)) //password check throw new AuthenticationRuntimeException("Login FAILED: invalid password for user '" + user.getName() + "'."); ISession session = Core.initializeSession(user, this.currentSessionId); return session; } } |
This executeAction() method will be called when a login-attempt is made, and will do the standard checks that Mendix does when logging in, plus call a microflow LoginAllowed in which you can do your own custom stuff. This can, for example, check whether the application is open for non-admin users, like this:
This microflow will be called by the CustomLoginAction in system context, which allows you to do almost anything! Just be careful for security and performance issues if you do too much here 🙂
[…] Note: You could also implement a more sophisticated blocking mechanism yourself, by added your own attribute(s) to Administration.Account, an then do a check on these attribute when the user logs in, as explained in my previous post. […]
Hi Bart,
Trying to implement this function but i receive an error on the first line.
LoginActionListener loginActionListener = new LoginActionListener();
Any ideas?
I forgot to include the source for LoginActionListener. This is now updated.
Hi Bart,
Thanks for the write up and explanation, great help!
However, with Mendix 6 this no longer works as it uses Deprecated libraries. Could you update this post?
thanks!
Is it still working with Mendix7? As mentioned above I tried to add this to startup microflow some how startup microflow is not triggered on app startup. So I called the microflow from default home page but the listener code is not called on signin button click.
The way to register a listener has changed. You new to use this :
Core.getListenersRegistry().addListener(loginActionListener);
Hi Bart, I’ve tried to implement this in Mendix 8. I’m not a java expert so it took a little work.
I can see this as being extremely powerful, however i can’t seem to get the correct error message back to the user after logon (if allowed is returned false) after an exception is forced.:
boolean allowed = Core.execute(getContext(), “MyModule.LoginAllowed”, parameters);
if (!allowed)
throw new AuthenticationRuntimeException(“Login FAILED: user “+this.userName);
I’ve debugged and it seems my custom microflow is returning the correct boolean whether or not i want the user to be logged in.
I get a generic username and password error message, instead of the above.
Any ideas?
Bump.
Any ideas on this? Even the default Mendix login machinery returns a generic “Invalid username or password” error when the user is temporarily blocked, rather than something more meaningful like “Maximum login attempts exceeded – try again later”.
Being able to present a custom response message seems like a trivial thing – why does Mendix have to make it so obscure/difficult??
Having a generic message instead of something specific is actually a security precaution to give an attacker as little info as possible.
Because of security reasons
Is this code still relevant and working for Mendix 8+? I am trying but get error
—–
{“type”:”LoginAction”,”user”:”superadmin”},”after”:[],”type”:”EventExtendedAction”}’, all database changes executed by this action were rolled back
at com.mendix.basis.actionmanagement.CoreActionHandlerImpl.processErrorState(CoreActionHandlerImpl.scala:151)
Caused by: com.mendix.core.CoreRuntimeException: An error occurred while instantiating action ‘CustomLoginAction’
at com.mendix.basis.actionmanagement.ActionRegistry.$anonfun$instantiatorFor$1(ActionRegistry.scala:59)
Caused by: java.lang.IllegalArgumentException: wrong number of arguments
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
Can I navigate to another page (for example when I want to show another form with code for 2FA) inside CustomLoginAction? I was trying to use microflow with ShowPage action, but it shows Login failed message on Login Form and nothing happens.