Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Oidc additional client auth types #58708

Merged
merged 10 commits into from
Sep 14, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ public class OpenIdConnectRealmSettings {
private OpenIdConnectRealmSettings() {
}

private static final List<String> SUPPORTED_SIGNATURE_ALGORITHMS =
List.of("HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512");
public static final List<String> SUPPORTED_SIGNATURE_ALGORITHMS =
List.of("HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512");
private static final List<String> RESPONSE_TYPES = List.of("code", "id_token", "id_token token");
public static final List<String> CLIENT_AUTH_METHODS = List.of("client_secret_basic", "client_secret_post", "client_secret_jwt");
public static final List<String> SUPPORTED_CLIENT_AUTH_JWT_ALGORITHMS = 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 +80,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 +211,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),
URLEncoder.encode(rpConfig.getClientSecret().toString(), StandardCharsets.UTF_8));
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)));
}
Comment on lines +486 to +489
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I am missing something obvious. But client_id is not added as one of the request parameters? The map returned from ClientSecretJWT.toParameters() only contains client_assertion and client_assertion_type if I read the code correctly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But client_id is not added as one of the request parameters?

It's not, it doesn't need to be. It is added as the iss claim of the JWT and validated from there.

only contains client_assertion and client_assertion_type

yes, these are the two parameters needed, see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. For a moment, I forgot how JWT is organized. It's too late for me ...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that feeling !!! :) No need to go through this today, it can definitely wait. I raised the PR because I had it ready since last week

} 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),
URLEncoder.encode(rpConfig.getClientSecret().toString(), StandardCharsets.UTF_8));
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 @@ -71,6 +72,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 @@ -268,9 +271,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 @@ -841,8 +842,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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: need a blank line before this method.

return new RelyingPartyConfiguration(
new ClientID("rp-my"),
Expand All @@ -851,6 +855,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 @@ -862,6 +868,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
Loading