From 387c8a5224864377db93fac3d0dd86c6812bee95 Mon Sep 17 00:00:00 2001 From: Ryan Liang <109499885+RyanL1997@users.noreply.github.com> Date: Thu, 12 Oct 2023 09:57:49 -0400 Subject: [PATCH] Code coverage of OBO Authentication (#3428) ### Description Code coverage of OBO Authentication * Category (Enhancement, New feature, Bug fix, Test fix, Refactoring, Maintenance, Documentation) Enhancement ### Issues Resolved * Resolve https://github.com/opensearch-project/security/issues/3101 ### Check List - [x] New functionality includes testing - [ ] New functionality has been documented - [x] Commits are signed per the DCO using --signoff By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. For more information on following Developer Certificate of Origin and signing off your commits, please check [here](https://github.com/opensearch-project/OpenSearch/blob/main/CONTRIBUTING.md#developer-certificate-of-origin). --------- Signed-off-by: Ryan Liang Signed-off-by: Darshit Chanpura Co-authored-by: Darshit Chanpura --- .../http/OnBehalfOfAuthenticator.java | 16 +- .../security/authtoken/jwt/JwtVendorTest.java | 172 +++++++-- .../http/OnBehalfOfAuthenticatorTest.java | 355 +++++++++++++++--- 3 files changed, 457 insertions(+), 86 deletions(-) diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index 8499b88f62..4ac3be335f 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -145,11 +145,6 @@ private AuthCredentials extractCredentials0(final SecurityRequest request) { return null; } - if (jwtParser == null) { - log.error("Missing Signing Key. JWT authentication will not work"); - return null; - } - String jwtToken = extractJwtFromHeader(request); if (jwtToken == null) { return null; @@ -193,6 +188,7 @@ private AuthCredentials extractCredentials0(final SecurityRequest request) { } catch (WeakKeyException e) { log.error("Cannot authenticate user with JWT because of ", e); + return null; } catch (Exception e) { if (log.isDebugEnabled()) { log.debug("Invalid or expired JWT token.", e); @@ -211,17 +207,13 @@ private String extractJwtFromHeader(SecurityRequest request) { return null; } - if (!BEARER.matcher(jwtToken).matches()) { - return null; - } - - if (jwtToken.toLowerCase().contains(BEARER_PREFIX)) { - jwtToken = jwtToken.substring(jwtToken.toLowerCase().indexOf(BEARER_PREFIX) + BEARER_PREFIX.length()); - } else { + if (!BEARER.matcher(jwtToken).matches() || !jwtToken.toLowerCase().contains(BEARER_PREFIX)) { logDebug("No Bearer scheme found in header"); return null; } + jwtToken = jwtToken.substring(jwtToken.toLowerCase().indexOf(BEARER_PREFIX) + BEARER_PREFIX.length()); + return jwtToken; } diff --git a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java index aa8faa284d..03cbd20b42 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -15,8 +15,13 @@ import org.apache.cxf.rs.security.jose.jwk.JsonWebKey; import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer; import org.apache.cxf.rs.security.jose.jwt.JwtToken; -import org.junit.Assert; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.opensearch.common.settings.Settings; import org.opensearch.security.support.ConfigConstants; @@ -24,29 +29,74 @@ import java.util.Optional; import java.util.function.LongSupplier; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + public class JwtVendorTest { + private Appender mockAppender; + private ArgumentCaptor logEventCaptor; + + @Test + public void testCreateJwkFromSettingsThrowsException() { + Settings faultySettings = Settings.builder().put("key.someProperty", "badValue").build(); + + Exception thrownException = assertThrows(Exception.class, () -> new JwtVendor(faultySettings, null)); + + String expectedMessagePart = "An error occurred during the creation of Jwk: "; + assertTrue(thrownException.getMessage().contains(expectedMessagePart)); + } + + @Test + public void testJsonWebKeyPropertiesSetFromJwkSettings() throws Exception { + Settings settings = Settings.builder().put("jwt.key.key1", "value1").put("jwt.key.key2", "value2").build(); + + JsonWebKey jwk = JwtVendor.createJwkFromSettings(settings); + + assertEquals("value1", jwk.getProperty("key1")); + assertEquals("value2", jwk.getProperty("key2")); + } + + @Test + public void testJsonWebKeyPropertiesSetFromSettings() { + Settings jwkSettings = Settings.builder().put("key1", "value1").put("key2", "value2").build(); + + JsonWebKey jwk = new JsonWebKey(); + for (String key : jwkSettings.keySet()) { + jwk.setProperty(key, jwkSettings.get(key)); + } + + assertEquals("value1", jwk.getProperty("key1")); + assertEquals("value2", jwk.getProperty("key2")); + } @Test public void testCreateJwkFromSettings() throws Exception { Settings settings = Settings.builder().put("signing_key", "abc123").build(); JsonWebKey jwk = JwtVendor.createJwkFromSettings(settings); - Assert.assertEquals("HS512", jwk.getAlgorithm()); - Assert.assertEquals("sig", jwk.getPublicKeyUse().toString()); - Assert.assertEquals("abc123", jwk.getProperty("k")); + assertEquals("HS512", jwk.getAlgorithm()); + assertEquals("sig", jwk.getPublicKeyUse().toString()); + assertEquals("abc123", jwk.getProperty("k")); } @Test public void testCreateJwkFromSettingsWithoutSigningKey() { Settings settings = Settings.builder().put("jwt", "").build(); - Throwable exception = Assert.assertThrows(RuntimeException.class, () -> { + Throwable exception = assertThrows(RuntimeException.class, () -> { try { JwtVendor.createJwkFromSettings(settings); } catch (Exception e) { throw new RuntimeException(e); } }); - Assert.assertEquals( + assertEquals( "java.lang.Exception: Settings for signing key is missing. Please specify at least the option signing_key with a shared secret.", exception.getMessage() ); @@ -72,15 +122,15 @@ public void testCreateJwtWithRoles() throws Exception { JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedJwt); JwtToken jwt = jwtConsumer.getJwtToken(); - Assert.assertEquals("cluster_0", jwt.getClaim("iss")); - Assert.assertEquals("admin", jwt.getClaim("sub")); - Assert.assertEquals("audience_0", jwt.getClaim("aud")); - Assert.assertNotNull(jwt.getClaim("iat")); - Assert.assertNotNull(jwt.getClaim("exp")); - Assert.assertEquals(expectedExp, jwt.getClaim("exp")); + assertEquals("cluster_0", jwt.getClaim("iss")); + assertEquals("admin", jwt.getClaim("sub")); + assertEquals("audience_0", jwt.getClaim("aud")); + assertNotNull(jwt.getClaim("iat")); + assertNotNull(jwt.getClaim("exp")); + assertEquals(expectedExp, jwt.getClaim("exp")); EncryptionDecryptionUtil encryptionUtil = new EncryptionDecryptionUtil(claimsEncryptionKey); - Assert.assertEquals(expectedRoles, encryptionUtil.decrypt(jwt.getClaim("er").toString())); - Assert.assertNull(jwt.getClaim("br")); + assertEquals(expectedRoles, encryptionUtil.decrypt(jwt.getClaim("er").toString())); + assertNull(jwt.getClaim("br")); } @Test @@ -111,20 +161,20 @@ public void testCreateJwtWithRoleSecurityMode() throws Exception { JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedJwt); JwtToken jwt = jwtConsumer.getJwtToken(); - Assert.assertEquals("cluster_0", jwt.getClaim("iss")); - Assert.assertEquals("admin", jwt.getClaim("sub")); - Assert.assertEquals("audience_0", jwt.getClaim("aud")); - Assert.assertNotNull(jwt.getClaim("iat")); - Assert.assertNotNull(jwt.getClaim("exp")); - Assert.assertEquals(expectedExp, jwt.getClaim("exp")); + assertEquals("cluster_0", jwt.getClaim("iss")); + assertEquals("admin", jwt.getClaim("sub")); + assertEquals("audience_0", jwt.getClaim("aud")); + assertNotNull(jwt.getClaim("iat")); + assertNotNull(jwt.getClaim("exp")); + assertEquals(expectedExp, jwt.getClaim("exp")); EncryptionDecryptionUtil encryptionUtil = new EncryptionDecryptionUtil(claimsEncryptionKey); - Assert.assertEquals(expectedRoles, encryptionUtil.decrypt(jwt.getClaim("er").toString())); - Assert.assertNotNull(jwt.getClaim("br")); - Assert.assertEquals(expectedBackendRoles, jwt.getClaim("br")); + assertEquals(expectedRoles, encryptionUtil.decrypt(jwt.getClaim("er").toString())); + assertNotNull(jwt.getClaim("br")); + assertEquals(expectedBackendRoles, jwt.getClaim("br")); } @Test - public void testCreateJwtWithBadExpiry() { + public void testCreateJwtWithNegativeExpiry() { String issuer = "cluster_0"; String subject = "admin"; String audience = "audience_0"; @@ -134,14 +184,40 @@ public void testCreateJwtWithBadExpiry() { Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); - Throwable exception = Assert.assertThrows(RuntimeException.class, () -> { + Throwable exception = assertThrows(RuntimeException.class, () -> { try { jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); } catch (Exception e) { throw new RuntimeException(e); } }); - Assert.assertEquals("java.lang.Exception: The expiration time should be a positive integer", exception.getMessage()); + assertEquals("java.lang.Exception: The expiration time should be a positive integer", exception.getMessage()); + } + + @Test + public void testCreateJwtWithExceededExpiry() throws Exception { + String issuer = "cluster_0"; + String subject = "admin"; + String audience = "audience_0"; + List roles = List.of("IT", "HR"); + List backendRoles = List.of("Sales", "Support"); + int expirySeconds = 900; + LongSupplier currentTime = () -> (long) 100; + String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); + + Throwable exception = assertThrows(RuntimeException.class, () -> { + try { + jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, true); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + assertEquals( + "java.lang.Exception: The provided expiration time exceeds the maximum allowed duration of 600 seconds", + exception.getMessage() + ); } @Test @@ -154,14 +230,14 @@ public void testCreateJwtWithBadEncryptionKey() { Settings settings = Settings.builder().put("signing_key", "abc123").build(); - Throwable exception = Assert.assertThrows(RuntimeException.class, () -> { + Throwable exception = assertThrows(RuntimeException.class, () -> { try { new JwtVendor(settings, Optional.empty()).createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); } catch (Exception e) { throw new RuntimeException(e); } }); - Assert.assertEquals("java.lang.IllegalArgumentException: encryption_key cannot be null", exception.getMessage()); + assertEquals("java.lang.IllegalArgumentException: encryption_key cannot be null", exception.getMessage()); } @Test @@ -175,13 +251,49 @@ public void testCreateJwtWithBadRoles() { Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); - Throwable exception = Assert.assertThrows(RuntimeException.class, () -> { + Throwable exception = assertThrows(RuntimeException.class, () -> { try { jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); } catch (Exception e) { throw new RuntimeException(e); } }); - Assert.assertEquals("java.lang.Exception: Roles cannot be null", exception.getMessage()); + assertEquals("java.lang.Exception: Roles cannot be null", exception.getMessage()); + } + + @Test + public void testCreateJwtLogsCorrectly() throws Exception { + mockAppender = mock(Appender.class); + logEventCaptor = ArgumentCaptor.forClass(LogEvent.class); + when(mockAppender.getName()).thenReturn("MockAppender"); + when(mockAppender.isStarted()).thenReturn(true); + Logger logger = (Logger) LogManager.getLogger(JwtVendor.class); + logger.addAppender(mockAppender); + logger.setLevel(Level.DEBUG); + + // Mock settings and other required dependencies + LongSupplier currentTime = () -> (long) 100; + String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); + + String issuer = "cluster_0"; + String subject = "admin"; + String audience = "audience_0"; + List roles = List.of("IT", "HR"); + List backendRoles = List.of("Sales", "Support"); + int expirySeconds = 300; + + JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); + + jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, false); + + verify(mockAppender, times(1)).append(logEventCaptor.capture()); + + LogEvent logEvent = logEventCaptor.getValue(); + String logMessage = logEvent.getMessage().getFormattedMessage(); + assertTrue(logMessage.startsWith("Created JWT:")); + + String[] parts = logMessage.split("\\."); + assertTrue(parts.length >= 3); } } diff --git a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java index 1ff6adee3a..b32792190f 100644 --- a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java @@ -11,30 +11,64 @@ package org.opensearch.security.http; +import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.Set; +import java.util.List; +import java.util.HashSet; +import java.util.Arrays; +import java.util.Optional; import javax.crypto.SecretKey; import com.google.common.io.BaseEncoding; import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.WeakKeyException; import org.apache.commons.lang3.RandomStringUtils; import org.apache.hc.core5.http.HttpHeaders; -import org.junit.Assert; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.ErrorHandler; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.opensearch.SpecialPermission; import org.opensearch.common.settings.Settings; +import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.filter.SecurityResponse; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.util.FakeRestRequest; import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.rest.RestRequest.Method.PUT; public class OnBehalfOfAuthenticatorTest { final static String clusterName = "cluster_0"; @@ -47,9 +81,27 @@ public class OnBehalfOfAuthenticatorTest { final static String signingKeyB64Encoded = BaseEncoding.base64().encode(signingKey.getBytes(StandardCharsets.UTF_8)); final static SecretKey secretKey = Keys.hmacShaKeyFor(signingKeyB64Encoded.getBytes(StandardCharsets.UTF_8)); + private static final String SECURITY_PREFIX = "/_plugins/_security/"; + private static final String ON_BEHALF_OF_SUFFIX = "api/generateonbehalfoftoken"; + private static final String ACCOUNT_SUFFIX = "api/account"; + + @Test + public void testReRequestAuthenticationReturnsEmptyOptional() { + OnBehalfOfAuthenticator authenticator = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + Optional result = authenticator.reRequestAuthentication(null, null); + assertFalse(result.isPresent()); + } + + @Test + public void testGetTypeReturnsExpectedType() { + OnBehalfOfAuthenticator authenticator = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + String type = authenticator.getType(); + assertEquals("onbehalfof_jwt", type); + } + @Test public void testNoKey() { - Exception exception = Assert.assertThrows( + Exception exception = assertThrows( RuntimeException.class, () -> extractCredentialsFromJwtHeader( null, @@ -58,12 +110,12 @@ public void testNoKey() { false ) ); - Assert.assertTrue(exception.getMessage().contains("Unable to find on behalf of authenticator signing key")); + assertTrue(exception.getMessage().contains("Unable to find on behalf of authenticator signing key")); } @Test public void testEmptyKey() { - Exception exception = Assert.assertThrows( + Exception exception = assertThrows( RuntimeException.class, () -> extractCredentialsFromJwtHeader( null, @@ -72,12 +124,12 @@ public void testEmptyKey() { false ) ); - Assert.assertTrue(exception.getMessage().contains("Unable to find on behalf of authenticator signing key")); + assertTrue(exception.getMessage().contains("Unable to find on behalf of authenticator signing key")); } @Test public void testBadKey() { - Exception exception = Assert.assertThrows( + Exception exception = assertThrows( RuntimeException.class, () -> extractCredentialsFromJwtHeader( BaseEncoding.base64().encode(new byte[] { 1, 3, 3, 4, 3, 6, 7, 8, 3, 10 }), @@ -86,7 +138,45 @@ public void testBadKey() { false ) ); - Assert.assertTrue(exception.getMessage().contains("The specified key byte array is 80 bits")); + assertTrue(exception.getMessage().contains("The specified key byte array is 80 bits")); + } + + @Test + public void testWeakKeyExceptionHandling() throws Exception { + Appender mockAppender = mock(Appender.class); + ErrorHandler mockErrorHandler = mock(ErrorHandler.class); + when(mockAppender.getHandler()).thenReturn(mockErrorHandler); + when(mockAppender.isStarted()).thenReturn(true); + + ArgumentCaptor logEventCaptor = ArgumentCaptor.forClass(LogEvent.class); + when(mockAppender.getName()).thenReturn("MockAppender"); + doNothing().when(mockAppender).append(logEventCaptor.capture()); + + Logger logger = (Logger) LogManager.getLogger(OnBehalfOfAuthenticator.class); + logger.addAppender(mockAppender); + + JwtParser mockJwtParser = mock(JwtParser.class); + when(mockJwtParser.parseClaimsJws(anyString())).thenThrow(new WeakKeyException("Test Exception")); + + Settings settings = Settings.builder().put("signing_key", "testKey").put("encryption_key", claimsEncryptionKey).build(); + OnBehalfOfAuthenticator auth = new OnBehalfOfAuthenticator(settings, "testCluster"); + + Field jwtParserField = OnBehalfOfAuthenticator.class.getDeclaredField("jwtParser"); + jwtParserField.setAccessible(true); + jwtParserField.set(auth, mockJwtParser); + + SecurityRequest mockedRequest = mock(SecurityRequest.class); + when(mockedRequest.header(anyString())).thenReturn("Bearer testToken"); + when(mockedRequest.path()).thenReturn("/some/sample/path"); + + auth.extractCredentials(mockedRequest, null); + + boolean foundLog = logEventCaptor.getAllValues() + .stream() + .anyMatch(event -> event.getMessage().getFormattedMessage().contains("Cannot authenticate user with JWT because of ")); + assertTrue(foundLog); + + logger.removeAppender(mockAppender); } @Test @@ -100,7 +190,7 @@ public void testTokenMissing() throws Exception { null ); - Assert.assertNull(credentials); + assertNull(credentials); } @Test @@ -116,7 +206,7 @@ public void testInvalid() throws Exception { new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), null ); - Assert.assertNull(credentials); + assertNull(credentials); } @Test @@ -136,7 +226,40 @@ public void testDisabled() throws Exception { new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), null ); - Assert.assertNull(credentials); + assertNull(credentials); + } + + @Test + public void testInvalidTokenException() { + Appender mockAppender = mock(Appender.class); + ArgumentCaptor logEventCaptor = ArgumentCaptor.forClass(LogEvent.class); + when(mockAppender.getName()).thenReturn("MockAppender"); + when(mockAppender.isStarted()).thenReturn(true); + Logger logger = (Logger) LogManager.getLogger(OnBehalfOfAuthenticator.class); + logger.addAppender(mockAppender); + logger.setLevel(Level.DEBUG); + doNothing().when(mockAppender).append(logEventCaptor.capture()); + + String invalidToken = "invalidToken"; + Settings settings = defaultSettings(); + + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(settings, clusterName); + + Map headers = Collections.singletonMap(HttpHeaders.AUTHORIZATION, "Bearer " + invalidToken); + + AuthCredentials credentials = jwtAuth.extractCredentials( + new FakeRestRequest(headers, Collections.emptyMap()).asSecurityRequest(), + null + ); + + assertNull(credentials); + + boolean foundLog = logEventCaptor.getAllValues() + .stream() + .anyMatch(event -> event.getMessage().getFormattedMessage().contains("Invalid or expired JWT token.")); + assertTrue(foundLog); + + logger.removeAppender(mockAppender); } @Test @@ -156,7 +279,7 @@ public void testNonSpecifyOBOSetting() throws Exception { new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), null ); - Assert.assertNotNull(credentials); + assertNotNull(credentials); } @Test @@ -182,11 +305,11 @@ public void testBearer() throws Exception { null ); - Assert.assertNotNull(credentials); - Assert.assertEquals("Leonard McCoy", credentials.getUsername()); - Assert.assertEquals(0, credentials.getSecurityRoles().size()); - Assert.assertEquals(0, credentials.getBackendRoles().size()); - Assert.assertThat(credentials.getAttributes(), equalTo(expectedAttributes)); + assertNotNull(credentials); + assertEquals("Leonard McCoy", credentials.getUsername()); + assertEquals(0, credentials.getSecurityRoles().size()); + assertEquals(0, credentials.getBackendRoles().size()); + assertThat(credentials.getAttributes(), equalTo(expectedAttributes)); } @Test @@ -208,7 +331,25 @@ public void testBearerWrongPosition() throws Exception { null ); - Assert.assertNull(credentials); + assertNull(credentials); + } + + @Test + public void testSecurityManagerCheck() { + SecurityManager mockSecurityManager = mock(SecurityManager.class); + System.setSecurityManager(mockSecurityManager); + + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer someToken"); + + try { + jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap<>()).asSecurityRequest(), null); + } finally { + System.setSecurityManager(null); + } + + verify(mockSecurityManager, times(2)).checkPermission(any(SpecialPermission.class)); } @Test @@ -227,11 +368,63 @@ public void testBasicAuthHeader() throws Exception { new FakeRestRequest(headers, Collections.emptyMap()).asSecurityRequest(), null ); - Assert.assertNull(credentials); + assertNull(credentials); + } + + @Test + public void testMissingBearerScheme() throws Exception { + Appender mockAppender = mock(Appender.class); + ArgumentCaptor logEventCaptor = ArgumentCaptor.forClass(LogEvent.class); + when(mockAppender.getName()).thenReturn("MockAppender"); + when(mockAppender.isStarted()).thenReturn(true); + Logger logger = (Logger) LogManager.getLogger(OnBehalfOfAuthenticator.class); + logger.addAppender(mockAppender); + logger.setLevel(Level.DEBUG); + doNothing().when(mockAppender).append(logEventCaptor.capture()); + + String craftedToken = "beaRerSomeActualToken"; // This token matches the BEARER pattern but doesn't contain the BEARER_PREFIX + + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + Map headers = Collections.singletonMap(HttpHeaders.AUTHORIZATION, craftedToken); + + AuthCredentials credentials = jwtAuth.extractCredentials( + new FakeRestRequest(headers, Collections.emptyMap()).asSecurityRequest(), + null + ); + + assertNull(credentials); + + boolean foundLog = logEventCaptor.getAllValues() + .stream() + .anyMatch(event -> event.getMessage().getFormattedMessage().contains("No Bearer scheme found in header")); + assertTrue(foundLog); + + logger.removeAppender(mockAppender); + } + + @Test + public void testMissingBearerPrefixInAuthHeader() { + String jwsToken = Jwts.builder() + .setIssuer(clusterName) + .setSubject("Leonard McCoy") + .setAudience("ext_0") + .signWith(secretKey, SignatureAlgorithm.HS512) + .compact(); + + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + + Map headers = Collections.singletonMap(HttpHeaders.AUTHORIZATION, jwsToken); + + AuthCredentials credentials = jwtAuth.extractCredentials( + new FakeRestRequest(headers, Collections.emptyMap()).asSecurityRequest(), + null + ); + + assertNull(credentials); } @Test - public void testRoles() throws Exception { + public void testPlainTextedRolesFromDrClaim() { final AuthCredentials credentials = extractCredentialsFromJwtHeader( signingKeyB64Encoded, @@ -240,10 +433,46 @@ public void testRoles() throws Exception { true ); - Assert.assertNotNull(credentials); - Assert.assertEquals("Leonard McCoy", credentials.getUsername()); - Assert.assertEquals(2, credentials.getSecurityRoles().size()); - Assert.assertEquals(0, credentials.getBackendRoles().size()); + assertNotNull(credentials); + assertEquals("Leonard McCoy", credentials.getUsername()); + assertEquals(2, credentials.getSecurityRoles().size()); + assertEquals(0, credentials.getBackendRoles().size()); + } + + @Test + public void testBackendRolesExtraction() { + String rolesString = "role1, role2 ,role3,role4 , role5"; + + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setIssuer(clusterName).setSubject("Test User").setAudience("audience_0").claim("br", rolesString), + true + ); + + assertNotNull(credentials); + + Set expectedBackendRoles = new HashSet<>(Arrays.asList("role1", "role2", "role3", "role4", "role5")); + Set actualBackendRoles = credentials.getBackendRoles(); + + assertTrue(actualBackendRoles.containsAll(expectedBackendRoles)); + } + + @Test + public void testRolesDecryptionFromErClaim() { + EncryptionDecryptionUtil util = new EncryptionDecryptionUtil(claimsEncryptionKey); + String encryptedRole = util.encrypt("admin,developer"); + + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setIssuer(clusterName).setSubject("Test User").setAudience("audience_0").claim("er", encryptedRole), + true + ); + + assertNotNull(credentials); + List expectedRoles = Arrays.asList("admin", "developer"); + assertTrue(credentials.getSecurityRoles().containsAll(expectedRoles)); } @Test @@ -256,9 +485,9 @@ public void testNullClaim() throws Exception { false ); - Assert.assertNotNull(credentials); - Assert.assertEquals("Leonard McCoy", credentials.getUsername()); - Assert.assertEquals(0, credentials.getBackendRoles().size()); + assertNotNull(credentials); + assertEquals("Leonard McCoy", credentials.getUsername()); + assertEquals(0, credentials.getBackendRoles().size()); } @Test @@ -271,10 +500,10 @@ public void testNonStringClaim() throws Exception { true ); - Assert.assertNotNull(credentials); - Assert.assertEquals("Leonard McCoy", credentials.getUsername()); - Assert.assertEquals(1, credentials.getSecurityRoles().size()); - Assert.assertTrue(credentials.getSecurityRoles().contains("123")); + assertNotNull(credentials); + assertEquals("Leonard McCoy", credentials.getUsername()); + assertEquals(1, credentials.getSecurityRoles().size()); + assertTrue(credentials.getSecurityRoles().contains("123")); } @Test @@ -287,10 +516,10 @@ public void testRolesMissing() throws Exception { false ); - Assert.assertNotNull(credentials); - Assert.assertEquals("Leonard McCoy", credentials.getUsername()); - Assert.assertEquals(0, credentials.getSecurityRoles().size()); - Assert.assertEquals(0, credentials.getBackendRoles().size()); + assertNotNull(credentials); + assertEquals("Leonard McCoy", credentials.getUsername()); + assertEquals(0, credentials.getSecurityRoles().size()); + assertEquals(0, credentials.getBackendRoles().size()); } @Test @@ -303,7 +532,20 @@ public void testWrongSubjectKey() throws Exception { false ); - Assert.assertNull(credentials); + assertNull(credentials); + } + + @Test + public void testMissingAudienceClaim() throws Exception { + + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setIssuer(clusterName).setSubject("Test User").claim("roles", "role1,role2"), + false + ); + + assertNull(credentials); } @Test @@ -316,7 +558,7 @@ public void testExp() throws Exception { false ); - Assert.assertNull(credentials); + assertNull(credentials); } @Test @@ -329,7 +571,7 @@ public void testNbf() throws Exception { false ); - Assert.assertNull(credentials); + assertNull(credentials); } @Test @@ -348,12 +590,12 @@ public void testRolesArray() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader(signingKeyB64Encoded, claimsEncryptionKey, builder, true); - Assert.assertNotNull(credentials); - Assert.assertEquals("Cluster_0", credentials.getUsername()); - Assert.assertEquals(3, credentials.getSecurityRoles().size()); - Assert.assertTrue(credentials.getSecurityRoles().contains("a")); - Assert.assertTrue(credentials.getSecurityRoles().contains("b")); - Assert.assertTrue(credentials.getSecurityRoles().contains("3rd")); + assertNotNull(credentials); + assertEquals("Cluster_0", credentials.getUsername()); + assertEquals(3, credentials.getSecurityRoles().size()); + assertTrue(credentials.getSecurityRoles().contains("a")); + assertTrue(credentials.getSecurityRoles().contains("b")); + assertTrue(credentials.getSecurityRoles().contains("3rd")); } @Test @@ -375,7 +617,28 @@ public void testDifferentIssuer() throws Exception { null ); - Assert.assertNull(credentials); + assertNull(credentials); + } + + @Test + public void testRequestNotAllowed() { + OnBehalfOfAuthenticator oboAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + + // Test POST on generate on-behalf-of token endpoint + SecurityRequest mockedRequest1 = mock(SecurityRequest.class); + when(mockedRequest1.header(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer someToken"); + when(mockedRequest1.path()).thenReturn(SECURITY_PREFIX + ON_BEHALF_OF_SUFFIX); + when(mockedRequest1.method()).thenReturn(POST); + assertFalse(oboAuth.isRequestAllowed(mockedRequest1)); + assertNull(oboAuth.extractCredentials(mockedRequest1, null)); + + // Test PUT on password changing endpoint + SecurityRequest mockedRequest2 = mock(SecurityRequest.class); + when(mockedRequest2.header(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer someToken"); + when(mockedRequest2.path()).thenReturn(SECURITY_PREFIX + ACCOUNT_SUFFIX); + when(mockedRequest2.method()).thenReturn(PUT); + assertFalse(oboAuth.isRequestAllowed(mockedRequest2)); + assertNull(oboAuth.extractCredentials(mockedRequest2, null)); } /** extracts a default user credential from a request header */ @@ -418,6 +681,10 @@ private Settings disableOBOSettings() { .build(); } + private Settings noSigningKeyOBOSettings() { + return Settings.builder().put("enabled", disableOBO).put("encryption_key", claimsEncryptionKey).build(); + } + private Settings nonSpecifyOBOSetting() { return Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); }