Skip to content

Commit 13491e0

Browse files
jkakavasywangd
authored andcommitted
Oidc additional client auth types (elastic#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 7779c1f commit 13491e0

File tree

13 files changed

+507
-181
lines changed

13 files changed

+507
-181
lines changed

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

+28-6
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,15 @@ public class OpenIdConnectRealmSettings {
3232
private OpenIdConnectRealmSettings() {
3333
}
3434

35-
private static final List<String> SUPPORTED_SIGNATURE_ALGORITHMS = Collections.unmodifiableList(
36-
Arrays.asList("HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512"));
37-
private static final List<String> RESPONSE_TYPES = Arrays.asList("code", "id_token", "id_token token");
35+
public static final List<String> SUPPORTED_SIGNATURE_ALGORITHMS =
36+
org.elasticsearch.common.collect.List.of(
37+
"HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512");
38+
private static final List<String> RESPONSE_TYPES = org.elasticsearch.common.collect.List.of(
39+
"code", "id_token", "id_token token");
40+
public static final List<String> CLIENT_AUTH_METHODS = org.elasticsearch.common.collect.List.of(
41+
"client_secret_basic", "client_secret_post", "client_secret_jwt");
42+
public static final List<String> SUPPORTED_CLIENT_AUTH_JWT_ALGORITHMS = org.elasticsearch.common.collect.List.of(
43+
"HS256", "HS384", "HS512");
3844
public static final String TYPE = "oidc";
3945

4046
public static final Setting.AffixSetting<String> RP_CLIENT_ID
@@ -78,7 +84,22 @@ private OpenIdConnectRealmSettings() {
7884
public static final Setting.AffixSetting<List<String>> RP_REQUESTED_SCOPES = Setting.affixKeySetting(
7985
RealmSettings.realmSettingPrefix(TYPE), "rp.requested_scopes",
8086
key -> Setting.listSetting(key, Collections.singletonList("openid"), Function.identity(), Setting.Property.NodeScope));
81-
87+
public static final Setting.AffixSetting<String> RP_CLIENT_AUTH_METHOD
88+
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "rp.client_auth_method",
89+
key -> new Setting<>(key, "client_secret_basic", Function.identity(), v -> {
90+
if (CLIENT_AUTH_METHODS.contains(v) == false) {
91+
throw new IllegalArgumentException(
92+
"Invalid value [" + v + "] for [" + key + "]. Allowed values are " + CLIENT_AUTH_METHODS + "}]");
93+
}
94+
}, Setting.Property.NodeScope));
95+
public static final Setting.AffixSetting<String> RP_CLIENT_AUTH_JWT_SIGNATURE_ALGORITHM
96+
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "rp.client_auth_jwt_signature_algorithm",
97+
key -> new Setting<>(key, "HS384", Function.identity(), v -> {
98+
if (SUPPORTED_CLIENT_AUTH_JWT_ALGORITHMS.contains(v) == false) {
99+
throw new IllegalArgumentException(
100+
"Invalid value [" + v + "] for [" + key + "]. Allowed values are " + SUPPORTED_CLIENT_AUTH_JWT_ALGORITHMS + "}]");
101+
}
102+
}, Setting.Property.NodeScope));
82103
public static final Setting.AffixSetting<String> OP_AUTHORIZATION_ENDPOINT
83104
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "op.authorization_endpoint",
84105
key -> Setting.simpleString(key, v -> {
@@ -194,8 +215,9 @@ public Iterator<Setting<?>> settings() {
194215
public static Set<Setting.AffixSetting<?>> getSettings() {
195216
final Set<Setting.AffixSetting<?>> set = Sets.newHashSet(
196217
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,
218+
RP_POST_LOGOUT_REDIRECT_URI, RP_CLIENT_AUTH_METHOD, RP_CLIENT_AUTH_JWT_SIGNATURE_ALGORITHM, OP_AUTHORIZATION_ENDPOINT,
219+
OP_TOKEN_ENDPOINT, OP_USERINFO_ENDPOINT, OP_ENDSESSION_ENDPOINT, OP_ISSUER, OP_JWKSET_PATH,
220+
POPULATE_USER_METADATA, HTTP_CONNECT_TIMEOUT, HTTP_CONNECTION_READ_TIMEOUT,
199221
HTTP_SOCKET_TIMEOUT, HTTP_MAX_CONNECTIONS, HTTP_MAX_ENDPOINT_CONNECTIONS, HTTP_PROXY_HOST, HTTP_PROXY_PORT,
200222
HTTP_PROXY_SCHEME, ALLOWED_CLOCK_SKEW);
201223
set.addAll(DelegatedAuthorizationSettings.getSettings(TYPE));

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

+26-6
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.name()),
478+
URLEncoder.encode(rpConfig.getClientSecret().toString(), StandardCharsets.UTF_8.name()));
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.name()),
475-
URLEncoder.encode(rpConfig.getClientSecret().toString(), StandardCharsets.UTF_8.name()));
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

+7-2
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;
@@ -73,6 +74,8 @@
7374
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.OP_USERINFO_ENDPOINT;
7475
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.POPULATE_USER_METADATA;
7576
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.PRINCIPAL_CLAIM;
77+
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_CLIENT_AUTH_JWT_SIGNATURE_ALGORITHM;
78+
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_CLIENT_AUTH_METHOD;
7679
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_CLIENT_ID;
7780
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_CLIENT_SECRET;
7881
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_POST_LOGOUT_REDIRECT_URI;
@@ -266,9 +269,11 @@ private RelyingPartyConfiguration buildRelyingPartyConfiguration(RealmConfig con
266269
requestedScope.add("openid");
267270
}
268271
final JWSAlgorithm signatureAlgorithm = JWSAlgorithm.parse(require(config, RP_SIGNATURE_ALGORITHM));
269-
272+
final ClientAuthenticationMethod clientAuthenticationMethod =
273+
ClientAuthenticationMethod.parse(require(config, RP_CLIENT_AUTH_METHOD));
274+
final JWSAlgorithm clientAuthJwtAlgorithm = JWSAlgorithm.parse(require(config, RP_CLIENT_AUTH_JWT_SIGNATURE_ALGORITHM));
270275
return new RelyingPartyConfiguration(clientId, clientSecret, redirectUri, responseType, requestedScope,
271-
signatureAlgorithm, postLogoutRedirectUri);
276+
signatureAlgorithm, clientAuthenticationMethod, clientAuthJwtAlgorithm, postLogoutRedirectUri);
272277
}
273278

274279
private OpenIdConnectProviderConfiguration buildOpenIdConnectProviderConfiguration(RealmConfig config) {

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

+17-1
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

+8
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;
@@ -892,8 +893,11 @@ private RelyingPartyConfiguration getDefaultRpConfig() throws URISyntaxException
892893
new ResponseType("id_token", "token"),
893894
new Scope("openid"),
894895
JWSAlgorithm.RS384,
896+
ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
897+
JWSAlgorithm.HS384,
895898
new URI("https://rp.elastic.co/successfull_logout"));
896899
}
900+
897901
private RelyingPartyConfiguration getRpConfig(String alg) throws URISyntaxException {
898902
return new RelyingPartyConfiguration(
899903
new ClientID("rp-my"),
@@ -902,6 +906,8 @@ private RelyingPartyConfiguration getRpConfig(String alg) throws URISyntaxExcept
902906
new ResponseType("id_token", "token"),
903907
new Scope("openid"),
904908
JWSAlgorithm.parse(alg),
909+
ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
910+
JWSAlgorithm.HS384,
905911
new URI("https://rp.elastic.co/successfull_logout"));
906912
}
907913

@@ -913,6 +919,8 @@ private RelyingPartyConfiguration getRpConfigNoAccessToken(String alg) throws UR
913919
new ResponseType("id_token"),
914920
new Scope("openid"),
915921
JWSAlgorithm.parse(alg),
922+
ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
923+
JWSAlgorithm.HS384,
916924
new URI("https://rp.elastic.co/successfull_logout"));
917925
}
918926

0 commit comments

Comments
 (0)