Skip to content

Commit 51e996d

Browse files
opensearch-trigger-bot[bot]github-actions[bot]peterniedcwperks
authored
[Backport 2.x] Fix unconsumed parameter exception when authenticating with jwtUrlParameter (opensearch-project#4065)
Backport ccea744 from opensearch-project#3975. --------- Signed-off-by: Craig Perkins <cwperx@amazon.com> Signed-off-by: Peter Nied <peternied@hotmail.com> Signed-off-by: Peter Nied <petern@amazon.com> Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Peter Nied <petern@amazon.com> Co-authored-by: Peter Nied <peternied@hotmail.com> Co-authored-by: Craig Perkins <cwperx@amazon.com>
1 parent e3f49c3 commit 51e996d

File tree

10 files changed

+192
-4
lines changed

10 files changed

+192
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*
5+
* The OpenSearch Contributors require contributions made to
6+
* this file be licensed under the Apache-2.0 license or a
7+
* compatible open source license.
8+
*
9+
*/
10+
package org.opensearch.security.http;
11+
12+
import java.security.KeyPair;
13+
import java.util.Base64;
14+
import java.util.List;
15+
import java.util.Map;
16+
17+
import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
18+
import org.apache.http.Header;
19+
import org.junit.ClassRule;
20+
import org.junit.Rule;
21+
import org.junit.Test;
22+
import org.junit.runner.RunWith;
23+
24+
import org.opensearch.test.framework.JwtConfigBuilder;
25+
import org.opensearch.test.framework.TestSecurityConfig;
26+
import org.opensearch.test.framework.cluster.ClusterManager;
27+
import org.opensearch.test.framework.cluster.LocalCluster;
28+
import org.opensearch.test.framework.cluster.TestRestClient;
29+
import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse;
30+
import org.opensearch.test.framework.log.LogsRule;
31+
32+
import io.jsonwebtoken.SignatureAlgorithm;
33+
import io.jsonwebtoken.security.Keys;
34+
35+
import static java.nio.charset.StandardCharsets.US_ASCII;
36+
import static org.apache.http.HttpHeaders.AUTHORIZATION;
37+
import static org.hamcrest.MatcherAssert.assertThat;
38+
import static org.hamcrest.Matchers.equalTo;
39+
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL;
40+
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.BASIC_AUTH_DOMAIN_ORDER;
41+
import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS;
42+
43+
@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class)
44+
@ThreadLeakScope(ThreadLeakScope.Scope.NONE)
45+
public class JwtAuthenticationWithUrlParamTests {
46+
47+
public static final String CLAIM_USERNAME = "preferred-username";
48+
public static final String CLAIM_ROLES = "backend-user-roles";
49+
public static final String POINTER_USERNAME = "/user_name";
50+
51+
private static final KeyPair KEY_PAIR = Keys.keyPairFor(SignatureAlgorithm.RS256);
52+
private static final String PUBLIC_KEY = new String(Base64.getEncoder().encode(KEY_PAIR.getPublic().getEncoded()), US_ASCII);
53+
54+
static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS);
55+
56+
private static final String TOKEN_URL_PARAM = "token";
57+
58+
private static final JwtAuthorizationHeaderFactory tokenFactory = new JwtAuthorizationHeaderFactory(
59+
KEY_PAIR.getPrivate(),
60+
CLAIM_USERNAME,
61+
CLAIM_ROLES,
62+
AUTHORIZATION
63+
);
64+
65+
public static final TestSecurityConfig.AuthcDomain JWT_AUTH_DOMAIN = new TestSecurityConfig.AuthcDomain(
66+
"jwt",
67+
BASIC_AUTH_DOMAIN_ORDER - 1
68+
).jwtHttpAuthenticator(
69+
new JwtConfigBuilder().jwtUrlParameter(TOKEN_URL_PARAM).signingKey(PUBLIC_KEY).subjectKey(CLAIM_USERNAME).rolesKey(CLAIM_ROLES)
70+
).backend("noop");
71+
72+
@ClassRule
73+
public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE)
74+
.anonymousAuth(false)
75+
.nodeSettings(
76+
Map.of("plugins.security.restapi.roles_enabled", List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()))
77+
)
78+
.authc(AUTHC_HTTPBASIC_INTERNAL)
79+
.authc(JWT_AUTH_DOMAIN)
80+
.users(ADMIN_USER)
81+
.build();
82+
83+
@Rule
84+
public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator");
85+
86+
@Test
87+
public void shouldAuthenticateWithJwtTokenInUrl_positive() {
88+
Header jwtToken = tokenFactory.generateValidToken(ADMIN_USER.getName());
89+
String jwtTokenValue = jwtToken.getValue();
90+
try (TestRestClient client = cluster.getRestClient()) {
91+
HttpResponse response = client.getAuthInfo(Map.of(TOKEN_URL_PARAM, jwtTokenValue));
92+
93+
response.assertStatusCode(200);
94+
String username = response.getTextFromJsonBody(POINTER_USERNAME);
95+
assertThat(username, equalTo(ADMIN_USER.getName()));
96+
}
97+
}
98+
99+
@Test
100+
public void testUnauthenticatedRequest() {
101+
try (TestRestClient client = cluster.getRestClient()) {
102+
HttpResponse response = client.getAuthInfo();
103+
104+
response.assertStatusCode(401);
105+
logsRule.assertThatContainExactly(String.format("No JWT token found in '%s' url parameter header", TOKEN_URL_PARAM));
106+
}
107+
}
108+
}

src/integrationTest/java/org/opensearch/test/framework/JwtConfigBuilder.java

+9
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
public class JwtConfigBuilder {
2020
private String jwtHeader;
21+
private String jwtUrlParameter;
2122
private String signingKey;
2223
private String subjectKey;
2324
private String rolesKey;
@@ -27,6 +28,11 @@ public JwtConfigBuilder jwtHeader(String jwtHeader) {
2728
return this;
2829
}
2930

31+
public JwtConfigBuilder jwtUrlParameter(String jwtUrlParameter) {
32+
this.jwtUrlParameter = jwtUrlParameter;
33+
return this;
34+
}
35+
3036
public JwtConfigBuilder signingKey(String signingKey) {
3137
this.signingKey = signingKey;
3238
return this;
@@ -51,6 +57,9 @@ public Map<String, Object> build() {
5157
if (isNoneBlank(jwtHeader)) {
5258
builder.put("jwt_header", jwtHeader);
5359
}
60+
if (isNoneBlank(jwtUrlParameter)) {
61+
builder.put("jwt_url_parameter", jwtUrlParameter);
62+
}
5463
if (isNoneBlank(subjectKey)) {
5564
builder.put("subject_key", subjectKey);
5665
}

src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java

+7
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.util.Arrays;
3838
import java.util.Collections;
3939
import java.util.List;
40+
import java.util.Map;
4041
import java.util.Objects;
4142
import java.util.Optional;
4243
import java.util.stream.Collectors;
@@ -114,6 +115,12 @@ public HttpResponse getAuthInfo(Header... headers) {
114115
return executeRequest(new HttpGet(getHttpServerUri() + "/_opendistro/_security/authinfo?pretty"), headers);
115116
}
116117

118+
public HttpResponse getAuthInfo(Map<String, String> urlParams, Header... headers) {
119+
String urlParamsString = "?"
120+
+ urlParams.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&"));
121+
return executeRequest(new HttpGet(getHttpServerUri() + "/_opendistro/_security/authinfo" + urlParamsString), headers);
122+
}
123+
117124
public void confirmCorrectCredentials(String expectedUserName) {
118125
HttpResponse response = getAuthInfo();
119126
assertThat(response, notNullValue());

src/main/java/org/opensearch/security/filter/NettyRequest.java

+34-1
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,17 @@
1414
import java.net.InetSocketAddress;
1515
import java.util.Collections;
1616
import java.util.HashMap;
17+
import java.util.HashSet;
1718
import java.util.List;
1819
import java.util.Map;
1920
import java.util.Optional;
21+
import java.util.Set;
2022
import java.util.TreeMap;
2123
import javax.net.ssl.SSLEngine;
2224

25+
import com.google.common.base.Supplier;
26+
import com.google.common.base.Suppliers;
27+
2328
import org.opensearch.http.netty4.Netty4HttpChannel;
2429
import org.opensearch.rest.RestRequest.Method;
2530
import org.opensearch.rest.RestUtils;
@@ -34,6 +39,7 @@ public class NettyRequest implements SecurityRequest {
3439

3540
protected final HttpRequest underlyingRequest;
3641
protected final Netty4HttpChannel underlyingChannel;
42+
protected final Supplier<CheckedAccessMap> parameters = Suppliers.memoize(() -> new CheckedAccessMap(params(uri())));
3743

3844
NettyRequest(final HttpRequest request, final Netty4HttpChannel channel) {
3945
this.underlyingRequest = request;
@@ -78,7 +84,12 @@ public String uri() {
7884

7985
@Override
8086
public Map<String, String> params() {
81-
return params(underlyingRequest.uri());
87+
return parameters.get();
88+
}
89+
90+
@Override
91+
public Set<String> getUnconsumedParams() {
92+
return parameters.get().accessedKeys();
8293
}
8394

8495
private static Map<String, String> params(String uri) {
@@ -96,4 +107,26 @@ private static Map<String, String> params(String uri) {
96107

97108
return params;
98109
}
110+
111+
/** Records access of any keys if explicitly requested from this map */
112+
private static class CheckedAccessMap extends HashMap<String, String> {
113+
private final Set<String> accessedKeys = new HashSet<>();
114+
115+
public CheckedAccessMap(final Map<String, String> map) {
116+
super(map);
117+
}
118+
119+
@Override
120+
public String get(final Object key) {
121+
// Never noticed this about java's map interface the getter is not generic
122+
if (key instanceof String) {
123+
accessedKeys.add((String) key);
124+
}
125+
return super.get(key);
126+
}
127+
128+
public Set<String> accessedKeys() {
129+
return accessedKeys;
130+
}
131+
}
99132
}

src/main/java/org/opensearch/security/filter/OpenSearchRequest.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
package org.opensearch.security.filter;
1313

1414
import java.net.InetSocketAddress;
15+
import java.util.HashMap;
1516
import java.util.List;
1617
import java.util.Map;
1718
import java.util.Optional;
19+
import java.util.Set;
1820
import javax.net.ssl.SSLEngine;
1921

2022
import org.opensearch.rest.RestRequest;
@@ -71,7 +73,18 @@ public String uri() {
7173

7274
@Override
7375
public Map<String, String> params() {
74-
return underlyingRequest.params();
76+
return new HashMap<>(underlyingRequest.params()) {
77+
@Override
78+
public String get(Object key) {
79+
return underlyingRequest.param((String) key);
80+
}
81+
};
82+
}
83+
84+
@Override
85+
public Set<String> getUnconsumedParams() {
86+
// params() Map consumes explict parameter access
87+
return Set.of();
7588
}
7689

7790
/** Gets access to the underlying request object */

src/main/java/org/opensearch/security/filter/SecurityRequest.java

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.util.List;
1616
import java.util.Map;
1717
import java.util.Optional;
18+
import java.util.Set;
1819
import java.util.stream.Stream;
1920
import javax.net.ssl.SSLEngine;
2021

@@ -49,4 +50,7 @@ default String header(final String headerName) {
4950

5051
/** The parameters associated with this request */
5152
Map<String, String> params();
53+
54+
/** The list of parameters that have been accessed but not recorded as being consumed */
55+
Set<String> getUnconsumedParams();
5256
}

src/main/java/org/opensearch/security/filter/SecurityRequestChannel.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414
import java.util.Optional;
1515

1616
/**
17-
* When a request is recieved by the security plugin this governs getting information about the request and complete with with a response
17+
* When a request is received by the security plugin this governs getting information about the request and complete with a response
1818
*/
1919
public interface SecurityRequestChannel extends SecurityRequest {
2020

2121
/** Associate a response with this channel */
2222
void queueForSending(final SecurityResponse response);
2323

24-
/** Acess the queued response */
24+
/** Access the queued response */
2525
Optional<SecurityResponse> getQueuedResponse();
2626
}

src/main/java/org/opensearch/security/filter/SecurityRestFilter.java

+8
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
import static org.opensearch.security.http.SecurityHttpServerTransport.CONTEXT_TO_RESTORE;
7373
import static org.opensearch.security.http.SecurityHttpServerTransport.EARLY_RESPONSE;
7474
import static org.opensearch.security.http.SecurityHttpServerTransport.IS_AUTHENTICATED;
75+
import static org.opensearch.security.http.SecurityHttpServerTransport.UNCONSUMED_PARAMS;
7576

7677
public class SecurityRestFilter {
7778

@@ -144,6 +145,13 @@ public void handleRequest(RestRequest request, RestChannel channel, NodeClient c
144145
}
145146
});
146147

148+
NettyAttribute.popFrom(request, UNCONSUMED_PARAMS).ifPresent(unconsumedParams -> {
149+
for (String unconsumedParam : unconsumedParams) {
150+
// Consume the parameter on the RestRequest
151+
request.param(unconsumedParam);
152+
}
153+
});
154+
147155
final SecurityRequestChannel requestChannel = SecurityRequestFactory.from(request, channel);
148156

149157
// Authenticate request

src/main/java/org/opensearch/security/http/SecurityHttpServerTransport.java

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626

2727
package org.opensearch.security.http;
2828

29+
import java.util.Set;
30+
2931
import org.opensearch.common.network.NetworkService;
3032
import org.opensearch.common.settings.ClusterSettings;
3133
import org.opensearch.common.settings.Settings;
@@ -47,6 +49,7 @@
4749
public class SecurityHttpServerTransport extends SecuritySSLNettyHttpServerTransport {
4850

4951
public static final AttributeKey<SecurityResponse> EARLY_RESPONSE = AttributeKey.newInstance("opensearch-http-early-response");
52+
public static final AttributeKey<Set<String>> UNCONSUMED_PARAMS = AttributeKey.newInstance("opensearch-http-request-consumed-params");
5053
public static final AttributeKey<ThreadContext.StoredContext> CONTEXT_TO_RESTORE = AttributeKey.newInstance(
5154
"opensearch-http-request-thread-context"
5255
);

src/main/java/org/opensearch/security/ssl/http/netty/Netty4HttpRequestHeaderVerifier.java

+3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import static org.opensearch.security.http.SecurityHttpServerTransport.EARLY_RESPONSE;
3737
import static org.opensearch.security.http.SecurityHttpServerTransport.IS_AUTHENTICATED;
3838
import static org.opensearch.security.http.SecurityHttpServerTransport.SHOULD_DECOMPRESS;
39+
import static org.opensearch.security.http.SecurityHttpServerTransport.UNCONSUMED_PARAMS;
3940

4041
@Sharable
4142
public class Netty4HttpRequestHeaderVerifier extends SimpleChannelInboundHandler<DefaultHttpRequest> {
@@ -84,6 +85,8 @@ public void channelRead0(ChannelHandlerContext ctx, DefaultHttpRequest msg) thro
8485
// If request channel is completed and a response is sent, then there was a failure during authentication
8586
restFilter.checkAndAuthenticateRequest(requestChannel);
8687

88+
ctx.channel().attr(UNCONSUMED_PARAMS).set(requestChannel.getUnconsumedParams());
89+
8790
ThreadContext.StoredContext contextToRestore = threadPool.getThreadContext().newStoredContext(false);
8891
ctx.channel().attr(CONTEXT_TO_RESTORE).set(contextToRestore);
8992

0 commit comments

Comments
 (0)