From adc7d1eb94896995ba679f3e647ccf8909ad1c8a Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 24 May 2023 12:02:30 -0700 Subject: [PATCH 01/19] Add OBO Authenticator into the Authc Backend list Signed-off-by: Ryan Liang --- .../security/auth/BackendRegistry.java | 2 -- .../http/HTTPOnBehalfOfJwtAuthenticator.java | 17 +++++++++++++++-- .../securityconf/DynamicConfigModelV7.java | 8 ++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index a36aced5e1..635811a7ae 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -224,8 +224,6 @@ public boolean authenticate(final RestRequest request, final RestChannel channel HTTPAuthenticator firstChallengingHttpAuthenticator = null; - //TODO: ADD OUR AUTHC BACKEND IN/BEFORE THIS LIST - //loop over all http/rest auth domains for (final AuthDomain authDomain: restAuthDomains) { if (isDebugEnabled) { diff --git a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java b/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java index 1fabd0874c..f00310fd4c 100644 --- a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java @@ -36,6 +36,7 @@ import org.opensearch.OpenSearchSecurityException; import org.opensearch.SpecialPermission; +import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; @@ -57,12 +58,19 @@ public class HTTPOnBehalfOfJwtAuthenticator implements HTTPAuthenticator { private String signingKey; private String encryptionKey; + private volatile boolean initialized; public HTTPOnBehalfOfJwtAuthenticator() { super(); init(); } + public HTTPOnBehalfOfJwtAuthenticator(Settings settings){ + this.signingKey = settings.get("signing_key"); + this.encryptionKey = settings.get("encryption_key"); + init(); + } + // FOR TESTING public HTTPOnBehalfOfJwtAuthenticator(String signingKey, String encryptionKey){ this.signingKey = signingKey; @@ -70,6 +78,10 @@ public HTTPOnBehalfOfJwtAuthenticator(String signingKey, String encryptionKey){ init(); } + public boolean isInitialized(){ + return initialized; + } + private void init() { try { @@ -265,8 +277,9 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { //TODO: #2615 FOR CONFIGURATION //For Testing - signingKey = "abcd1234"; - encryptionKey = RandomStringUtils.randomAlphanumeric(16); + signingKey = dcm.getDynamicOnBehalfOfSettings().get("signing_key"); + encryptionKey = dcm.getDynamicOnBehalfOfSettings().get("encryption_key"); + initialized = signingKey != null && encryptionKey != null; } } diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index c3e3792c5c..b89791f175 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -54,6 +54,8 @@ import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.auth.blocking.ClientBlockRegistry; import org.opensearch.security.auth.internal.InternalAuthenticationBackend; +import org.opensearch.security.auth.internal.NoOpAuthenticationBackend; +import org.opensearch.security.http.HTTPOnBehalfOfJwtAuthenticator; import org.opensearch.security.securityconf.impl.v7.ConfigV7; import org.opensearch.security.securityconf.impl.v7.ConfigV7.Authc; import org.opensearch.security.securityconf.impl.v7.ConfigV7.AuthcDomain; @@ -312,6 +314,12 @@ private void buildAAA() { } } + Settings oboSettings = getDynamicOnBehalfOfSettings(); + if (oboSettings.get("signing_key") != null && oboSettings.get("encryption_key") != null) { + final AuthDomain _ad = new AuthDomain(new NoOpAuthenticationBackend(Settings.EMPTY, null), new HTTPOnBehalfOfJwtAuthenticator(getDynamicOnBehalfOfSettings()), false, 0); + restAuthDomains0.add(_ad); + } + List originalDestroyableComponents = destroyableComponents; restAuthDomains = Collections.unmodifiableSortedSet(restAuthDomains0); From cc3ec27c4eef58acef411132be68c7c50f94a5f8 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Thu, 25 May 2023 16:15:54 -0700 Subject: [PATCH 02/19] Fix the logic of feching er/dr for rolesObject Signed-off-by: Ryan Liang --- .../http/HTTPOnBehalfOfJwtAuthenticator.java | 20 +++---------- .../security/authtoken/jwt/JwtVendorTest.java | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java b/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java index f00310fd4c..f936d22103 100644 --- a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java @@ -28,7 +28,7 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.WeakKeyException; -import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.hc.core5.http.HttpHeaders; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -176,21 +176,9 @@ private AuthCredentials extractCredentials0(final RestRequest request) { final String audience = claims.getAudience(); - //TODO: GET ROLESCLAIM DEPENDING ON THE STATUS OF BWC MODE. ON: er / OFF: dr - Object rolesObject = null; String[] roles; - try { - rolesObject = claims.get("er"); - } catch (Throwable e) { - log.debug("No encrypted role founded in the claim, continue searching for decrypted roles."); - } - - try { - rolesObject = claims.get("dr"); - } catch (Throwable e) { - log.debug("No decrypted role founded in the claim."); - } + Object rolesObject = ObjectUtils.firstNonNull(claims.get("er"), claims.get("dr")); if (rolesObject == null) { log.warn( @@ -202,7 +190,6 @@ private AuthCredentials extractCredentials0(final RestRequest request) { // Extracting roles based on the compatbility mode String decryptedRoles = rolesClaim; if (rolesObject == claims.get("er")) { - //TODO: WHERE TO GET THE ENCRYTION KEY decryptedRoles = EncryptionDecryptionUtil.decrypt(encryptionKey, rolesClaim); } roles = Arrays.stream(decryptedRoles.split(",")).map(String::trim).toArray(String[]::new); @@ -226,9 +213,11 @@ private AuthCredentials extractCredentials0(final RestRequest request) { return ac; } catch (WeakKeyException e) { + System.out.println("Error MSG1!" + e.getMessage()); log.error("Cannot authenticate user with JWT because of ", e); return null; } catch (Exception e) { + System.out.println("Error MSG2!" + e.getMessage()); if(log.isDebugEnabled()) { log.debug("Invalid or expired JWT token.", e); } @@ -275,7 +264,6 @@ private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) @Subscribe public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { - //TODO: #2615 FOR CONFIGURATION //For Testing signingKey = dcm.getDynamicOnBehalfOfSettings().get("signing_key"); encryptionKey = dcm.getDynamicOnBehalfOfSettings().get("encryption_key"); 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 3330477721..b70697d8c9 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -11,6 +11,8 @@ package org.opensearch.security.authtoken.jwt; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.List; import java.util.function.LongSupplier; @@ -23,6 +25,8 @@ import org.opensearch.common.settings.Settings; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class JwtVendorTest { @Test @@ -116,4 +120,30 @@ public void testCreateJwtWithBadRoles() throws Exception { jwtVendor.createJwt(issuer, subject, audience, expirySecond, roles); } + + //For Manual Testing + @Test + public void testCreateJwtWithRolesAndEncyptionKey() throws Exception { + String issuer = "cluster_0"; + String subject = "craig"; + String audience = "audience_0"; + List roles = List.of("admin", "HR"); + Integer expirySeconds = 10000; + LongSupplier currentTime = () -> (System.currentTimeMillis() / 1000); + String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + String signingKey = Base64.getEncoder().encodeToString("jwt signing key for an on behalf of token authentication backend for testing of extensions".getBytes(StandardCharsets.UTF_8)); + String encryptionKey = Base64.getEncoder().encodeToString("encryptionKey".getBytes(StandardCharsets.UTF_8)); + System.out.println("The encryptionkey is:" + encryptionKey); + Settings settings = Settings.builder().put("signing_key", signingKey).put("encryption_key", encryptionKey).build(); + + JwtVendor jwtVendor = new JwtVendor(settings, currentTime); + String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); + + JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedJwt); + JwtToken jwt = jwtConsumer.getJwtToken(); + + System.out.println("JWT: " + encodedJwt); + + assertTrue(true); + } } From 544b6437205e759493e9a94d8202f9ead9258ce0 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Fri, 26 May 2023 11:54:19 -0700 Subject: [PATCH 03/19] Fix of the OBO unit tests of Bearer header Signed-off-by: Ryan Liang --- .../http/HTTPOnBehalfOfJwtAuthenticator.java | 11 +--------- .../HTTPOnBehalfOfJwtAuthenticatorTest.java | 22 +++++++------------ 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java b/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java index f936d22103..11551a41a5 100644 --- a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java @@ -161,7 +161,7 @@ private AuthCredentials extractCredentials0(final RestRequest request) { } final int index; - if((index = jwtToken.toLowerCase().indexOf(BEARER_PREFIX)) > -1) { //detect Bearer + if(jwtToken != null && (index = jwtToken.toLowerCase().indexOf(BEARER_PREFIX)) > -1) { //detect Bearer jwtToken = jwtToken.substring(index+BEARER_PREFIX.length()); } else { if(log.isDebugEnabled()) { @@ -261,13 +261,4 @@ private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) return kf.generatePublic(spec); } - @Subscribe - public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { - - //For Testing - signingKey = dcm.getDynamicOnBehalfOfSettings().get("signing_key"); - encryptionKey = dcm.getDynamicOnBehalfOfSettings().get("encryption_key"); - initialized = signingKey != null && encryptionKey != null; - } - } diff --git a/src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTest.java b/src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTest.java index 5a6c5ec41c..eb6ec3ac01 100644 --- a/src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTest.java @@ -31,9 +31,6 @@ import org.junit.Assert; import org.junit.Test; -import com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator; - -import org.opensearch.common.settings.Settings; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.util.FakeRestRequest; @@ -88,7 +85,7 @@ public void testBadKey() throws Exception { @Test public void testTokenMissing() throws Exception { - HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(BaseEncoding.base64().encode(secretKeyBytes),claimsEncryptionKey); + HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(signingKey,claimsEncryptionKey); Map headers = new HashMap(); AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); @@ -101,7 +98,7 @@ public void testInvalid() throws Exception { String jwsToken = "123invalidtoken.."; - HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(BaseEncoding.base64().encode(secretKeyBytes), claimsEncryptionKey); + HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(signingKey, claimsEncryptionKey); Map headers = new HashMap(); headers.put("Authorization", "Bearer "+jwsToken); @@ -114,7 +111,7 @@ public void testBearer() throws Exception { String jwsToken = Jwts.builder().setSubject("Leonard McCoy").setAudience("ext_0").signWith(secretKey, SignatureAlgorithm.HS512).compact(); - HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(BaseEncoding.base64().encode(secretKeyBytes), claimsEncryptionKey); + HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(signingKey, claimsEncryptionKey); Map headers = new HashMap(); headers.put("Authorization", "Bearer "+jwsToken); @@ -129,11 +126,9 @@ public void testBearer() throws Exception { @Test public void testBearerWrongPosition() throws Exception { - Settings settings = Settings.builder().put("signing_key", BaseEncoding.base64().encode(secretKeyBytes)).build(); - - String jwsToken = Jwts.builder().setSubject("Leonard McCoy").signWith(secretKey, SignatureAlgorithm.HS512).compact(); + String jwsToken = Jwts.builder().setSubject("Leonard McCoy").setAudience("ext_0").signWith(secretKey, SignatureAlgorithm.HS512).compact(); + HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(signingKey, claimsEncryptionKey); - HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); Map headers = new HashMap(); headers.put("Authorization", jwsToken + "Bearer " + " 123"); @@ -145,11 +140,10 @@ public void testBearerWrongPosition() throws Exception { @Test public void testBasicAuthHeader() throws Exception { - Settings settings = Settings.builder().put("signing_key", BaseEncoding.base64().encode(secretKeyBytes)).build(); - HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); + String jwsToken = Jwts.builder().setSubject("Leonard McCoy").setAudience("ext_0").signWith(secretKey, SignatureAlgorithm.HS512).compact(); + HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(signingKey, claimsEncryptionKey); - String basicAuth = BaseEncoding.base64().encode("user:password".getBytes(StandardCharsets.UTF_8)); - Map headers = Collections.singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + basicAuth); + Map headers = Collections.singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + jwsToken); AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, Collections.emptyMap()), null); Assert.assertNull(credentials); From 4b413bef718d7a932090bfb118cb23e6050fb144 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Fri, 2 Jun 2023 11:35:52 -0700 Subject: [PATCH 04/19] Set up oboconfig Signed-off-by: Ryan Liang --- .../http/OnBehalfOfJwtAuthenticationTest.java | 239 ++++++++++++++++++ .../test/framework/OnBehalfOfConfig.java | 44 ++++ .../test/framework/TestSecurityConfig.java | 11 + .../test/framework/cluster/LocalCluster.java | 6 + 4 files changed, 300 insertions(+) create mode 100644 src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java create mode 100644 src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java new file mode 100644 index 0000000000..9e78274c34 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -0,0 +1,239 @@ +package org.opensearch.security.http; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.message.BasicHeader; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.client.Client; +import org.opensearch.client.RestHighLevelClient; +import org.opensearch.test.framework.JwtConfigBuilder; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.log.LogsRule; + +import java.io.IOException; +import java.security.KeyPair; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +import static java.nio.charset.StandardCharsets.US_ASCII; +import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; +import static org.opensearch.client.RequestOptions.DEFAULT; +import static org.opensearch.rest.RestStatus.FORBIDDEN; +import static org.opensearch.security.Song.*; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.BASIC_AUTH_DOMAIN_ORDER; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; +import static org.opensearch.test.framework.cluster.SearchRequestFactory.queryStringQueryRequest; +import static org.opensearch.test.framework.matcher.ExceptionMatcherAssert.assertThatThrownBy; +import static org.opensearch.test.framework.matcher.OpenSearchExceptionMatchers.statusException; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.*; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.searchHitContainsFieldWithValue; + + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class OnBehalfOfJwtAuthenticationTest { + + public static final String CLAIM_USERNAME = "test-user"; + public static final String CLAIM_ROLES = "backend-user-roles"; + + public static final String USER_SUPERHERO = "superhero"; + public static final String USERNAME_ROOT = "root"; + public static final String ROLE_ADMIN = "role_admin"; + public static final String ROLE_DEVELOPER = "role_developer"; + public static final String ROLE_QA = "role_qa"; + public static final String ROLE_CTO = "role_cto"; + public static final String ROLE_CEO = "role_ceo"; + public static final String ROLE_VP = "role_vp"; + public static final String POINTER_BACKEND_ROLES = "/backend_roles"; + public static final String POINTER_USERNAME = "/user_name"; + + public static final String QA_DEPARTMENT = "qa-department"; + + public static final String CLAIM_DEPARTMENT = "department"; + + public static final String DEPARTMENT_SONG_INDEX_PATTERN = String.format("song_lyrics_${attr.jwt.%s}", CLAIM_DEPARTMENT); + + public static final String QA_SONG_INDEX_NAME = String.format("song_lyrics_%s", QA_DEPARTMENT); + + private static final KeyPair KEY_PAIR = Keys.keyPairFor(SignatureAlgorithm.RS256); + private static final String PUBLIC_KEY = new String(Base64.getEncoder().encode(KEY_PAIR.getPublic().getEncoded()), US_ASCII); + + static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + private static final String JWT_AUTH_HEADER = "jwt-auth"; + + private static final JwtAuthorizationHeaderFactory tokenFactory = new JwtAuthorizationHeaderFactory( + KEY_PAIR.getPrivate(), + CLAIM_USERNAME, + CLAIM_ROLES, + JWT_AUTH_HEADER); + + public static final String SONG_ID_1 = "song-id-01"; + + public static final TestSecurityConfig.Role DEPARTMENT_SONG_LISTENER_ROLE = new TestSecurityConfig.Role("department-song-listener-role") + .indexPermissions("indices:data/read/search").on(DEPARTMENT_SONG_INDEX_PATTERN); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder() + .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) + .nodeSettings(Map.of("plugins.security.restapi.roles_enabled", List.of("user_" + ADMIN_USER.getName() +"__" + ALL_ACCESS.getName()))) + .authc(AUTHC_HTTPBASIC_INTERNAL).users(ADMIN_USER).roles(DEPARTMENT_SONG_LISTENER_ROLE).config() + .build(); + + @Rule + public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator"); + + @BeforeClass + public static void createTestData() { + try (Client client = cluster.getInternalNodeClient()) { + client.prepareIndex(QA_SONG_INDEX_NAME).setId(SONG_ID_1).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0].asMap()).get(); + } + try(TestRestClient client = cluster.getRestClient(ADMIN_USER)){ + client.createRoleMapping(ROLE_VP, DEPARTMENT_SONG_LISTENER_ROLE.getName()); + } + } + + @Test + public void shouldAuthenticateWithJwtToken_positive() { + try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USER_SUPERHERO))){ + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(username)); + } + } + + @Test + public void shouldAuthenticateWithJwtToken_positiveWithAnotherUsername() { + try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USERNAME_ROOT))){ + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USERNAME_ROOT)); + } + } + + @Test + public void shouldAuthenticateWithJwtToken_failureLackingUserName() { + try(TestRestClient client = cluster.getRestClient(tokenFactory.generateTokenWithoutPreferredUsername(USER_SUPERHERO))){ + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + logsRule.assertThatContainExactly("No subject found in JWT token"); + } + } + + @Test + public void shouldAuthenticateWithJwtToken_failureExpiredToken() { + try(TestRestClient client = cluster.getRestClient(tokenFactory.generateExpiredToken(USER_SUPERHERO))){ + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + logsRule.assertThatContainExactly("Invalid or expired JWT token."); + } + } + + @Test + public void shouldAuthenticateWithJwtToken_failureIncorrectFormatOfToken() { + Header header = new BasicHeader(AUTHORIZATION, "not.a.token"); + try(TestRestClient client = cluster.getRestClient(header)){ + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + logsRule.assertThatContainExactly(String.format("No JWT token found in '%s' header header", JWT_AUTH_HEADER)); + } + } + + @Test + public void shouldAuthenticateWithJwtToken_failureIncorrectSignature() { + KeyPair incorrectKeyPair = Keys.keyPairFor(SignatureAlgorithm.RS256); + Header header = tokenFactory.generateTokenSignedWithKey(incorrectKeyPair.getPrivate(), USER_SUPERHERO); + try(TestRestClient client = cluster.getRestClient(header)){ + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + logsRule.assertThatContainExactly("Invalid or expired JWT token."); + } + } + + @Test + public void shouldReadRolesFromToken_positiveFirstRoleSet() { + Header header = tokenFactory.generateValidToken(USER_SUPERHERO, ROLE_ADMIN, ROLE_DEVELOPER, ROLE_QA); + try(TestRestClient client = cluster.getRestClient(header)){ + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(roles, hasSize(3)); + assertThat(roles, containsInAnyOrder(ROLE_ADMIN, ROLE_DEVELOPER, ROLE_QA)); + } + } + + @Test + public void shouldReadRolesFromToken_positiveSecondRoleSet() { + Header header = tokenFactory.generateValidToken(USER_SUPERHERO, ROLE_CTO, ROLE_CEO, ROLE_VP); + try(TestRestClient client = cluster.getRestClient(header)){ + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(roles, hasSize(3)); + assertThat(roles, containsInAnyOrder(ROLE_CTO, ROLE_CEO, ROLE_VP)); + } + } + + @Test + public void shouldExposeTokenClaimsAsUserAttributes_positive() throws IOException { + String[] roles = { ROLE_VP }; + Map additionalClaims = Map.of(CLAIM_DEPARTMENT, QA_DEPARTMENT); + Header header = tokenFactory.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); + try(RestHighLevelClient client = cluster.getRestHighLevelClient(List.of(header))){ + SearchRequest searchRequest = queryStringQueryRequest(QA_SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS); + + SearchResponse response = client.search(searchRequest, DEFAULT); + + assertThat(response, isSuccessfulSearchResponse()); + assertThat(response, numberOfTotalHitsIsEqualTo(1)); + assertThat(response, searchHitsContainDocumentWithId(0, QA_SONG_INDEX_NAME, SONG_ID_1)); + assertThat(response, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + } + } + + @Test + public void shouldExposeTokenClaimsAsUserAttributes_negative() throws IOException { + String[] roles = { ROLE_VP }; + Map additionalClaims = Map.of(CLAIM_DEPARTMENT, "department-without-access-to-qa-song-index"); + Header header = tokenFactory.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); + try(RestHighLevelClient client = cluster.getRestHighLevelClient(List.of(header))){ + SearchRequest searchRequest = queryStringQueryRequest(QA_SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS); + + assertThatThrownBy(() -> client.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java b/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java new file mode 100644 index 0000000000..d68c0d73d2 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java @@ -0,0 +1,44 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.test.framework; + +import java.io.IOException; + +import org.apache.commons.lang3.StringUtils; + +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +public class OnBehalfOfConfig implements ToXContentObject { + private String signing_key; + private String encryption_key; + + public OnBehalfOfConfig signing_key(String signing_key) { + this.signing_key = signing_key; + return this; + } + + public OnBehalfOfConfig encryption_key(String encryption_key) { + this.encryption_key = encryption_key; + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, ToXContent.Params params) throws IOException { + xContentBuilder.startObject(); + xContentBuilder.field("signing_key", signing_key); + if (StringUtils.isNoneBlank(encryption_key)){ + xContentBuilder.field("encryption_key", encryption_key); + } + xContentBuilder.endObject(); + return xContentBuilder; + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 43b98b02ce..b47fbed32d 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -114,6 +114,11 @@ public TestSecurityConfig xff(XffConfig xffConfig) { config.xffConfig(xffConfig); return this; } + + public TestSecurityConfig onBehalfOf(OnBehalfOfConfig onBehalfOfConfig){ + config.onBehalfOfConfig(onBehalfOfConfig); + return this; + } public TestSecurityConfig authc(AuthcDomain authcDomain) { config.authc(authcDomain); @@ -170,6 +175,7 @@ public static class Config implements ToXContentObject { private Boolean doNotFailOnForbidden; private XffConfig xffConfig; + private OnBehalfOfConfig onBehalfOfConfig; private Map authcDomainMap = new LinkedHashMap<>(); private AuthFailureListeners authFailureListeners; @@ -190,6 +196,11 @@ public Config xffConfig(XffConfig xffConfig) { return this; } + public Config onBehalfOfConfig(OnBehalfOfConfig onBehalfOfConfig) { + this.onBehalfOfConfig = onBehalfOfConfig; + return this; + } + public Config authc(AuthcDomain authcDomain) { authcDomainMap.put(authcDomain.id, authcDomain); return this; diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java index e9bb7b5be5..13465294e5 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -55,6 +55,7 @@ import org.opensearch.test.framework.AuditConfiguration; import org.opensearch.test.framework.AuthFailureListeners; import org.opensearch.test.framework.AuthzDomain; +import org.opensearch.test.framework.OnBehalfOfConfig; import org.opensearch.test.framework.RolesMapping; import org.opensearch.test.framework.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; @@ -443,6 +444,11 @@ public Builder xff(XffConfig xffConfig){ return this; } + public Builder onBehalfOf(OnBehalfOfConfig onBehalfOfConfig){ + testSecurityConfig.onBehalfOf(onBehalfOfConfig); + return this; + } + public Builder loadConfigurationIntoIndex(boolean loadConfigurationIntoIndex) { this.loadConfigurationIntoIndex = loadConfigurationIntoIndex; return this; From a4d67e6e044f5721548ca2d3b99a08a29331dea6 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 7 Jun 2023 13:24:13 -0700 Subject: [PATCH 05/19] Adding this obo config to xcontent builder and remove unused imports Signed-off-by: Ryan Liang --- .../org/opensearch/test/framework/TestSecurityConfig.java | 4 ++++ .../security/http/HTTPOnBehalfOfJwtAuthenticator.java | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index b47fbed32d..9ab1e70e8c 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -221,6 +221,10 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params xContentBuilder.startObject(); xContentBuilder.startObject("dynamic"); + if (onBehalfOfConfig != null) { + xContentBuilder.field("on_behalf_of", onBehalfOfConfig); + } + if (anonymousAuth || (xffConfig != null)) { xContentBuilder.startObject("http"); xContentBuilder.field("anonymous_auth_enabled", anonymousAuth); diff --git a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java b/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java index 11551a41a5..55012f45ae 100644 --- a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java @@ -32,7 +32,6 @@ import org.apache.hc.core5.http.HttpHeaders; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.greenrobot.eventbus.Subscribe; import org.opensearch.OpenSearchSecurityException; import org.opensearch.SpecialPermission; @@ -42,7 +41,6 @@ import org.opensearch.rest.RestRequest; import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; -import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.user.AuthCredentials; public class HTTPOnBehalfOfJwtAuthenticator implements HTTPAuthenticator { From 73d37978b3b791a103c79a5a511b748967fd697a Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Thu, 8 Jun 2023 10:15:13 -0700 Subject: [PATCH 06/19] Adding this obo config to xcontent builder and create obo authz header factory Signed-off-by: Ryan Liang --- .../http/OnBehalfOfJwtAuthenticationTest.java | 241 +++++------------- ...BehalfOfJwtAuthorizationHeaderFactory.java | 58 +++++ .../test/framework/OnBehalfOfConfig.java | 40 +-- 3 files changed, 141 insertions(+), 198 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index 9e78274c34..d9702b2ec8 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -1,5 +1,11 @@ package org.opensearch.security.http; +import java.io.IOException; +import java.security.KeyPair; +import java.util.Base64; +import java.util.List; +import java.util.Map; + import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; @@ -10,6 +16,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; + import org.opensearch.action.search.SearchRequest; import org.opensearch.action.search.SearchResponse; import org.opensearch.client.Client; @@ -21,12 +28,6 @@ import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.log.LogsRule; -import java.io.IOException; -import java.security.KeyPair; -import java.util.Base64; -import java.util.List; -import java.util.Map; - import static java.nio.charset.StandardCharsets.US_ASCII; import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.hamcrest.MatcherAssert.assertThat; @@ -49,191 +50,75 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class OnBehalfOfJwtAuthenticationTest { - public static final String CLAIM_USERNAME = "test-user"; - public static final String CLAIM_ROLES = "backend-user-roles"; + public static final String CLAIM_USERNAME = "test-user"; + public static final String CLAIM_ROLES = "backend-user-roles"; + + public static final String USER_SUPERHERO = "superhero"; + public static final String USERNAME_ROOT = "root"; + public static final String ROLE_ADMIN = "role_admin"; + public static final String ROLE_DEVELOPER = "role_developer"; + public static final String ROLE_QA = "role_qa"; + public static final String ROLE_CTO = "role_cto"; + public static final String ROLE_CEO = "role_ceo"; + public static final String ROLE_VP = "role_vp"; + public static final String POINTER_BACKEND_ROLES = "/backend_roles"; + public static final String POINTER_USERNAME = "/user_name"; + + public static final String QA_DEPARTMENT = "qa-department"; - public static final String USER_SUPERHERO = "superhero"; - public static final String USERNAME_ROOT = "root"; - public static final String ROLE_ADMIN = "role_admin"; - public static final String ROLE_DEVELOPER = "role_developer"; - public static final String ROLE_QA = "role_qa"; - public static final String ROLE_CTO = "role_cto"; - public static final String ROLE_CEO = "role_ceo"; - public static final String ROLE_VP = "role_vp"; - public static final String POINTER_BACKEND_ROLES = "/backend_roles"; - public static final String POINTER_USERNAME = "/user_name"; + public static final String CLAIM_DEPARTMENT = "department"; - public static final String QA_DEPARTMENT = "qa-department"; + public static final String DEPARTMENT_SONG_INDEX_PATTERN = String.format("song_lyrics_${attr.jwt.%s}", CLAIM_DEPARTMENT); - public static final String CLAIM_DEPARTMENT = "department"; + public static final String QA_SONG_INDEX_NAME = String.format("song_lyrics_%s", QA_DEPARTMENT); - public static final String DEPARTMENT_SONG_INDEX_PATTERN = String.format("song_lyrics_${attr.jwt.%s}", CLAIM_DEPARTMENT); + private static final KeyPair KEY_PAIR = Keys.keyPairFor(SignatureAlgorithm.RS256); + private static final String PUBLIC_KEY = new String(Base64.getEncoder().encode(KEY_PAIR.getPublic().getEncoded()), US_ASCII); - public static final String QA_SONG_INDEX_NAME = String.format("song_lyrics_%s", QA_DEPARTMENT); + static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); - private static final KeyPair KEY_PAIR = Keys.keyPairFor(SignatureAlgorithm.RS256); - private static final String PUBLIC_KEY = new String(Base64.getEncoder().encode(KEY_PAIR.getPublic().getEncoded()), US_ASCII); + private static final String JWT_AUTH_HEADER = "jwt-auth"; - static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + private static final JwtAuthorizationHeaderFactory tokenFactory = new JwtAuthorizationHeaderFactory( + KEY_PAIR.getPrivate(), + CLAIM_USERNAME, + CLAIM_ROLES, + JWT_AUTH_HEADER); - private static final String JWT_AUTH_HEADER = "jwt-auth"; + public static final String SONG_ID_1 = "song-id-01"; - private static final JwtAuthorizationHeaderFactory tokenFactory = new JwtAuthorizationHeaderFactory( - KEY_PAIR.getPrivate(), - CLAIM_USERNAME, - CLAIM_ROLES, - JWT_AUTH_HEADER); + public static final TestSecurityConfig.Role DEPARTMENT_SONG_LISTENER_ROLE = new TestSecurityConfig.Role("department-song-listener-role") + .indexPermissions("indices:data/read/search").on(DEPARTMENT_SONG_INDEX_PATTERN); - public static final String SONG_ID_1 = "song-id-01"; + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder() + .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) + .nodeSettings(Map.of("plugins.security.restapi.roles_enabled", List.of("user_" + ADMIN_USER.getName() +"__" + ALL_ACCESS.getName()))) + .authc(AUTHC_HTTPBASIC_INTERNAL).users(ADMIN_USER).roles(DEPARTMENT_SONG_LISTENER_ROLE).config() + .build(); - public static final TestSecurityConfig.Role DEPARTMENT_SONG_LISTENER_ROLE = new TestSecurityConfig.Role("department-song-listener-role") - .indexPermissions("indices:data/read/search").on(DEPARTMENT_SONG_INDEX_PATTERN); + @Rule + public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator"); - @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) - .nodeSettings(Map.of("plugins.security.restapi.roles_enabled", List.of("user_" + ADMIN_USER.getName() +"__" + ALL_ACCESS.getName()))) - .authc(AUTHC_HTTPBASIC_INTERNAL).users(ADMIN_USER).roles(DEPARTMENT_SONG_LISTENER_ROLE).config() - .build(); + @BeforeClass + public static void createTestData() { + try (Client client = cluster.getInternalNodeClient()) { + client.prepareIndex(QA_SONG_INDEX_NAME).setId(SONG_ID_1).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0].asMap()).get(); + } + try(TestRestClient client = cluster.getRestClient(ADMIN_USER)){ + client.createRoleMapping(ROLE_VP, DEPARTMENT_SONG_LISTENER_ROLE.getName()); + } + } - @Rule - public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator"); + @Test + public void shouldAuthenticateWithJwtToken_positive() { + try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USER_SUPERHERO))){ - @BeforeClass - public static void createTestData() { - try (Client client = cluster.getInternalNodeClient()) { - client.prepareIndex(QA_SONG_INDEX_NAME).setId(SONG_ID_1).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0].asMap()).get(); - } - try(TestRestClient client = cluster.getRestClient(ADMIN_USER)){ - client.createRoleMapping(ROLE_VP, DEPARTMENT_SONG_LISTENER_ROLE.getName()); - } - } + TestRestClient.HttpResponse response = client.getAuthInfo(); - @Test - public void shouldAuthenticateWithJwtToken_positive() { - try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USER_SUPERHERO))){ - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - String username = response.getTextFromJsonBody(POINTER_USERNAME); - assertThat(username, equalTo(username)); - } - } - - @Test - public void shouldAuthenticateWithJwtToken_positiveWithAnotherUsername() { - try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USERNAME_ROOT))){ - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - String username = response.getTextFromJsonBody(POINTER_USERNAME); - assertThat(username, equalTo(USERNAME_ROOT)); - } - } - - @Test - public void shouldAuthenticateWithJwtToken_failureLackingUserName() { - try(TestRestClient client = cluster.getRestClient(tokenFactory.generateTokenWithoutPreferredUsername(USER_SUPERHERO))){ - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - logsRule.assertThatContainExactly("No subject found in JWT token"); - } - } - - @Test - public void shouldAuthenticateWithJwtToken_failureExpiredToken() { - try(TestRestClient client = cluster.getRestClient(tokenFactory.generateExpiredToken(USER_SUPERHERO))){ - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - logsRule.assertThatContainExactly("Invalid or expired JWT token."); - } - } - - @Test - public void shouldAuthenticateWithJwtToken_failureIncorrectFormatOfToken() { - Header header = new BasicHeader(AUTHORIZATION, "not.a.token"); - try(TestRestClient client = cluster.getRestClient(header)){ - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - logsRule.assertThatContainExactly(String.format("No JWT token found in '%s' header header", JWT_AUTH_HEADER)); - } - } - - @Test - public void shouldAuthenticateWithJwtToken_failureIncorrectSignature() { - KeyPair incorrectKeyPair = Keys.keyPairFor(SignatureAlgorithm.RS256); - Header header = tokenFactory.generateTokenSignedWithKey(incorrectKeyPair.getPrivate(), USER_SUPERHERO); - try(TestRestClient client = cluster.getRestClient(header)){ - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - logsRule.assertThatContainExactly("Invalid or expired JWT token."); - } - } - - @Test - public void shouldReadRolesFromToken_positiveFirstRoleSet() { - Header header = tokenFactory.generateValidToken(USER_SUPERHERO, ROLE_ADMIN, ROLE_DEVELOPER, ROLE_QA); - try(TestRestClient client = cluster.getRestClient(header)){ - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); - assertThat(roles, hasSize(3)); - assertThat(roles, containsInAnyOrder(ROLE_ADMIN, ROLE_DEVELOPER, ROLE_QA)); - } - } - - @Test - public void shouldReadRolesFromToken_positiveSecondRoleSet() { - Header header = tokenFactory.generateValidToken(USER_SUPERHERO, ROLE_CTO, ROLE_CEO, ROLE_VP); - try(TestRestClient client = cluster.getRestClient(header)){ - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); - assertThat(roles, hasSize(3)); - assertThat(roles, containsInAnyOrder(ROLE_CTO, ROLE_CEO, ROLE_VP)); - } - } - - @Test - public void shouldExposeTokenClaimsAsUserAttributes_positive() throws IOException { - String[] roles = { ROLE_VP }; - Map additionalClaims = Map.of(CLAIM_DEPARTMENT, QA_DEPARTMENT); - Header header = tokenFactory.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); - try(RestHighLevelClient client = cluster.getRestHighLevelClient(List.of(header))){ - SearchRequest searchRequest = queryStringQueryRequest(QA_SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS); - - SearchResponse response = client.search(searchRequest, DEFAULT); - - assertThat(response, isSuccessfulSearchResponse()); - assertThat(response, numberOfTotalHitsIsEqualTo(1)); - assertThat(response, searchHitsContainDocumentWithId(0, QA_SONG_INDEX_NAME, SONG_ID_1)); - assertThat(response, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); - } - } - - @Test - public void shouldExposeTokenClaimsAsUserAttributes_negative() throws IOException { - String[] roles = { ROLE_VP }; - Map additionalClaims = Map.of(CLAIM_DEPARTMENT, "department-without-access-to-qa-song-index"); - Header header = tokenFactory.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); - try(RestHighLevelClient client = cluster.getRestHighLevelClient(List.of(header))){ - SearchRequest searchRequest = queryStringQueryRequest(QA_SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS); - - assertThatThrownBy(() -> client.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - } + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(username)); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java new file mode 100644 index 0000000000..7516cf4a5e --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java @@ -0,0 +1,58 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security.http; + +import java.util.List; +import java.util.function.LongSupplier; + +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.message.BasicHeader; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.authtoken.jwt.JwtVendor; + +import static java.util.Objects.requireNonNull; + +class OnBehalfOfJwtAuthorizationHeaderFactory { + + private final String issuer; + private final String subject; + private final String audience; + private final List roles; + private final String encryption_key; + private final String signing_key; + private final String headerName; + private final Integer expirySeconds; + + + public OnBehalfOfJwtAuthorizationHeaderFactory(String signing_key, String issuer, String subject, String audience, List roles ,Integer expirySeconds, String headerName, String encryption_key) { + this.signing_key = requireNonNull(signing_key, "signing key is required"); + this.issuer = requireNonNull(issuer, "Issuer is required"); + this.subject = requireNonNull(subject, "Subject is required"); + this.audience = requireNonNull(audience, "Audience is required."); + this.roles = requireNonNull(roles, "Roles claim is required"); + this.expirySeconds = requireNonNull(expirySeconds, "Expiry is required"); + this.headerName = requireNonNull(headerName, "Header name is required"); + this.encryption_key = encryption_key; + } + + Header generateValidToken() throws Exception { + LongSupplier currentTime = () -> (System.currentTimeMillis() / 1000); + Settings settings = Settings.builder().put("signing_key", signing_key).put("encryption_key", encryption_key).build(); + JwtVendor jwtVendor = new JwtVendor(settings, currentTime); + String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); + + return toHeader(encodedJwt); + } + + private BasicHeader toHeader(String token) { + return new BasicHeader(headerName, token); + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java b/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java index d68c0d73d2..cf9be42114 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java @@ -18,27 +18,27 @@ import org.opensearch.core.xcontent.XContentBuilder; public class OnBehalfOfConfig implements ToXContentObject { - private String signing_key; - private String encryption_key; + private String signing_key; + private String encryption_key; - public OnBehalfOfConfig signing_key(String signing_key) { - this.signing_key = signing_key; - return this; - } + public OnBehalfOfConfig signing_key(String signing_key) { + this.signing_key = signing_key; + return this; + } - public OnBehalfOfConfig encryption_key(String encryption_key) { - this.encryption_key = encryption_key; - return this; - } + public OnBehalfOfConfig encryption_key(String encryption_key) { + this.encryption_key = encryption_key; + return this; + } - @Override - public XContentBuilder toXContent(XContentBuilder xContentBuilder, ToXContent.Params params) throws IOException { - xContentBuilder.startObject(); - xContentBuilder.field("signing_key", signing_key); - if (StringUtils.isNoneBlank(encryption_key)){ - xContentBuilder.field("encryption_key", encryption_key); - } - xContentBuilder.endObject(); - return xContentBuilder; - } + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, ToXContent.Params params) throws IOException { + xContentBuilder.startObject(); + xContentBuilder.field("signing_key", signing_key); + if (StringUtils.isNoneBlank(encryption_key)){ + xContentBuilder.field("encryption_key", encryption_key); + } + xContentBuilder.endObject(); + return xContentBuilder; + } } From bbf2c8a924b133cedd0bb723aa464bb42f707b56 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Thu, 8 Jun 2023 11:56:12 -0700 Subject: [PATCH 07/19] Done happy testing case Signed-off-by: Ryan Liang --- .../http/OnBehalfOfJwtAuthenticationTest.java | 43 ++++++++++++------- ...BehalfOfJwtAuthorizationHeaderFactory.java | 2 +- .../securityconf/DynamicConfigModelV7.java | 2 +- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index d9702b2ec8..a63230c5f2 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -1,6 +1,7 @@ package org.opensearch.security.http; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.security.KeyPair; import java.util.Base64; import java.util.List; @@ -22,6 +23,7 @@ import org.opensearch.client.Client; import org.opensearch.client.RestHighLevelClient; import org.opensearch.test.framework.JwtConfigBuilder; +import org.opensearch.test.framework.OnBehalfOfConfig; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; @@ -54,14 +56,7 @@ public class OnBehalfOfJwtAuthenticationTest { public static final String CLAIM_ROLES = "backend-user-roles"; public static final String USER_SUPERHERO = "superhero"; - public static final String USERNAME_ROOT = "root"; - public static final String ROLE_ADMIN = "role_admin"; - public static final String ROLE_DEVELOPER = "role_developer"; - public static final String ROLE_QA = "role_qa"; - public static final String ROLE_CTO = "role_cto"; - public static final String ROLE_CEO = "role_ceo"; public static final String ROLE_VP = "role_vp"; - public static final String POINTER_BACKEND_ROLES = "/backend_roles"; public static final String POINTER_USERNAME = "/user_name"; public static final String QA_DEPARTMENT = "qa-department"; @@ -73,17 +68,31 @@ public class OnBehalfOfJwtAuthenticationTest { public static final String QA_SONG_INDEX_NAME = String.format("song_lyrics_%s", QA_DEPARTMENT); private static final KeyPair KEY_PAIR = Keys.keyPairFor(SignatureAlgorithm.RS256); - private static final String PUBLIC_KEY = new String(Base64.getEncoder().encode(KEY_PAIR.getPublic().getEncoded()), US_ASCII); static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); private static final String JWT_AUTH_HEADER = "jwt-auth"; - private static final JwtAuthorizationHeaderFactory tokenFactory = new JwtAuthorizationHeaderFactory( - KEY_PAIR.getPrivate(), - CLAIM_USERNAME, - CLAIM_ROLES, - JWT_AUTH_HEADER); + public static final String issuer = "cluster_0"; + public static final String subject = "testUser"; + public static final String audience = "audience_0"; + public static final Integer expirySeconds = 100000; + public static final String headerName = "Bearer"; + public static final List roles = List.of("admin", "HR"); + + private static final String signingKey = Base64.getEncoder().encodeToString("jwt signing key for an on behalf of token authentication backend for testing of extensions".getBytes(StandardCharsets.UTF_8)); + private static final String encryptionKey = Base64.getEncoder().encodeToString("encryptionKey".getBytes(StandardCharsets.UTF_8)); + + private static final OnBehalfOfJwtAuthorizationHeaderFactory tokenFactory = new OnBehalfOfJwtAuthorizationHeaderFactory( + signingKey, + issuer, + subject, + audience, + roles, + expirySeconds, + headerName, + encryptionKey + ); public static final String SONG_ID_1 = "song-id-01"; @@ -94,7 +103,7 @@ public class OnBehalfOfJwtAuthenticationTest { public static final LocalCluster cluster = new LocalCluster.Builder() .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) .nodeSettings(Map.of("plugins.security.restapi.roles_enabled", List.of("user_" + ADMIN_USER.getName() +"__" + ALL_ACCESS.getName()))) - .authc(AUTHC_HTTPBASIC_INTERNAL).users(ADMIN_USER).roles(DEPARTMENT_SONG_LISTENER_ROLE).config() + .authc(AUTHC_HTTPBASIC_INTERNAL).onBehalfOf(new OnBehalfOfConfig().signing_key(signingKey).encryption_key(encryptionKey)) .build(); @Rule @@ -112,13 +121,15 @@ public static void createTestData() { @Test public void shouldAuthenticateWithJwtToken_positive() { - try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USER_SUPERHERO))){ + try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken())){ TestRestClient.HttpResponse response = client.getAuthInfo(); response.assertStatusCode(200); String username = response.getTextFromJsonBody(POINTER_USERNAME); - assertThat(username, equalTo(username)); + assertThat("testUser", equalTo(username)); + } catch (Exception e) { + throw new RuntimeException(e); } } } diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java index 7516cf4a5e..3d5fcf91aa 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java @@ -33,7 +33,7 @@ class OnBehalfOfJwtAuthorizationHeaderFactory { public OnBehalfOfJwtAuthorizationHeaderFactory(String signing_key, String issuer, String subject, String audience, List roles ,Integer expirySeconds, String headerName, String encryption_key) { - this.signing_key = requireNonNull(signing_key, "signing key is required"); + this.signing_key = requireNonNull(signing_key, "Signing key is required"); this.issuer = requireNonNull(issuer, "Issuer is required"); this.subject = requireNonNull(subject, "Subject is required"); this.audience = requireNonNull(audience, "Audience is required."); diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index b89791f175..d4abc5dfbe 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -316,7 +316,7 @@ private void buildAAA() { Settings oboSettings = getDynamicOnBehalfOfSettings(); if (oboSettings.get("signing_key") != null && oboSettings.get("encryption_key") != null) { - final AuthDomain _ad = new AuthDomain(new NoOpAuthenticationBackend(Settings.EMPTY, null), new HTTPOnBehalfOfJwtAuthenticator(getDynamicOnBehalfOfSettings()), false, 0); + final AuthDomain _ad = new AuthDomain(new NoOpAuthenticationBackend(Settings.EMPTY, null), new HTTPOnBehalfOfJwtAuthenticator(getDynamicOnBehalfOfSettings()), false, -1); restAuthDomains0.add(_ad); } From 114de00a047be8c9f72c5f21ff81d71979ae8afd Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Thu, 8 Jun 2023 13:32:43 -0700 Subject: [PATCH 08/19] Fix the header Signed-off-by: Ryan Liang --- build.gradle | 3 +- .../http/OnBehalfOfJwtAuthenticationTest.java | 66 +------------------ ...BehalfOfJwtAuthorizationHeaderFactory.java | 2 +- .../security/OpenSearchSecurityPlugin.java | 3 - .../http/HTTPOnBehalfOfJwtAuthenticator.java | 1 + 5 files changed, 7 insertions(+), 68 deletions(-) diff --git a/build.gradle b/build.gradle index ef187e290f..ef813052c9 100644 --- a/build.gradle +++ b/build.gradle @@ -301,6 +301,7 @@ configurations { force "io.netty:netty-transport:${versions.netty}" force "io.netty:netty-transport-native-unix-common:${versions.netty}" force "org.apache.bcel:bcel:6.6.0" // This line should be removed once Spotbugs is upgraded to 4.7.4 + force "com.github.luben:zstd-jni:${versions.zstd}" } } @@ -424,7 +425,7 @@ dependencies { runtimeOnly 'com.fasterxml.woodstox:woodstox-core:6.4.0' runtimeOnly 'org.apache.ws.xmlschema:xmlschema-core:2.2.5' runtimeOnly 'org.apache.santuario:xmlsec:2.2.3' - runtimeOnly 'com.github.luben:zstd-jni:1.5.2-1' + runtimeOnly "com.github.luben:zstd-jni:${versions.zstd}" runtimeOnly 'org.checkerframework:checker-qual:3.5.0' runtimeOnly "org.bouncycastle:bcpkix-jdk15on:${versions.bouncycastle}" diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index a63230c5f2..dcc6323f97 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -1,83 +1,40 @@ package org.opensearch.security.http; -import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.security.KeyPair; import java.util.Base64; import java.util.List; import java.util.Map; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.security.Keys; -import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.message.BasicHeader; -import org.junit.BeforeClass; import org.junit.ClassRule; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchResponse; -import org.opensearch.client.Client; -import org.opensearch.client.RestHighLevelClient; -import org.opensearch.test.framework.JwtConfigBuilder; import org.opensearch.test.framework.OnBehalfOfConfig; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; -import org.opensearch.test.framework.log.LogsRule; -import static java.nio.charset.StandardCharsets.US_ASCII; -import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; -import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; -import static org.opensearch.client.RequestOptions.DEFAULT; -import static org.opensearch.rest.RestStatus.FORBIDDEN; -import static org.opensearch.security.Song.*; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; -import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.BASIC_AUTH_DOMAIN_ORDER; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; -import static org.opensearch.test.framework.cluster.SearchRequestFactory.queryStringQueryRequest; -import static org.opensearch.test.framework.matcher.ExceptionMatcherAssert.assertThatThrownBy; -import static org.opensearch.test.framework.matcher.OpenSearchExceptionMatchers.statusException; -import static org.opensearch.test.framework.matcher.SearchResponseMatchers.*; -import static org.opensearch.test.framework.matcher.SearchResponseMatchers.searchHitContainsFieldWithValue; @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class OnBehalfOfJwtAuthenticationTest { - public static final String CLAIM_USERNAME = "test-user"; - public static final String CLAIM_ROLES = "backend-user-roles"; - - public static final String USER_SUPERHERO = "superhero"; - public static final String ROLE_VP = "role_vp"; public static final String POINTER_USERNAME = "/user_name"; - public static final String QA_DEPARTMENT = "qa-department"; - - public static final String CLAIM_DEPARTMENT = "department"; - - public static final String DEPARTMENT_SONG_INDEX_PATTERN = String.format("song_lyrics_${attr.jwt.%s}", CLAIM_DEPARTMENT); - - public static final String QA_SONG_INDEX_NAME = String.format("song_lyrics_%s", QA_DEPARTMENT); - - private static final KeyPair KEY_PAIR = Keys.keyPairFor(SignatureAlgorithm.RS256); - static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); - private static final String JWT_AUTH_HEADER = "jwt-auth"; - public static final String issuer = "cluster_0"; public static final String subject = "testUser"; public static final String audience = "audience_0"; public static final Integer expirySeconds = 100000; - public static final String headerName = "Bearer"; + public static final String headerName = "Authorization"; public static final List roles = List.of("admin", "HR"); private static final String signingKey = Base64.getEncoder().encodeToString("jwt signing key for an on behalf of token authentication backend for testing of extensions".getBytes(StandardCharsets.UTF_8)); @@ -94,31 +51,14 @@ public class OnBehalfOfJwtAuthenticationTest { encryptionKey ); - public static final String SONG_ID_1 = "song-id-01"; - - public static final TestSecurityConfig.Role DEPARTMENT_SONG_LISTENER_ROLE = new TestSecurityConfig.Role("department-song-listener-role") - .indexPermissions("indices:data/read/search").on(DEPARTMENT_SONG_INDEX_PATTERN); - @ClassRule public static final LocalCluster cluster = new LocalCluster.Builder() .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) .nodeSettings(Map.of("plugins.security.restapi.roles_enabled", List.of("user_" + ADMIN_USER.getName() +"__" + ALL_ACCESS.getName()))) - .authc(AUTHC_HTTPBASIC_INTERNAL).onBehalfOf(new OnBehalfOfConfig().signing_key(signingKey).encryption_key(encryptionKey)) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .onBehalfOf(new OnBehalfOfConfig().signing_key(signingKey).encryption_key(encryptionKey)) .build(); - @Rule - public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator"); - - @BeforeClass - public static void createTestData() { - try (Client client = cluster.getInternalNodeClient()) { - client.prepareIndex(QA_SONG_INDEX_NAME).setId(SONG_ID_1).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0].asMap()).get(); - } - try(TestRestClient client = cluster.getRestClient(ADMIN_USER)){ - client.createRoleMapping(ROLE_VP, DEPARTMENT_SONG_LISTENER_ROLE.getName()); - } - } - @Test public void shouldAuthenticateWithJwtToken_positive() { try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken())){ diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java index 3d5fcf91aa..108806cd24 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java @@ -53,6 +53,6 @@ Header generateValidToken() throws Exception { } private BasicHeader toHeader(String token) { - return new BasicHeader(headerName, token); + return new BasicHeader(headerName, "Bearer " + token); } } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 9bf13957da..a65eb9ab79 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -838,8 +838,6 @@ public Collection createComponents(Client localClient, ClusterService cl securityRestHandler = new SecurityRestFilter(backendRegistry, auditLog, threadPool, principalExtractor, settings, configPath, compatConfig); - - HTTPOnBehalfOfJwtAuthenticator acInstance = new HTTPOnBehalfOfJwtAuthenticator(); final DynamicConfigFactory dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih); dcf.registerDCFListener(backendRegistry); @@ -848,7 +846,6 @@ public Collection createComponents(Client localClient, ClusterService cl dcf.registerDCFListener(xffResolver); dcf.registerDCFListener(evaluator); dcf.registerDCFListener(securityRestHandler); - dcf.registerDCFListener(acInstance); if (!(auditLog instanceof NullAuditLog)) { // Don't register if advanced modules is disabled in which case auditlog is instance of NullAuditLog dcf.registerDCFListener(auditLog); diff --git a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java b/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java index 55012f45ae..f4ac33fe76 100644 --- a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java @@ -216,6 +216,7 @@ private AuthCredentials extractCredentials0(final RestRequest request) { return null; } catch (Exception e) { System.out.println("Error MSG2!" + e.getMessage()); + e.printStackTrace(); if(log.isDebugEnabled()) { log.debug("Invalid or expired JWT token.", e); } From 960326bb77aae7b6a34d721f569d90651fb95e45 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Thu, 8 Jun 2023 13:38:59 -0700 Subject: [PATCH 09/19] Revert the temorary fix of zstd library Signed-off-by: Ryan Liang --- build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index ef813052c9..ef187e290f 100644 --- a/build.gradle +++ b/build.gradle @@ -301,7 +301,6 @@ configurations { force "io.netty:netty-transport:${versions.netty}" force "io.netty:netty-transport-native-unix-common:${versions.netty}" force "org.apache.bcel:bcel:6.6.0" // This line should be removed once Spotbugs is upgraded to 4.7.4 - force "com.github.luben:zstd-jni:${versions.zstd}" } } @@ -425,7 +424,7 @@ dependencies { runtimeOnly 'com.fasterxml.woodstox:woodstox-core:6.4.0' runtimeOnly 'org.apache.ws.xmlschema:xmlschema-core:2.2.5' runtimeOnly 'org.apache.santuario:xmlsec:2.2.3' - runtimeOnly "com.github.luben:zstd-jni:${versions.zstd}" + runtimeOnly 'com.github.luben:zstd-jni:1.5.2-1' runtimeOnly 'org.checkerframework:checker-qual:3.5.0' runtimeOnly "org.bouncycastle:bcpkix-jdk15on:${versions.bouncycastle}" From 1d24b45745ffb693d2ba4ad90c937a7223170dd5 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Thu, 8 Jun 2023 21:32:29 +0000 Subject: [PATCH 10/19] Fix build break Signed-off-by: Peter Nied --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ef187e290f..536744d92e 100644 --- a/build.gradle +++ b/build.gradle @@ -424,7 +424,7 @@ dependencies { runtimeOnly 'com.fasterxml.woodstox:woodstox-core:6.4.0' runtimeOnly 'org.apache.ws.xmlschema:xmlschema-core:2.2.5' runtimeOnly 'org.apache.santuario:xmlsec:2.2.3' - runtimeOnly 'com.github.luben:zstd-jni:1.5.2-1' + runtimeOnly 'com.github.luben:zstd-jni:1.5.5-3' runtimeOnly 'org.checkerframework:checker-qual:3.5.0' runtimeOnly "org.bouncycastle:bcpkix-jdk15on:${versions.bouncycastle}" From da94b822efde63164b40fdc62bb207dd9e7b9484 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Thu, 8 Jun 2023 21:33:43 +0000 Subject: [PATCH 11/19] Make authenticator name more concise Signed-off-by: Peter Nied --- .../opensearch/security/OpenSearchSecurityPlugin.java | 6 +++--- ...Authenticator.java => OnBehalfOfAuthenticator.java} | 6 +++--- ...catorTest.java => OnBehalfOfAuthenticatorTest.java} | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) rename src/main/java/org/opensearch/security/http/{HTTPOnBehalfOfJwtAuthenticator.java => OnBehalfOfAuthenticator.java} (97%) rename src/test/java/org/opensearch/security/http/{HTTPOnBehalfOfJwtAuthenticatorTest.java => OnBehalfOfAuthenticatorTest.java} (94%) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 9bf13957da..0a0726c0a7 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -141,7 +141,7 @@ import org.opensearch.security.dlic.rest.validation.PasswordValidator; import org.opensearch.security.filter.SecurityFilter; import org.opensearch.security.filter.SecurityRestFilter; -import org.opensearch.security.http.HTTPOnBehalfOfJwtAuthenticator; +import org.opensearch.security.http.OnBehalfOfAuthenticator; import org.opensearch.security.http.SecurityHttpServerTransport; import org.opensearch.security.http.SecurityNonSslHttpServerTransport; import org.opensearch.security.http.XFFResolver; @@ -839,7 +839,7 @@ public Collection createComponents(Client localClient, ClusterService cl securityRestHandler = new SecurityRestFilter(backendRegistry, auditLog, threadPool, principalExtractor, settings, configPath, compatConfig); - HTTPOnBehalfOfJwtAuthenticator acInstance = new HTTPOnBehalfOfJwtAuthenticator(); + final OnBehalfOfAuthenticator onBehalfOfAuthenticator = new OnBehalfOfAuthenticator(); final DynamicConfigFactory dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih); dcf.registerDCFListener(backendRegistry); @@ -848,7 +848,7 @@ public Collection createComponents(Client localClient, ClusterService cl dcf.registerDCFListener(xffResolver); dcf.registerDCFListener(evaluator); dcf.registerDCFListener(securityRestHandler); - dcf.registerDCFListener(acInstance); + dcf.registerDCFListener(onBehalfOfAuthenticator); if (!(auditLog instanceof NullAuditLog)) { // Don't register if advanced modules is disabled in which case auditlog is instance of NullAuditLog dcf.registerDCFListener(auditLog); diff --git a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java similarity index 97% rename from src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java rename to src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index 1fabd0874c..c65c2c591a 100644 --- a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -44,7 +44,7 @@ import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.user.AuthCredentials; -public class HTTPOnBehalfOfJwtAuthenticator implements HTTPAuthenticator { +public class OnBehalfOfAuthenticator implements HTTPAuthenticator { protected final Logger log = LogManager.getLogger(this.getClass()); @@ -58,13 +58,13 @@ public class HTTPOnBehalfOfJwtAuthenticator implements HTTPAuthenticator { private String signingKey; private String encryptionKey; - public HTTPOnBehalfOfJwtAuthenticator() { + public OnBehalfOfAuthenticator() { super(); init(); } // FOR TESTING - public HTTPOnBehalfOfJwtAuthenticator(String signingKey, String encryptionKey){ + public OnBehalfOfAuthenticator(String signingKey, String encryptionKey){ this.signingKey = signingKey; this.encryptionKey = encryptionKey; init(); diff --git a/src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTest.java b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java similarity index 94% rename from src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTest.java rename to src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java index 5a6c5ec41c..dabd1e1a54 100644 --- a/src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java @@ -37,7 +37,7 @@ import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.util.FakeRestRequest; -public class HTTPOnBehalfOfJwtAuthenticatorTest { +public class OnBehalfOfAuthenticatorTest { final static byte[] secretKeyBytes = new byte[1024]; final static String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); final static SecretKey secretKey; @@ -88,7 +88,7 @@ public void testBadKey() throws Exception { @Test public void testTokenMissing() throws Exception { - HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(BaseEncoding.base64().encode(secretKeyBytes),claimsEncryptionKey); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(BaseEncoding.base64().encode(secretKeyBytes),claimsEncryptionKey); Map headers = new HashMap(); AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); @@ -101,7 +101,7 @@ public void testInvalid() throws Exception { String jwsToken = "123invalidtoken.."; - HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(BaseEncoding.base64().encode(secretKeyBytes), claimsEncryptionKey); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(BaseEncoding.base64().encode(secretKeyBytes), claimsEncryptionKey); Map headers = new HashMap(); headers.put("Authorization", "Bearer "+jwsToken); @@ -114,7 +114,7 @@ public void testBearer() throws Exception { String jwsToken = Jwts.builder().setSubject("Leonard McCoy").setAudience("ext_0").signWith(secretKey, SignatureAlgorithm.HS512).compact(); - HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(BaseEncoding.base64().encode(secretKeyBytes), claimsEncryptionKey); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(BaseEncoding.base64().encode(secretKeyBytes), claimsEncryptionKey); Map headers = new HashMap(); headers.put("Authorization", "Bearer "+jwsToken); @@ -280,7 +280,7 @@ private AuthCredentials extractCredentialsFromJwtHeader( final JwtBuilder jwtBuilder, final Boolean bwcPluginCompatibilityMode) { final String jwsToken = jwtBuilder.signWith(secretKey, SignatureAlgorithm.HS512).compact(); - final HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(signingKey, encryptionKey); + final OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(signingKey, encryptionKey); final Map headers = Map.of("Authorization", "Bearer " + jwsToken); return jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap<>()), null); } From 5913b57456d29530dd19a68da665ced0be9ce298 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Thu, 8 Jun 2023 21:47:25 +0000 Subject: [PATCH 12/19] Clean up how roles are (de)encrypted --- .../jwt/EncryptionDecryptionUtil.java | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java index 16d1248820..9fb407128c 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java @@ -19,38 +19,39 @@ import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; +import org.opensaml.xmlsec.encryption.P; + public class EncryptionDecryptionUtil { public static String encrypt(final String secret, final String data) { - - byte[] decodedKey = Base64.getDecoder().decode(secret); - - try { - Cipher cipher = Cipher.getInstance("AES"); - // rebuild key using SecretKeySpec - SecretKey originalKey = new SecretKeySpec(Arrays.copyOf(decodedKey, 16), "AES"); - cipher.init(Cipher.ENCRYPT_MODE, originalKey); - byte[] cipherText = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); - return Base64.getEncoder().encodeToString(cipherText); - } catch (Exception e) { - throw new RuntimeException( - "Error occured while encrypting data", e); - } + final Cipher cipher = createCipherFromSecret(secret); + final byte[] cipherText = createCipherText(cipher, data.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(cipherText); } public static String decrypt(final String secret, final String encryptedString) { + final Cipher cipher = createCipherFromSecret(secret); + final byte[] cipherText = createCipherText(cipher, Base64.getDecoder().decode(encryptedString)); + return new String(cipherText, StandardCharsets.UTF_8); + } - byte[] decodedKey = Base64.getDecoder().decode(secret); - + private static Cipher createCipherFromSecret(final String secret) { try { - Cipher cipher = Cipher.getInstance("AES"); - // rebuild key using SecretKeySpec - SecretKey originalKey = new SecretKeySpec(Arrays.copyOf(decodedKey, 16), "AES"); + final byte[] decodedKey = Base64.getDecoder().decode(secret); + final Cipher cipher = Cipher.getInstance("AES"); + final SecretKey originalKey = new SecretKeySpec(Arrays.copyOf(decodedKey, 16), "AES"); cipher.init(Cipher.DECRYPT_MODE, originalKey); - byte[] cipherText = cipher.doFinal(Base64.getDecoder().decode(encryptedString)); - return new String(cipherText, StandardCharsets.UTF_8); - } catch (Exception e) { - throw new RuntimeException("Error occured while decrypting data", e); + return cipher; + } catch (final Exception e) { + throw new RuntimeException("Error creating cipher from secret"); + } + } + + private static byte[] createCipherText(final Cipher cipher, final byte[] data) { + try { + return cipher.doFinal(data); + } catch (final Exception e) { + throw new RuntimeException("The cipher was unable to perform pass over data"); } } } From 437d3159d51838256c9f5ce06528b3ecad968420 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Thu, 8 Jun 2023 21:58:46 +0000 Subject: [PATCH 13/19] Single constructor Signed-off-by: Peter Nied --- .../security/authtoken/jwt/JwtVendor.java | 21 +++---------------- .../security/authtoken/jwt/JwtVendorTest.java | 9 ++++---- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java index 5328453c98..99ef47b370 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -12,6 +12,7 @@ package org.opensearch.security.authtoken.jwt; import java.time.Instant; +import java.util.Optional; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -53,24 +54,8 @@ public class JwtVendor { private ConfigModel configModel; private ThreadContext threadContext; - public JwtVendor(Settings settings) { - JoseJwtProducer jwtProducer = new JoseJwtProducer(); - try { - this.signingKey = createJwkFromSettings(settings); - } catch (Exception e) { - throw new RuntimeException(e); - } - this.jwtProducer = jwtProducer; - if (settings.get("encryption_key") == null) { - throw new RuntimeException("encryption_key cannot be null"); - } else { - this.claimsEncryptionKey = settings.get("encryption_key"); - } - timeProvider = System::currentTimeMillis; - } - //For testing the expiration in the future - public JwtVendor(Settings settings, final LongSupplier timeProvider) { + public JwtVendor(Settings settings, final Optional timeProvider) { JoseJwtProducer jwtProducer = new JoseJwtProducer(); try { this.signingKey = createJwkFromSettings(settings); @@ -83,7 +68,7 @@ public JwtVendor(Settings settings, final LongSupplier timeProvider) { } else { this.claimsEncryptionKey = settings.get("encryption_key"); } - this.timeProvider = timeProvider; + this.timeProvider = timeProvider.orElseGet(() -> System::currentTimeMillis); } /* 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 3330477721..7d4fbb99d4 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -12,6 +12,7 @@ package org.opensearch.security.authtoken.jwt; import java.util.List; +import java.util.Optional; import java.util.function.LongSupplier; import org.apache.commons.lang3.RandomStringUtils; @@ -56,7 +57,7 @@ public void testCreateJwtWithRoles() throws Exception { Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); Long expectedExp = currentTime.getAsLong() + (expirySeconds * 1000); - JwtVendor jwtVendor = new JwtVendor(settings, currentTime); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedJwt); @@ -82,7 +83,7 @@ public void testCreateJwtWithBadExpiry() throws Exception { String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); - JwtVendor jwtVendor = new JwtVendor(settings); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); } @@ -96,7 +97,7 @@ public void testCreateJwtWithBadEncryptionKey() throws Exception { Integer expirySeconds = 300; Settings settings = Settings.builder().put("signing_key", "abc123").build(); - JwtVendor jwtVendor = new JwtVendor(settings); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); } @@ -112,7 +113,7 @@ public void testCreateJwtWithBadRoles() throws Exception { Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); - JwtVendor jwtVendor = new JwtVendor(settings); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); jwtVendor.createJwt(issuer, subject, audience, expirySecond, roles); } From 4eda3dbeb33a7d72357552399e817b99c75fae75 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Thu, 8 Jun 2023 21:59:05 +0000 Subject: [PATCH 14/19] More JwtVendor cleanup - but not sure if this works? Signed-off-by: Peter Nied --- .../security/authtoken/jwt/JwtVendor.java | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java index 99ef47b370..93efc93b06 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -29,12 +29,12 @@ import org.apache.cxf.rs.security.jose.jwt.JwtClaims; import org.apache.cxf.rs.security.jose.jwt.JwtToken; import org.apache.cxf.rs.security.jose.jwt.JwtUtils; +import org.apache.kafka.common.utils.SystemTime; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensearch.common.settings.Settings; import org.opensearch.common.transport.TransportAddress; -import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; @@ -51,10 +51,8 @@ public class JwtVendor { private final LongSupplier timeProvider; //TODO: Relocate/Remove them at once we make the descisions about the `roles` - private ConfigModel configModel; - private ThreadContext threadContext; + private ConfigModel configModel; // This never gets assigned, how does this work at all? - //For testing the expiration in the future public JwtVendor(Settings settings, final Optional timeProvider) { JoseJwtProducer jwtProducer = new JoseJwtProducer(); try { @@ -108,22 +106,7 @@ static JsonWebKey createJwkFromSettings(Settings settings) throws Exception { } } - //TODO:Getting roles from User - public Map prepareClaimsForUser(User user, ThreadPool threadPool) { - Map claims = new HashMap<>(); - this.threadContext = threadPool.getThreadContext(); - final TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); - Set mappedRoles = mapRoles(user, caller); - claims.put("sub", user.getName()); - claims.put("roles", String.join(",", mappedRoles)); - return claims; - } - - public Set mapRoles(final User user, final TransportAddress caller) { - return this.configModel.mapSecurityRoles(user, caller); - } - - public String createJwt(String issuer, String subject, String audience, Integer expirySeconds, List roles) throws Exception { + String createJwt(String issuer, String subject, String audience, Integer expirySeconds, List roles) throws Exception { long timeMillis = timeProvider.getAsLong(); Instant now = Instant.ofEpochMilli(timeProvider.getAsLong()); From 64732bd59807f90e2cade1e726a6691a5751942f Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Thu, 8 Jun 2023 22:40:22 +0000 Subject: [PATCH 15/19] Misc cleanup Signed-off-by: Peter Nied --- .../http/OnBehalfOfAuthenticator.java | 124 ++++++------------ .../http/OnBehalfOfAuthenticatorTest.java | 22 +++- 2 files changed, 55 insertions(+), 91 deletions(-) diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index 7cc01251cd..eb5fa434df 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -20,6 +20,7 @@ import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; +import java.util.Objects; import java.util.Map.Entry; import java.util.regex.Pattern; @@ -49,75 +50,51 @@ public class OnBehalfOfAuthenticator implements HTTPAuthenticator { private static final Pattern BEARER = Pattern.compile("^\\s*Bearer\\s.*", Pattern.CASE_INSENSITIVE); private static final String BEARER_PREFIX = "bearer "; + private static final String SUBJECT_CLAIM = "sub"; - //TODO: TO SEE IF WE NEED THE FINAL FOR FOLLOWING - private JwtParser jwtParser; - private String subjectKey; + private final JwtParser jwtParser; + private final String encryptionKey; - private String signingKey; - private String encryptionKey; - private volatile boolean initialized; - - public OnBehalfOfAuthenticator() { - super(); - init(); - } - - public OnBehalfOfAuthenticator(Settings settings){ - this.signingKey = settings.get("signing_key"); - this.encryptionKey = settings.get("encryption_key"); - init(); - } - - // FOR TESTING - public OnBehalfOfAuthenticator(String signingKey, String encryptionKey){ - this.signingKey = signingKey; - this.encryptionKey = encryptionKey; - init(); + public OnBehalfOfAuthenticator(Settings settings) { + encryptionKey = settings.get("encryption_key"); + jwtParser = initParser(settings.get("signing_key")); } - public boolean isInitialized(){ - return initialized; - } - - private void init() { + private JwtParser initParser(final String signingKey) { + if (signingKey == null || signingKey.length() == 0) { + throw new RuntimeException("Unable to find on behalf of authenticator signing key"); + } try { - if(signingKey == null || signingKey.length() == 0) { - log.error("signingKey must not be null or empty. JWT authentication will not work"); - } else { + final String minmalKeyFormat = signingKey + .replace("-----BEGIN PUBLIC KEY-----\n", "") + .replace("-----END PUBLIC KEY-----", ""); - signingKey = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", ""); - signingKey = signingKey.replace("-----END PUBLIC KEY-----", ""); + final byte[] decoded = Decoders.BASE64.decode(minmalKeyFormat); + Key key = null; - byte[] decoded = Decoders.BASE64.decode(signingKey); - Key key = null; - - try { - key = getPublicKey(decoded, "RSA"); - } catch (Exception e) { - log.debug("No public RSA key, try other algos ({})", e.toString()); - } - - try { - key = getPublicKey(decoded, "EC"); - } catch (Exception e) { - log.debug("No public ECDSA key, try other algos ({})", e.toString()); - } + try { + key = getPublicKey(decoded, "RSA"); + } catch (Exception e) { + log.debug("No public RSA key, try other algos ({})", e.toString()); + } - if(key != null) { - jwtParser = Jwts.parser().setSigningKey(key); - } else { - jwtParser = Jwts.parser().setSigningKey(decoded); - } + try { + key = getPublicKey(decoded, "EC"); + } catch (Exception e) { + log.debug("No public ECDSA key, try other algos ({})", e.toString()); + } + if (Objects.nonNull(key)) { + return Jwts.parser().setSigningKey(key); } + // Fallback to the decoded signing key + // TODO: Should we ever do this, I think no?? + return Jwts.parser().setSigningKey(decoded); } catch (Throwable e) { log.error("Error while creating JWT authenticator", e); throw new RuntimeException(e); } - - subjectKey = "sub"; } @Override @@ -170,9 +147,17 @@ private AuthCredentials extractCredentials0(final RestRequest request) { try { final Claims claims = jwtParser.parseClaimsJws(jwtToken).getBody(); - final String subject = extractSubject(claims, request); + final String subject = claims.getSubject(); + if (Objects.isNull(subject)) { + log.error("Valid jwt on behalf of token with no subject"); + return null; + } final String audience = claims.getAudience(); + if (Objects.isNull(subject)) { + log.error("Valid jwt on behalf of token with no audience"); + return null; + } String[] roles; @@ -193,15 +178,6 @@ private AuthCredentials extractCredentials0(final RestRequest request) { roles = Arrays.stream(decryptedRoles.split(",")).map(String::trim).toArray(String[]::new); } - if (subject == null) { - log.error("No subject found in JWT token"); - return null; - } - - if (audience == null) { - log.error("No audience found in JWT token"); - } - final AuthCredentials ac = new AuthCredentials(subject, roles).markComplete(); for(Entry claim: claims.entrySet()) { @@ -234,26 +210,6 @@ public String getType() { return "onbehalfof_jwt"; } - //TODO: Extract the audience (ext_id) and inject it into thread context - - protected String extractSubject(final Claims claims, final RestRequest request) { - String subject = claims.getSubject(); - if(subjectKey != null) { - // try to get roles from claims, first as Object to avoid having to catch the ExpectedTypeException - Object subjectObject = claims.get(subjectKey, Object.class); - if(subjectObject == null) { - log.warn("Failed to get subject from JWT claims, check if subject_key '{}' is correct.", subjectKey); - return null; - } - // We expect a String. If we find something else, convert to String but issue a warning - if(!(subjectObject instanceof String)) { - log.warn("Expected type String in the JWT for subject_key {}, but value was '{}' ({}). Will convert this value to String.", subjectKey, subjectObject, subjectObject.getClass()); - } - subject = String.valueOf(subjectObject); - } - return subject; - } - private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) throws NoSuchAlgorithmException, InvalidKeySpecException { X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); KeyFactory kf = KeyFactory.getInstance(algo); diff --git a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java index 0a874fceef..f1d5d87222 100644 --- a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java @@ -31,6 +31,7 @@ import org.junit.Assert; import org.junit.Test; +import org.opensearch.common.settings.Settings; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.util.FakeRestRequest; @@ -43,9 +44,9 @@ public class OnBehalfOfAuthenticatorTest { new SecureRandom().nextBytes(secretKeyBytes); secretKey = Keys.hmacShaKeyFor(secretKeyBytes); } - final static String signingKey = BaseEncoding.base64().encode(secretKeyBytes); + @Test public void testNoKey() throws Exception { @@ -85,7 +86,7 @@ public void testBadKey() throws Exception { @Test public void testTokenMissing() throws Exception { - OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(signingKey, claimsEncryptionKey); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); Map headers = new HashMap(); AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); @@ -98,7 +99,7 @@ public void testInvalid() throws Exception { String jwsToken = "123invalidtoken.."; - OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(signingKey, claimsEncryptionKey); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); Map headers = new HashMap(); headers.put("Authorization", "Bearer "+jwsToken); @@ -111,7 +112,7 @@ public void testBearer() throws Exception { String jwsToken = Jwts.builder().setSubject("Leonard McCoy").setAudience("ext_0").signWith(secretKey, SignatureAlgorithm.HS512).compact(); - OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(signingKey, claimsEncryptionKey); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); Map headers = new HashMap(); headers.put("Authorization", "Bearer "+jwsToken); @@ -127,7 +128,7 @@ public void testBearer() throws Exception { public void testBearerWrongPosition() throws Exception { String jwsToken = Jwts.builder().setSubject("Leonard McCoy").setAudience("ext_0").signWith(secretKey, SignatureAlgorithm.HS512).compact(); - OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(signingKey, claimsEncryptionKey); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); Map headers = new HashMap(); headers.put("Authorization", jwsToken + "Bearer " + " 123"); @@ -141,7 +142,7 @@ public void testBearerWrongPosition() throws Exception { @Test public void testBasicAuthHeader() throws Exception { String jwsToken = Jwts.builder().setSubject("Leonard McCoy").setAudience("ext_0").signWith(secretKey, SignatureAlgorithm.HS512).compact(); - OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(signingKey, claimsEncryptionKey); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); Map headers = Collections.singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + jwsToken); @@ -274,8 +275,15 @@ private AuthCredentials extractCredentialsFromJwtHeader( final JwtBuilder jwtBuilder, final Boolean bwcPluginCompatibilityMode) { final String jwsToken = jwtBuilder.signWith(secretKey, SignatureAlgorithm.HS512).compact(); - final OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(signingKey, encryptionKey); + final OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); final Map headers = Map.of("Authorization", "Bearer " + jwsToken); return jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap<>()), null); } + + private Settings defaultSettings() { + return Settings.builder() + .put("signing_key", signingKey) + .put("encryption_key", claimsEncryptionKey) + .build(); + } } From 861b58838ff2e9cfcd5d8fe3dd93dab785d7c505 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Thu, 8 Jun 2023 22:41:29 +0000 Subject: [PATCH 16/19] Remove bad import Signed-off-by: Peter Nied --- .../security/authtoken/jwt/EncryptionDecryptionUtil.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java index 9fb407128c..b2e2102edd 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java @@ -19,8 +19,6 @@ import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; -import org.opensaml.xmlsec.encryption.P; - public class EncryptionDecryptionUtil { public static String encrypt(final String secret, final String data) { From 86b4fd984005c584688c09c2443d1dc146e9e9d3 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Fri, 9 Jun 2023 20:52:11 +0000 Subject: [PATCH 17/19] Add endpoint and test that interactions with JwtVendor Signed-off-by: Peter Nied --- .../http/OnBehalfOfJwtAuthenticationTest.java | 1 + .../security/OpenSearchSecurityPlugin.java | 2 + .../onbehalf/CreateOnBehalfOfToken.java | 141 ++++++++++++++++++ .../security/authtoken/jwt/JwtVendor.java | 11 +- .../dlic/rest/api/AccountApiTest.java | 5 +- 5 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index dcc6323f97..82cb286043 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -61,6 +61,7 @@ public class OnBehalfOfJwtAuthenticationTest { @Test public void shouldAuthenticateWithJwtToken_positive() { + // TODO: This integration test should use an endpoint to get an OnBehalfOf token, not generate it try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken())){ TestRestClient.HttpResponse response = client.getAuthInfo(); diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index a323e81c77..a756ce0b5d 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -117,6 +117,7 @@ import org.opensearch.search.query.QuerySearchResult; import org.opensearch.security.action.configupdate.ConfigUpdateAction; import org.opensearch.security.action.configupdate.TransportConfigUpdateAction; +import org.opensearch.security.action.onbehalf.CreateOnBehalfOfToken; import org.opensearch.security.action.whoami.TransportWhoAmIAction; import org.opensearch.security.action.whoami.WhoAmIAction; import org.opensearch.security.auditlog.AuditLog; @@ -477,6 +478,7 @@ public List getRestHandlers(Settings settings, RestController restC Objects.requireNonNull(cs), Objects.requireNonNull(adminDns), Objects.requireNonNull(cr))); handlers.add(new SecurityConfigUpdateAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); handlers.add(new SecurityWhoAmIAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); + handlers.add(new CreateOnBehalfOfToken(settings, threadPool)); handlers.addAll( SecurityRestApiActions.getHandler( settings, diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java new file mode 100644 index 0000000000..aa1557cd7e --- /dev/null +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java @@ -0,0 +1,141 @@ +package org.opensearch.security.action.onbehalf; + +import java.io.IOException; +import java.util.List; + +import org.apache.cxf.rs.security.jose.jwt.JwtToken; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.FailedNodeException; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.nodes.TransportNodesAction; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.inject.Provider; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.io.stream.StreamOutput; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.auth.BackendRegistry; +import org.opensearch.security.authtoken.jwt.JwtVendor; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.securityconf.DynamicConfigFactory; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.TransportRequest; +import org.opensearch.transport.TransportService; +import org.opensearch.rest.BaseRestHandler; + +import java.io.IOException; +import java.nio.file.Path; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.bytes.BytesReference; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestController; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestRequest.Method; +import org.opensearch.rest.RestStatus; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; +import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.ssl.SecurityKeyStore; +import org.opensearch.security.ssl.transport.PrincipalExtractor; +import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.client.node.NodeClient; +import org.opensearch.security.user.User; + +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; + +public class CreateOnBehalfOfToken extends BaseRestHandler { + + private final JwtVendor vendor; + private final ThreadPool threadPool; + + public CreateOnBehalfOfToken(final Settings settings, final ThreadPool threadPool) { + Settings testSettings = Settings.builder() + .put("signing_key", "1234567890123456") + .put("encryption_key", "1234567890123456").build(); + + this.vendor = new JwtVendor(testSettings, Optional.empty()); + this.threadPool = threadPool; + } + + @Override + public String getName() { + return getClass().getSimpleName(); + } + + @Override + public List routes() { + return addRoutesPrefix( + ImmutableList.of( + new Route(Method.POST, "/user/onbehalfof") + ) + ); + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + switch (request.method()) { + case POST: + return handlePost(request, client); + default: + throw new IllegalArgumentException(request.method() + " not supported"); + } + } + + private RestChannelConsumer handlePost(RestRequest request, NodeClient client) throws IOException { + return new RestChannelConsumer() { + @Override + public void accept(RestChannel channel) throws Exception { + final XContentBuilder builder = channel.newBuilder(); + BytesRestResponse response; + + try { + builder.startObject(); + final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + builder.field("user_name", "nothing"); + builder.field("user", user.toString()); + final String token = vendor.createJwt("me", user.getName(), "self-issued", 60, List.of("1", "2", "3")); + builder.field("field_token", token); + builder.endObject(); + + response = new BytesRestResponse(RestStatus.OK, builder); + } catch (final Exception exception) { + System.out.println(exception.toString()); + builder.startObject() + .field("error", exception.toString()) + .endObject(); + + response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); + } + builder.close(); + channel.sendResponse(response); + } + }; + } + +} diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java index 93efc93b06..1587dcab8c 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -45,15 +45,15 @@ public class JwtVendor { private static JsonMapObjectReaderWriter jsonMapReaderWriter = new JsonMapObjectReaderWriter(); - private String claimsEncryptionKey; - private JsonWebKey signingKey; - private JoseJwtProducer jwtProducer; + private final String claimsEncryptionKey; + private final JsonWebKey signingKey; + private final JoseJwtProducer jwtProducer; private final LongSupplier timeProvider; //TODO: Relocate/Remove them at once we make the descisions about the `roles` private ConfigModel configModel; // This never gets assigned, how does this work at all? - public JwtVendor(Settings settings, final Optional timeProvider) { + public JwtVendor(final Settings settings, final Optional timeProvider) { JoseJwtProducer jwtProducer = new JoseJwtProducer(); try { this.signingKey = createJwkFromSettings(settings); @@ -67,6 +67,7 @@ public JwtVendor(Settings settings, final Optional timeProvider) { this.claimsEncryptionKey = settings.get("encryption_key"); } this.timeProvider = timeProvider.orElseGet(() -> System::currentTimeMillis); + this.configModel = null; } /* @@ -106,7 +107,7 @@ static JsonWebKey createJwkFromSettings(Settings settings) throws Exception { } } - String createJwt(String issuer, String subject, String audience, Integer expirySeconds, List roles) throws Exception { + public String createJwt(String issuer, String subject, String audience, Integer expirySeconds, List roles) throws Exception { long timeMillis = timeProvider.getAsLong(); Instant now = Instant.ofEpochMilli(timeProvider.getAsLong()); diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java index 9f6bcb65c9..393cebb88e 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java @@ -39,7 +39,7 @@ public AccountApiTest(){ BASE_ENDPOINT = getEndpointPrefix() + "/api/"; ENDPOINT = getEndpointPrefix() + "/api/account"; } - + @Test public void testGetAccount() throws Exception { // arrange @@ -73,6 +73,9 @@ public void testGetAccount() throws Exception { assertNotNull(body.getAsList("custom_attribute_names").size()); assertNotNull(body.getAsSettings("tenants")); assertNotNull(body.getAsList("roles")); + + response = rh.executePostRequest(getEndpointPrefix() + "/api/user/onbehalfof", "", encodeBasicHeader(testUser, testPass)); + System.out.println(response.getBody()); } @Test From d15044b79d2c849d3105d81ae45f733b4548b153 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Mon, 12 Jun 2023 16:10:01 +0000 Subject: [PATCH 18/19] AlFix to do both decrypt and encrypt tokens Signed-off-by: Peter Nied --- .../jwt/EncryptionDecryptionUtil.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java index b2e2102edd..c0e2429948 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java @@ -22,26 +22,26 @@ public class EncryptionDecryptionUtil { public static String encrypt(final String secret, final String data) { - final Cipher cipher = createCipherFromSecret(secret); + final Cipher cipher = createCipherFromSecret(secret, CipherMode.ENCRYPT); final byte[] cipherText = createCipherText(cipher, data.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(cipherText); } public static String decrypt(final String secret, final String encryptedString) { - final Cipher cipher = createCipherFromSecret(secret); + final Cipher cipher = createCipherFromSecret(secret, CipherMode.DECRYPT); final byte[] cipherText = createCipherText(cipher, Base64.getDecoder().decode(encryptedString)); return new String(cipherText, StandardCharsets.UTF_8); } - private static Cipher createCipherFromSecret(final String secret) { + private static Cipher createCipherFromSecret(final String secret, final CipherMode mode) { try { final byte[] decodedKey = Base64.getDecoder().decode(secret); final Cipher cipher = Cipher.getInstance("AES"); final SecretKey originalKey = new SecretKeySpec(Arrays.copyOf(decodedKey, 16), "AES"); - cipher.init(Cipher.DECRYPT_MODE, originalKey); + cipher.init(mode.opmode, originalKey); return cipher; } catch (final Exception e) { - throw new RuntimeException("Error creating cipher from secret"); + throw new RuntimeException("Error creating cipher from secret in mode " + mode.name()); } } @@ -52,4 +52,13 @@ private static byte[] createCipherText(final Cipher cipher, final byte[] data) { throw new RuntimeException("The cipher was unable to perform pass over data"); } } + + private enum CipherMode { + ENCRYPT(Cipher.ENCRYPT_MODE), + DECRYPT(Cipher.DECRYPT_MODE); + private final int opmode; + private CipherMode(final int opmode) { + this.opmode = opmode; + } + } } From 7f3e40e31a4175aa2ad4f50372d1347e9c41b758 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Mon, 12 Jun 2023 16:40:37 +0000 Subject: [PATCH 19/19] Add basic parameters and update test to use them Signed-off-by: Peter Nied --- .../onbehalf/CreateOnBehalfOfToken.java | 26 ++++++++++++++----- .../dlic/rest/api/AccountApiTest.java | 2 +- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java index aa1557cd7e..219594ad98 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java @@ -113,14 +113,28 @@ private RestChannelConsumer handlePost(RestRequest request, NodeClient client) t public void accept(RestChannel channel) throws Exception { final XContentBuilder builder = channel.newBuilder(); BytesRestResponse response; - try { - builder.startObject(); + final Map requestBody = request.contentOrSourceParamParser().map(); + final String reason = (String)requestBody.getOrDefault("reason", null); + + final Integer tokenDuration = Optional.ofNullable(requestBody.get("duration")) + .map(value -> (String)value) + .map(Integer::parseInt) + .map(value -> Math.min(value, 72)) // Max duration is 72 hours + .orElse(24); // Fallback to default; + + final String source = "self-issued"; final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - builder.field("user_name", "nothing"); - builder.field("user", user.toString()); - final String token = vendor.createJwt("me", user.getName(), "self-issued", 60, List.of("1", "2", "3")); - builder.field("field_token", token); + + builder.startObject(); + builder.field("user", user.getName()); + final String token = vendor.createJwt(/* TODO: Update the issuer to represent the cluster */"OpenSearch", + user.getName(), + source, + tokenDuration, + user.getSecurityRoles().stream().collect(Collectors.toList())); + builder.field("onBehalfOfToken", token); + builder.field("duration", tokenDuration); builder.endObject(); response = new BytesRestResponse(RestStatus.OK, builder); diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java index 393cebb88e..c323b15e6c 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java @@ -74,7 +74,7 @@ public void testGetAccount() throws Exception { assertNotNull(body.getAsSettings("tenants")); assertNotNull(body.getAsList("roles")); - response = rh.executePostRequest(getEndpointPrefix() + "/api/user/onbehalfof", "", encodeBasicHeader(testUser, testPass)); + response = rh.executePostRequest(getEndpointPrefix() + "/api/user/onbehalfof", "{\"reason\":\"Test generation\"}", encodeBasicHeader(testUser, testPass)); System.out.println(response.getBody()); }