From 9865ea2bfbb50682f322377dbd42830c15e5915f Mon Sep 17 00:00:00 2001 From: exceptionfactory Date: Wed, 27 Oct 2021 15:16:18 -0500 Subject: [PATCH] NIFI-9322 Refactored OIDC and SAML Access Resources - Removed parent AccessResource from OIDCAccessResource and SAMLAccessResource to avoid unexpected inherited methods - Moved Token Expiration validation from AccessResource to StandardBearerTokenProvider Signed-off-by: Nathan Gough This closes #5489. --- .../apache/nifi/web/api/AccessResource.java | 34 ++----- .../nifi/web/api/OIDCAccessResource.java | 10 +- .../nifi/web/api/SAMLAccessResource.java | 48 ++++++--- .../provider/StandardBearerTokenProvider.java | 26 ++++- .../token/LoginAuthenticationToken.java | 17 +--- .../StandardBearerTokenProviderTest.java | 99 +++++++++++++++---- 6 files changed, 156 insertions(+), 78 deletions(-) diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java index b184518403..4f29cde096 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java @@ -106,7 +106,7 @@ public class AccessResource extends ApplicationResource { private BearerTokenResolver bearerTokenResolver; private KnoxService knoxService; private KerberosService kerberosService; - protected LogoutRequestManager logoutRequestManager; + private LogoutRequestManager logoutRequestManager; /** * Retrieves the access configuration for this NiFi. @@ -348,8 +348,7 @@ public class AccessResource extends ApplicationResource { final String expirationFromProperties = properties.getKerberosAuthenticationExpiration(); long expiration = Math.round(FormatUtils.getPreciseTimeDuration(expirationFromProperties, TimeUnit.MILLISECONDS)); final String rawIdentity = authentication.getName(); - String mappedIdentity = IdentityMappingUtil.mapIdentity(rawIdentity, IdentityMappingUtil.getIdentityMappings(properties)); - expiration = validateTokenExpiration(expiration, mappedIdentity); + final String mappedIdentity = IdentityMappingUtil.mapIdentity(rawIdentity, IdentityMappingUtil.getIdentityMappings(properties)); final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(mappedIdentity, expiration, "KerberosService"); final String token = bearerTokenProvider.getBearerToken(loginAuthenticationToken); @@ -416,8 +415,8 @@ public class AccessResource extends ApplicationResource { // attempt to authenticate final AuthenticationResponse authenticationResponse = loginIdentityProvider.authenticate(new LoginCredentials(username, password)); final String rawIdentity = authenticationResponse.getIdentity(); - String mappedIdentity = IdentityMappingUtil.mapIdentity(rawIdentity, IdentityMappingUtil.getIdentityMappings(properties)); - long expiration = validateTokenExpiration(authenticationResponse.getExpiration(), mappedIdentity); + final String mappedIdentity = IdentityMappingUtil.mapIdentity(rawIdentity, IdentityMappingUtil.getIdentityMappings(properties)); + final long expiration = authenticationResponse.getExpiration(); // create the authentication token loginAuthenticationToken = new LoginAuthenticationToken(mappedIdentity, expiration, authenticationResponse.getIssuer()); @@ -506,7 +505,7 @@ public class AccessResource extends ApplicationResource { httpServletResponse.sendRedirect(getNiFiLogoutCompleteUri()); } - LogoutRequest completeLogoutRequest(final HttpServletResponse httpServletResponse) { + private LogoutRequest completeLogoutRequest(final HttpServletResponse httpServletResponse) { LogoutRequest logoutRequest = null; final Optional cookieValue = getLogoutRequestIdentifier(); @@ -522,24 +521,7 @@ public class AccessResource extends ApplicationResource { return logoutRequest; } - long validateTokenExpiration(long proposedTokenExpiration, String identity) { - final long maxExpiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS); - final long minExpiration = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES); - - if (proposedTokenExpiration > maxExpiration) { - logger.warn(String.format("Max token expiration exceeded. Setting expiration to %s from %s for %s", maxExpiration, - proposedTokenExpiration, identity)); - proposedTokenExpiration = maxExpiration; - } else if (proposedTokenExpiration < minExpiration) { - logger.warn(String.format("Min token expiration not met. Setting expiration to %s from %s for %s", minExpiration, - proposedTokenExpiration, identity)); - proposedTokenExpiration = minExpiration; - } - - return proposedTokenExpiration; - } - - String getNiFiLogoutCompleteUri() { + private String getNiFiLogoutCompleteUri() { return getNiFiUri() + "logout-complete"; } @@ -548,7 +530,7 @@ public class AccessResource extends ApplicationResource { * * @param httpServletResponse HTTP Servlet Response */ - protected void removeLogoutRequestCookie(final HttpServletResponse httpServletResponse) { + private void removeLogoutRequestCookie(final HttpServletResponse httpServletResponse) { applicationCookieService.removeCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER); } @@ -557,7 +539,7 @@ public class AccessResource extends ApplicationResource { * * @return Optional Logout Request Identifier */ - protected Optional getLogoutRequestIdentifier() { + private Optional getLogoutRequestIdentifier() { return applicationCookieService.getCookieValue(httpServletRequest, ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/OIDCAccessResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/OIDCAccessResource.java index 673e52982a..cc952a7cc4 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/OIDCAccessResource.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/OIDCAccessResource.java @@ -74,7 +74,7 @@ import java.util.regex.Pattern; value = OIDCEndpoints.OIDC_ACCESS_ROOT, description = "Endpoints for obtaining an access token or checking access status." ) -public class OIDCAccessResource extends AccessResource { +public class OIDCAccessResource extends ApplicationResource { private static final Logger logger = LoggerFactory.getLogger(OIDCAccessResource.class); private static final String OIDC_ID_TOKEN_AUTHN_ERROR = "Unable to exchange authorization for ID token: "; @@ -115,7 +115,7 @@ public class OIDCAccessResource extends AccessResource { public void oidcRequest(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception { // only consider user specific access over https if (!httpServletRequest.isSecure()) { - forwardToLoginMessagePage(httpServletRequest, httpServletResponse, AUTHENTICATION_NOT_ENABLED_MSG); + forwardToLoginMessagePage(httpServletRequest, httpServletResponse, AccessResource.AUTHENTICATION_NOT_ENABLED_MSG); return; } @@ -199,7 +199,7 @@ public class OIDCAccessResource extends AccessResource { public Response oidcExchange(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) { // only consider user specific access over https if (!httpServletRequest.isSecure()) { - throw new AuthenticationNotSupportedException(AUTHENTICATION_NOT_ENABLED_MSG); + throw new AuthenticationNotSupportedException(AccessResource.AUTHENTICATION_NOT_ENABLED_MSG); } // ensure oidc is enabled @@ -238,7 +238,7 @@ public class OIDCAccessResource extends AccessResource { ) public void oidcLogout(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception { if (!httpServletRequest.isSecure()) { - throw new IllegalStateException(AUTHENTICATION_NOT_ENABLED_MSG); + throw new IllegalStateException(AccessResource.AUTHENTICATION_NOT_ENABLED_MSG); } if (!oidcService.isOidcEnabled()) { @@ -468,7 +468,7 @@ public class OIDCAccessResource extends AccessResource { // only consider user specific access over https if (!httpServletRequest.isSecure()) { - forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle, AUTHENTICATION_NOT_ENABLED_MSG); + forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle, AccessResource.AUTHENTICATION_NOT_ENABLED_MSG); return null; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/SAMLAccessResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/SAMLAccessResource.java index a37442882e..537aa0b6d5 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/SAMLAccessResource.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/SAMLAccessResource.java @@ -27,6 +27,7 @@ import org.apache.nifi.idp.IdpType; import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.web.api.cookie.ApplicationCookieName; import org.apache.nifi.web.security.logout.LogoutRequest; +import org.apache.nifi.web.security.logout.LogoutRequestManager; import org.apache.nifi.web.security.saml.SAMLCredentialStore; import org.apache.nifi.web.security.saml.SAMLEndpoints; import org.apache.nifi.web.security.saml.SAMLService; @@ -61,7 +62,7 @@ import java.util.stream.Collectors; value = SAMLEndpoints.SAML_ACCESS_ROOT, description = "Endpoints for authenticating, obtaining an access token or logging out of a configured SAML authentication provider." ) -public class SAMLAccessResource extends AccessResource { +public class SAMLAccessResource extends ApplicationResource { private static final Logger logger = LoggerFactory.getLogger(SAMLAccessResource.class); private static final String SAML_METADATA_MEDIA_TYPE = "application/samlmetadata+xml"; @@ -73,6 +74,7 @@ public class SAMLAccessResource extends AccessResource { private SAMLStateManager samlStateManager; private SAMLCredentialStore samlCredentialStore; private IdpUserGroupService idpUserGroupService; + private LogoutRequestManager logoutRequestManager; @GET @Consumes(MediaType.WILDCARD) @@ -85,7 +87,7 @@ public class SAMLAccessResource extends AccessResource { public Response samlMetadata(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) { // only consider user specific access over https if (!httpServletRequest.isSecure()) { - throw new AuthenticationNotSupportedException(AUTHENTICATION_NOT_ENABLED_MSG); + throw new AuthenticationNotSupportedException(AccessResource.AUTHENTICATION_NOT_ENABLED_MSG); } // ensure saml is enabled @@ -212,7 +214,7 @@ public class SAMLAccessResource extends AccessResource { // create the login token final String rawIdentity = samlService.getUserIdentity(samlCredential); final String mappedIdentity = IdentityMappingUtil.mapIdentity(rawIdentity, IdentityMappingUtil.getIdentityMappings(properties)); - final long expiration = validateTokenExpiration(samlService.getAuthExpiration(), mappedIdentity); + final long expiration = samlService.getAuthExpiration(); final String issuer = samlCredential.getRemoteEntityID(); final LoginAuthenticationToken loginToken = new LoginAuthenticationToken(mappedIdentity, mappedIdentity, expiration, issuer); @@ -255,7 +257,7 @@ public class SAMLAccessResource extends AccessResource { // only consider user specific access over https if (!httpServletRequest.isSecure()) { - throw new AuthenticationNotSupportedException(AUTHENTICATION_NOT_ENABLED_MSG); + throw new AuthenticationNotSupportedException(AccessResource.AUTHENTICATION_NOT_ENABLED_MSG); } // ensure saml is enabled @@ -446,19 +448,21 @@ public class SAMLAccessResource extends AccessResource { assert(isSamlEnabled(httpServletRequest, httpServletResponse, !LOGGING_IN)); // complete the logout request if one exists - final LogoutRequest completedLogoutRequest = completeLogoutRequest(httpServletResponse); + final Optional cookieValue = getLogoutRequestIdentifier(); + if (cookieValue.isPresent()) { + final String logoutRequestIdentifier = cookieValue.get(); + final LogoutRequest logoutRequest = logoutRequestManager.complete(logoutRequestIdentifier); - // if a logout request was completed, then delete the stored SAMLCredential for that user - if (completedLogoutRequest != null) { - final String userIdentity = completedLogoutRequest.getMappedUserIdentity(); + final String mappedUserIdentity = logoutRequest.getMappedUserIdentity(); + samlCredentialStore.delete(mappedUserIdentity); + idpUserGroupService.deleteUserGroups(mappedUserIdentity); - logger.info("Removing cached SAML information for " + userIdentity); - samlCredentialStore.delete(userIdentity); - - logger.info("Removing cached SAML Groups for " + userIdentity); - idpUserGroupService.deleteUserGroups(userIdentity); + logger.info("Logout Request [{}] Identity [{}] SAML Local Logout Completed", logoutRequestIdentifier, mappedUserIdentity); + } else { + logger.warn("Logout Request Cookie [{}] not found", ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER.getCookieName()); } + removeLogoutRequestCookie(httpServletResponse); // redirect to logout landing page httpServletResponse.sendRedirect(getNiFiLogoutCompleteUri()); } @@ -488,7 +492,7 @@ public class SAMLAccessResource extends AccessResource { // only consider user specific access over https if (!httpServletRequest.isSecure()) { - forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle, AUTHENTICATION_NOT_ENABLED_MSG); + forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle, AccessResource.AUTHENTICATION_NOT_ENABLED_MSG); return false; } @@ -508,6 +512,18 @@ public class SAMLAccessResource extends AccessResource { return applicationCookieService.getCookieValue(httpServletRequest, ApplicationCookieName.SAML_REQUEST_IDENTIFIER); } + private void removeLogoutRequestCookie(final HttpServletResponse httpServletResponse) { + applicationCookieService.removeCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER); + } + + private Optional getLogoutRequestIdentifier() { + return applicationCookieService.getCookieValue(httpServletRequest, ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER); + } + + private String getNiFiLogoutCompleteUri() { + return getNiFiUri() + "logout-complete"; + } + public void setSamlService(SAMLService samlService) { this.samlService = samlService; } @@ -531,4 +547,8 @@ public class SAMLAccessResource extends AccessResource { protected NiFiProperties getProperties() { return properties; } + + public void setLogoutRequestManager(LogoutRequestManager logoutRequestManager) { + this.logoutRequestManager = logoutRequestManager; + } } \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/provider/StandardBearerTokenProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/provider/StandardBearerTokenProvider.java index 95185b67fc..aca6e40cf1 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/provider/StandardBearerTokenProvider.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/provider/StandardBearerTokenProvider.java @@ -32,6 +32,8 @@ import org.slf4j.LoggerFactory; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; import java.util.Date; import java.util.Objects; import java.util.UUID; @@ -44,6 +46,10 @@ public class StandardBearerTokenProvider implements BearerTokenProvider { private static final String URL_ENCODED_CHARACTER_SET = StandardCharsets.UTF_8.name(); + private static final Duration MAXIMUM_EXPIRATION = Duration.ofHours(12); + + private static final Duration MINIMUM_EXPIRATION = Duration.ofMinutes(1); + private final JwsSignerProvider jwsSignerProvider; public StandardBearerTokenProvider(final JwsSignerProvider jwsSignerProvider) { @@ -64,7 +70,7 @@ public class StandardBearerTokenProvider implements BearerTokenProvider { final String issuer = getUrlEncoded(loginAuthenticationToken.getIssuer()); final Date now = new Date(); - final Date expirationTime = new Date(loginAuthenticationToken.getExpiration()); + final Date expirationTime = getExpirationTime(loginAuthenticationToken); final JWTClaimsSet claims = new JWTClaimsSet.Builder() .jwtID(UUID.randomUUID().toString()) .subject(subject) @@ -78,6 +84,24 @@ public class StandardBearerTokenProvider implements BearerTokenProvider { return getSignedBearerToken(claims); } + private Date getExpirationTime(final LoginAuthenticationToken loginAuthenticationToken) { + Instant expiration = Instant.ofEpochMilli(loginAuthenticationToken.getExpiration()); + + final Instant maximumExpiration = Instant.now().plus(MAXIMUM_EXPIRATION); + final Instant minimumExpiration = Instant.now().plus(MINIMUM_EXPIRATION); + + final String identity = loginAuthenticationToken.getName(); + if (expiration.isAfter(maximumExpiration)) { + LOGGER.warn("Identity [{}] Token Expiration [{}] greater than maximum [{}]", identity, expiration, MAXIMUM_EXPIRATION); + expiration = maximumExpiration; + } else if (expiration.isBefore(minimumExpiration)) { + LOGGER.warn("Identity [{}] Token Expiration [{}] less than minimum [{}]", identity, expiration, MINIMUM_EXPIRATION); + expiration = minimumExpiration; + } + + return Date.from(expiration); + } + private String getSignedBearerToken(final JWTClaimsSet claims) { final Date expirationTime = claims.getExpirationTime(); final JwsSignerContainer jwsSignerContainer = jwsSignerProvider.getJwsSignerContainer(expirationTime.toInstant()); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationToken.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationToken.java index 35912398ce..c0e895c58e 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationToken.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationToken.java @@ -19,8 +19,7 @@ package org.apache.nifi.web.security.token; import org.apache.nifi.security.util.CertificateUtils; import org.springframework.security.authentication.AbstractAuthenticationToken; -import java.text.SimpleDateFormat; -import java.util.Calendar; +import java.time.Instant; /** * This is an Authentication Token for logging in. Once a user is authenticated, they can be issued an ID token. @@ -57,8 +56,7 @@ public class LoginAuthenticationToken extends AbstractAuthenticationToken { this.identity = identity; this.username = username; this.issuer = issuer; - Calendar now = Calendar.getInstance(); - this.expiration = now.getTimeInMillis() + expiration; + this.expiration = Instant.now().plusMillis(expiration).toEpochMilli(); } @Override @@ -98,20 +96,15 @@ public class LoginAuthenticationToken extends AbstractAuthenticationToken { @Override public String toString() { - Calendar expirationTime = Calendar.getInstance(); - expirationTime.setTimeInMillis(getExpiration()); - long remainingTime = expirationTime.getTimeInMillis() - Calendar.getInstance().getTimeInMillis(); - - SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss.SSS"); - dateFormat.setTimeZone(expirationTime.getTimeZone()); - String expirationTimeString = dateFormat.format(expirationTime.getTime()); + final Instant expirationTime = Instant.ofEpochMilli(expiration); + long remainingTime = expirationTime.toEpochMilli() - Instant.now().toEpochMilli(); return new StringBuilder("LoginAuthenticationToken for ") .append(getName()) .append(" issued by ") .append(getIssuer()) .append(" expiring at ") - .append(expirationTimeString) + .append(expirationTime) .append(" [") .append(getExpiration()) .append(" ms, ") diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/provider/StandardBearerTokenProviderTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/provider/StandardBearerTokenProviderTest.java index 668d04c8a7..da3bb202a4 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/provider/StandardBearerTokenProviderTest.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/provider/StandardBearerTokenProviderTest.java @@ -27,34 +27,41 @@ import com.nimbusds.jwt.SignedJWT; import org.apache.nifi.web.security.jwt.jws.JwsSignerContainer; import org.apache.nifi.web.security.jwt.jws.JwsSignerProvider; import org.apache.nifi.web.security.token.LoginAuthenticationToken; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.interfaces.RSAPublicKey; import java.text.ParseException; +import java.time.Duration; import java.time.Instant; import java.util.Collections; +import java.util.Date; import java.util.UUID; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.when; -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class StandardBearerTokenProviderTest { private static final String USERNAME = "USERNAME"; private static final String IDENTITY = "IDENTITY"; - private static final long EXPIRATION = 60; + private static final Duration EXPIRATION = Duration.ofHours(1); + + private static final Duration MAXIMUM_DURATION_EXCEEDED = Duration.parse("PT12H5M"); + + private static final Duration MINIMUM_DURATION_EXCEEDED = Duration.parse("PT30S"); private static final String ISSUER = "ISSUER"; @@ -73,7 +80,7 @@ public class StandardBearerTokenProviderTest { private JWSSigner jwsSigner; - @Before + @BeforeEach public void setProvider() throws NoSuchAlgorithmException { provider = new StandardBearerTokenProvider(jwsSignerProvider); @@ -86,24 +93,76 @@ public class StandardBearerTokenProviderTest { @Test public void testGetBearerToken() throws ParseException, JOSEException { - final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(IDENTITY, USERNAME, EXPIRATION, ISSUER); - final String keyIdentifier = UUID.randomUUID().toString(); - final JwsSignerContainer jwsSignerContainer = new JwsSignerContainer(keyIdentifier, JWS_ALGORITHM, jwsSigner); - when(jwsSignerProvider.getJwsSignerContainer(isA(Instant.class))).thenReturn(jwsSignerContainer); + final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(IDENTITY, USERNAME, EXPIRATION.toMillis(), ISSUER); + setSignerProvider(); final String bearerToken = provider.getBearerToken(loginAuthenticationToken); - final SignedJWT signedJwt = SignedJWT.parse(bearerToken); - assertTrue("Verification Failed", signedJwt.verify(jwsVerifier)); - + final SignedJWT signedJwt = assertTokenVerified(bearerToken); final JWTClaimsSet claims = signedJwt.getJWTClaimsSet(); - assertNotNull("Issue Time not found", claims.getIssueTime()); - assertNotNull("Not Before Time Time not found", claims.getNotBeforeTime()); - assertNotNull("Expiration Time Time not found", claims.getExpirationTime()); + assertNotNull(claims.getIssueTime(), "Issue Time not found"); + assertNotNull(claims.getNotBeforeTime(), "Not Before Time not found"); + + final Date claimExpirationTime = claims.getExpirationTime(); + assertNotNull(claimExpirationTime, "Expiration Time not found"); + + final Date loginExpirationTime = new Date(loginAuthenticationToken.getExpiration()); + assertEquals(loginExpirationTime.toString(), claimExpirationTime.toString(), "Expiration Time not matched"); + assertEquals(ISSUER, claims.getIssuer()); assertEquals(Collections.singletonList(ISSUER), claims.getAudience()); assertEquals(IDENTITY, claims.getSubject()); assertEquals(USERNAME, claims.getClaim(SupportedClaim.PREFERRED_USERNAME.getClaim())); assertNotNull("JSON Web Token Identifier not found", claims.getJWTID()); } + + @Test + public void testGetBearerTokenExpirationMaximum() throws ParseException, JOSEException { + final long expiration = MAXIMUM_DURATION_EXCEEDED.toMillis(); + final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(IDENTITY, USERNAME, expiration, ISSUER); + setSignerProvider(); + + final String bearerToken = provider.getBearerToken(loginAuthenticationToken); + + final SignedJWT signedJwt = assertTokenVerified(bearerToken); + final JWTClaimsSet claims = signedJwt.getJWTClaimsSet(); + final Date claimExpirationTime = claims.getExpirationTime(); + assertNotNull(claimExpirationTime, "Expiration Time not found"); + + final Date loginExpirationTime = new Date(loginAuthenticationToken.getExpiration()); + assertNotSame(loginExpirationTime.toString(), claimExpirationTime.toString(), "Expiration Time matched"); + + assertTrue(claimExpirationTime.toInstant().isBefore(loginExpirationTime.toInstant()), "Claim Expiration after Login Expiration"); + } + + @Test + public void testGetBearerTokenExpirationMinimum() throws ParseException, JOSEException { + final long expiration = MINIMUM_DURATION_EXCEEDED.toMillis(); + final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(IDENTITY, USERNAME, expiration, ISSUER); + setSignerProvider(); + + final String bearerToken = provider.getBearerToken(loginAuthenticationToken); + + final SignedJWT signedJwt = assertTokenVerified(bearerToken); + final JWTClaimsSet claims = signedJwt.getJWTClaimsSet(); + final Date claimExpirationTime = claims.getExpirationTime(); + assertNotNull(claimExpirationTime, "Expiration Time not found"); + + final Date loginExpirationTime = new Date(loginAuthenticationToken.getExpiration()); + assertNotSame(loginExpirationTime.toString(), claimExpirationTime.toString(), "Expiration Time matched"); + + assertTrue(claimExpirationTime.toInstant().isAfter(loginExpirationTime.toInstant()), "Claim Expiration before Login Expiration"); + } + + private void setSignerProvider() { + final String keyIdentifier = UUID.randomUUID().toString(); + final JwsSignerContainer jwsSignerContainer = new JwsSignerContainer(keyIdentifier, JWS_ALGORITHM, jwsSigner); + when(jwsSignerProvider.getJwsSignerContainer(isA(Instant.class))).thenReturn(jwsSignerContainer); + } + + private SignedJWT assertTokenVerified(final String bearerToken) throws ParseException, JOSEException { + final SignedJWT signedJwt = SignedJWT.parse(bearerToken); + assertTrue(signedJwt.verify(jwsVerifier), "Verification Failed"); + return signedJwt; + } }