Skip to content

Commit 229daee

Browse files
authored
Add client support for SASL extensions (#231)
Signed-off-by: Marko Strukelj <[email protected]>
1 parent 10d3c0e commit 229daee

File tree

5 files changed

+109
-6
lines changed

5 files changed

+109
-6
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,16 @@ You may also specify a pause time between requests in order not to flood the aut
10651065
The default value is '0', meaning 'no pause'. Provide the value greater than '0' to set the pause time between attempts in milliseconds:
10661066
- `oauth.http.retry.pause.millis` (e.g.: "500" - if a retry is attempted, there will first be a half-a-second pause)
10671067

1068+
### Configuring the SASL extensions
1069+
1070+
If your Kafka Broker uses some other custom `OAUTHBEARER` implementation, you may need to pass it SASL extensions options.
1071+
These are key:value pairs representing a client context, that are sent to the Kafka Broker when the new session is started.
1072+
1073+
You can pass SASL extensions options by using `oauth.sasl.extension.` as a key prefix:
1074+
- `oauth.sasl.extension.KEY` (e.g.: "VALUE" - replace KEY with the actual SASL extension key name, and VALUE with the actual value)
1075+
1076+
For example, you could add multiple sasl extensions options: `oauth.sasl.extension.key1="value1" oauth.sasl.extension.key2="value2"`
1077+
10681078

10691079
### Configuring the re-authentication on the client
10701080

oauth-client/src/main/java/io/strimzi/kafka/oauth/client/ClientConfig.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ public class ClientConfig extends Config {
6666
*/
6767
public static final String OAUTH_CLIENT_ASSERTION_TYPE = "oauth.client.assertion.type";
6868

69+
/**
70+
* A prefix to use to pass SASL extensions options
71+
*/
72+
public static final String OAUTH_SASL_EXTENSION_PREFIX = "oauth.sasl.extension.";
73+
6974
/**
7075
* Create a new instance
7176
*/

oauth-client/src/main/java/io/strimzi/kafka/oauth/client/JaasClientOauthLoginCallbackHandler.java

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import io.strimzi.kafka.oauth.services.OAuthMetrics;
2020
import io.strimzi.kafka.oauth.services.Services;
2121
import org.apache.kafka.common.security.auth.AuthenticateCallbackHandler;
22+
import org.apache.kafka.common.security.auth.SaslExtensions;
23+
import org.apache.kafka.common.security.auth.SaslExtensionsCallback;
2224
import org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule;
2325
import org.apache.kafka.common.security.oauthbearer.OAuthBearerToken;
2426
import org.apache.kafka.common.security.oauthbearer.OAuthBearerTokenCallback;
@@ -33,10 +35,13 @@
3335
import java.io.IOException;
3436
import java.net.URI;
3537
import java.net.URISyntaxException;
38+
import java.util.Collections;
39+
import java.util.LinkedHashMap;
3640
import java.util.List;
3741
import java.util.Map;
3842
import java.util.Properties;
3943
import java.util.Set;
44+
import java.util.regex.Pattern;
4045

4146
import static io.strimzi.kafka.oauth.common.ConfigUtil.getConnectTimeout;
4247
import static io.strimzi.kafka.oauth.common.ConfigUtil.getReadTimeout;
@@ -89,15 +94,24 @@ public class JaasClientOauthLoginCallbackHandler implements AuthenticateCallback
8994
private final ClientMetricsHandler authenticatorMetrics = new ClientMetricsHandler();
9095
private boolean includeAcceptHeader;
9196

97+
// Using ordered map helps with predictable logging output which can be tested
98+
private final Map<String, String> saslExtensions = new LinkedHashMap<>();
99+
private static final Pattern SASL_KEY_VALIDATION_PATTERN = Pattern.compile("[A-Za-z]+");
100+
private static final Pattern SASL_VALUE_VALIDATION_PATTERN = Pattern.compile("[\\x21-\\x7E \t\r\n]+");
101+
102+
92103
@Override
93104
public void configure(Map<String, ?> configs, String saslMechanism, List<AppConfigurationEntry> jaasConfigEntries) {
94105
if (!OAuthBearerLoginModule.OAUTHBEARER_MECHANISM.equals(saslMechanism)) {
95106
throw new IllegalArgumentException("Unexpected SASL mechanism: " + saslMechanism);
96107
}
97108

98-
for (AppConfigurationEntry e: jaasConfigEntries) {
109+
Map<String, ?> options = Collections.emptyMap();
110+
111+
if (!jaasConfigEntries.isEmpty()) {
112+
options = jaasConfigEntries.get(0).getOptions();
99113
Properties p = new Properties();
100-
p.putAll(e.getOptions());
114+
p.putAll(options);
101115
config = new ClientConfig(p);
102116
}
103117

@@ -166,6 +180,17 @@ public void configure(Map<String, ?> configs, String saslMechanism, List<AppConf
166180

167181
String configId = configureMetrics(configs);
168182

183+
// Process extensions configuration
184+
for (String key: options.keySet()) {
185+
if (key.startsWith(ClientConfig.OAUTH_SASL_EXTENSION_PREFIX)) {
186+
String value = config.getValue(key, "");
187+
key = key.substring(ClientConfig.OAUTH_SASL_EXTENSION_PREFIX.length());
188+
189+
validateSaslExtension(key, value);
190+
saslExtensions.put(key, value);
191+
}
192+
}
193+
169194
if (LOG.isDebugEnabled()) {
170195
LOG.debug("Configured JaasClientOauthLoginCallbackHandler:"
171196
+ "\n configId: " + configId
@@ -191,7 +216,17 @@ public void configure(Map<String, ?> configs, String saslMechanism, List<AppConf
191216
+ "\n retries: " + retries
192217
+ "\n retryPauseMillis: " + retryPauseMillis
193218
+ "\n enableMetrics: " + enableMetrics
194-
+ "\n includeAcceptHeader: " + includeAcceptHeader);
219+
+ "\n includeAcceptHeader: " + includeAcceptHeader
220+
+ "\n saslExtensions: " + saslExtensions);
221+
}
222+
}
223+
224+
private void validateSaslExtension(String key, String value) {
225+
if (!SASL_KEY_VALIDATION_PATTERN.matcher(key).matches() || "auth".equals(key)) {
226+
throw new ConfigException("Invalid sasl extension key: '" + key + "' ('" + ClientConfig.OAUTH_SASL_EXTENSION_PREFIX + key + "')");
227+
}
228+
if (!SASL_VALUE_VALIDATION_PATTERN.matcher(value).matches()) {
229+
throw new ConfigException("Invalid sasl extension value for key: '" + key + "' ('" + ClientConfig.OAUTH_SASL_EXTENSION_PREFIX + key + "')");
195230
}
196231
}
197232

@@ -332,12 +367,19 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback
332367
for (Callback callback : callbacks) {
333368
if (callback instanceof OAuthBearerTokenCallback) {
334369
handleCallback((OAuthBearerTokenCallback) callback);
370+
} else if (callback instanceof SaslExtensionsCallback) {
371+
handleExtensionsCallback((SaslExtensionsCallback) callback);
335372
} else {
336373
throw new UnsupportedCallbackException(callback);
337374
}
338375
}
339376
}
340377

378+
private void handleExtensionsCallback(SaslExtensionsCallback callback) {
379+
SaslExtensions extensions = new SaslExtensions(saslExtensions);
380+
callback.extensions(extensions);
381+
}
382+
341383
private void handleCallback(OAuthBearerTokenCallback callback) throws IOException {
342384
if (callback.token() != null) {
343385
throw new IllegalArgumentException("Callback had a token already");

testsuite/mockoauth-tests/src/test/java/io/strimzi/testsuite/oauth/mockoauth/JaasClientConfigTest.java

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ public void doTest() throws Exception {
7070

7171
testAllConfigOptions();
7272

73+
testSaslExtensions();
74+
7375
testAccessTokenLocation();
7476

7577
testRefreshTokenLocation();
@@ -93,7 +95,7 @@ private void testAllConfigOptions() throws IOException {
9395
attrs.put(ClientConfig.OAUTH_PASSWORD_GRANT_PASSWORD, "password");
9496
attrs.put(ClientConfig.OAUTH_USERNAME_CLAIM, "username-claim");
9597
attrs.put(ClientConfig.OAUTH_FALLBACK_USERNAME_CLAIM, "fallback-username-claim");
96-
attrs.put(ClientConfig.OAUTH_FALLBACK_USERNAME_PREFIX, "username-prefix");
98+
attrs.put(ClientConfig.OAUTH_FALLBACK_USERNAME_PREFIX, "fallback-username-prefix");
9799
attrs.put(ClientConfig.OAUTH_SCOPE, "scope");
98100
attrs.put(ClientConfig.OAUTH_AUDIENCE, "audience");
99101
attrs.put(ClientConfig.OAUTH_ACCESS_TOKEN_IS_JWT, "false");
@@ -104,6 +106,8 @@ private void testAllConfigOptions() throws IOException {
104106
attrs.put(ClientConfig.OAUTH_HTTP_RETRY_PAUSE_MILLIS, "500");
105107
attrs.put(ClientConfig.OAUTH_ENABLE_METRICS, "true");
106108
attrs.put(ClientConfig.OAUTH_INCLUDE_ACCEPT_HEADER, "false");
109+
attrs.put(ClientConfig.OAUTH_SASL_EXTENSION_PREFIX + "poolid", "poolid-value");
110+
attrs.put(ClientConfig.OAUTH_SASL_EXTENSION_PREFIX + "group.ref", "group-ref-value");
107111

108112

109113
AppConfigurationEntry jaasConfig = new AppConfigurationEntry("org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, attrs);
@@ -116,6 +120,18 @@ private void testAllConfigOptions() throws IOException {
116120
LogLineReader logReader = new LogLineReader(Common.LOG_PATH);
117121
logReader.readNext();
118122

123+
try {
124+
loginHandler.configure(clientProps, "OAUTHBEARER", Collections.singletonList(jaasConfig));
125+
} catch (Exception e) {
126+
Assert.assertTrue("Is a ConfigException", e instanceof ConfigException);
127+
Assert.assertTrue("Invalid sasl extension key: " + e.getMessage(), e.getMessage().contains("Invalid sasl extension key: 'group.ref'"));
128+
}
129+
130+
logReader.readNext();
131+
132+
attrs.remove(ClientConfig.OAUTH_SASL_EXTENSION_PREFIX + "group.ref");
133+
attrs.put(ClientConfig.OAUTH_SASL_EXTENSION_PREFIX + "group", "group-ref-value");
134+
119135
loginHandler.configure(clientProps, "OAUTHBEARER", Collections.singletonList(jaasConfig));
120136

121137
Common.checkLog(logReader, "configId", "config-id",
@@ -139,7 +155,8 @@ private void testAllConfigOptions() throws IOException {
139155
"retries", "3",
140156
"retryPauseMillis", "500",
141157
"enableMetrics", "true",
142-
"includeAcceptHeader", "false");
158+
"includeAcceptHeader", "false",
159+
"saslExtensions", "\\{poolid=poolid-value, group=group-ref-value\\}");
143160

144161

145162
// we could not check tokenEndpointUri and token in the same run
@@ -357,6 +374,34 @@ private void testValidConfigurations() {
357374
}
358375
}
359376

377+
private void testSaslExtensions() throws Exception {
378+
String testClient = "testclient";
379+
String testSecret = "testsecret";
380+
381+
changeAuthServerMode("jwks", "mode_200");
382+
changeAuthServerMode("token", "mode_200");
383+
createOAuthClient(testClient, testSecret);
384+
385+
Map<String, String> oauthConfig = new HashMap<>();
386+
oauthConfig.put(ClientConfig.OAUTH_TOKEN_ENDPOINT_URI, TOKEN_ENDPOINT_URI);
387+
oauthConfig.put(ClientConfig.OAUTH_CLIENT_ID, testClient);
388+
oauthConfig.put(ClientConfig.OAUTH_CLIENT_SECRET, testSecret);
389+
oauthConfig.put(ClientConfig.OAUTH_SSL_TRUSTSTORE_LOCATION, "../docker/target/kafka/certs/ca-truststore.p12");
390+
oauthConfig.put(ClientConfig.OAUTH_SSL_TRUSTSTORE_PASSWORD, "changeit");
391+
oauthConfig.put(ClientConfig.OAUTH_SASL_EXTENSION_PREFIX + "extoption", "optionvalue");
392+
393+
LogLineReader logReader = new LogLineReader(Common.LOG_PATH);
394+
logReader.readNext();
395+
396+
// If it fails with 'Unknown signing key' it means that Kafka has not managed to load JWKS keys yet
397+
// due to jwks endpoint returning status 404 initially
398+
initJaasWithRetry(oauthConfig);
399+
400+
List<String> lines = logReader.readNext();
401+
// Check in the log that SASL extensions have been properly set
402+
checkLogForRegex(lines, ".*LoginManager.*extensionsMap=\\{extoption=optionvalue\\}.*");
403+
}
404+
360405
private void testAccessTokenLocation() throws Exception {
361406

362407
String testClient = "testclient";
@@ -369,7 +414,7 @@ private void testAccessTokenLocation() throws Exception {
369414
String accessToken = loginWithClientSecret(TOKEN_ENDPOINT_URI, testClient, testSecret, "../docker/target/kafka/certs/ca-truststore.p12", "changeit");
370415

371416
Path accessTokenFilePath = Paths.get("target/access_token_file");
372-
Files.write(accessTokenFilePath, accessToken.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE_NEW);
417+
Files.write(accessTokenFilePath, accessToken.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
373418
try {
374419
LogLineReader logReader = new LogLineReader(Common.LOG_PATH);
375420
logReader.readNext();

testsuite/mockoauth-tests/src/test/resources/simplelogger.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
org.slf4j.simpleLogger.logFile=target/test.log
33
org.slf4j.simpleLogger.showDateTime=true
44
org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS
5+
org.slf4j.simpleLogger.log.org.apache.kafka.common.security=TRACE
56
org.slf4j.simpleLogger.log.org.apache.kafka=OFF
67
org.slf4j.simpleLogger.log.io.strimzi=TRACE

0 commit comments

Comments
 (0)