Skip to content

Commit

Permalink
Oidc additional client auth types (#58708) (#62289)
Browse files Browse the repository at this point in the history
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.

Co-authored-by: Ioannis Kakavas <ioannis@elastic.co>
  • Loading branch information
ywangd and jkakavas committed Sep 16, 2020
1 parent 24a24d0 commit a11dfbe
Show file tree
Hide file tree
Showing 13 changed files with 507 additions and 181 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,15 @@ public class OpenIdConnectRealmSettings {
private OpenIdConnectRealmSettings() {
}

private static final List<String> SUPPORTED_SIGNATURE_ALGORITHMS = Collections.unmodifiableList(
Arrays.asList("HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512"));
private static final List<String> RESPONSE_TYPES = Arrays.asList("code", "id_token", "id_token token");
public static final List<String> SUPPORTED_SIGNATURE_ALGORITHMS =
org.elasticsearch.common.collect.List.of(
"HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512");
private static final List<String> RESPONSE_TYPES = org.elasticsearch.common.collect.List.of(
"code", "id_token", "id_token token");
public static final List<String> CLIENT_AUTH_METHODS = org.elasticsearch.common.collect.List.of(
"client_secret_basic", "client_secret_post", "client_secret_jwt");
public static final List<String> SUPPORTED_CLIENT_AUTH_JWT_ALGORITHMS = org.elasticsearch.common.collect.List.of(
"HS256", "HS384", "HS512");
public static final String TYPE = "oidc";

public static final Setting.AffixSetting<String> RP_CLIENT_ID
Expand Down Expand Up @@ -78,7 +84,22 @@ private OpenIdConnectRealmSettings() {
public static final Setting.AffixSetting<List<String>> RP_REQUESTED_SCOPES = Setting.affixKeySetting(
RealmSettings.realmSettingPrefix(TYPE), "rp.requested_scopes",
key -> Setting.listSetting(key, Collections.singletonList("openid"), Function.identity(), Setting.Property.NodeScope));

public static final Setting.AffixSetting<String> RP_CLIENT_AUTH_METHOD
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "rp.client_auth_method",
key -> new Setting<>(key, "client_secret_basic", Function.identity(), v -> {
if (CLIENT_AUTH_METHODS.contains(v) == false) {
throw new IllegalArgumentException(
"Invalid value [" + v + "] for [" + key + "]. Allowed values are " + CLIENT_AUTH_METHODS + "}]");
}
}, Setting.Property.NodeScope));
public static final Setting.AffixSetting<String> RP_CLIENT_AUTH_JWT_SIGNATURE_ALGORITHM
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "rp.client_auth_jwt_signature_algorithm",
key -> new Setting<>(key, "HS384", Function.identity(), v -> {
if (SUPPORTED_CLIENT_AUTH_JWT_ALGORITHMS.contains(v) == false) {
throw new IllegalArgumentException(
"Invalid value [" + v + "] for [" + key + "]. Allowed values are " + SUPPORTED_CLIENT_AUTH_JWT_ALGORITHMS + "}]");
}
}, Setting.Property.NodeScope));
public static final Setting.AffixSetting<String> OP_AUTHORIZATION_ENDPOINT
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "op.authorization_endpoint",
key -> Setting.simpleString(key, v -> {
Expand Down Expand Up @@ -194,8 +215,9 @@ public Iterator<Setting<?>> settings() {
public static Set<Setting.AffixSetting<?>> getSettings() {
final Set<Setting.AffixSetting<?>> set = Sets.newHashSet(
RP_CLIENT_ID, RP_REDIRECT_URI, RP_RESPONSE_TYPE, RP_REQUESTED_SCOPES, RP_CLIENT_SECRET, RP_SIGNATURE_ALGORITHM,
RP_POST_LOGOUT_REDIRECT_URI, OP_AUTHORIZATION_ENDPOINT, OP_TOKEN_ENDPOINT, OP_USERINFO_ENDPOINT,
OP_ENDSESSION_ENDPOINT, OP_ISSUER, OP_JWKSET_PATH, POPULATE_USER_METADATA, HTTP_CONNECT_TIMEOUT, HTTP_CONNECTION_READ_TIMEOUT,
RP_POST_LOGOUT_REDIRECT_URI, RP_CLIENT_AUTH_METHOD, RP_CLIENT_AUTH_JWT_SIGNATURE_ALGORITHM, OP_AUTHORIZATION_ENDPOINT,
OP_TOKEN_ENDPOINT, OP_USERINFO_ENDPOINT, OP_ENDSESSION_ENDPOINT, OP_ISSUER, OP_JWKSET_PATH,
POPULATE_USER_METADATA, HTTP_CONNECT_TIMEOUT, HTTP_CONNECTION_READ_TIMEOUT,
HTTP_SOCKET_TIMEOUT, HTTP_MAX_CONNECTIONS, HTTP_MAX_ENDPOINT_CONNECTIONS, HTTP_PROXY_HOST, HTTP_PROXY_PORT,
HTTP_PROXY_SCHEME, ALLOWED_CLOCK_SKEW);
set.addAll(DelegatedAuthorizationSettings.getSettings(TYPE));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import com.nimbusds.oauth2.sdk.ErrorObject;
import com.nimbusds.oauth2.sdk.ResponseType;
import com.nimbusds.oauth2.sdk.TokenErrorResponse;
import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
import com.nimbusds.oauth2.sdk.auth.ClientSecretJWT;
import com.nimbusds.oauth2.sdk.auth.Secret;
import com.nimbusds.oauth2.sdk.id.State;
import com.nimbusds.oauth2.sdk.token.AccessToken;
Expand Down Expand Up @@ -85,6 +87,7 @@
import org.elasticsearch.watcher.ResourceWatcherService;
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
import org.elasticsearch.xpack.core.security.authc.RealmSettings;
import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings;
import org.elasticsearch.xpack.core.ssl.SSLConfiguration;
import org.elasticsearch.xpack.core.ssl.SSLService;

Expand Down Expand Up @@ -463,19 +466,36 @@ private void exchangeCodeForToken(AuthorizationCode code, ActionListener<Tuple<A
try {
final AuthorizationCodeGrant codeGrant = new AuthorizationCodeGrant(code, rpConfig.getRedirectUri());
final HttpPost httpPost = new HttpPost(opConfig.getTokenEndpoint());
httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
final List<NameValuePair> params = new ArrayList<>();
for (Map.Entry<String, List<String>> entry : codeGrant.toParameters().entrySet()) {
// All parameters of AuthorizationCodeGrant are singleton lists
params.add(new BasicNameValuePair(entry.getKey(), entry.getValue().get(0)));
}
if (rpConfig.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) {
UsernamePasswordCredentials creds =
new UsernamePasswordCredentials(URLEncoder.encode(rpConfig.getClientId().getValue(), StandardCharsets.UTF_8.name()),
URLEncoder.encode(rpConfig.getClientSecret().toString(), StandardCharsets.UTF_8.name()));
httpPost.addHeader(new BasicScheme().authenticate(creds, httpPost, null));
} else if (rpConfig.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
params.add(new BasicNameValuePair("client_id", rpConfig.getClientId().getValue()));
params.add(new BasicNameValuePair("client_secret", rpConfig.getClientSecret().toString()));
} else if (rpConfig.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
ClientSecretJWT clientSecretJWT = new ClientSecretJWT(rpConfig.getClientId(), opConfig.getTokenEndpoint(),
rpConfig.getClientAuthenticationJwtAlgorithm(), new Secret(rpConfig.getClientSecret().toString()));
for (Map.Entry<String, List<String>> entry : clientSecretJWT.toParameters().entrySet()) {
// Both client_assertion and client_assertion_type are singleton lists
params.add(new BasicNameValuePair(entry.getKey(), entry.getValue().get(0)));
}
} else {
tokensListener.onFailure(new ElasticsearchSecurityException("Failed to exchange code for Id Token using Token Endpoint." +
"Expected client authentication method to be one of " + OpenIdConnectRealmSettings.CLIENT_AUTH_METHODS
+ " but was [" + rpConfig.getClientAuthenticationMethod() + "]"));
}
httpPost.setEntity(new UrlEncodedFormEntity(params));
httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
UsernamePasswordCredentials creds =
new UsernamePasswordCredentials(URLEncoder.encode(rpConfig.getClientId().getValue(), StandardCharsets.UTF_8.name()),
URLEncoder.encode(rpConfig.getClientSecret().toString(), StandardCharsets.UTF_8.name()));
httpPost.addHeader(new BasicScheme().authenticate(creds, httpPost, null));
SpecialPermission.check();
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {

httpClient.execute(httpPost, new FutureCallback<HttpResponse>() {
@Override
public void completed(HttpResponse result) {
Expand All @@ -496,7 +516,7 @@ public void cancelled() {
});
return null;
});
} catch (AuthenticationException | UnsupportedEncodingException e) {
} catch (AuthenticationException | UnsupportedEncodingException | JOSEException e) {
tokensListener.onFailure(
new ElasticsearchSecurityException("Failed to exchange code for Id Token using the Token Endpoint.", e));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.ResponseType;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.oauth2.sdk.id.Issuer;
import com.nimbusds.oauth2.sdk.id.State;
Expand Down Expand Up @@ -73,6 +74,8 @@
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.OP_USERINFO_ENDPOINT;
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.POPULATE_USER_METADATA;
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.PRINCIPAL_CLAIM;
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_CLIENT_AUTH_JWT_SIGNATURE_ALGORITHM;
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_CLIENT_AUTH_METHOD;
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_CLIENT_ID;
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_CLIENT_SECRET;
import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_POST_LOGOUT_REDIRECT_URI;
Expand Down Expand Up @@ -266,9 +269,11 @@ private RelyingPartyConfiguration buildRelyingPartyConfiguration(RealmConfig con
requestedScope.add("openid");
}
final JWSAlgorithm signatureAlgorithm = JWSAlgorithm.parse(require(config, RP_SIGNATURE_ALGORITHM));

final ClientAuthenticationMethod clientAuthenticationMethod =
ClientAuthenticationMethod.parse(require(config, RP_CLIENT_AUTH_METHOD));
final JWSAlgorithm clientAuthJwtAlgorithm = JWSAlgorithm.parse(require(config, RP_CLIENT_AUTH_JWT_SIGNATURE_ALGORITHM));
return new RelyingPartyConfiguration(clientId, clientSecret, redirectUri, responseType, requestedScope,
signatureAlgorithm, postLogoutRedirectUri);
signatureAlgorithm, clientAuthenticationMethod, clientAuthJwtAlgorithm, postLogoutRedirectUri);
}

private OpenIdConnectProviderConfiguration buildOpenIdConnectProviderConfiguration(RealmConfig config) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.oauth2.sdk.ResponseType;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
import com.nimbusds.oauth2.sdk.id.ClientID;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.settings.SecureString;
Expand All @@ -26,15 +27,22 @@ public class RelyingPartyConfiguration {
private final Scope requestedScope;
private final JWSAlgorithm signatureAlgorithm;
private final URI postLogoutRedirectUri;
private final ClientAuthenticationMethod clientAuthenticationMethod;
private final JWSAlgorithm clientAuthenticationJwtAlgorithm;

public RelyingPartyConfiguration(ClientID clientId, SecureString clientSecret, URI redirectUri, ResponseType responseType,
Scope requestedScope, JWSAlgorithm algorithm, @Nullable URI postLogoutRedirectUri) {
Scope requestedScope, JWSAlgorithm algorithm, ClientAuthenticationMethod clientAuthenticationMethod,
JWSAlgorithm clientAuthenticationJwtAlgorithm, @Nullable URI postLogoutRedirectUri) {
this.clientId = Objects.requireNonNull(clientId, "clientId must be provided");
this.clientSecret = Objects.requireNonNull(clientSecret, "clientSecret must be provided");
this.redirectUri = Objects.requireNonNull(redirectUri, "redirectUri must be provided");
this.responseType = Objects.requireNonNull(responseType, "responseType must be provided");
this.requestedScope = Objects.requireNonNull(requestedScope, "responseType must be provided");
this.signatureAlgorithm = Objects.requireNonNull(algorithm, "algorithm must be provided");
this.clientAuthenticationMethod = Objects.requireNonNull(clientAuthenticationMethod,
"clientAuthenticationMethod must be provided");
this.clientAuthenticationJwtAlgorithm = Objects.requireNonNull(clientAuthenticationJwtAlgorithm,
"clientAuthenticationJwtAlgorithm must be provided");
this.postLogoutRedirectUri = postLogoutRedirectUri;
}

Expand Down Expand Up @@ -65,4 +73,12 @@ public JWSAlgorithm getSignatureAlgorithm() {
public URI getPostLogoutRedirectUri() {
return postLogoutRedirectUri;
}

public ClientAuthenticationMethod getClientAuthenticationMethod() {
return clientAuthenticationMethod;
}

public JWSAlgorithm getClientAuthenticationJwtAlgorithm() {
return clientAuthenticationJwtAlgorithm;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.nimbusds.jwt.proc.BadJWTException;
import com.nimbusds.oauth2.sdk.ResponseType;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
import com.nimbusds.oauth2.sdk.auth.Secret;
import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.oauth2.sdk.id.Issuer;
Expand Down Expand Up @@ -892,8 +893,11 @@ private RelyingPartyConfiguration getDefaultRpConfig() throws URISyntaxException
new ResponseType("id_token", "token"),
new Scope("openid"),
JWSAlgorithm.RS384,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
JWSAlgorithm.HS384,
new URI("https://rp.elastic.co/successfull_logout"));
}

private RelyingPartyConfiguration getRpConfig(String alg) throws URISyntaxException {
return new RelyingPartyConfiguration(
new ClientID("rp-my"),
Expand All @@ -902,6 +906,8 @@ private RelyingPartyConfiguration getRpConfig(String alg) throws URISyntaxExcept
new ResponseType("id_token", "token"),
new Scope("openid"),
JWSAlgorithm.parse(alg),
ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
JWSAlgorithm.HS384,
new URI("https://rp.elastic.co/successfull_logout"));
}

Expand All @@ -913,6 +919,8 @@ private RelyingPartyConfiguration getRpConfigNoAccessToken(String alg) throws UR
new ResponseType("id_token"),
new Scope("openid"),
JWSAlgorithm.parse(alg),
ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
JWSAlgorithm.HS384,
new URI("https://rp.elastic.co/successfull_logout"));
}

Expand Down

0 comments on commit a11dfbe

Please sign in to comment.