Skip to content

Commit b67c23f

Browse files
committed
fix(core): MFA timeout
* If MFA is not valid anymore, then the new exception is thrown - it must be specifically managed by client (now just by the new GUI). BREAKING CHANGE: The unit of the config property 'mfaAuthTimeout' has been changed from hours to minutes (for easier testing).
1 parent cc1e49f commit b67c23f

File tree

5 files changed

+67
-32
lines changed

5 files changed

+67
-32
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ public class PerunPrincipal {
2525
// Specifies if the principal has initialized authZResolver
2626
private volatile boolean authzInitialized = false;
2727
// Keywords of additionalInformations
28-
public static final String MFA_TIMESTAMP = "mfaTimestamp";
28+
public static final String AUTH_TIME = "authTime";
29+
public static final String ACR_MFA = "acrMfa";
2930
public static final String ISSUER = "issuer";
3031
public static final String ACCESS_TOKEN = "accessToken";
3132

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@
180180
<prop key="perun.userInfoEndpoint.extSourceLogin">eduperson_unique_id, eduperson_principal_name, saml2_nameid_persistent, eduperson_targeted_id, voperson_external_id</prop>
181181
<prop key="perun.userInfoEndpoint.extSourceName">target_issuer</prop>
182182
<prop key="perun.userInfoEndpoint.extSourceFriendlyName">target_backend, display_name, text</prop>
183-
<prop key="perun.introspectionEndpoint.mfaAuthTimeout">24</prop>
183+
<prop key="perun.introspectionEndpoint.mfaAuthTimeout">1440</prop>
184184
<prop key="perun.introspectionEndpoint.mfaAcrValue">https://refeds.org/profile/mfa</prop>
185185
<prop key="perun.enforceMfa">false</prop>
186186
</props>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package cz.metacentrum.perun.core.api.exceptions;
2+
3+
import cz.metacentrum.perun.core.api.exceptions.rt.PerunRuntimeException;
4+
5+
public class MfaTimeoutException extends PerunRuntimeException {
6+
static final long serialVersionUID = 0;
7+
8+
public MfaTimeoutException(String message) {
9+
super(message);
10+
}
11+
12+
public MfaTimeoutException(String message, Throwable cause) {
13+
super(message, cause);
14+
}
15+
16+
public MfaTimeoutException(Throwable cause) {
17+
super(cause);
18+
}
19+
}

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

Lines changed: 34 additions & 23 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.MfaTimeoutException;
5859
import cz.metacentrum.perun.core.api.exceptions.PolicyNotExistsException;
5960
import cz.metacentrum.perun.core.api.exceptions.ResourceNotExistsException;
6061
import cz.metacentrum.perun.core.api.exceptions.RoleAlreadySetException;
@@ -100,8 +101,9 @@
100101

101102
import static cz.metacentrum.perun.core.api.AuthzResolver.MFA_CRITICAL_ATTR;
102103
import static cz.metacentrum.perun.core.api.PerunPrincipal.ACCESS_TOKEN;
104+
import static cz.metacentrum.perun.core.api.PerunPrincipal.ACR_MFA;
105+
import static cz.metacentrum.perun.core.api.PerunPrincipal.AUTH_TIME;
103106
import static cz.metacentrum.perun.core.api.PerunPrincipal.ISSUER;
104-
import static cz.metacentrum.perun.core.api.PerunPrincipal.MFA_TIMESTAMP;
105107
import static org.apache.commons.lang3.StringUtils.isBlank;
106108

107109
/**
@@ -2563,7 +2565,7 @@ public static synchronized void refreshAuthz(PerunSession sess) {
25632565
sess.getPerunPrincipal().getRoles().clear();
25642566
}
25652567

2566-
if (isAuthorizedByMfa(sess)) {
2568+
if (isAuthorizedByMfa(sess, false)) {
25672569
sess.getPerunPrincipal().getRoles().putAuthzRole(Role.MFA);
25682570
}
25692571
}
@@ -2624,10 +2626,6 @@ public static void refreshMfa(PerunSession sess) throws ExpiredTokenException, M
26242626
throw new MFAuthenticationException("MFA enforcement is turned off");
26252627
}
26262628

2627-
if (!BeansUtils.getCoreConfig().getRequestUserInfoEndpoint()) {
2628-
throw new MFAuthenticationException("Cannot verify MFA - UserInfo endpoint not configured.");
2629-
}
2630-
26312629
String accessToken = sess.getPerunPrincipal().getAdditionalInformations().get(ACCESS_TOKEN);
26322630
if (accessToken == null) {
26332631
throw new MFAuthenticationException("Cannot verify MFA - access token is missing.");
@@ -2638,7 +2636,7 @@ public static void refreshMfa(PerunSession sess) throws ExpiredTokenException, M
26382636
throw new MFAuthenticationException("Cannot verify MFA - issuer is missing.");
26392637
}
26402638

2641-
if (isAuthorizedByMfa(sess)) {
2639+
if (isAuthorizedByMfa(sess, true)) {
26422640
sess.getPerunPrincipal().getRoles().putAuthzRole(Role.MFA);
26432641
}
26442642
}
@@ -4357,34 +4355,47 @@ private static Map<String, Integer> createMappingOfValues(PerunBean complementar
43574355

43584356
/**
43594357
* Checks, if principal was authorized by Multi-factor authentication.
4360-
* The information is resolved in UserInfoEndpointCall and stored in principal's additionalInformations.
4361-
* The timestamp of MFA must not be older than mfa timeout set in config.
4358+
* The information is resolved from headers (apache IntrospectionEndpoint call) and stored in principal's additionalInformations.
4359+
* Check if the auth time + mfa timeout is not older than the current time
4360+
* auth time = time of the first authentication
4361+
* mfa timeout = amount of time defined in the config for how long the MFA should be valid (since SFA)
43624362
*
43634363
* @param sess session
4364+
* @param throwError if this method should throw errors or just return boolean
43644365
* @return true if principal authorized by MFA in allowed limit, false otherwise
43654366
*/
4366-
private static boolean isAuthorizedByMfa(PerunSession sess) {
4367+
private static boolean isAuthorizedByMfa(PerunSession sess, boolean throwError) {
43674368
if (!BeansUtils.getCoreConfig().isEnforceMfa()) {
43684369
return false;
43694370
}
43704371

4371-
if (!sess.getPerunPrincipal().getAdditionalInformations().containsKey(MFA_TIMESTAMP)) {
4372-
return false;
4373-
}
4374-
4375-
String returnedTimestamp = sess.getPerunPrincipal().getAdditionalInformations().get(MFA_TIMESTAMP);
4376-
Instant parsedReturnedTimestamp;
4372+
String returnedAuthTime = sess.getPerunPrincipal().getAdditionalInformations().get(AUTH_TIME);
4373+
Instant parsedReturnedAuthTime;
43774374
try {
4378-
parsedReturnedTimestamp = Instant.parse(returnedTimestamp);
4375+
parsedReturnedAuthTime = Instant.parse(returnedAuthTime);
43794376
} catch (DateTimeParseException e) {
4380-
throw new InternalErrorException("MFA timestamp " + returnedTimestamp + " could not be parsed", e);
4377+
throw new InternalErrorException("MFA timestamp " + returnedAuthTime + " could not be parsed", e);
43814378
}
4382-
if (parsedReturnedTimestamp.isAfter(Instant.now())) {
4383-
throw new InternalErrorException("MFA auth timestamp " + returnedTimestamp + " was greater than current time");
4379+
if (parsedReturnedAuthTime.isAfter(Instant.now())) {
4380+
throw new InternalErrorException("MFA auth timestamp " + returnedAuthTime + " was greater than current time");
43844381
}
43854382

4386-
long mfaTimeoutInSec = Duration.ofHours(BeansUtils.getCoreConfig().getMfaAuthTimeout()).getSeconds();
4387-
Instant mfaValidUntil = parsedReturnedTimestamp.plusSeconds(mfaTimeoutInSec);
4388-
return mfaValidUntil.isAfter(Instant.now());
4383+
long mfaTimeoutInSec = Duration.ofMinutes(BeansUtils.getCoreConfig().getMfaAuthTimeout()).getSeconds();
4384+
Instant mfaValidUntil = parsedReturnedAuthTime.plusSeconds(mfaTimeoutInSec);
4385+
4386+
// check if the auth time + mfa timeout > the current time
4387+
if (mfaValidUntil.isAfter(Instant.now())) {
4388+
// if user has MFA and it is still valid
4389+
return sess.getPerunPrincipal().getAdditionalInformations().containsKey(ACR_MFA);
4390+
} else {
4391+
if (!throwError) return false;
4392+
if (sess.getPerunPrincipal().getAdditionalInformations().containsKey(ACR_MFA)) {
4393+
// MFA is no longer valid
4394+
throw new MfaTimeoutException("Your MFA timestamp " + returnedAuthTime + " is not valid anymore, you'll need to reauthenticate");
4395+
} else {
4396+
// user is authenticated by SFA but the mfa timeout would cause an error, so we need to reauthenticate this user
4397+
throw new MfaTimeoutException("Your single factor authentication timestamp " + returnedAuthTime + " is not valid anymore, you'll need to reauthenticate");
4398+
}
4399+
}
43894400
}
43904401
}

perun-rpc/src/main/java/cz/metacentrum/perun/rpc/Api.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,9 @@
6767
import java.util.regex.Pattern;
6868

6969
import static cz.metacentrum.perun.core.api.PerunPrincipal.ACCESS_TOKEN;
70+
import static cz.metacentrum.perun.core.api.PerunPrincipal.ACR_MFA;
71+
import static cz.metacentrum.perun.core.api.PerunPrincipal.AUTH_TIME;
7072
import static cz.metacentrum.perun.core.api.PerunPrincipal.ISSUER;
71-
import static cz.metacentrum.perun.core.api.PerunPrincipal.MFA_TIMESTAMP;
7273
import static org.apache.commons.lang3.StringUtils.isEmpty;
7374
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
7475

@@ -271,14 +272,17 @@ else if (isNotEmpty(req.getHeader(OIDC_CLAIM_SUB))) {
271272
}
272273
extSourceLoaString = "-1";
273274

274-
// get MFA timestamp
275+
// store auth_time to additional information
276+
String authTimestamp = req.getHeader(OIDC_CLAIM_AUTH_TIME);
277+
if (isNotEmpty(authTimestamp)) {
278+
Instant authReadableTimestamp = Instant.ofEpochSecond(Long.parseLong(authTimestamp));
279+
additionalInformations.put(AUTH_TIME, authReadableTimestamp.toString());
280+
}
281+
282+
// store MFA flag to additional information
275283
String acr = req.getHeader(OIDC_CLAIM_ACR);
276284
if (isNotEmpty(acr) && acr.equals(BeansUtils.getCoreConfig().getIntrospectionEndpointMfaAcrValue())) {
277-
String mfaTimestamp = req.getHeader(OIDC_CLAIM_AUTH_TIME);
278-
if (isNotEmpty(mfaTimestamp)) {
279-
Instant mfaReadableTimestamp = Instant.ofEpochSecond(Long.parseLong(mfaTimestamp));
280-
additionalInformations.put(MFA_TIMESTAMP, mfaReadableTimestamp.toString());
281-
}
285+
additionalInformations.put(ACR_MFA, "mfa");
282286
}
283287

284288
if (BeansUtils.getCoreConfig().getRequestUserInfoEndpoint()) {

0 commit comments

Comments
 (0)