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..c4292386c6 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -0,0 +1,134 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.http; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.message.BasicHeader; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +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 static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasKey; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class OnBehalfOfJwtAuthenticationTest { + + public static final String POINTER_USERNAME = "/user_name"; + + static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + private static final String signingKey = Base64.getEncoder() + .encodeToString( + "jwt signing key for an on behalf of token authentication backend for testing of OBO authentication".getBytes( + StandardCharsets.UTF_8 + ) + ); + private static final String encryptionKey = Base64.getEncoder().encodeToString("encryptionKey".getBytes(StandardCharsets.UTF_8)); + public static final String ADMIN_USER_NAME = "admin"; + public static final String DEFAULT_PASSWORD = "secret"; + public static final String OBO_TOKEN_REASON = "{\"reason\":\"Test generation\"}"; + public static final String OBO_ENDPOINT_PREFIX = "_plugins/_security/api/user/onbehalfof"; + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .users(ADMIN_USER) + .nodeSettings( + Map.of( + "plugins.security.allow_default_init_securityindex", + true, + "plugins.security.restapi.roles_enabled", + List.of("user_admin__all_access") + ) + ) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .onBehalfOf(new OnBehalfOfConfig().signing_key(signingKey).encryption_key(encryptionKey)) + .build(); + + @Test + public void shouldAuthenticateWithOBOTokenEndPoint() { + Header adminOboAuthHeader; + + try (TestRestClient client = cluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { + + client.assertCorrectCredentials(ADMIN_USER_NAME); + + TestRestClient.HttpResponse response = client.postJson(OBO_ENDPOINT_PREFIX, OBO_TOKEN_REASON); + response.assertStatusCode(200); + + Map oboEndPointResponse = response.getBodyAs(Map.class); + assertThat(oboEndPointResponse, allOf(aMapWithSize(3), hasKey("user"), hasKey("onBehalfOfToken"), hasKey("duration"))); + + String encodedOboTokenStr = oboEndPointResponse.get("onBehalfOfToken").toString(); + + adminOboAuthHeader = new BasicHeader("Authorization", "Bearer " + encodedOboTokenStr); + } + + try (TestRestClient client = cluster.getRestClient(adminOboAuthHeader)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + response.assertStatusCode(200); + + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(ADMIN_USER_NAME)); + } + } + + @Test + public void shouldNotAuthenticateWithATemperedOBOToken() { + Header adminOboAuthHeader; + + try (TestRestClient client = cluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { + + client.assertCorrectCredentials(ADMIN_USER_NAME); + + TestRestClient.HttpResponse response = client.postJson(OBO_ENDPOINT_PREFIX, OBO_TOKEN_REASON); + response.assertStatusCode(200); + + Map oboEndPointResponse = response.getBodyAs(Map.class); + assertThat(oboEndPointResponse, allOf(aMapWithSize(3), hasKey("user"), hasKey("onBehalfOfToken"), hasKey("duration"))); + + String encodedOboTokenStr = oboEndPointResponse.get("onBehalfOfToken").toString(); + StringBuilder stringBuilder = new StringBuilder(encodedOboTokenStr); + stringBuilder.deleteCharAt(encodedOboTokenStr.length() - 1); + String temperedOboTokenStr = stringBuilder.toString(); + + adminOboAuthHeader = new BasicHeader("Authorization", "Bearer " + temperedOboTokenStr); + } + + try (TestRestClient client = cluster.getRestClient(adminOboAuthHeader)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + response.assertStatusCode(401); + response.getBody().contains("Unauthorized"); + } + } +} 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..2061e21c23 --- /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 a702102e6b..160d8455dc 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -115,6 +115,11 @@ public TestSecurityConfig xff(XffConfig xffConfig) { return this; } + public TestSecurityConfig onBehalfOf(OnBehalfOfConfig onBehalfOfConfig) { + config.onBehalfOfConfig(onBehalfOfConfig); + return this; + } + public TestSecurityConfig authc(AuthcDomain authcDomain) { config.authc(authcDomain); return this; @@ -171,6 +176,7 @@ public static class Config implements ToXContentObject { private Boolean doNotFailOnForbidden; private XffConfig xffConfig; + private OnBehalfOfConfig onBehalfOfConfig; private Map authcDomainMap = new LinkedHashMap<>(); private AuthFailureListeners authFailureListeners; @@ -191,6 +197,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; @@ -211,6 +222,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/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java index 539e15fb57..64207ead5b 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; @@ -471,6 +472,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; diff --git a/src/integrationTest/resources/config.yml b/src/integrationTest/resources/config.yml index 1fbea10e28..17aeb1881d 100644 --- a/src/integrationTest/resources/config.yml +++ b/src/integrationTest/resources/config.yml @@ -16,5 +16,7 @@ config: type: "internal" config: {} on_behalf_of: - signing_key: "signing key" - encryption_key: "encryption key" + # The decoded signing key is: This is the jwt signing key for an on behalf of token authentication backend for testing of extensions + # The decoded encryption key is: encryptionKey + signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" + encryption_key: "ZW5jcnlwdGlvbktleQ==" diff --git a/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java b/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java index 3468bb89af..ee152a0d95 100644 --- a/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java +++ b/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java @@ -13,21 +13,13 @@ import java.nio.file.Path; import java.security.AccessController; -import java.security.Key; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; import java.security.PrivilegedAction; -import java.security.PublicKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; import java.util.Collection; import java.util.Map.Entry; import java.util.regex.Pattern; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.WeakKeyException; import org.apache.hc.core5.http.HttpHeaders; import org.apache.logging.log4j.LogManager; @@ -43,6 +35,7 @@ import org.opensearch.rest.RestStatus; import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.user.AuthCredentials; +import org.opensearch.security.util.keyUtil; public class HTTPJwtAuthenticator implements HTTPAuthenticator { @@ -63,44 +56,8 @@ public class HTTPJwtAuthenticator implements HTTPAuthenticator { public HTTPJwtAuthenticator(final Settings settings, final Path configPath) { super(); - JwtParser _jwtParser = null; - - try { - String signingKey = settings.get("signing_key"); - - if (signingKey == null || signingKey.length() == 0) { - log.error("signingKey must not be null or empty. JWT authentication will not work"); - } else { - - signingKey = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", ""); - signingKey = signingKey.replace("-----END PUBLIC KEY-----", ""); - - 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()); - } - - if (key != null) { - _jwtParser = Jwts.parser().setSigningKey(key); - } else { - _jwtParser = Jwts.parser().setSigningKey(decoded); - } - - } - } catch (Throwable e) { - log.error("Error creating JWT authenticator. JWT authentication will not work", e); - throw new RuntimeException(e); - } + String signingKey = settings.get("signing_key"); + JwtParser _jwtParser = keyUtil.keyAlgorithmCheck(signingKey, log); jwtUrlParameter = settings.get("jwt_url_parameter"); jwtHeaderName = settings.get("jwt_header", HttpHeaders.AUTHORIZATION); @@ -282,11 +239,4 @@ protected String[] extractRoles(final Claims claims, final RestRequest request) return roles; } - private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) throws NoSuchAlgorithmException, - InvalidKeySpecException { - X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); - KeyFactory kf = KeyFactory.getInstance(algo); - return kf.generatePublic(spec); - } - } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 55d15d3e0d..c32f4c7078 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.CreateOnBehalfOfTokenAction; import org.opensearch.security.action.whoami.TransportWhoAmIAction; import org.opensearch.security.action.whoami.WhoAmIAction; import org.opensearch.security.auditlog.AuditLog; @@ -141,7 +142,6 @@ 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.SecurityHttpServerTransport; import org.opensearch.security.http.SecurityNonSslHttpServerTransport; import org.opensearch.security.http.XFFResolver; @@ -215,6 +215,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile SslExceptionHandler sslExceptionHandler; private volatile Client localClient; private final boolean disabled; + private volatile DynamicConfigFactory dcf; private final List demoCertHashes = new ArrayList(3); private volatile SecurityFilter sf; private volatile IndexResolverReplacer irr; @@ -531,6 +532,9 @@ public List getRestHandlers( principalExtractor ) ); + CreateOnBehalfOfTokenAction cobot = new CreateOnBehalfOfTokenAction(settings, threadPool, Objects.requireNonNull(cs)); + dcf.registerDCFListener(cobot); + handlers.add(cobot); handlers.addAll( SecurityRestApiActions.getHandler( settings, @@ -1029,17 +1033,13 @@ public Collection createComponents( configPath, compatConfig ); - - HTTPOnBehalfOfJwtAuthenticator acInstance = new HTTPOnBehalfOfJwtAuthenticator(); - - final DynamicConfigFactory dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih); + dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih); dcf.registerDCFListener(backendRegistry); dcf.registerDCFListener(compatConfig); dcf.registerDCFListener(irr); 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/action/onbehalf/CreateOnBehalfOfTokenAction.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java new file mode 100644 index 0000000000..1f8f13c000 --- /dev/null +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java @@ -0,0 +1,157 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.action.onbehalf; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableList; +import org.greenrobot.eventbus.Subscribe; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.transport.TransportAddress; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestRequest.Method; +import org.opensearch.rest.RestStatus; +import org.opensearch.security.authtoken.jwt.JwtVendor; +import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.securityconf.DynamicConfigModel; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; +import org.opensearch.threadpool.ThreadPool; + +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; + +public class CreateOnBehalfOfTokenAction extends BaseRestHandler { + + private JwtVendor vendor; + private final ThreadPool threadPool; + private final ClusterService clusterService; + + private ConfigModel configModel; + + private DynamicConfigModel dcm; + + @Subscribe + public void onConfigModelChanged(ConfigModel configModel) { + this.configModel = configModel; + } + + @Subscribe + public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { + this.dcm = dcm; + if (dcm.getDynamicOnBehalfOfSettings().get("signing_key") != null + && dcm.getDynamicOnBehalfOfSettings().get("encryption_key") != null) { + this.vendor = new JwtVendor(dcm.getDynamicOnBehalfOfSettings(), Optional.empty()); + } else { + this.vendor = null; + } + } + + public CreateOnBehalfOfTokenAction(final Settings settings, final ThreadPool threadPool, final ClusterService clusterService) { + this.threadPool = threadPool; + this.clusterService = clusterService; + } + + @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 { + if (vendor == null) { + channel.sendResponse( + new BytesRestResponse(RestStatus.SERVICE_UNAVAILABLE, "on_behalf_of configuration is not being configured") + ); + return; + } + + final String clusterIdentifier = clusterService.getClusterName().value(); + + 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, 10 * 60)) // Max duration is 10 minutes + .orElse(5 * 60); // Fallback to default of 5 minutes; + + final String service = (String) requestBody.getOrDefault("service", "self-issued"); + final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final TransportAddress caller = threadPool.getThreadContext() + .getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + Set mappedRoles = mapRoles(user, caller); + + builder.startObject(); + builder.field("user", user.getName()); + + final String token = vendor.createJwt( + clusterIdentifier, + user.getName(), + service, + tokenDuration, + mappedRoles.stream().collect(Collectors.toList()), + user.getRoles().stream().collect(Collectors.toList()) + ); + builder.field("onBehalfOfToken", token); + builder.field("duration", tokenDuration + " seconds"); + builder.endObject(); + + response = new BytesRestResponse(RestStatus.OK, builder); + } catch (final Exception exception) { + builder.startObject().field("error", exception.toString()).endObject(); + + response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); + } + builder.close(); + channel.sendResponse(response); + } + }; + } + + public Set mapRoles(final User user, final TransportAddress caller) { + return this.configModel.mapSecurityRoles(user, caller); + } + +} diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 0a287d19f5..37a53cf4c0 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -59,6 +59,7 @@ import org.opensearch.security.auth.blocking.ClientBlockRegistry; import org.opensearch.security.auth.internal.NoOpAuthenticationBackend; import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.http.OnBehalfOfAuthenticator; import org.opensearch.security.http.XFFResolver; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.ssl.util.Utils; @@ -606,6 +607,11 @@ private User impersonate(final RestRequest request, final User originalUser) thr // loop over all http/rest auth domains for (final AuthDomain authDomain : restAuthDomains) { final AuthenticationBackend authenticationBackend = authDomain.getBackend(); + + if (authDomain.getHttpAuthenticator() instanceof OnBehalfOfAuthenticator) { + continue; + } + final User impersonatedUser = checkExistsAndAuthz( restImpersonationCache, new User(impersonatedUserHeader), diff --git a/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java b/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java index 1f149aabcf..299a1a4577 100644 --- a/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java +++ b/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java @@ -46,7 +46,9 @@ public String getType() { @Override public User authenticate(final AuthCredentials credentials) { - return new User(credentials.getUsername(), credentials.getBackendRoles(), credentials); + User user = new User(credentials.getUsername(), credentials.getBackendRoles(), credentials); + user.addSecurityRoles(credentials.getSecurityRoles()); + return user; } @Override 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 54461f6b7e..e38a48cde3 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java @@ -22,34 +22,45 @@ public class EncryptionDecryptionUtil { public static String encrypt(final String secret, final String data) { + final Cipher cipher = createCipherFromSecret(secret, CipherMode.ENCRYPT); + final byte[] cipherText = createCipherText(cipher, data.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(cipherText); + } - byte[] decodedKey = Base64.getDecoder().decode(secret); + public static String decrypt(final String secret, final String encryptedString) { + 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, final CipherMode mode) { 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 byte[] decodedKey = Base64.getDecoder().decode(secret); + final Cipher cipher = Cipher.getInstance("AES"); + final SecretKey originalKey = new SecretKeySpec(Arrays.copyOf(decodedKey, 16), "AES"); + cipher.init(mode.opmode, originalKey); + return cipher; + } catch (final Exception e) { + throw new RuntimeException("Error creating cipher from secret in mode " + mode.name()); } } - public static String decrypt(final String secret, final String encryptedString) { + 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"); + } + } - byte[] decodedKey = Base64.getDecoder().decode(secret); + private enum CipherMode { + ENCRYPT(Cipher.ENCRYPT_MODE), + DECRYPT(Cipher.DECRYPT_MODE); - try { - Cipher cipher = Cipher.getInstance("AES"); - // rebuild key using SecretKeySpec - 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); + private final int opmode; + + private CipherMode(final int opmode) { + this.opmode = opmode; } } } 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 f2f59b111b..3cec339647 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -12,10 +12,8 @@ package org.opensearch.security.authtoken.jwt; import java.time.Instant; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.Optional; import java.util.function.LongSupplier; import com.google.common.base.Strings; @@ -32,28 +30,18 @@ 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; -import org.opensearch.threadpool.ThreadPool; public class JwtVendor { private static final Logger logger = LogManager.getLogger(JwtVendor.class); 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; - private ThreadContext threadContext; - - public JwtVendor(Settings settings) { + public JwtVendor(final Settings settings, final Optional timeProvider) { JoseJwtProducer jwtProducer = new JoseJwtProducer(); try { this.signingKey = createJwkFromSettings(settings); @@ -66,32 +54,19 @@ public JwtVendor(Settings settings) { } else { this.claimsEncryptionKey = settings.get("encryption_key"); } - timeProvider = System::currentTimeMillis; - } - - // For testing the expiration in the future - public JwtVendor(Settings settings, final LongSupplier timeProvider) { - 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"); + if (timeProvider.isPresent()) { + this.timeProvider = timeProvider.get(); } else { - this.claimsEncryptionKey = settings.get("encryption_key"); + this.timeProvider = () -> System.currentTimeMillis() / 1000; } - this.timeProvider = timeProvider; } /* - * The default configuration of this web key should be: - * KeyType: OCTET - * PublicKeyUse: SIGN - * Encryption Algorithm: HS512 - * */ + * The default configuration of this web key should be: + * KeyType: OCTET + * PublicKeyUse: SIGN + * Encryption Algorithm: HS512 + * */ static JsonWebKey createJwkFromSettings(Settings settings) throws Exception { String signingKey = settings.get("signing_key"); @@ -122,22 +97,14 @@ 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 { + public String createJwt( + String issuer, + String subject, + String audience, + Integer expirySeconds, + List roles, + List backendRoles + ) throws Exception { long timeMillis = timeProvider.getAsLong(); Instant now = Instant.ofEpochMilli(timeProvider.getAsLong()); @@ -156,16 +123,15 @@ public String createJwt(String issuer, String subject, String audience, Integer jwtClaims.setNotBefore(timeMillis); if (expirySeconds == null) { - long expiryTime = timeProvider.getAsLong() + (300 * 1000); + long expiryTime = timeProvider.getAsLong() + 300; jwtClaims.setExpiryTime(expiryTime); } else if (expirySeconds > 0) { - long expiryTime = timeProvider.getAsLong() + (expirySeconds * 1000); + long expiryTime = timeProvider.getAsLong() + expirySeconds; jwtClaims.setExpiryTime(expiryTime); } else { throw new Exception("The expiration time should be a positive integer"); } - // TODO: IF USER ENABLES THE BWC MODE, WE ARE EXPECTING TO SET PLAIN TEXT ROLE AS `dr` if (roles != null) { String listOfRoles = String.join(",", roles); jwtClaims.setProperty("er", EncryptionDecryptionUtil.encrypt(claimsEncryptionKey, listOfRoles)); @@ -173,6 +139,8 @@ public String createJwt(String issuer, String subject, String audience, Integer throw new Exception("Roles cannot be null"); } + /* TODO: If the backendRoles is not null and the BWC Mode is on, put them into the "dbr" claim */ + String encodedJwt = jwtProducer.processJwt(jwt); if (logger.isDebugEnabled()) { diff --git a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java b/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java deleted file mode 100644 index c980956fb8..0000000000 --- a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java +++ /dev/null @@ -1,277 +0,0 @@ -/* - * 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. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.http; - -import java.security.AccessController; -import java.security.Key; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.PrivilegedAction; -import java.security.PublicKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; -import java.util.Arrays; -import java.util.Map.Entry; -import java.util.regex.Pattern; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.security.WeakKeyException; -import org.apache.commons.lang3.RandomStringUtils; -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; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.rest.RestChannel; -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 { - - protected final Logger log = LogManager.getLogger(this.getClass()); - - private static final Pattern BEARER = Pattern.compile("^\\s*Bearer\\s.*", Pattern.CASE_INSENSITIVE); - private static final String BEARER_PREFIX = "bearer "; - - // TODO: TO SEE IF WE NEED THE FINAL FOR FOLLOWING - private JwtParser jwtParser; - private String subjectKey; - - private String signingKey; - private String encryptionKey; - - public HTTPOnBehalfOfJwtAuthenticator() { - super(); - init(); - } - - // FOR TESTING - public HTTPOnBehalfOfJwtAuthenticator(String signingKey, String encryptionKey) { - this.signingKey = signingKey; - this.encryptionKey = encryptionKey; - init(); - } - - private void init() { - - try { - if (signingKey == null || signingKey.length() == 0) { - log.error("signingKey must not be null or empty. JWT authentication will not work"); - } else { - - signingKey = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", ""); - signingKey = signingKey.replace("-----END PUBLIC KEY-----", ""); - - 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()); - } - - if (key != null) { - jwtParser = Jwts.parser().setSigningKey(key); - } else { - jwtParser = Jwts.parser().setSigningKey(decoded); - } - - } - } catch (Throwable e) { - log.error("Error while creating JWT authenticator", e); - throw new RuntimeException(e); - } - - subjectKey = "sub"; - } - - @Override - @SuppressWarnings("removal") - public AuthCredentials extractCredentials(RestRequest request, ThreadContext context) throws OpenSearchSecurityException { - final SecurityManager sm = System.getSecurityManager(); - - if (sm != null) { - sm.checkPermission(new SpecialPermission()); - } - - AuthCredentials creds = AccessController.doPrivileged(new PrivilegedAction() { - @Override - public AuthCredentials run() { - return extractCredentials0(request); - } - }); - - return creds; - } - - private AuthCredentials extractCredentials0(final RestRequest request) { - if (jwtParser == null) { - log.error("Missing Signing Key. JWT authentication will not work"); - return null; - } - - String jwtToken = request.header(HttpHeaders.AUTHORIZATION); - - if (jwtToken == null || jwtToken.length() == 0) { - if (log.isDebugEnabled()) { - log.debug("No JWT token found in '{}' header", HttpHeaders.AUTHORIZATION); - } - return null; - } - - if (!BEARER.matcher(jwtToken).matches()) { - jwtToken = null; - } - - final int index; - if ((index = jwtToken.toLowerCase().indexOf(BEARER_PREFIX)) > -1) { // detect Bearer - jwtToken = jwtToken.substring(index + BEARER_PREFIX.length()); - } else { - if (log.isDebugEnabled()) { - log.debug("No Bearer scheme found in header"); - } - } - - try { - final Claims claims = jwtParser.parseClaimsJws(jwtToken).getBody(); - - final String subject = extractSubject(claims, 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."); - } - - if (rolesObject == null) { - log.warn("Failed to get roles from JWT claims. Check if this key is correct and available in the JWT payload."); - roles = new String[0]; - } else { - final String rolesClaim = rolesObject.toString(); - - // 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); - } - - 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()) { - ac.addAttribute("attr.jwt." + claim.getKey(), String.valueOf(claim.getValue())); - } - - return ac; - - } catch (WeakKeyException e) { - log.error("Cannot authenticate user with JWT because of ", e); - return null; - } catch (Exception e) { - if (log.isDebugEnabled()) { - log.debug("Invalid or expired JWT token.", e); - } - return null; - } - } - - @Override - public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) { - return false; - } - - @Override - 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); - return kf.generatePublic(spec); - } - - @Subscribe - public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { - - // TODO: #2615 FOR CONFIGURATION - // For Testing - signingKey = "abcd1234"; - encryptionKey = RandomStringUtils.randomAlphanumeric(16); - } - -} diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java new file mode 100644 index 0000000000..863ec179cb --- /dev/null +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -0,0 +1,209 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.http; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Arrays; +import java.util.List; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.security.WeakKeyException; +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; + +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; +import org.opensearch.security.auth.HTTPAuthenticator; +import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; +import org.opensearch.security.user.AuthCredentials; +import org.opensearch.security.util.keyUtil; + +public class OnBehalfOfAuthenticator implements HTTPAuthenticator { + + protected final Logger log = LogManager.getLogger(this.getClass()); + + 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"; + + private final JwtParser jwtParser; + private final String encryptionKey; + + public OnBehalfOfAuthenticator(Settings settings) { + encryptionKey = settings.get("encryption_key"); + jwtParser = initParser(settings.get("signing_key")); + } + + private JwtParser initParser(final String signingKey) { + JwtParser _jwtParser = keyUtil.keyAlgorithmCheck(signingKey, log); + if (_jwtParser != null) { + return _jwtParser; + } else { + throw new RuntimeException("Unable to find on behalf of authenticator signing key"); + } + } + + private List extractSecurityRolesFromClaims(Claims claims) { + Object rolesObject = ObjectUtils.firstNonNull(claims.get("er"), claims.get("dr")); + List roles; + + if (rolesObject == null) { + log.warn("This is a malformed On-behalf-of Token"); + roles = List.of(); + } else { + final String rolesClaim = rolesObject.toString(); + + // Extracting roles based on the compatbility mode + String decryptedRoles = rolesClaim; + if (rolesObject == claims.get("er")) { + decryptedRoles = EncryptionDecryptionUtil.decrypt(encryptionKey, rolesClaim); + } + roles = Arrays.stream(decryptedRoles.split(",")).map(String::trim).collect(Collectors.toList()); + } + + return roles; + } + + private String[] extractBackendRolesFromClaims(Claims claims) { + // Object backendRolesObject = ObjectUtils.firstNonNull(claims.get("ebr"), claims.get("dbr")); + if (!claims.containsKey("dbr")) { + return null; + } + + Object backendRolesObject = claims.get("dbr"); + String[] backendRoles; + + if (backendRolesObject == null) { + log.warn("This is a malformed On-behalf-of Token"); + backendRoles = new String[0]; + } else { + final String backendRolesClaim = backendRolesObject.toString(); + + // Extracting roles based on the compatibility mode + String decryptedBackendRoles = backendRolesClaim; + backendRoles = Arrays.stream(decryptedBackendRoles.split(",")).map(String::trim).toArray(String[]::new); + } + + return backendRoles; + } + + @Override + @SuppressWarnings("removal") + public AuthCredentials extractCredentials(RestRequest request, ThreadContext context) throws OpenSearchSecurityException { + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + AuthCredentials creds = AccessController.doPrivileged(new PrivilegedAction() { + @Override + public AuthCredentials run() { + return extractCredentials0(request); + } + }); + + return creds; + } + + private AuthCredentials extractCredentials0(final RestRequest request) { + if (jwtParser == null) { + log.error("Missing Signing Key. JWT authentication will not work"); + return null; + } + + String jwtToken = request.header(HttpHeaders.AUTHORIZATION); + + if (jwtToken == null || jwtToken.length() == 0) { + if (log.isDebugEnabled()) { + log.debug("No JWT token found in '{}' header", HttpHeaders.AUTHORIZATION); + } + return null; + } + + if (!BEARER.matcher(jwtToken).matches()) { + jwtToken = null; + } + + if (jwtToken != null && Pattern.compile(BEARER_PREFIX).matcher(jwtToken.toLowerCase()).find()) { + jwtToken = jwtToken.substring(jwtToken.toLowerCase().indexOf(BEARER_PREFIX) + BEARER_PREFIX.length()); + } else { + if (log.isDebugEnabled()) { + log.debug("No Bearer scheme found in header"); + } + } + + if (jwtToken == null) { + return null; + } + + try { + final Claims claims = jwtParser.parseClaimsJws(jwtToken).getBody(); + + 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(audience)) { + log.error("Valid jwt on behalf of token with no audience"); + return null; + } + + List roles = extractSecurityRolesFromClaims(claims); + String[] backendRoles = extractBackendRolesFromClaims(claims); + + final AuthCredentials ac = new AuthCredentials(subject, roles, backendRoles).markComplete(); + + for (Entry claim : claims.entrySet()) { + ac.addAttribute("attr.jwt." + claim.getKey(), String.valueOf(claim.getValue())); + } + + return ac; + + } catch (WeakKeyException e) { + log.error("Cannot authenticate user with JWT because of ", e); + return null; + } catch (Exception e) { + e.printStackTrace(); + if (log.isDebugEnabled()) { + log.debug("Invalid or expired JWT token.", e); + } + return null; + } + } + + @Override + public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) { + return false; + } + + @Override + public String getType() { + return "onbehalfof_jwt"; + } + +} diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index a386e70093..3f51524e2c 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.OnBehalfOfAuthenticator; 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; @@ -362,6 +364,17 @@ 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 OnBehalfOfAuthenticator(getDynamicOnBehalfOfSettings()), + false, + -1 + ); + restAuthDomains0.add(_ad); + } + List originalDestroyableComponents = destroyableComponents; restAuthDomains = Collections.unmodifiableSortedSet(restAuthDomains0); diff --git a/src/main/java/org/opensearch/security/user/AuthCredentials.java b/src/main/java/org/opensearch/security/user/AuthCredentials.java index cab3eab6fd..f939fa28b8 100644 --- a/src/main/java/org/opensearch/security/user/AuthCredentials.java +++ b/src/main/java/org/opensearch/security/user/AuthCredentials.java @@ -32,6 +32,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -48,6 +49,7 @@ public final class AuthCredentials { private final String username; private byte[] password; private Object nativeCredentials; + private final Set securityRoles = new HashSet(); private final Set backendRoles = new HashSet(); private boolean complete; private final byte[] internalPasswordHash; @@ -94,6 +96,28 @@ public AuthCredentials(final String username, String... backendRoles) { this(username, null, null, backendRoles); } + /** + * Create new credentials with a username, a initial optional set of roles and empty password/native credentials + * @param username The username, must not be null or empty + * @param securityRoles The internal roles the user has been mapped to + * @param backendRoles set of roles this user is a member of + * @throws IllegalArgumentException if username is null or empty + */ + public AuthCredentials(final String username, List securityRoles, String... backendRoles) { + this(username, null, null, backendRoles); + this.securityRoles.addAll(securityRoles); + } + + private AuthCredentials( + final String username, + byte[] password, + Object nativeCredentials, + List securityRoles, + String... backendRoles + ) { + this(username, null, null, backendRoles); + } + private AuthCredentials(final String username, byte[] password, Object nativeCredentials, String... backendRoles) { super(); @@ -203,6 +227,14 @@ public Set getBackendRoles() { return new HashSet(backendRoles); } + /** + * + * @return Defensive copy of the security roles this user is member of. + */ + public Set getSecurityRoles() { + return new HashSet(securityRoles); + } + public boolean isComplete() { return complete; } diff --git a/src/main/java/org/opensearch/security/util/keyUtil.java b/src/main/java/org/opensearch/security/util/keyUtil.java new file mode 100644 index 0000000000..214af6da31 --- /dev/null +++ b/src/main/java/org/opensearch/security/util/keyUtil.java @@ -0,0 +1,73 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.util; + +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import org.apache.logging.log4j.Logger; + +import java.security.Key; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; +import java.util.Objects; + +public class keyUtil { + + public static JwtParser keyAlgorithmCheck(final String signingKey, final Logger log) { + if (signingKey == null || signingKey.length() == 0) { + log.error("Unable to find signing key"); + return null; + } else { + try { + Key key = null; + + final String minimalKeyFormat = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", "") + .replace("-----END PUBLIC KEY-----", ""); + + final byte[] decoded = Base64.getDecoder().decode(minimalKeyFormat); + + 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 (final Exception e) { + log.debug("No public ECDSA key, try other algos ({})", e.toString()); + } + + if (Objects.nonNull(key)) { + return Jwts.parser().setSigningKey(key); + } + + return Jwts.parser().setSigningKey(decoded); + } catch (Throwable e) { + log.error("Error while creating JWT authenticator", e); + throw new RuntimeException(e); + } + } + } + + private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) throws NoSuchAlgorithmException, + InvalidKeySpecException { + X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); + KeyFactory kf = KeyFactory.getInstance(algo); + return kf.generatePublic(spec); + } + +} 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 55ce10df65..a278a526b3 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; @@ -47,15 +48,16 @@ public void testCreateJwtWithRoles() throws Exception { String subject = "admin"; String audience = "audience_0"; List roles = List.of("IT", "HR"); + List backendRoles = List.of("Sales"); String expectedRoles = "IT,HR"; Integer expirySeconds = 300; LongSupplier currentTime = () -> (int) 100; String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); - Long expectedExp = currentTime.getAsLong() + (expirySeconds * 1000); + Long expectedExp = currentTime.getAsLong() + expirySeconds; - JwtVendor jwtVendor = new JwtVendor(settings, currentTime); - String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); + String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles); JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedJwt); JwtToken jwt = jwtConsumer.getJwtToken(); @@ -80,9 +82,9 @@ 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); + jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of()); } @Test(expected = Exception.class) @@ -94,9 +96,9 @@ 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); + jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of()); } @Test(expected = Exception.class) @@ -110,8 +112,9 @@ 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); + jwtVendor.createJwt(issuer, subject, audience, expirySecond, roles, List.of()); } + } 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 c07b0f1333..a3a267f7f7 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,6 +74,7 @@ public void testGetAccount() throws Exception { assertNotNull(body.getAsList("custom_attribute_names").size()); assertNotNull(body.getAsSettings("tenants")); assertNotNull(body.getAsList("roles")); + } @Test diff --git a/src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTest.java b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java similarity index 59% rename from src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTest.java rename to src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java index 06f19a888a..fc93acc1f8 100644 --- a/src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java @@ -12,7 +12,7 @@ package org.opensearch.security.http; import java.nio.charset.StandardCharsets; -import java.security.SecureRandom; +import java.util.Base64; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -31,70 +31,70 @@ 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; -public class HTTPOnBehalfOfJwtAuthenticatorTest { - final static byte[] secretKeyBytes = new byte[1024]; +public class OnBehalfOfAuthenticatorTest { final static String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); - final static SecretKey secretKey; - - static { - new SecureRandom().nextBytes(secretKeyBytes); - secretKey = Keys.hmacShaKeyFor(secretKeyBytes); - } - final static String signingKey = BaseEncoding.base64().encode(secretKeyBytes); + final static String signingKey = + "This is my super safe signing key that no one will ever be able to guess. It's would take billions of years and the world's most powerful quantum computer to crack"; + final static String signingKeyB64Encoded = BaseEncoding.base64().encode(signingKey.getBytes(StandardCharsets.UTF_8)); + final static SecretKey secretKey = Keys.hmacShaKeyFor(signingKeyB64Encoded.getBytes(StandardCharsets.UTF_8)); @Test public void testNoKey() throws Exception { - final AuthCredentials credentials = extractCredentialsFromJwtHeader( - null, - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy"), - false - ); - - Assert.assertNull(credentials); + try { + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + null, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy"), + false + ); + Assert.fail("Expected a RuntimeException"); + } catch (RuntimeException e) { + Assert.assertTrue(e.getMessage().contains("Unable to find on behalf of authenticator signing key")); + } } @Test public void testEmptyKey() throws Exception { - final AuthCredentials credentials = extractCredentialsFromJwtHeader( - "", - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy"), - false - ); - - Assert.assertNull(credentials); + try { + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + null, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy"), + false + ); + Assert.fail("Expected a RuntimeException"); + } catch (RuntimeException e) { + Assert.assertTrue(e.getMessage().contains("Unable to find on behalf of authenticator signing key")); + } } @Test public void testBadKey() throws Exception { - final AuthCredentials credentials = extractCredentialsFromJwtHeader( - BaseEncoding.base64().encode(new byte[] { 1, 3, 3, 4, 3, 6, 7, 8, 3, 10 }), - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy"), - false - ); - - Assert.assertNull(credentials); + try { + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + BaseEncoding.base64().encode(new byte[] { 1, 3, 3, 4, 3, 6, 7, 8, 3, 10 }), + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy"), + false + ); + Assert.fail("Expected a WeakKeyException"); + } catch (RuntimeException e) { + Assert.assertTrue(e.getMessage().contains("The specified key byte array is 80 bits")); + } } @Test public void testTokenMissing() throws Exception { - HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator( - BaseEncoding.base64().encode(secretKeyBytes), - claimsEncryptionKey - ); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); Map headers = new HashMap(); AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); @@ -107,10 +107,7 @@ public void testInvalid() throws Exception { String jwsToken = "123invalidtoken.."; - HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator( - BaseEncoding.base64().encode(secretKeyBytes), - claimsEncryptionKey - ); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); @@ -124,13 +121,10 @@ public void testBearer() throws Exception { String jwsToken = Jwts.builder() .setSubject("Leonard McCoy") .setAudience("ext_0") - .signWith(secretKey, SignatureAlgorithm.HS512) + .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), SignatureAlgorithm.HS512) .compact(); - HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator( - BaseEncoding.base64().encode(secretKeyBytes), - claimsEncryptionKey - ); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); @@ -138,6 +132,7 @@ public void testBearer() throws Exception { Assert.assertNotNull(credentials); Assert.assertEquals("Leonard McCoy", credentials.getUsername()); + Assert.assertEquals(0, credentials.getSecurityRoles().size()); Assert.assertEquals(0, credentials.getBackendRoles().size()); Assert.assertEquals(2, credentials.getAttributes().size()); } @@ -145,11 +140,13 @@ 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(); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); - HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); Map headers = new HashMap(); headers.put("Authorization", jwsToken + "Bearer " + " 123"); @@ -160,11 +157,14 @@ 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(); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); - 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); @@ -175,24 +175,25 @@ public void testRoles() throws Exception { List roles = List.of("IT", "HR"); final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, + signingKeyB64Encoded, claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy").claim("dr", "role1,role2"), + Jwts.builder().setSubject("Leonard McCoy").claim("dr", "role1,role2").setAudience("svc1"), true ); Assert.assertNotNull(credentials); Assert.assertEquals("Leonard McCoy", credentials.getUsername()); - Assert.assertEquals(2, credentials.getBackendRoles().size()); + Assert.assertEquals(2, credentials.getSecurityRoles().size()); + Assert.assertEquals(0, credentials.getBackendRoles().size()); } @Test public void testNullClaim() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, + signingKeyB64Encoded, claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy").claim("dr", null), + Jwts.builder().setSubject("Leonard McCoy").claim("dr", null).setAudience("svc1"), false ); @@ -205,30 +206,31 @@ public void testNullClaim() throws Exception { public void testNonStringClaim() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, + signingKeyB64Encoded, claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy").claim("dr", 123L), + Jwts.builder().setSubject("Leonard McCoy").claim("dr", 123L).setAudience("svc1"), true ); Assert.assertNotNull(credentials); Assert.assertEquals("Leonard McCoy", credentials.getUsername()); - Assert.assertEquals(1, credentials.getBackendRoles().size()); - Assert.assertTrue(credentials.getBackendRoles().contains("123")); + Assert.assertEquals(1, credentials.getSecurityRoles().size()); + Assert.assertTrue(credentials.getSecurityRoles().contains("123")); } @Test public void testRolesMissing() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, + signingKeyB64Encoded, claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy"), + Jwts.builder().setSubject("Leonard McCoy").setAudience("svc1"), false ); Assert.assertNotNull(credentials); Assert.assertEquals("Leonard McCoy", credentials.getUsername()); + Assert.assertEquals(0, credentials.getSecurityRoles().size()); Assert.assertEquals(0, credentials.getBackendRoles().size()); } @@ -236,9 +238,9 @@ public void testRolesMissing() throws Exception { public void testWrongSubjectKey() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, + signingKeyB64Encoded, claimsEncryptionKey, - Jwts.builder().claim("roles", "role1,role2").claim("asub", "Dr. Who"), + Jwts.builder().claim("roles", "role1,role2").claim("asub", "Dr. Who").setAudience("svc1"), false ); @@ -249,7 +251,7 @@ public void testWrongSubjectKey() throws Exception { public void testExp() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, + signingKeyB64Encoded, claimsEncryptionKey, Jwts.builder().setSubject("Expired").setExpiration(new Date(100)), false @@ -262,7 +264,7 @@ public void testExp() throws Exception { public void testNbf() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, + signingKeyB64Encoded, claimsEncryptionKey, Jwts.builder().setSubject("Expired").setNotBefore(new Date(System.currentTimeMillis() + (1000 * 36000))), false @@ -277,26 +279,36 @@ public void testRolesArray() throws Exception { JwtBuilder builder = Jwts.builder() .setPayload("{" + "\"sub\": \"Cluster_0\"," + "\"aud\": \"ext_0\"," + "\"dr\": \"a,b,3rd\"" + "}"); - final AuthCredentials credentials = extractCredentialsFromJwtHeader(signingKey, claimsEncryptionKey, builder, true); + final AuthCredentials credentials = extractCredentialsFromJwtHeader(signingKeyB64Encoded, claimsEncryptionKey, builder, true); Assert.assertNotNull(credentials); Assert.assertEquals("Cluster_0", credentials.getUsername()); - Assert.assertEquals(3, credentials.getBackendRoles().size()); - Assert.assertTrue(credentials.getBackendRoles().contains("a")); - Assert.assertTrue(credentials.getBackendRoles().contains("b")); - Assert.assertTrue(credentials.getBackendRoles().contains("3rd")); + Assert.assertEquals(3, credentials.getSecurityRoles().size()); + Assert.assertTrue(credentials.getSecurityRoles().contains("a")); + Assert.assertTrue(credentials.getSecurityRoles().contains("b")); + Assert.assertTrue(credentials.getSecurityRoles().contains("3rd")); } /** extracts a default user credential from a request header */ private AuthCredentials extractCredentialsFromJwtHeader( - final String signingKey, + final String signingKeyB64Encoded, final String encryptionKey, 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( + Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", encryptionKey).build() + ); + + final String jwsToken = jwtBuilder.signWith( + Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), + SignatureAlgorithm.HS512 + ).compact(); 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", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); + } } diff --git a/src/test/resources/config.yml b/src/test/resources/config.yml index c4bb432125..3663b3c706 100644 --- a/src/test/resources/config.yml +++ b/src/test/resources/config.yml @@ -96,6 +96,3 @@ config: multi_rolespan_enabled: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null - on_behalf_of: - signing_key: "signing key" - encryption_key: "encryption key" diff --git a/src/test/resources/restapi/securityconfig.json b/src/test/resources/restapi/securityconfig.json index 13bc7c23a6..4e4b1bba63 100644 --- a/src/test/resources/restapi/securityconfig.json +++ b/src/test/resources/restapi/securityconfig.json @@ -153,9 +153,7 @@ "do_not_fail_on_forbidden":false, "multi_rolespan_enabled":false, "hosts_resolver_mode":"ip-only", - "do_not_fail_on_forbidden_empty":false, - "on_behalf_of": { - } + "do_not_fail_on_forbidden_empty":false } } diff --git a/src/test/resources/restapi/securityconfig_nondefault.json b/src/test/resources/restapi/securityconfig_nondefault.json index e30ca9148b..a3f2a307d6 100644 --- a/src/test/resources/restapi/securityconfig_nondefault.json +++ b/src/test/resources/restapi/securityconfig_nondefault.json @@ -172,8 +172,8 @@ "hosts_resolver_mode" : "ip-only", "do_not_fail_on_forbidden_empty" : false, "on_behalf_of": { - "signing_key": "signing key", - "encryption_key": "encryption key" + "signing_key": "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z", + "encryption_key": "ZW5jcnlwdGlvbktleQ==" } } }