Skip to content

Commit

Permalink
Check acr claim in ID token
Browse files Browse the repository at this point in the history
received by a OIDC external provider.
- If present, MFA is skipped
- If not, MFA is performed
  • Loading branch information
rmiccoli committed Feb 21, 2025
1 parent 2a16b96 commit 097abe4
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
*/
package it.infn.mw.iam.authn.oidc;

import java.text.ParseException;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.mitre.openid.connect.client.OIDCAuthenticationProvider;
import org.mitre.openid.connect.model.OIDCAuthenticationToken;
Expand All @@ -29,6 +31,7 @@
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;

import it.infn.mw.iam.authn.common.config.AuthenticationValidator;
Expand All @@ -37,6 +40,7 @@
import it.infn.mw.iam.authn.util.Authorities;
import it.infn.mw.iam.authn.util.SessionTimeoutHelper;
import it.infn.mw.iam.persistence.model.IamAccount;
import it.infn.mw.iam.persistence.model.IamAuthority;
import it.infn.mw.iam.persistence.model.IamTotpMfa;
import it.infn.mw.iam.persistence.repository.IamAccountRepository;
import it.infn.mw.iam.persistence.repository.IamTotpMfaRepository;
Expand Down Expand Up @@ -88,9 +92,17 @@ public Authentication authenticate(Authentication authentication) throws Authent

Optional<IamTotpMfa> totpMfaOptional = totpMfaRepository.findByAccount(account.get());

boolean isAcrPresent = false;

try {
isAcrPresent = token.getIdToken().getJWTClaimsSet().getClaim("acr") != null;
} catch (ParseException e) {
LOG.error("Error parsing JWT claims: {}", e.getMessage());
}

// Checking to see if we can find an active MFA secret attached to the user's account. If so,
// MFA is enabled on the account
if (totpMfaOptional.isPresent() && totpMfaOptional.get().isActive()) {
if (totpMfaOptional.isPresent() && totpMfaOptional.get().isActive() && !isAcrPresent) {
// Add PRE_AUTHENTICATED role to the user. This grants them access to the /iam/verify
// endpoint
List<GrantedAuthority> currentAuthorities = List.of(Authorities.ROLE_PRE_AUTHENTICATED);
Expand All @@ -107,7 +119,7 @@ public Authentication authenticate(Authentication authentication) throws Authent
// AUTHENTICATED user, granting their normal authorities
extToken = new OidcExternalAuthenticationToken(token,
Date.from(sessionTimeoutHelper.getDefaultSessionExpirationTime()),
account.get().getUsername(), null, user.getAuthorities());
account.get().getUsername(), null, convert(account.get().getAuthorities()));
extToken.setAuthenticationMethodReferences(refs);
}

Expand All @@ -119,6 +131,12 @@ public Authentication authenticate(Authentication authentication) throws Authent
}
}

private List<GrantedAuthority> convert(Set<IamAuthority> authorities) {
return authorities.stream()
.map(auth -> new SimpleGrantedAuthority(auth.getAuthority()))
.collect(Collectors.toList());
}

@Override
public boolean supports(Class<?> authentication) {
return (PendingOIDCAuthenticationToken.class.isAssignableFrom(authentication)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public void oidcAccountLinkingWorks() throws Exception {
String state = (String) session.getAttribute("state");
String nonce = (String) session.getAttribute("nonce");

oidcProvider.prepareTokenResponse(TEST_OIDC_CLIENT_ID, TEST_100_USER, nonce);
oidcProvider.prepareTokenResponse(TEST_OIDC_CLIENT_ID, TEST_100_USER, nonce, null);

session =
(MockHttpSession) mvc
Expand Down Expand Up @@ -185,7 +185,7 @@ public void oidcAccountLinkingFailsSinceOidcIdIsAlreadyBoundToAnotherUser() thro
String state = (String) session.getAttribute("state");
String nonce = (String) session.getAttribute("nonce");

oidcProvider.prepareTokenResponse(TEST_OIDC_CLIENT_ID, "test-user", nonce);
oidcProvider.prepareTokenResponse(TEST_OIDC_CLIENT_ID, "test-user", nonce, null);

session =
(MockHttpSession) mvc
Expand Down Expand Up @@ -246,7 +246,7 @@ public void oidcAccountLinkingFailsSinceOidcIdIsAlreadyBoundToAuthenticatedUser(
String state = (String) session.getAttribute("state");
String nonce = (String) session.getAttribute("nonce");

oidcProvider.prepareTokenResponse(TEST_OIDC_CLIENT_ID, "test-user", nonce);
oidcProvider.prepareTokenResponse(TEST_OIDC_CLIENT_ID, "test-user", nonce, null);
session =
(MockHttpSession) mvc
.perform(get("/openid_connect_login").param("state", state)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public void testOidcUnregisteredUserRedirectedToRegisterPage() throws JOSEExcept
CodeRequestHolder ru = buildCodeRequest(sessionCookie, response);

String tokenResponse = mockOidcProvider.prepareTokenResponse(OidcTestConfig.TEST_OIDC_CLIENT_ID,
"unregistered", ru.nonce);
"unregistered", ru.nonce, null);

prepareSuccessResponse(tokenResponse);

Expand Down Expand Up @@ -127,7 +127,7 @@ public void testOidcRegisteredUserRedirectToHome() throws JOSEException, JsonPro
CodeRequestHolder ru = buildCodeRequest(sessionCookie, response);

String tokenResponse = mockOidcProvider.prepareTokenResponse(OidcTestConfig.TEST_OIDC_CLIENT_ID,
"test-user", ru.nonce);
"test-user", ru.nonce, null);

prepareSuccessResponse(tokenResponse);

Expand Down Expand Up @@ -183,7 +183,7 @@ public void testOidcUserRedirectToMfaVerifyPageIfMfaIsActive()
CodeRequestHolder ru = buildCodeRequest(sessionCookie, response);

String tokenResponse = mockOidcProvider.prepareTokenResponse(OidcTestConfig.TEST_OIDC_CLIENT_ID,
"test-with-mfa", ru.nonce);
"test-with-mfa", ru.nonce, null);

prepareSuccessResponse(tokenResponse);

Expand All @@ -197,4 +197,34 @@ public void testOidcUserRedirectToMfaVerifyPageIfMfaIsActive()

}

@Test
public void testOidcUserRedirectToHomeIfMfaIsActiveAndAcrPresentInIdToken()
throws JOSEException, JsonProcessingException, RestClientException {

RestTemplate rt = noRedirectRestTemplate();
ResponseEntity<String> response = rt.getForEntity(openidConnectLoginURL(), String.class);

checkAuthorizationEndpointRedirect(response);
HttpHeaders requestHeaders = new HttpHeaders();

String sessionCookie = extractSessionCookie(response);
requestHeaders.add("Cookie", sessionCookie);

CodeRequestHolder ru = buildCodeRequest(sessionCookie, response);

String tokenResponse = mockOidcProvider.prepareTokenResponse(OidcTestConfig.TEST_OIDC_CLIENT_ID,
"test-with-mfa", ru.nonce, "acr");

prepareSuccessResponse(tokenResponse);

response = rt.postForEntity(openidConnectLoginURL(), ru.requestEntity, String.class);
verifyMockServerCalls();

assertThat(response.getStatusCode(), equalTo(HttpStatus.FOUND));
assertNotNull(response.getHeaders().getLocation());

assertThat(response.getHeaders().getLocation().toString(), equalTo(landingPageURL()));

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public void testValidatorError() throws JOSEException,
CodeRequestHolder ru = buildCodeRequest(sessionCookie, response);

String tokenResponse = mockOidcProvider.prepareTokenResponse(OidcTestConfig.TEST_OIDC_CLIENT_ID,
"unregistered", ru.nonce);
"unregistered", ru.nonce, null);

prepareSuccessResponse(tokenResponse);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public void externalOidcRegistrationCreatesDisabledAccount() throws Exception {
String state = (String) session.getAttribute("state");
String nonce = (String) session.getAttribute("nonce");

oidcProvider.prepareTokenResponse(TEST_OIDC_CLIENT_ID, TEST_100_USER, nonce);
oidcProvider.prepareTokenResponse(TEST_OIDC_CLIENT_ID, TEST_100_USER, nonce, null);

session = (MockHttpSession) mvc
.perform(
Expand Down Expand Up @@ -168,7 +168,7 @@ public void externalOidcRegistrationCreatesDisabledAccount() throws Exception {
state = (String) session.getAttribute("state");
nonce = (String) session.getAttribute("nonce");

oidcProvider.prepareTokenResponse(TEST_OIDC_CLIENT_ID, TEST_100_USER, nonce);
oidcProvider.prepareTokenResponse(TEST_OIDC_CLIENT_ID, TEST_100_USER, nonce, null);

session = (MockHttpSession) mvc
.perform(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,23 @@ public MockOIDCProvider(ObjectMapper mapper, JWKSetKeyStore keyStore) {
this.mapper = mapper;
}

public String buildIdToken(String clientId, String sub, String nonce) throws JOSEException {
return buildIdToken(OidcTestConfig.TEST_OIDC_ISSUER, clientId, sub, nonce);
public String buildIdToken(String clientId, String sub, String nonce, String acr)
throws JOSEException {
return buildIdToken(OidcTestConfig.TEST_OIDC_ISSUER, clientId, sub, nonce, acr);
}

public String buildIdToken(String issuer, String clientId, String sub, String nonce)
public String buildIdToken(String issuer, String clientId, String sub, String nonce, String acr)
throws JOSEException {
IdTokenBuilder builder = new IdTokenBuilder(keyStore, signingAlgo);
return builder.issuer(issuer).sub(sub).audience(clientId).nonce(nonce).build();
IdTokenBuilder builder = new IdTokenBuilder(keyStore, signingAlgo).issuer(issuer)
.sub(sub)
.audience(clientId)
.nonce(nonce);

if (acr != null && !acr.isEmpty()) {
builder.customClaim("acr", "https://refeds.org/profile/MFA");
}

return builder.build();
}

public String prepareErrorResponse(String error, String errorDescription)
Expand All @@ -74,17 +83,17 @@ public void prepareError(String error, String errorDescription) {
clientError = new OidcClientError("Token request error", error, errorDescription, null);
}

public String prepareTokenResponse(String clientId, String sub, String nonce)
public String prepareTokenResponse(String clientId, String sub, String nonce, String acr)
throws JOSEException, JsonProcessingException {
return prepareTokenResponse(OidcTestConfig.TEST_OIDC_ISSUER, clientId, sub, nonce);
return prepareTokenResponse(OidcTestConfig.TEST_OIDC_ISSUER, clientId, sub, nonce, acr);
}

public String prepareTokenResponse(String issuer, String clientId, String sub, String nonce)
throws JOSEException, JsonProcessingException {
public String prepareTokenResponse(String issuer, String clientId, String sub, String nonce,
String acr) throws JOSEException, JsonProcessingException {

TokenResponse tokenResponse = new TokenResponse();
tokenResponse.setAccessToken(UUID.randomUUID().toString());
tokenResponse.setIdToken(buildIdToken(issuer, clientId, sub, nonce));
tokenResponse.setIdToken(buildIdToken(issuer, clientId, sub, nonce, acr));

lastTokenResponse = mapper.writeValueAsString(tokenResponse);

Expand Down

0 comments on commit 097abe4

Please sign in to comment.