Skip to content

Commit 1ce9e6b

Browse files
committed
Test OAuth2 support with JWT tokens
1 parent 214e042 commit 1ce9e6b

File tree

3 files changed

+170
-86
lines changed

3 files changed

+170
-86
lines changed

ci/start-broker.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ wait_for_message() {
1212

1313
make -C "${PWD}"/tls-gen/basic
1414

15+
rm -rf rabbitmq-configuration
1516
mkdir -p rabbitmq-configuration/tls
1617
cp -R "${PWD}"/tls-gen/basic/result/* rabbitmq-configuration/tls
1718
chmod o+r rabbitmq-configuration/tls/*
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) 2024 Broadcom. All Rights Reserved.
2+
// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
// If you have any questions regarding licensing, please contact us at
17+
18+
package com.rabbitmq.client.amqp.impl;
19+
20+
import com.google.gson.Gson;
21+
import com.google.gson.reflect.TypeToken;
22+
import com.rabbitmq.client.amqp.oauth.Token;
23+
import java.util.Base64;
24+
import java.util.List;
25+
import java.util.Map;
26+
import org.jose4j.jws.AlgorithmIdentifiers;
27+
import org.jose4j.jws.JsonWebSignature;
28+
import org.jose4j.jwt.JwtClaims;
29+
import org.jose4j.jwt.NumericDate;
30+
import org.jose4j.jwt.consumer.JwtConsumer;
31+
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
32+
import org.jose4j.keys.HmacKey;
33+
34+
final class JwtTestUtils {
35+
36+
private static final String BASE64_KEY = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGH";
37+
private static final HmacKey KEY = new HmacKey(Base64.getDecoder().decode(BASE64_KEY));
38+
private static final String AUDIENCE = "rabbitmq";
39+
private static final Gson GSON = new Gson();
40+
private static final TypeToken<Map<String, Object>> MAP_TYPE = new TypeToken<>() {};
41+
42+
private JwtTestUtils() {}
43+
44+
static String token(long expirationTime) {
45+
try {
46+
JwtClaims claims = new JwtClaims();
47+
claims.setIssuer("unit_test");
48+
claims.setAudience(AUDIENCE);
49+
claims.setExpirationTime(NumericDate.fromMilliseconds(expirationTime));
50+
claims.setStringListClaim(
51+
"scope", List.of("rabbitmq.configure:*/*", "rabbitmq.write:*/*", "rabbitmq.read:*/*"));
52+
53+
JsonWebSignature signature = new JsonWebSignature();
54+
55+
signature.setKeyIdHeaderValue("token-key");
56+
signature.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256);
57+
signature.setKey(KEY);
58+
signature.setPayload(claims.toJson());
59+
return signature.getCompactSerialization();
60+
} catch (Exception e) {
61+
System.out.println("ERROR " + e.getMessage());
62+
throw new RuntimeException(e);
63+
}
64+
}
65+
66+
static Token parseToken(String tokenAsString) {
67+
long expirationTime;
68+
try {
69+
JwtConsumer consumer =
70+
new JwtConsumerBuilder()
71+
.setExpectedAudience(AUDIENCE)
72+
// we do not validate the expiration time
73+
.setEvaluationTime(NumericDate.fromMilliseconds(0))
74+
.setVerificationKey(KEY)
75+
.build();
76+
JwtClaims claims = consumer.processToClaims(tokenAsString);
77+
expirationTime = claims.getExpirationTime().getValueInMillis();
78+
} catch (Exception e) {
79+
throw new RuntimeException(e);
80+
}
81+
return new Token() {
82+
@Override
83+
public String value() {
84+
return tokenAsString;
85+
}
86+
87+
@Override
88+
public long expirationTime() {
89+
return expirationTime;
90+
}
91+
};
92+
}
93+
94+
static Map<String, Object> parse(String json) {
95+
return GSON.fromJson(json, MAP_TYPE);
96+
}
97+
}

src/test/java/com/rabbitmq/client/amqp/impl/Oauth2Test.java

Lines changed: 72 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
import static com.rabbitmq.client.amqp.ConnectionSettings.SASL_MECHANISM_PLAIN;
2121
import static com.rabbitmq.client.amqp.impl.Assertions.assertThat;
22+
import static com.rabbitmq.client.amqp.impl.HttpTestUtils.startHttpServer;
23+
import static com.rabbitmq.client.amqp.impl.JwtTestUtils.*;
2224
import static com.rabbitmq.client.amqp.impl.TestConditions.BrokerVersion.RABBITMQ_4_1_0;
2325
import static com.rabbitmq.client.amqp.impl.TestUtils.*;
2426
import static java.lang.System.currentTimeMillis;
@@ -32,22 +34,15 @@
3234
import com.rabbitmq.client.amqp.impl.TestUtils.DisabledIfOauth2AuthBackendNotEnabled;
3335
import com.rabbitmq.client.amqp.impl.TestUtils.Sync;
3436
import com.rabbitmq.client.amqp.oauth.HttpTokenRequester;
35-
import com.rabbitmq.client.amqp.oauth.Token;
3637
import com.rabbitmq.client.amqp.oauth.TokenParser;
38+
import com.rabbitmq.client.amqp.oauth.TokenRequester;
3739
import com.sun.net.httpserver.Headers;
40+
import com.sun.net.httpserver.HttpHandler;
3841
import com.sun.net.httpserver.HttpServer;
3942
import java.io.OutputStream;
4043
import java.time.Duration;
41-
import java.util.Base64;
4244
import java.util.Collections;
43-
import java.util.List;
44-
import org.jose4j.jws.AlgorithmIdentifiers;
45-
import org.jose4j.jws.JsonWebSignature;
46-
import org.jose4j.jwt.JwtClaims;
47-
import org.jose4j.jwt.NumericDate;
48-
import org.jose4j.jwt.consumer.JwtConsumer;
49-
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
50-
import org.jose4j.keys.HmacKey;
45+
import java.util.function.LongSupplier;
5146
import org.jose4j.lang.JoseException;
5247
import org.junit.jupiter.api.AfterEach;
5348
import org.junit.jupiter.api.Test;
@@ -57,32 +52,9 @@
5752
@DisabledIfOauth2AuthBackendNotEnabled
5853
public class Oauth2Test {
5954

60-
private static final String BASE64_KEY = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGH";
61-
private static final HmacKey KEY = new HmacKey(Base64.getDecoder().decode(BASE64_KEY));
6255
Environment environment;
6356
HttpServer server;
6457

65-
private static String token(long expirationTime) {
66-
JwtClaims claims = new JwtClaims();
67-
claims.setIssuer("unit_test");
68-
claims.setAudience("rabbitmq");
69-
claims.setExpirationTime(NumericDate.fromMilliseconds(expirationTime));
70-
claims.setStringListClaim(
71-
"scope", List.of("rabbitmq.configure:*/*", "rabbitmq.write:*/*", "rabbitmq.read:*/*"));
72-
73-
JsonWebSignature signature = new JsonWebSignature();
74-
75-
signature.setKeyIdHeaderValue("token-key");
76-
signature.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256);
77-
signature.setKey(KEY);
78-
signature.setPayload(claims.toJson());
79-
try {
80-
return signature.getCompactSerialization();
81-
} catch (JoseException e) {
82-
throw new RuntimeException(e);
83-
}
84-
}
85-
8658
@AfterEach
8759
void tearDown() {
8860
if (this.server != null) {
@@ -108,7 +80,7 @@ void validTokenShouldSucceed() {
10880
@Test
10981
@BrokerVersionAtLeast(RABBITMQ_4_1_0)
11082
void connectionShouldBeClosedWhenTokenExpires(TestInfo info) throws JoseException {
111-
String q = TestUtils.name(info);
83+
String q = name(info);
11284
long expiry = currentTimeMillis() + ofSeconds(2).toMillis();
11385
String token = token(expiry);
11486
Sync connectionClosedSync = sync();
@@ -156,63 +128,19 @@ void connectionShouldBeClosedWhenTokenExpires(TestInfo info) throws JoseExceptio
156128

157129
@Test
158130
@BrokerVersionAtLeast(RABBITMQ_4_1_0)
159-
void httpClientProviderShouldGetToken(TestInfo info) throws Exception {
160-
String q = TestUtils.name(info);
161-
131+
void openingConnectionWithHttpValidTokenShouldWork(TestInfo info) throws Exception {
132+
String q = name(info);
162133
int port = randomNetworkPort();
163134
String contextPath = "/uaa/oauth/token";
164135

165136
this.server =
166-
HttpTestUtils.startHttpServer(
167-
port,
168-
contextPath,
169-
exchange -> {
170-
long expirationTime = currentTimeMillis() + 60_000;
171-
byte[] data = token(expirationTime).getBytes(UTF_8);
172-
Headers responseHeaders = exchange.getResponseHeaders();
173-
responseHeaders.set("content-type", "application/json");
174-
exchange.sendResponseHeaders(200, data.length);
175-
OutputStream responseBody = exchange.getResponseBody();
176-
responseBody.write(data);
177-
responseBody.close();
178-
});
179-
180-
HttpTokenRequester tokenRequester =
181-
new HttpTokenRequester(
182-
"http://localhost:" + port + contextPath,
183-
"",
184-
"",
185-
"",
186-
Collections.emptyMap(),
187-
null,
188-
null,
189-
null,
190-
null);
137+
startHttpServer(port, contextPath, tokenHttpHandler(() -> currentTimeMillis() + 60_000));
138+
139+
TokenRequester tokenRequester = httpTokenRequester("http://localhost:" + port + contextPath);
191140
TokenParser tokenParser =
192-
tokenAsString -> {
193-
long expirationTime;
194-
try {
195-
JwtConsumer consumer =
196-
new JwtConsumerBuilder()
197-
.setExpectedAudience("rabbitmq")
198-
.setVerificationKey(KEY)
199-
.build();
200-
JwtClaims claims = consumer.processToClaims(tokenAsString);
201-
expirationTime = claims.getExpirationTime().getValueInMillis();
202-
} catch (Exception e) {
203-
throw new RuntimeException(e);
204-
}
205-
return new Token() {
206-
@Override
207-
public String value() {
208-
return tokenAsString;
209-
}
210-
211-
@Override
212-
public long expirationTime() {
213-
return expirationTime;
214-
}
215-
};
141+
json -> {
142+
String compact = parse(json).get("value").toString();
143+
return parseToken(compact);
216144
};
217145

218146
CredentialsProvider credentialsProvider =
@@ -224,5 +152,63 @@ public long expirationTime() {
224152
.credentialsProvider(credentialsProvider)
225153
.build();
226154
c.management().queue(q).exclusive(true).declare();
155+
Publisher publisher = c.publisherBuilder().queue(q).build();
156+
Sync consumeSync = TestUtils.sync();
157+
c.consumerBuilder()
158+
.queue(q)
159+
.messageHandler(
160+
(ctx, msg) -> {
161+
ctx.accept();
162+
consumeSync.down();
163+
})
164+
.build();
165+
publisher.publish(publisher.message(), ctx -> {});
166+
assertThat(consumeSync).completes();
167+
}
168+
169+
@Test
170+
@BrokerVersionAtLeast(RABBITMQ_4_1_0)
171+
void openingConnectionWithHttpExpiredTokenShouldFail() throws Exception {
172+
int port = randomNetworkPort();
173+
String contextPath = "/uaa/oauth/token";
174+
175+
this.server =
176+
startHttpServer(port, contextPath, tokenHttpHandler(() -> currentTimeMillis() - 60_000));
177+
178+
TokenRequester tokenRequester = httpTokenRequester("http://localhost:" + port + contextPath);
179+
TokenParser tokenParser =
180+
json -> {
181+
String compact = parse(json).get("value").toString();
182+
return parseToken(compact);
183+
};
184+
185+
CredentialsProvider credentialsProvider =
186+
new OAuthCredentialsProvider(tokenRequester, tokenParser);
187+
assertThatThrownBy(
188+
() ->
189+
environment
190+
.connectionBuilder()
191+
.saslMechanism(SASL_MECHANISM_PLAIN)
192+
.credentialsProvider(credentialsProvider)
193+
.build())
194+
.isInstanceOf(AmqpException.AmqpSecurityException.class);
195+
}
196+
197+
private static HttpHandler tokenHttpHandler(LongSupplier expirationTimeSupplier) {
198+
return exchange -> {
199+
long expirationTime = expirationTimeSupplier.getAsLong();
200+
String token = token(expirationTime);
201+
byte[] data = String.format("{ 'value': '%s' }", token).replace('\'', '"').getBytes(UTF_8);
202+
Headers responseHeaders = exchange.getResponseHeaders();
203+
responseHeaders.set("content-type", "application/json");
204+
exchange.sendResponseHeaders(200, data.length);
205+
OutputStream responseBody = exchange.getResponseBody();
206+
responseBody.write(data);
207+
responseBody.close();
208+
};
209+
}
210+
211+
private static TokenRequester httpTokenRequester(String uri) {
212+
return new HttpTokenRequester(uri, "", "", "", Collections.emptyMap(), null, null, null, null);
227213
}
228214
}

0 commit comments

Comments
 (0)