Skip to content

Commit 65520d8

Browse files
authored
Merge pull request #3984 from HejdaJakub/mfaPercentageTimestamp
feat(core): use percentage property to force MFA log in
2 parents fdf4fdd + 1118be8 commit 65520d8

File tree

6 files changed

+97
-15
lines changed

6 files changed

+97
-15
lines changed

perun-base/src/main/java/cz/metacentrum/perun/core/api/CoreConfig.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ public void initBeansUtils() {
9696
private List<String> userInfoEndpointExtSourceFriendlyName;
9797
private String introspectionEndpointMfaAcrValue;
9898
private int mfaAuthTimeout;
99+
private int mfaAuthTimeoutPercentageForceLogIn;
99100
private boolean enforceMfa;
100101
private int idpLoginValidity;
101102
private List<String> idpLoginValidityExceptions;
@@ -806,6 +807,14 @@ public void setMfaAuthTimeout(int mfaAuthTimeout) {
806807
this.mfaAuthTimeout = mfaAuthTimeout;
807808
}
808809

810+
public int getMfaAuthTimeoutPercentageForceLogIn() {
811+
return mfaAuthTimeoutPercentageForceLogIn;
812+
}
813+
814+
public void setMfaAuthTimeoutPercentageForceLogIn(int mfaAuthTimeoutPercentageForceLogIn) {
815+
this.mfaAuthTimeoutPercentageForceLogIn = mfaAuthTimeoutPercentageForceLogIn;
816+
}
817+
809818
public boolean isEnforceMfa() {
810819
return enforceMfa;
811820
}

perun-base/src/main/resources/perun-base.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
<property name="userInfoEndpointExtSourceName" value="${perun.userInfoEndpoint.extSourceName}"/>
7979
<property name="userInfoEndpointExtSourceFriendlyName" value="#{'${perun.userInfoEndpoint.extSourceFriendlyName}'.split('\s*,\s*')}"/>
8080
<property name="mfaAuthTimeout" value="${perun.introspectionEndpoint.mfaAuthTimeout}"/>
81+
<property name="mfaAuthTimeoutPercentageForceLogIn" value="${perun.introspectionEndpoint.mfaAuthTimeoutPercentageForceLogIn}"/>
8182
<property name="enforceMfa" value="${perun.enforceMfa}"/>
8283
<property name="introspectionEndpointMfaAcrValue" value="${perun.introspectionEndpoint.mfaAcrValue}"/>
8384
<property name="idpLoginValidity" value="${perun.idpLoginValidity}"/>
@@ -181,6 +182,7 @@
181182
<prop key="perun.userInfoEndpoint.extSourceName">target_issuer</prop>
182183
<prop key="perun.userInfoEndpoint.extSourceFriendlyName">target_backend, display_name, text</prop>
183184
<prop key="perun.introspectionEndpoint.mfaAuthTimeout">1440</prop>
185+
<prop key="perun.introspectionEndpoint.mfaAuthTimeoutPercentageForceLogIn">75</prop>
184186
<prop key="perun.introspectionEndpoint.mfaAcrValue">https://refeds.org/profile/mfa</prop>
185187
<prop key="perun.enforceMfa">false</prop>
186188
</props>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package cz.metacentrum.perun.core.api.exceptions;
2+
3+
import cz.metacentrum.perun.core.api.exceptions.rt.PerunRuntimeException;
4+
5+
/**
6+
* This exception is thrown when principal has roles always requiring MFA and the auth time is older than the limit defined in the config
7+
* @author Jakub Hejda <[email protected]>
8+
*/
9+
public class MfaRoleTimeoutException extends PerunRuntimeException {
10+
static final long serialVersionUID = 0;
11+
12+
public MfaRoleTimeoutException(String message) {
13+
super(message);
14+
}
15+
16+
public MfaRoleTimeoutException(String message, Throwable cause) {
17+
super(message, cause);
18+
}
19+
20+
public MfaRoleTimeoutException(Throwable cause) {
21+
super(cause);
22+
}
23+
}

perun-core/src/main/java/cz/metacentrum/perun/core/api/exceptions/MfaTimeoutException.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
import cz.metacentrum.perun.core.api.exceptions.rt.PerunRuntimeException;
44

5+
/**
6+
* This exception is thrown when principal is performing MFA-requiring action and the auth time is older than the limit defined in the config
7+
* @author Jakub Hejda <[email protected]>
8+
*/
59
public class MfaTimeoutException extends PerunRuntimeException {
610
static final long serialVersionUID = 0;
711

perun-core/src/main/java/cz/metacentrum/perun/core/blImpl/AuthzResolverBlImpl.java

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import cz.metacentrum.perun.core.api.exceptions.MfaInvalidRolesException;
5656
import cz.metacentrum.perun.core.api.exceptions.MfaPrivilegeException;
5757
import cz.metacentrum.perun.core.api.exceptions.MfaRolePrivilegeException;
58+
import cz.metacentrum.perun.core.api.exceptions.MfaRoleTimeoutException;
5859
import cz.metacentrum.perun.core.api.exceptions.MfaTimeoutException;
5960
import cz.metacentrum.perun.core.api.exceptions.PolicyNotExistsException;
6061
import cz.metacentrum.perun.core.api.exceptions.ResourceNotExistsException;
@@ -4272,7 +4273,11 @@ private static void checkMfaForHavingRole(PerunSession sess, AuthzRoles roles) {
42724273
}
42734274

42744275
if (!requireMfaRoles.isEmpty() && !sess.getPerunPrincipal().getRoles().hasRole(Role.MFA)) {
4275-
throw new MfaRolePrivilegeException(sess, requireMfaRoles.get(0));
4276+
if (checkAuthValidityForMFA(sess)) {
4277+
throw new MfaRolePrivilegeException(sess, requireMfaRoles.get(0));
4278+
} else {
4279+
throw new MfaRoleTimeoutException("Your MFA timestamp is not valid anymore, you'll need to reauthenticate");
4280+
}
42764281
}
42774282
}
42784283

@@ -4368,6 +4373,27 @@ private static boolean isAuthorizedByMfa(PerunSession sess, boolean throwError)
43684373
return false;
43694374
}
43704375

4376+
if (checkAuthValidityForMFA(sess)) {
4377+
// true if user has MFA and it is still valid
4378+
return sessionHasMfa(sess);
4379+
} else {
4380+
if (!throwError) return false;
4381+
if (sessionHasMfa(sess)) {
4382+
// MFA is no longer valid
4383+
throw new MfaTimeoutException("Your MFA timestamp is not valid anymore, you'll need to reauthenticate");
4384+
} else {
4385+
// user is authenticated by SFA but the mfa timeout would cause an error, so we need to reauthenticate this user
4386+
throw new MfaTimeoutException("Your single factor authentication timestamp is not valid anymore, you'll need to reauthenticate");
4387+
}
4388+
}
4389+
}
4390+
4391+
/**
4392+
* Check if the auth time + mfa timeout (reduced by percentage from config) > the current time
4393+
* @param sess session
4394+
* @return true if the auth timestamp is not too old to perform step-up
4395+
*/
4396+
private static boolean checkAuthValidityForMFA(PerunSession sess) {
43714397
String returnedAuthTime = sess.getPerunPrincipal().getAdditionalInformations().get(AUTH_TIME);
43724398
Instant parsedReturnedAuthTime;
43734399
try {
@@ -4380,21 +4406,27 @@ private static boolean isAuthorizedByMfa(PerunSession sess, boolean throwError)
43804406
}
43814407

43824408
long mfaTimeoutInSec = Duration.ofMinutes(BeansUtils.getCoreConfig().getMfaAuthTimeout()).getSeconds();
4383-
Instant mfaValidUntil = parsedReturnedAuthTime.plusSeconds(mfaTimeoutInSec);
4384-
4385-
// check if the auth time + mfa timeout > the current time
4386-
if (mfaValidUntil.isAfter(Instant.now())) {
4387-
// if user has MFA and it is still valid
4388-
return sess.getPerunPrincipal().getAdditionalInformations().containsKey(ACR_MFA);
4389-
} else {
4390-
if (!throwError) return false;
4391-
if (sess.getPerunPrincipal().getAdditionalInformations().containsKey(ACR_MFA)) {
4392-
// MFA is no longer valid
4393-
throw new MfaTimeoutException("Your MFA timestamp " + returnedAuthTime + " is not valid anymore, you'll need to reauthenticate");
4394-
} else {
4395-
// user is authenticated by SFA but the mfa timeout would cause an error, so we need to reauthenticate this user
4396-
throw new MfaTimeoutException("Your single factor authentication timestamp " + returnedAuthTime + " is not valid anymore, you'll need to reauthenticate");
4409+
double mfaTimeoutPercentage = 1;
4410+
// if the current session is SFA, we want to force log in with both factors earlier (e.g. 75% of mfaAuthTimeout) due to the first executed MFA since authentication time
4411+
// -> we want to avoid situation when the validity is e.g. 60 minutes, user executes MFA (just second factor) after 59 minutes and after one minute he/she would need to log in again with both factors
4412+
if (!sessionHasMfa(sess)) {
4413+
mfaTimeoutPercentage = (double) BeansUtils.getCoreConfig().getMfaAuthTimeoutPercentageForceLogIn() / 100;
4414+
if (mfaTimeoutPercentage < 0 || mfaTimeoutPercentage > 1) {
4415+
throw new InternalErrorException("MFA auth timestamp percentage force logout " + mfaTimeoutPercentage + " is not between 0 and 100");
43974416
}
43984417
}
4418+
4419+
Instant mfaValidUntil = parsedReturnedAuthTime.plusSeconds((long) (mfaTimeoutInSec * mfaTimeoutPercentage));
4420+
4421+
return mfaValidUntil.isAfter(Instant.now());
4422+
}
4423+
4424+
/**
4425+
* Check if the perun principal contains acr_mfa. It means that user has been authenticated by MFA.
4426+
* @param sess session
4427+
* @return true if principal contains acr_mfa
4428+
*/
4429+
private static boolean sessionHasMfa(PerunSession sess) {
4430+
return sess.getPerunPrincipal().getAdditionalInformations().containsKey(ACR_MFA);
43994431
}
44004432
}

perun-core/src/test/java/cz/metacentrum/perun/core/api/AuthzResolverIntegrationTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import org.junit.Ignore;
2020
import org.junit.Test;
2121

22+
import java.time.Instant;
23+
import java.time.temporal.ChronoUnit;
2224
import java.util.ArrayList;
2325
import java.util.Arrays;
2426
import java.util.Collections;
@@ -1800,10 +1802,17 @@ public void mfaCriticalRole() throws Exception {
18001802
boolean originalForce = BeansUtils.getCoreConfig().isEnforceMfa();
18011803
boolean originalCriticalRole = AuthzResolverImpl.getRoleManagementRules(Role.PERUNADMIN).isMfaCriticalRole();
18021804
AuthzResolver.setRole(sess, createdUser, null, Role.PERUNADMIN);
1805+
int originalMfaAuthTimeout = BeansUtils.getCoreConfig().getMfaAuthTimeout();
1806+
int originalMfaAuthTimeoutPercentageForceLogIn = BeansUtils.getCoreConfig().getMfaAuthTimeoutPercentageForceLogIn();
1807+
String originalAdditionalInfoAuthTime = session.getPerunPrincipal().getAdditionalInformations().get("authTime");
18031808

18041809
try {
18051810
BeansUtils.getCoreConfig().setEnforceMfa(true);
18061811
AuthzResolverImpl.getRoleManagementRules(Role.PERUNADMIN).setMfaCriticalRole(true);
1812+
BeansUtils.getCoreConfig().setMfaAuthTimeout(60);
1813+
BeansUtils.getCoreConfig().setMfaAuthTimeoutPercentageForceLogIn(75);
1814+
// mock auth time for this test
1815+
session.getPerunPrincipal().getAdditionalInformations().put("authTime", Instant.now().minus(20, ChronoUnit.SECONDS).toString());
18071816
assertThatExceptionOfType(MfaRolePrivilegeException.class).isThrownBy(
18081817
() -> AuthzResolver.refreshAuthz(session)
18091818
);
@@ -1814,6 +1823,9 @@ public void mfaCriticalRole() throws Exception {
18141823
} finally {
18151824
AuthzResolverImpl.getRoleManagementRules(Role.PERUNADMIN).setMfaCriticalRole(originalCriticalRole);
18161825
BeansUtils.getCoreConfig().setEnforceMfa(originalForce);
1826+
BeansUtils.getCoreConfig().setMfaAuthTimeout(originalMfaAuthTimeout);
1827+
BeansUtils.getCoreConfig().setMfaAuthTimeoutPercentageForceLogIn(originalMfaAuthTimeoutPercentageForceLogIn);
1828+
session.getPerunPrincipal().getAdditionalInformations().put("authTime", originalAdditionalInfoAuthTime);
18171829
}
18181830
}
18191831

0 commit comments

Comments
 (0)