Skip to content

Commit

Permalink
[Feature/Extension] Add oboauthcbackend registry and set up e2e endpo…
Browse files Browse the repository at this point in the history
…int testing flow (opensearch-project#2857)

* Add OBO Authbackend

Signed-off-by: Peter Nied <petern@amazon.com>
Signed-off-by: Ryan Liang <jiallian@amazon.com>
Co-authored-by: Peter Nied <petern@amazon.com>
  • Loading branch information
RyanL1997 and peternied committed Jul 7, 2023
1 parent 8c3c639 commit 88f32e9
Show file tree
Hide file tree
Showing 23 changed files with 869 additions and 513 deletions.
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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<String, Object> 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");
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -171,6 +176,7 @@ public static class Config implements ToXContentObject {

private Boolean doNotFailOnForbidden;
private XffConfig xffConfig;
private OnBehalfOfConfig onBehalfOfConfig;
private Map<String, AuthcDomain> authcDomainMap = new LinkedHashMap<>();

private AuthFailureListeners authFailureListeners;
Expand All @@ -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;
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions src/integrationTest/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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=="
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand All @@ -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);
Expand Down Expand Up @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<String> demoCertHashes = new ArrayList<String>(3);
private volatile SecurityFilter sf;
private volatile IndexResolverReplacer irr;
Expand Down Expand Up @@ -531,6 +532,9 @@ public List<RestHandler> getRestHandlers(
principalExtractor
)
);
CreateOnBehalfOfTokenAction cobot = new CreateOnBehalfOfTokenAction(settings, threadPool, Objects.requireNonNull(cs));
dcf.registerDCFListener(cobot);
handlers.add(cobot);
handlers.addAll(
SecurityRestApiActions.getHandler(
settings,
Expand Down Expand Up @@ -1029,17 +1033,13 @@ public Collection<Object> 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);
Expand Down
Loading

0 comments on commit 88f32e9

Please sign in to comment.