Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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}"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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.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.*;
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);

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 = "Authorization";
public static final List<String> 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
);

@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))
.build();

@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();

response.assertStatusCode(200);
String username = response.getTextFromJsonBody(POINTER_USERNAME);
assertThat("testUser", equalTo(username));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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, "Bearer " + token);
}
}
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 @@ -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);
Expand Down Expand Up @@ -170,6 +175,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 @@ -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;
Expand All @@ -210,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);
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 @@ -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;
Expand Down
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.CreateOnBehalfOfToken;
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,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;
Expand Down Expand Up @@ -477,6 +478,7 @@ public List<RestHandler> 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,
Expand Down Expand Up @@ -838,17 +840,13 @@ public Collection<Object> 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);
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