Skip to content

Commit d1e1bdc

Browse files
committed
Version 2.0
Making the complex simple After reviewing and feedback we released 2.0 of the module. Please upgrade to this version! When your mark this module as your favorite marketplace content you will receive notifications about new releases. Breaking change: - We removed the capability to use MFA from login.html (too complex) Improvements: - Max login attempts and Max MFA attempts can be configured with constants (default is 3) - After these attempts the user will be blocked according to the Mendix platform default and is released after 5 minutes (but read https://docs.mendix.com/refguide/login-behavior) - Improved logging message when user is blocked (so it's in line with unblocks by the Core runtime) Changed components: - SUB_MFA_Validate - MultiFactorAuthLoginAction.java - New contstants MaxLoginAttempts and MaxAttemptsMFA
1 parent 275b824 commit d1e1bdc

7 files changed

Lines changed: 28 additions & 374 deletions

File tree

2fa.mpr

0 Bytes
Binary file not shown.

Output/MFAmodule_Mx8188_v2_0.mpk

134 KB
Binary file not shown.

README.md

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ The MFA code is validated first and only then the module creates a user session
2525

2626
[https://swimlanes.io/u/4o7jaAOjY](https://swimlanes.io/u/4o7jaAOjY)
2727

28+
We depricated the login.html compatibility in commbination of MFA. This will make the code more simple and safer.
29+
2830
## How did we prove that this module is secure?
2931
At the point in time after login in the first step:
3032

@@ -39,7 +41,6 @@ Scenarios to cover:
3941
- Default login via login.html for accounts with MFA disabled.
4042
- Default login via widgets for accounts with MFA disabled.
4143
- Default login for webservice and REST accounts.
42-
- Login by a customized login.html with MFA enabled (login-with-mfa.html + login-mfa.js + Authenticator app code only. Not compatible code sent by SMS or E-mail).
4344
- Login by default widgets but extended with ability to enter MFA code with MFA enabled.
4445
- Native mobile login
4546

@@ -79,9 +80,7 @@ After startup configuration:
7980

8081
4. Add snippet `SN_MFA_LoginPage` / `SN_Login_Native` to your login page
8182

82-
5. If applicable move the `login-with-mfa.html` and `js/login-mfa.js` from the resources directory to your theme directory to support login actions with MFA from these pages.
83-
84-
6. Set the constant `EnabledMFA` to true to get started!
83+
5. Set the constant `EnabledMFA` to true to get started!
8584

8685
**Keep in mind when upgrading the module from the Appstore in the future:**
8786

@@ -99,20 +98,6 @@ For native mobile we needed to change the sign in nanoflow activity to save the
9998

10099
When extending the LoginAction class and trying to set parameters from this class in our extended class, we found out this was not possible in combination with the super.execute() method. We decided to use createSession. We have already validated the username and password in the first step and the MFA object can't be modified/created by the anonymous user (and is also checked twice).
101100

102-
We also wanted the module to be compatible via a login.html variant and the custom login-with-mfa.html. Therefore, it is necessary to send the MFA code together with your username and password. We need to pass this MFA code through the header because the payload is stripped by the Core LoginAction functionality.
103-
104-
Login-with-mfa.html:
105-
106-
![alt text](https://github.com/appronto/multifactor-authentication/blob/main/Output/Signin.png?raw=true)
107-
108-
Login-mfa.js:
109-
110-
![alt text](https://github.com/appronto/multifactor-authentication/blob/main/Output/Signin2.png?raw=true)
111-
112-
MultiFactorAuthLoginAction.java:
113-
114-
![alt text](https://github.com/appronto/multifactor-authentication/blob/main/Output/Signin3.png?raw=true)
115-
116101
## Please report issues
117102

118103
Have you found an issue or a vulnerability in this module, please reach out to [pim@appronto.nl](mailto:pim@appronto.nl). I will reward you with a nice goodie bag and will publish the new version to the Marketplace.

javasource/mfamodule/helpers/MultiFactorAuthLoginAction.java

Lines changed: 15 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,16 @@ public class MultiFactorAuthLoginAction extends LoginAction{
2626
private String currentSessionId;
2727
public final static String USER_NAME_PARAM = "userName";
2828
public final static String PASSWORD_PARAM = "password";
29-
public final static String MFACODE_PARAM = "mfaCode";
3029
private Map<String, ? extends Object> params;
3130

32-
33-
private mfamodule.proxies.MFA newMfaObject;
34-
35-
private String mfaCode;
3631

3732
public MultiFactorAuthLoginAction(Map<String, ? extends Object> params) {
3833
super(Core.createSystemContext(), params);
3934
this.params = params;
4035
this.userName = (String) params.get(USER_NAME_PARAM);
4136
this.password = (String) params.get(PASSWORD_PARAM);
4237
this.currentSessionId = (String)params.get("currentSessionId");
43-
this.request = (IMxRuntimeRequest)params.get("request");
44-
this.mfaCode = request.getHeader(MFACODE_PARAM);
45-
38+
this.request = (IMxRuntimeRequest)params.get("request");
4639
}
4740

4841
@Override
@@ -61,26 +54,11 @@ public ISession execute() throws Exception {
6154
//check if there is mfa for the user from the first call
6255
userMfaObj = mfamodule.proxies.microflows.Microflows.dS_MFA_GET(oldSession.createContext());
6356

64-
if(userMfaObj != null && userMfaObj.getMendixObject().getMember(oldSession.createContext(),"Username").hasReadAccess(oldSession.createContext()) &&userMfaObj.getUsername() !=null) {
57+
if(userMfaObj != null && userMfaObj.getMendixObject().getMember(oldSession.createContext(),"Username").hasReadAccess(oldSession.createContext()) && userMfaObj.getUsername() != null ) {
6558
if ( mfamodule.proxies.microflows.Microflows.sUB_MFA_Validate(oldSession.createContext(), userMfaObj, userMfaObj.getUsername()) ) {
6659
IUser user = Core.getUser(sysContext, userMfaObj.getUsername());
6760
return Core.initializeSession(user, this.currentSessionId);
6861
}
69-
else if (userMfaObj.getUsername() != null && userMfaObj.getUsername() != "") {
70-
IUser user = Core.getUser(sysContext, userMfaObj.getUsername());
71-
if(user != null) {
72-
Object obj = (Integer)user.getMendixObject().getValue(sysContext,"FailedLogins")+1;
73-
user.getMendixObject().setValue(sysContext,"FailedLogins",obj);
74-
if ( (Integer)user.getMendixObject().getValue(sysContext,"FailedLogins") >= 3) {
75-
user.getMendixObject().setValue(sysContext,"Blocked",true);
76-
Core.commit(sysContext, user.getMendixObject());
77-
_logNode.debug( "Custom MFA to much attempts FAILED: user '" + userMfaObj.getUsername() + "' blocked" );
78-
throw new UserBlockedException("Custom MFA check: User '"+ userMfaObj.getUsername() + "' blocked");
79-
}
80-
Core.commit(sysContext, user.getMendixObject());
81-
}
82-
return oldSession;
83-
}
8462
else {
8563
return oldSession;
8664
}
@@ -117,54 +95,31 @@ else if (user.getUserRoleNames().isEmpty()) {
11795
else if( !Core.authenticate(sysContext, user, this.password)) {
11896
Object obj = (Integer)user.getMendixObject().getValue(sysContext,"FailedLogins")+1;
11997
user.getMendixObject().setValue(sysContext,"FailedLogins",obj);
120-
if ( (Integer)user.getMendixObject().getValue(sysContext,"FailedLogins") >= 3) {
98+
if ( (Integer)user.getMendixObject().getValue(sysContext,"FailedLogins") >= mfamodule.proxies.constants.Constants.getMaxLoginAttempts()) {
12199
user.getMendixObject().setValue(sysContext,"Blocked",true);
122100
Core.commit(sysContext, user.getMendixObject());
123101
_logNode.debug( "Custom Login FAILED: user '" + this.userName + "' blocked" );
102+
Core.getLogger("Core").info( "User blocked: '" + this.userName + "'" );
103+
if (oldSession != null) { Core.logout(oldSession); }
124104
throw new UserBlockedException("Custom login: User '"+ this.userName + "' blocked");
125105
}
126106
Core.commit(sysContext, user.getMendixObject());
127107
_logNode.debug( "Custom Login FAILED: invalid password for user '" + this.userName + "'." );
128108
throw new AuthenticationRuntimeException(" Custom Login FAILED for user '" + this.userName + "'.");
129109
}
130-
110+
131111
// from this point user+pass is validated:
132-
if( mfamodule.proxies.constants.Constants.getEnabledMFA() && mfaCode != null && mfaCode != "" ) {
133-
_logNode.debug("MFA enabled and code provided for "+ this.userName);
134-
IMendixObject newObj = Core.instantiate(sysContext, MFA.entityName);
135-
newMfaObject = MFA.initialize(sysContext, newObj );
136-
newMfaObject.setCode(mfaCode);
137-
newMfaObject.setUsername(this.userName);
138-
Core.commit(sysContext, newMfaObject.getMendixObject());
112+
if ( oldSession != null && mfamodule.proxies.constants.Constants.getEnabledMFA()
113+
&& mfamodule.proxies.microflows.Microflows.sUB_MFA_Validate(oldSession.createContext(), userMfaObj, this.userName) ) {
139114

140-
if ( mfamodule.proxies.microflows.Microflows.sUB_MFA_Validate(sysContext, newMfaObject, this.userName) ) {
141-
_logNode.debug("Validated code for "+ this.userName);
142-
return super.execute();
143-
}
144-
else {
145-
_logNode.debug( "Custom MFA code validation FAILED: mfa check for user '" + this.userName + "' with code '"+mfaCode+"'." );
146-
Object obj = (Integer)user.getMendixObject().getValue(sysContext,"FailedLogins")+1;
147-
user.getMendixObject().setValue(sysContext,"FailedLogins",obj);
148-
if ( (Integer)user.getMendixObject().getValue(sysContext,"FailedLogins") >= 3) {
149-
user.getMendixObject().setValue(sysContext,"Blocked",true);
150-
Core.commit(sysContext, user.getMendixObject());
151-
_logNode.debug( "Custom MFA to much attempts FAILED: user '" + this.userName + "' blocked" );
152-
throw new UserBlockedException("Custom MFA check: User '"+ this.userName + "' blocked");
115+
if( !mfamodule.proxies.microflows.Microflows.sUB_MFA_UserDisabledCheck(sysContext, this.userName) ) {
116+
_logNode.debug("MFA enabled and Mendix session available ready for "+ this.userName);
117+
return oldSession;
118+
}
119+
else {
120+
_logNode.debug("MFA enabled and Mendix session available redirect for "+ this.userName);
121+
return super.execute();
153122
}
154-
Core.commit(sysContext, user.getMendixObject());
155-
throw new AuthenticationRuntimeException(" Custom Login FAILED for user '" + this.userName + "'.");
156-
}
157-
}
158-
else if (oldSession != null && mfamodule.proxies.constants.Constants.getEnabledMFA()
159-
&& mfamodule.proxies.microflows.Microflows.sUB_MFA_Validate(oldSession.createContext(), userMfaObj, this.userName)) {
160-
if( !mfamodule.proxies.microflows.Microflows.sUB_MFA_UserDisabledCheck(sysContext, this.userName) ) {
161-
_logNode.debug("MFA enabled and Mendix session available ready voor MFA for "+ this.userName);
162-
return oldSession;
163-
}
164-
else {
165-
_logNode.debug("MFA enabled and Mendix session available redirect for "+ this.userName);
166-
return super.execute();
167-
}
168123
}
169124
else if( mfamodule.proxies.constants.Constants.getEnabledMFA()
170125
&& !mfamodule.proxies.microflows.Microflows.sUB_MFA_UserDisabledCheck(sysContext, this.userName) ) {

javasource/mfamodule/proxies/constants/Constants.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,14 @@ public static java.lang.String getLognode()
1919
{
2020
return (java.lang.String)Core.getConfiguration().getConstantValue("MFAmodule.Lognode");
2121
}
22+
23+
public static java.lang.Long getMaxAttemptsMFA()
24+
{
25+
return (java.lang.Long)Core.getConfiguration().getConstantValue("MFAmodule.MaxAttemptsMFA");
26+
}
27+
28+
public static java.lang.Long getMaxLoginAttempts()
29+
{
30+
return (java.lang.Long)Core.getConfiguration().getConstantValue("MFAmodule.MaxLoginAttempts");
31+
}
2232
}

resources/MFA/js/login-mfa.js

Lines changed: 0 additions & 154 deletions
This file was deleted.

0 commit comments

Comments
 (0)