Skip to content

Commit 5d341dc

Browse files
authored
Oidc additional client auth types (#58708)
The OpenID Connect specification defines a number of ways for a client (RP) to authenticate itself to the OP when accessing the Token Endpoint. We currently only support `client_secret_basic`. This change introduces support for 2 additional authentication methods, namely `client_secret_post` (where the client credentials are passed in the body of the POST request to the OP) and `client_secret_jwt` where the client constructs a JWT and signs it using the the client secret as a key. Support for the above, and especially `client_secret_jwt` in our integration tests meant that the OP we use ( Connect2id server ) should be able to validate the JWT that we send it from the RP. Since we run the OP in docker and it listens on an ephemeral port we would have no way of knowing the port so that we can configure the ES running via the testcluster to know the "correct" Token Endpoint, and even if we did, this would not be the Token Endpoint URL that the OP would think it listens on. To alleviate this, we run an ES single node cluster in docker, alongside the OP so that we can configured it with the correct hostname and port within the docker network.
1 parent 1d0e50e commit 5d341dc

File tree

13 files changed

+502
-180
lines changed

13 files changed

+502
-180
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/oidc/OpenIdConnectRealmSettings.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ public class OpenIdConnectRealmSettings {
3232
private OpenIdConnectRealmSettings() {
3333
}
3434

35-
private static final List<String> SUPPORTED_SIGNATURE_ALGORITHMS =
36-
List.of("HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512");
35+
public static final List<String> SUPPORTED_SIGNATURE_ALGORITHMS =
36+
List.of("HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512");
3737
private static final List<String> RESPONSE_TYPES = List.of("code", "id_token", "id_token token");
38+
public static final List<String> CLIENT_AUTH_METHODS = List.of("client_secret_basic", "client_secret_post", "client_secret_jwt");
39+
public static final List<String> SUPPORTED_CLIENT_AUTH_JWT_ALGORITHMS = List.of("HS256", "HS384", "HS512");
3840
public static final String TYPE = "oidc";
3941

4042
public static final Setting.AffixSetting<String> RP_CLIENT_ID
@@ -78,7 +80,22 @@ private OpenIdConnectRealmSettings() {
7880
public static final Setting.AffixSetting<List<String>> RP_REQUESTED_SCOPES = Setting.affixKeySetting(
7981
RealmSettings.realmSettingPrefix(TYPE), "rp.requested_scopes",
8082
key -> Setting.listSetting(key, Collections.singletonList("openid"), Function.identity(), Setting.Property.NodeScope));
81-
83+
public static final Setting.AffixSetting<String> RP_CLIENT_AUTH_METHOD
84+
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "rp.client_auth_method",
85+
key -> new Setting<>(key, "client_secret_basic", Function.identity(), v -> {
86+
if (CLIENT_AUTH_METHODS.contains(v) == false) {
87+
throw new IllegalArgumentException(
88+
"Invalid value [" + v + "] for [" + key + "]. Allowed values are " + CLIENT_AUTH_METHODS + "}]");
89+
}
90+
}, Setting.Property.NodeScope));
91+
public static final Setting.AffixSetting<String> RP_CLIENT_AUTH_JWT_SIGNATURE_ALGORITHM
92+
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "rp.client_auth_jwt_signature_algorithm",
93+
key -> new Setting<>(key, "HS384", Function.identity(), v -> {
94+
if (SUPPORTED_CLIENT_AUTH_JWT_ALGORITHMS.contains(v) == false) {
95+
throw new IllegalArgumentException(
96+
"Invalid value [" + v + "] for [" + key + "]. Allowed values are " + SUPPORTED_CLIENT_AUTH_JWT_ALGORITHMS + "}]");
97+
}
98+
}, Setting.Property.NodeScope));
8299
public static final Setting.AffixSetting<String> OP_AUTHORIZATION_ENDPOINT
83100
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "op.authorization_endpoint",
84101
key -> Setting.simpleString(key, v -> {
@@ -194,8 +211,9 @@ public Iterator<Setting<?>> settings() {
194211
public static Set<Setting.AffixSetting<?>> getSettings() {
195212
final Set<Setting.AffixSetting<?>> set = Sets.newHashSet(
196213
RP_CLIENT_ID, RP_REDIRECT_URI, RP_RESPONSE_TYPE, RP_REQUESTED_SCOPES, RP_CLIENT_SECRET, RP_SIGNATURE_ALGORITHM,
197-
RP_POST_LOGOUT_REDIRECT_URI, OP_AUTHORIZATION_ENDPOINT, OP_TOKEN_ENDPOINT, OP_USERINFO_ENDPOINT,
198-
OP_ENDSESSION_ENDPOINT, OP_ISSUER, OP_JWKSET_PATH, POPULATE_USER_METADATA, HTTP_CONNECT_TIMEOUT, HTTP_CONNECTION_READ_TIMEOUT,
214+
RP_POST_LOGOUT_REDIRECT_URI, RP_CLIENT_AUTH_METHOD, RP_CLIENT_AUTH_JWT_SIGNATURE_ALGORITHM, OP_AUTHORIZATION_ENDPOINT,
215+
OP_TOKEN_ENDPOINT, OP_USERINFO_ENDPOINT, OP_ENDSESSION_ENDPOINT, OP_ISSUER, OP_JWKSET_PATH,
216+
POPULATE_USER_METADATA, HTTP_CONNECT_TIMEOUT, HTTP_CONNECTION_READ_TIMEOUT,
199217
HTTP_SOCKET_TIMEOUT, HTTP_MAX_CONNECTIONS, HTTP_MAX_ENDPOINT_CONNECTIONS, HTTP_PROXY_HOST, HTTP_PROXY_PORT,
200218
HTTP_PROXY_SCHEME, ALLOWED_CLOCK_SKEW);
201219
set.addAll(DelegatedAuthorizationSettings.getSettings(TYPE));

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticator.java

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import com.nimbusds.oauth2.sdk.ErrorObject;
2323
import com.nimbusds.oauth2.sdk.ResponseType;
2424
import com.nimbusds.oauth2.sdk.TokenErrorResponse;
25+
import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
26+
import com.nimbusds.oauth2.sdk.auth.ClientSecretJWT;
2527
import com.nimbusds.oauth2.sdk.auth.Secret;
2628
import com.nimbusds.oauth2.sdk.id.State;
2729
import com.nimbusds.oauth2.sdk.token.AccessToken;
@@ -85,6 +87,7 @@
8587
import org.elasticsearch.watcher.ResourceWatcherService;
8688
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
8789
import org.elasticsearch.xpack.core.security.authc.RealmSettings;
90+
import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings;
8891
import org.elasticsearch.xpack.core.ssl.SSLConfiguration;
8992
import org.elasticsearch.xpack.core.ssl.SSLService;
9093

@@ -463,19 +466,36 @@ private void exchangeCodeForToken(AuthorizationCode code, ActionListener<Tuple<A
463466
try {
464467
final AuthorizationCodeGrant codeGrant = new AuthorizationCodeGrant(code, rpConfig.getRedirectUri());
465468
final HttpPost httpPost = new HttpPost(opConfig.getTokenEndpoint());
469+
httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
466470
final List<NameValuePair> params = new ArrayList<>();
467471
for (Map.Entry<String, List<String>> entry : codeGrant.toParameters().entrySet()) {
468472
// All parameters of AuthorizationCodeGrant are singleton lists
469473
params.add(new BasicNameValuePair(entry.getKey(), entry.getValue().get(0)));
470474
}
475+
if (rpConfig.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) {
476+
UsernamePasswordCredentials creds =
477+
new UsernamePasswordCredentials(URLEncoder.encode(rpConfig.getClientId().getValue(), StandardCharsets.UTF_8),
478+
URLEncoder.encode(rpConfig.getClientSecret().toString(), StandardCharsets.UTF_8));
479+
httpPost.addHeader(new BasicScheme().authenticate(creds, httpPost, null));
480+
} else if (rpConfig.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
481+
params.add(new BasicNameValuePair("client_id", rpConfig.getClientId().getValue()));
482+
params.add(new BasicNameValuePair("client_secret", rpConfig.getClientSecret().toString()));
483+
} else if (rpConfig.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
484+
ClientSecretJWT clientSecretJWT = new ClientSecretJWT(rpConfig.getClientId(), opConfig.getTokenEndpoint(),
485+
rpConfig.getClientAuthenticationJwtAlgorithm(), new Secret(rpConfig.getClientSecret().toString()));
486+
for (Map.Entry<String, List<String>> entry : clientSecretJWT.toParameters().entrySet()) {
487+
// Both client_assertion and client_assertion_type are singleton lists
488+
params.add(new BasicNameValuePair(entry.getKey(), entry.getValue().get(0)));
489+
}
490+
} else {
491+
tokensListener.onFailure(new ElasticsearchSecurityException("Failed to exchange code for Id Token using Token Endpoint." +
492+
"Expected client authentication method to be one of " + OpenIdConnectRealmSettings.CLIENT_AUTH_METHODS
493+
+ " but was [" + rpConfig.getClientAuthenticationMethod() + "]"));
494+
}
471495
httpPost.setEntity(new UrlEncodedFormEntity(params));
472-
httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
473-
UsernamePasswordCredentials creds =
474-
new UsernamePasswordCredentials(URLEncoder.encode(rpConfig.getClientId().getValue(), StandardCharsets.UTF_8),
475-
URLEncoder.encode(rpConfig.getClientSecret().toString(), StandardCharsets.UTF_8));
476-
httpPost.addHeader(new BasicScheme().authenticate(creds, httpPost, null));
477496
SpecialPermission.check();
478497
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
498+
479499
httpClient.execute(httpPost, new FutureCallback<HttpResponse>() {
480500
@Override
481501
public void completed(HttpResponse result) {
@@ -496,7 +516,7 @@ public void cancelled() {
496516
});
497517
return null;
498518
});
499-
} catch (AuthenticationException | UnsupportedEncodingException e) {
519+
} catch (AuthenticationException | UnsupportedEncodingException | JOSEException e) {
500520
tokensListener.onFailure(
501521
new ElasticsearchSecurityException("Failed to exchange code for Id Token using the Token Endpoint.", e));
502522
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.nimbusds.oauth2.sdk.ParseException;
1313
import com.nimbusds.oauth2.sdk.ResponseType;
1414
import com.nimbusds.oauth2.sdk.Scope;
15+
import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
1516
import com.nimbusds.oauth2.sdk.id.ClientID;
1617
import com.nimbusds.oauth2.sdk.id.Issuer;
1718
import com.nimbusds.oauth2.sdk.id.State;
@@ -71,6 +72,8 @@
7172
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.OP_USERINFO_ENDPOINT;
7273
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.POPULATE_USER_METADATA;
7374
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.PRINCIPAL_CLAIM;
75+
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_CLIENT_AUTH_JWT_SIGNATURE_ALGORITHM;
76+
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_CLIENT_AUTH_METHOD;
7477
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_CLIENT_ID;
7578
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_CLIENT_SECRET;
7679
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_POST_LOGOUT_REDIRECT_URI;
@@ -264,9 +267,11 @@ private RelyingPartyConfiguration buildRelyingPartyConfiguration(RealmConfig con
264267
requestedScope.add("openid");
265268
}
266269
final JWSAlgorithm signatureAlgorithm = JWSAlgorithm.parse(require(config, RP_SIGNATURE_ALGORITHM));
267-
270+
final ClientAuthenticationMethod clientAuthenticationMethod =
271+
ClientAuthenticationMethod.parse(require(config, RP_CLIENT_AUTH_METHOD));
272+
final JWSAlgorithm clientAuthJwtAlgorithm = JWSAlgorithm.parse(require(config, RP_CLIENT_AUTH_JWT_SIGNATURE_ALGORITHM));
268273
return new RelyingPartyConfiguration(clientId, clientSecret, redirectUri, responseType, requestedScope,
269-
signatureAlgorithm, postLogoutRedirectUri);
274+
signatureAlgorithm, clientAuthenticationMethod, clientAuthJwtAlgorithm, postLogoutRedirectUri);
270275
}
271276

272277
private OpenIdConnectProviderConfiguration buildOpenIdConnectProviderConfiguration(RealmConfig config) {

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/RelyingPartyConfiguration.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.nimbusds.jose.JWSAlgorithm;
99
import com.nimbusds.oauth2.sdk.ResponseType;
1010
import com.nimbusds.oauth2.sdk.Scope;
11+
import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
1112
import com.nimbusds.oauth2.sdk.id.ClientID;
1213
import org.elasticsearch.common.Nullable;
1314
import org.elasticsearch.common.settings.SecureString;
@@ -26,15 +27,22 @@ public class RelyingPartyConfiguration {
2627
private final Scope requestedScope;
2728
private final JWSAlgorithm signatureAlgorithm;
2829
private final URI postLogoutRedirectUri;
30+
private final ClientAuthenticationMethod clientAuthenticationMethod;
31+
private final JWSAlgorithm clientAuthenticationJwtAlgorithm;
2932

3033
public RelyingPartyConfiguration(ClientID clientId, SecureString clientSecret, URI redirectUri, ResponseType responseType,
31-
Scope requestedScope, JWSAlgorithm algorithm, @Nullable URI postLogoutRedirectUri) {
34+
Scope requestedScope, JWSAlgorithm algorithm, ClientAuthenticationMethod clientAuthenticationMethod,
35+
JWSAlgorithm clientAuthenticationJwtAlgorithm, @Nullable URI postLogoutRedirectUri) {
3236
this.clientId = Objects.requireNonNull(clientId, "clientId must be provided");
3337
this.clientSecret = Objects.requireNonNull(clientSecret, "clientSecret must be provided");
3438
this.redirectUri = Objects.requireNonNull(redirectUri, "redirectUri must be provided");
3539
this.responseType = Objects.requireNonNull(responseType, "responseType must be provided");
3640
this.requestedScope = Objects.requireNonNull(requestedScope, "responseType must be provided");
3741
this.signatureAlgorithm = Objects.requireNonNull(algorithm, "algorithm must be provided");
42+
this.clientAuthenticationMethod = Objects.requireNonNull(clientAuthenticationMethod,
43+
"clientAuthenticationMethod must be provided");
44+
this.clientAuthenticationJwtAlgorithm = Objects.requireNonNull(clientAuthenticationJwtAlgorithm,
45+
"clientAuthenticationJwtAlgorithm must be provided");
3846
this.postLogoutRedirectUri = postLogoutRedirectUri;
3947
}
4048

@@ -65,4 +73,12 @@ public JWSAlgorithm getSignatureAlgorithm() {
6573
public URI getPostLogoutRedirectUri() {
6674
return postLogoutRedirectUri;
6775
}
76+
77+
public ClientAuthenticationMethod getClientAuthenticationMethod() {
78+
return clientAuthenticationMethod;
79+
}
80+
81+
public JWSAlgorithm getClientAuthenticationJwtAlgorithm() {
82+
return clientAuthenticationJwtAlgorithm;
83+
}
6884
}

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.nimbusds.jwt.proc.BadJWTException;
2828
import com.nimbusds.oauth2.sdk.ResponseType;
2929
import com.nimbusds.oauth2.sdk.Scope;
30+
import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
3031
import com.nimbusds.oauth2.sdk.auth.Secret;
3132
import com.nimbusds.oauth2.sdk.id.ClientID;
3233
import com.nimbusds.oauth2.sdk.id.Issuer;
@@ -886,8 +887,11 @@ private RelyingPartyConfiguration getDefaultRpConfig() throws URISyntaxException
886887
new ResponseType("id_token", "token"),
887888
new Scope("openid"),
888889
JWSAlgorithm.RS384,
890+
ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
891+
JWSAlgorithm.HS384,
889892
new URI("https://rp.elastic.co/successfull_logout"));
890893
}
894+
891895
private RelyingPartyConfiguration getRpConfig(String alg) throws URISyntaxException {
892896
return new RelyingPartyConfiguration(
893897
new ClientID("rp-my"),
@@ -896,6 +900,8 @@ private RelyingPartyConfiguration getRpConfig(String alg) throws URISyntaxExcept
896900
new ResponseType("id_token", "token"),
897901
new Scope("openid"),
898902
JWSAlgorithm.parse(alg),
903+
ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
904+
JWSAlgorithm.HS384,
899905
new URI("https://rp.elastic.co/successfull_logout"));
900906
}
901907

@@ -907,6 +913,8 @@ private RelyingPartyConfiguration getRpConfigNoAccessToken(String alg) throws UR
907913
new ResponseType("id_token"),
908914
new Scope("openid"),
909915
JWSAlgorithm.parse(alg),
916+
ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
917+
JWSAlgorithm.HS384,
910918
new URI("https://rp.elastic.co/successfull_logout"));
911919
}
912920

0 commit comments

Comments
 (0)