diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateAction.java new file mode 100644 index 0000000000000..b27a71e202e55 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateAction.java @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.oidc; + +import org.elasticsearch.action.Action; +import org.elasticsearch.common.io.stream.Writeable; + +/** + * Action for initiating an authentication process using OpenID Connect + */ +public final class OpenIdConnectAuthenticateAction extends Action { + + public static final OpenIdConnectAuthenticateAction INSTANCE = new OpenIdConnectAuthenticateAction(); + public static final String NAME = "cluster:admin/xpack/security/oidc/authenticate"; + + private OpenIdConnectAuthenticateAction() { + super(NAME); + } + + @Override + public OpenIdConnectAuthenticateResponse newResponse() { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + @Override + public Writeable.Reader getResponseReader() { + return OpenIdConnectAuthenticateResponse::new; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateRequest.java new file mode 100644 index 0000000000000..1e27e02e607fc --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateRequest.java @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.oidc; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +/** + * Represents a request for authentication using OpenID Connect + */ +public class OpenIdConnectAuthenticateRequest extends ActionRequest { + + /** + * The URI where the OP redirected the browser after the authentication attempt. This is passed as is from the + * facilitator entity (i.e. Kibana) + */ + private String redirectUri; + + /** + * The state value that we generated or the facilitator provided for this specific flow and that should be stored at the user's session + * with the facilitator + */ + private String state; + + /** + * The nonce value that we generated or the facilitator provided for this specific flow and that should be stored at the user's session + * with the facilitator + */ + private String nonce; + + public OpenIdConnectAuthenticateRequest() { + + } + + public OpenIdConnectAuthenticateRequest(StreamInput in) throws IOException { + super.readFrom(in); + redirectUri = in.readString(); + state = in.readString(); + nonce = in.readString(); + } + + public String getRedirectUri() { + return redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getNonce() { + return nonce; + } + + public void setNonce(String nonce) { + this.nonce = nonce; + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (Strings.isNullOrEmpty(state)) { + validationException = addValidationError("state parameter is missing", validationException); + } + if (Strings.isNullOrEmpty(nonce)) { + validationException = addValidationError("nonce parameter is missing", validationException); + } + if (Strings.isNullOrEmpty(redirectUri)) { + validationException = addValidationError("redirect_uri parameter is missing", validationException); + } + return validationException; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(redirectUri); + out.writeString(state); + out.writeString(nonce); + } + + @Override + public void readFrom(StreamInput in) { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + public String toString() { + return "{redirectUri=" + redirectUri + ", state=" + state + ", nonce=" + nonce + "}"; + } +} + diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateRequestBuilder.java new file mode 100644 index 0000000000000..cbdd13aec0463 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateRequestBuilder.java @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.oidc; + +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.client.ElasticsearchClient; + +/** + * Request builder for populating a {@link OpenIdConnectAuthenticateRequest} + */ +public class OpenIdConnectAuthenticateRequestBuilder + extends ActionRequestBuilder { + + public OpenIdConnectAuthenticateRequestBuilder(ElasticsearchClient client) { + super(client, OpenIdConnectAuthenticateAction.INSTANCE, new OpenIdConnectAuthenticateRequest()); + } + + public OpenIdConnectAuthenticateRequestBuilder redirectUri(String redirectUri) { + request.setRedirectUri(redirectUri); + return this; + } + + public OpenIdConnectAuthenticateRequestBuilder state(String state) { + request.setState(state); + return this; + } + + public OpenIdConnectAuthenticateRequestBuilder nonce(String nonce) { + request.setNonce(nonce); + return this; + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateResponse.java new file mode 100644 index 0000000000000..93b7c6b292ae9 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateResponse.java @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.oidc; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.unit.TimeValue; + +import java.io.IOException; + +public class OpenIdConnectAuthenticateResponse extends ActionResponse { + private String principal; + private String accessTokenString; + private String refreshTokenString; + private TimeValue expiresIn; + + public OpenIdConnectAuthenticateResponse(String principal, String accessTokenString, String refreshTokenString, TimeValue expiresIn) { + this.principal = principal; + this.accessTokenString = accessTokenString; + this.refreshTokenString = refreshTokenString; + this.expiresIn = expiresIn; + } + + public OpenIdConnectAuthenticateResponse(StreamInput in) throws IOException { + super.readFrom(in); + principal = in.readString(); + accessTokenString = in.readString(); + refreshTokenString = in.readString(); + expiresIn = in.readTimeValue(); + } + + public String getPrincipal() { + return principal; + } + + public String getAccessTokenString() { + return accessTokenString; + } + + public String getRefreshTokenString() { + return refreshTokenString; + } + + public TimeValue getExpiresIn() { + return expiresIn; + } + + @Override + public void readFrom(StreamInput in) { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(principal); + out.writeString(accessTokenString); + out.writeString(refreshTokenString); + out.writeTimeValue(expiresIn); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectLogoutAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectLogoutAction.java new file mode 100644 index 0000000000000..482484a7dedee --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectLogoutAction.java @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.oidc; + +import org.elasticsearch.action.Action; +import org.elasticsearch.common.io.stream.Writeable; + +public class OpenIdConnectLogoutAction extends Action { + + public static final OpenIdConnectLogoutAction INSTANCE = new OpenIdConnectLogoutAction(); + public static final String NAME = "cluster:admin/xpack/security/oidc/logout"; + + private OpenIdConnectLogoutAction() { + super(NAME); + } + + @Override + public OpenIdConnectLogoutResponse newResponse() { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + @Override + public Writeable.Reader getResponseReader() { + return OpenIdConnectLogoutResponse::new; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectLogoutRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectLogoutRequest.java new file mode 100644 index 0000000000000..777df403ecab3 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectLogoutRequest.java @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.oidc; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +public final class OpenIdConnectLogoutRequest extends ActionRequest { + + private String token; + @Nullable + private String refreshToken; + + public OpenIdConnectLogoutRequest() { + + } + + public OpenIdConnectLogoutRequest(StreamInput in) throws IOException { + super.readFrom(in); + token = in.readString(); + refreshToken = in.readOptionalString(); + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (Strings.isNullOrEmpty(token)) { + validationException = addValidationError("token is missing", validationException); + } + return validationException; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(token); + out.writeOptionalString(refreshToken); + } + + @Override + public void readFrom(StreamInput in) { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectLogoutResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectLogoutResponse.java new file mode 100644 index 0000000000000..e725701e01c7e --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectLogoutResponse.java @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.oidc; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +public final class OpenIdConnectLogoutResponse extends ActionResponse { + + private String endSessionUrl; + + public OpenIdConnectLogoutResponse(StreamInput in) throws IOException { + super.readFrom(in); + this.endSessionUrl = in.readString(); + } + + public OpenIdConnectLogoutResponse(String endSessionUrl) { + this.endSessionUrl = endSessionUrl; + } + + @Override + public void readFrom(StreamInput in) { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(endSessionUrl); + } + + public String toString() { + return "{endSessionUrl=" + endSessionUrl + "}"; + } + + public String getEndSessionUrl() { + return endSessionUrl; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationAction.java new file mode 100644 index 0000000000000..2aa82c7286cec --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationAction.java @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.oidc; + +import org.elasticsearch.action.Action; +import org.elasticsearch.common.io.stream.Writeable; + +public class OpenIdConnectPrepareAuthenticationAction extends Action { + + public static final OpenIdConnectPrepareAuthenticationAction INSTANCE = new OpenIdConnectPrepareAuthenticationAction(); + public static final String NAME = "cluster:admin/xpack/security/oidc/prepare"; + + private OpenIdConnectPrepareAuthenticationAction() { + super(NAME); + } + + @Override + public OpenIdConnectPrepareAuthenticationResponse newResponse() { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + @Override + public Writeable.Reader getResponseReader() { + return OpenIdConnectPrepareAuthenticationResponse::new; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationRequest.java new file mode 100644 index 0000000000000..8f6d616981b39 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationRequest.java @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.oidc; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +/** + * Represents a request to prepare an OAuth 2.0 authorization request + */ +public class OpenIdConnectPrepareAuthenticationRequest extends ActionRequest { + + /** + * The name of the OpenID Connect realm in the configuration that should be used for authentication + */ + private String realmName; + /** + * In case of a + * 3rd party initiated authentication, the + * issuer that the User Agent needs to be redirected to for authentication + */ + private String issuer; + private String loginHint; + private String state; + private String nonce; + + public String getRealmName() { + return realmName; + } + + public String getState() { + return state; + } + + public String getNonce() { + return nonce; + } + + public String getIssuer() { + return issuer; + } + + public String getLoginHint() { + return loginHint; + } + + public void setRealmName(String realmName) { + this.realmName = realmName; + } + + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + public void setState(String state) { + this.state = state; + } + + public void setNonce(String nonce) { + this.nonce = nonce; + } + + public void setLoginHint(String loginHint) { + this.loginHint = loginHint; + } + + public OpenIdConnectPrepareAuthenticationRequest() { + } + + public OpenIdConnectPrepareAuthenticationRequest(StreamInput in) throws IOException { + super.readFrom(in); + realmName = in.readOptionalString(); + issuer = in.readOptionalString(); + loginHint = in.readOptionalString(); + state = in.readOptionalString(); + nonce = in.readOptionalString(); + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (Strings.hasText(realmName) == false && Strings.hasText(issuer) == false) { + validationException = addValidationError("one of [realm, issuer] must be provided", null); + } + if (Strings.hasText(realmName) && Strings.hasText(issuer)) { + validationException = addValidationError("only one of [realm, issuer] can be provided in the same request", null); + } + return validationException; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalString(realmName); + out.writeOptionalString(issuer); + out.writeOptionalString(loginHint); + out.writeOptionalString(state); + out.writeOptionalString(nonce); + } + + @Override + public void readFrom(StreamInput in) { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + public String toString() { + return "{realmName=" + realmName + ", issuer=" + issuer + ", login_hint=" + + loginHint + ", state=" + state + ", nonce=" + nonce + "}"; + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationRequestBuilder.java new file mode 100644 index 0000000000000..b7992345a105a --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationRequestBuilder.java @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.oidc; + +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.client.ElasticsearchClient; + +/** + * Request builder for populating a {@link OpenIdConnectPrepareAuthenticationRequest} + */ +public class OpenIdConnectPrepareAuthenticationRequestBuilder + extends ActionRequestBuilder { + + public OpenIdConnectPrepareAuthenticationRequestBuilder(ElasticsearchClient client) { + super(client, OpenIdConnectPrepareAuthenticationAction.INSTANCE, new OpenIdConnectPrepareAuthenticationRequest()); + } + + public OpenIdConnectPrepareAuthenticationRequestBuilder realmName(String name) { + request.setRealmName(name); + return this; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationResponse.java new file mode 100644 index 0000000000000..c8a70e65b8111 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationResponse.java @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.oidc; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +/** + * A response object that contains the OpenID Connect Authentication Request as a URL and the state and nonce values that were + * generated for this request. + */ +public class OpenIdConnectPrepareAuthenticationResponse extends ActionResponse implements ToXContentObject { + + private String authenticationRequestUrl; + /* + * The oAuth2 state parameter used for CSRF protection. + */ + private String state; + /* + * String value used to associate a Client session with an ID Token, and to mitigate replay attacks. + */ + private String nonce; + + public OpenIdConnectPrepareAuthenticationResponse(String authorizationEndpointUrl, String state, String nonce) { + this.authenticationRequestUrl = authorizationEndpointUrl; + this.state = state; + this.nonce = nonce; + } + + public OpenIdConnectPrepareAuthenticationResponse(StreamInput in) throws IOException { + super.readFrom(in); + authenticationRequestUrl = in.readString(); + state = in.readString(); + nonce = in.readString(); + } + + public String getAuthenticationRequestUrl() { + return authenticationRequestUrl; + } + + public String getState() { + return state; + } + + public String getNonce() { + return nonce; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(authenticationRequestUrl); + out.writeString(state); + out.writeString(nonce); + } + + public String toString() { + return "{authenticationRequestUrl=" + authenticationRequestUrl + ", state=" + state + ", nonce=" + nonce + "}"; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("redirect", authenticationRequestUrl); + builder.field("state", state); + builder.field("nonce", nonce); + builder.endObject(); + return builder; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/InternalRealmsSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/InternalRealmsSettings.java index 8b2ef18406830..dd4a843345298 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/InternalRealmsSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/InternalRealmsSettings.java @@ -10,6 +10,7 @@ import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings; +import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings; import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; @@ -34,6 +35,7 @@ public static Set> getSettings() { set.addAll(PkiRealmSettings.getSettings()); set.addAll(SamlRealmSettings.getSettings()); set.addAll(KerberosRealmSettings.getSettings()); + set.addAll(OpenIdConnectRealmSettings.getSettings()); return Collections.unmodifiableSet(set); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmSettings.java index 913fcba3d33c8..0c35525f1debb 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmSettings.java @@ -6,6 +6,8 @@ package org.elasticsearch.xpack.core.security.authc; import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.SecureSetting; +import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; @@ -55,6 +57,17 @@ public static Setting.AffixSetting simpleString(String realmType, String return Setting.affixKeySetting(realmSettingPrefix(realmType), suffix, key -> Setting.simpleString(key, properties)); } + /** + * Create a {@link SecureSetting#secureString secure string} {@link Setting} object of a realm of + * with the provided type and setting suffix. + * + * @param realmType The type of the realm, used within the setting prefix + * @param suffix The suffix of the setting (everything following the realm name in the affix setting) + */ + public static Setting.AffixSetting secureString(String realmType, String suffix) { + return Setting.affixKeySetting(realmSettingPrefix(realmType), suffix, key -> SecureSetting.secureString(key, null)); + } + /** * Create a {@link Function} that acts as a factory an {@link org.elasticsearch.common.settings.Setting.AffixSetting}. * The {@code Function} takes the realm-type as an argument. diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/oidc/OpenIdConnectRealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/oidc/OpenIdConnectRealmSettings.java new file mode 100644 index 0000000000000..b88056a4f24e7 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/oidc/OpenIdConnectRealmSettings.java @@ -0,0 +1,208 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.authc.oidc; + +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.authc.RealmSettings; +import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings; +import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + + +public class OpenIdConnectRealmSettings { + + private OpenIdConnectRealmSettings() { + } + + private static final List SUPPORTED_SIGNATURE_ALGORITHMS = Collections.unmodifiableList( + Arrays.asList("HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512")); + private static final List RESPONSE_TYPES = Arrays.asList("code", "id_token", "id_token token"); + public static final String TYPE = "oidc"; + + public static final Setting.AffixSetting RP_CLIENT_ID + = RealmSettings.simpleString(TYPE, "rp.client_id", Setting.Property.NodeScope); + public static final Setting.AffixSetting RP_CLIENT_SECRET + = RealmSettings.secureString(TYPE, "rp.client_secret"); + public static final Setting.AffixSetting RP_REDIRECT_URI + = Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "rp.redirect_uri", + key -> Setting.simpleString(key, v -> { + try { + new URI(v); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid value [" + v + "] for [" + key + "]. Not a valid URI.", e); + } + }, Setting.Property.NodeScope)); + public static final Setting.AffixSetting RP_POST_LOGOUT_REDIRECT_URI + = Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "rp.post_logout_redirect_uri", + key -> Setting.simpleString(key, v -> { + try { + new URI(v); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid value [" + v + "] for [" + key + "]. Not a valid URI.", e); + } + }, Setting.Property.NodeScope)); + public static final Setting.AffixSetting RP_RESPONSE_TYPE + = Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "rp.response_type", + key -> Setting.simpleString(key, v -> { + if (RESPONSE_TYPES.contains(v) == false) { + throw new IllegalArgumentException( + "Invalid value [" + v + "] for [" + key + "]. Allowed values are " + RESPONSE_TYPES + ""); + } + }, Setting.Property.NodeScope)); + public static final Setting.AffixSetting RP_SIGNATURE_ALGORITHM + = Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "rp.signature_algorithm", + key -> new Setting<>(key, "RS256", Function.identity(), v -> { + if (SUPPORTED_SIGNATURE_ALGORITHMS.contains(v) == false) { + throw new IllegalArgumentException( + "Invalid value [" + v + "] for [" + key + "]. Allowed values are " + SUPPORTED_SIGNATURE_ALGORITHMS + "}]"); + } + }, Setting.Property.NodeScope)); + public static final Setting.AffixSetting> 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 OP_NAME + = RealmSettings.simpleString(TYPE, "op.name", Setting.Property.NodeScope); + public static final Setting.AffixSetting OP_AUTHORIZATION_ENDPOINT + = Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "op.authorization_endpoint", + key -> Setting.simpleString(key, v -> { + try { + new URI(v); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid value [" + v + "] for [" + key + "]. Not a valid URI.", e); + } + }, Setting.Property.NodeScope)); + public static final Setting.AffixSetting OP_TOKEN_ENDPOINT + = Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "op.token_endpoint", + key -> Setting.simpleString(key, v -> { + try { + new URI(v); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid value [" + v + "] for [" + key + "]. Not a valid URI.", e); + } + }, Setting.Property.NodeScope)); + public static final Setting.AffixSetting OP_USERINFO_ENDPOINT + = Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "op.userinfo_endpoint", + key -> Setting.simpleString(key, v -> { + try { + new URI(v); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid value [" + v + "] for [" + key + "]. Not a valid URI.", e); + } + }, Setting.Property.NodeScope)); + public static final Setting.AffixSetting OP_ENDSESSION_ENDPOINT + = Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "op.endsession_endpoint", + key -> Setting.simpleString(key, v -> { + try { + new URI(v); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid value [" + v + "] for [" + key + "]. Not a valid URI.", e); + } + }, Setting.Property.NodeScope)); + public static final Setting.AffixSetting OP_ISSUER + = RealmSettings.simpleString(TYPE, "op.issuer", Setting.Property.NodeScope); + public static final Setting.AffixSetting OP_JWKSET_PATH + = RealmSettings.simpleString(TYPE, "op.jwkset_path", Setting.Property.NodeScope); + + public static final Setting.AffixSetting ALLOWED_CLOCK_SKEW + = Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "allowed_clock_skew", + key -> Setting.timeSetting(key, TimeValue.timeValueSeconds(60), Setting.Property.NodeScope)); + public static final Setting.AffixSetting POPULATE_USER_METADATA = Setting.affixKeySetting( + RealmSettings.realmSettingPrefix(TYPE), "populate_user_metadata", + key -> Setting.boolSetting(key, true, Setting.Property.NodeScope)); + private static final TimeValue DEFAULT_TIMEOUT = TimeValue.timeValueSeconds(5); + public static final Setting.AffixSetting HTTP_CONNECT_TIMEOUT + = Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "http.connect_timeout", + key -> Setting.timeSetting(key, DEFAULT_TIMEOUT, Setting.Property.NodeScope)); + public static final Setting.AffixSetting HTTP_CONNECTION_READ_TIMEOUT + = Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "http.connection_read_timeout", + key -> Setting.timeSetting(key, DEFAULT_TIMEOUT, Setting.Property.NodeScope)); + public static final Setting.AffixSetting HTTP_SOCKET_TIMEOUT + = Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "http.socket_timeout", + key -> Setting.timeSetting(key, DEFAULT_TIMEOUT, Setting.Property.NodeScope)); + public static final Setting.AffixSetting HTTP_MAX_CONNECTIONS + = Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "http.max_connections", + key -> Setting.intSetting(key, 200, Setting.Property.NodeScope)); + public static final Setting.AffixSetting HTTP_MAX_ENDPOINT_CONNECTIONS + = Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "http.max_endpoint_connections", + key -> Setting.intSetting(key, 200, Setting.Property.NodeScope)); + + public static final ClaimSetting PRINCIPAL_CLAIM = new ClaimSetting("principal"); + public static final ClaimSetting GROUPS_CLAIM = new ClaimSetting("groups"); + public static final ClaimSetting NAME_CLAIM = new ClaimSetting("name"); + public static final ClaimSetting DN_CLAIM = new ClaimSetting("dn"); + public static final ClaimSetting MAIL_CLAIM = new ClaimSetting("mail"); + + public static Set> getSettings() { + final Set> 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_NAME, OP_AUTHORIZATION_ENDPOINT, OP_TOKEN_ENDPOINT, OP_USERINFO_ENDPOINT, + OP_ENDSESSION_ENDPOINT, OP_ISSUER, OP_JWKSET_PATH, HTTP_CONNECT_TIMEOUT, HTTP_CONNECTION_READ_TIMEOUT, HTTP_SOCKET_TIMEOUT, + HTTP_MAX_CONNECTIONS, HTTP_MAX_ENDPOINT_CONNECTIONS, ALLOWED_CLOCK_SKEW); + set.addAll(DelegatedAuthorizationSettings.getSettings(TYPE)); + set.addAll(RealmSettings.getStandardSettings(TYPE)); + set.addAll(SSLConfigurationSettings.getRealmSettings(TYPE)); + set.addAll(PRINCIPAL_CLAIM.settings()); + set.addAll(GROUPS_CLAIM.settings()); + set.addAll(DN_CLAIM.settings()); + set.addAll(NAME_CLAIM.settings()); + set.addAll(MAIL_CLAIM.settings()); + return set; + } + + /** + * The OIDC realm offers a number of settings that rely on claim values that are populated by the OP in the ID Token or the User Info + * response. + * Each claim has 2 settings: + *
    + *
  • The name of the OpenID Connect claim to use
  • + *
  • An optional java pattern (regex) to apply to that claim value in order to extract the substring that should be used.
  • + *
+ * For example, the Elasticsearch User Principal could be configured to come from the OpenID Connect standard claim "email", + * and extract only the local-part of the user's email address (i.e. the name before the '@'). + * This class encapsulates those 2 settings. + */ + public static final class ClaimSetting { + public static final String CLAIMS_PREFIX = "claims."; + public static final String CLAIM_PATTERNS_PREFIX = "claim_patterns."; + + private final Setting.AffixSetting claim; + private final Setting.AffixSetting pattern; + + public ClaimSetting(String name) { + claim = RealmSettings.simpleString(TYPE, CLAIMS_PREFIX + name, Setting.Property.NodeScope); + pattern = RealmSettings.simpleString(TYPE, CLAIM_PATTERNS_PREFIX + name, Setting.Property.NodeScope); + } + + public Collection> settings() { + return Arrays.asList(getClaim(), getPattern()); + } + + public String name(RealmConfig config) { + return getClaim().getConcreteSettingForNamespace(config.name()).getKey(); + } + + public Setting.AffixSetting getClaim() { + return claim; + } + + public Setting.AffixSetting getPattern() { + return pattern; + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java index 2190d6e63bafc..c929fb3bfd348 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java @@ -37,6 +37,7 @@ public final class ClusterPrivilege extends Privilege { private static final Automaton MANAGE_SECURITY_AUTOMATON = patterns("cluster:admin/xpack/security/*"); private static final Automaton MANAGE_SAML_AUTOMATON = patterns("cluster:admin/xpack/security/saml/*", InvalidateTokenAction.NAME, RefreshTokenAction.NAME); + private static final Automaton MANAGE_OIDC_AUTOMATON = patterns("cluster:admin/xpack/security/oidc/*"); private static final Automaton MANAGE_TOKEN_AUTOMATON = patterns("cluster:admin/xpack/security/token/*"); private static final Automaton MONITOR_AUTOMATON = patterns("cluster:monitor/*"); private static final Automaton MONITOR_ML_AUTOMATON = patterns("cluster:monitor/xpack/ml/*"); @@ -82,6 +83,7 @@ public final class ClusterPrivilege extends Privilege { public static final ClusterPrivilege TRANSPORT_CLIENT = new ClusterPrivilege("transport_client", TRANSPORT_CLIENT_AUTOMATON); public static final ClusterPrivilege MANAGE_SECURITY = new ClusterPrivilege("manage_security", MANAGE_SECURITY_AUTOMATON); public static final ClusterPrivilege MANAGE_SAML = new ClusterPrivilege("manage_saml", MANAGE_SAML_AUTOMATON); + public static final ClusterPrivilege MANAGE_OIDC = new ClusterPrivilege("manage_oidc", MANAGE_OIDC_AUTOMATON); public static final ClusterPrivilege MANAGE_PIPELINE = new ClusterPrivilege("manage_pipeline", "cluster:admin/ingest/pipeline/*"); public static final ClusterPrivilege MANAGE_CCR = new ClusterPrivilege("manage_ccr", MANAGE_CCR_AUTOMATON); public static final ClusterPrivilege READ_CCR = new ClusterPrivilege("read_ccr", READ_CCR_AUTOMATON); @@ -109,6 +111,7 @@ public final class ClusterPrivilege extends Privilege { .put("transport_client", TRANSPORT_CLIENT) .put("manage_security", MANAGE_SECURITY) .put("manage_saml", MANAGE_SAML) + .put("manage_oidc", MANAGE_OIDC) .put("manage_pipeline", MANAGE_PIPELINE) .put("manage_rollup", MANAGE_ROLLUP) .put("manage_ccr", MANAGE_CCR) diff --git a/x-pack/plugin/security/build.gradle b/x-pack/plugin/security/build.gradle index afc39d5df5010..63b531b1f3319 100644 --- a/x-pack/plugin/security/build.gradle +++ b/x-pack/plugin/security/build.gradle @@ -56,6 +56,16 @@ dependencies { compile "org.apache.httpcomponents:httpclient-cache:${versions.httpclient}" compile 'com.google.guava:guava:19.0' + // Dependencies for oidc + compile "com.nimbusds:oauth2-oidc-sdk:6.5" + compile "com.nimbusds:nimbus-jose-jwt:4.41.2" + compile "com.nimbusds:lang-tag:1.4.4" + compile "com.sun.mail:jakarta.mail:1.6.3" + compile "net.jcip:jcip-annotations:1.0" + compile "net.minidev:json-smart:2.3" + compile "net.minidev:accessors-smart:1.2" + compile "org.ow2.asm:asm:7.1" + testCompile 'org.elasticsearch:securemock:1.2' testCompile "org.elasticsearch:mocksocket:${versions.mocksocket}" //testCompile "org.yaml:snakeyaml:${versions.snakeyaml}" @@ -160,7 +170,7 @@ forbiddenPatterns { } forbiddenApisMain { - signaturesFiles += files('forbidden/ldap-signatures.txt', 'forbidden/xml-signatures.txt') + signaturesFiles += files('forbidden/ldap-signatures.txt', 'forbidden/xml-signatures.txt', 'forbidden/oidc-signatures.txt') } // classes are missing, e.g. com.ibm.icu.lang.UCharacter @@ -257,7 +267,9 @@ thirdPartyAudit { 'net.sf.ehcache.Ehcache', 'net.sf.ehcache.Element', // [missing classes] SLF4j includes an optional class that depends on an extension class (!) - 'org.slf4j.ext.EventData' + 'org.slf4j.ext.EventData', + // Optional dependency of oauth2-oidc-sdk that we don't need since we do not support AES-SIV for JWE + 'org.cryptomator.siv.SivMode' ) ignoreViolations ( @@ -278,7 +290,13 @@ if (project.runtimeJavaVersion > JavaVersion.VERSION_1_8) { 'javax.xml.bind.JAXBElement', 'javax.xml.bind.JAXBException', 'javax.xml.bind.Unmarshaller', - 'javax.xml.bind.UnmarshallerHandler' + 'javax.xml.bind.UnmarshallerHandler', + 'javax.activation.ActivationDataFlavor', + 'javax.activation.DataContentHandler', + 'javax.activation.DataHandler', + 'javax.activation.DataSource', + 'javax.activation.FileDataSource', + 'javax.activation.FileTypeMap' ) } diff --git a/x-pack/plugin/security/forbidden/oidc-signatures.txt b/x-pack/plugin/security/forbidden/oidc-signatures.txt new file mode 100644 index 0000000000000..05a2babdbe73c --- /dev/null +++ b/x-pack/plugin/security/forbidden/oidc-signatures.txt @@ -0,0 +1,3 @@ +@defaultMessage Blocking methods should not be used for HTTP requests. Use CloseableHttpAsyncClient instead +com.nimbusds.oauth2.sdk.http.HTTPRequest#send(javax.net.ssl.HostnameVerifier, javax.net.ssl.SSLSocketFactory) +com.nimbusds.oauth2.sdk.http.HTTPRequest#send() \ No newline at end of file diff --git a/x-pack/plugin/security/licenses/accessors-smart-1.2.jar.sha1 b/x-pack/plugin/security/licenses/accessors-smart-1.2.jar.sha1 new file mode 100644 index 0000000000000..e8e174c88c7a4 --- /dev/null +++ b/x-pack/plugin/security/licenses/accessors-smart-1.2.jar.sha1 @@ -0,0 +1 @@ +c592b500269bfde36096641b01238a8350f8aa31 \ No newline at end of file diff --git a/x-pack/plugin/security/licenses/accessors-smart-LICENSE.txt b/x-pack/plugin/security/licenses/accessors-smart-LICENSE.txt new file mode 100644 index 0000000000000..d645695673349 --- /dev/null +++ b/x-pack/plugin/security/licenses/accessors-smart-LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/x-pack/plugin/security/licenses/accessors-smart-NOTICE.txt b/x-pack/plugin/security/licenses/accessors-smart-NOTICE.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/x-pack/plugin/security/licenses/asm-7.1.jar.sha1 b/x-pack/plugin/security/licenses/asm-7.1.jar.sha1 new file mode 100644 index 0000000000000..3a53b2ef7f941 --- /dev/null +++ b/x-pack/plugin/security/licenses/asm-7.1.jar.sha1 @@ -0,0 +1 @@ +fa29aa438674ff19d5e1386d2c3527a0267f291e \ No newline at end of file diff --git a/x-pack/plugin/security/licenses/asm-LICENSE.txt b/x-pack/plugin/security/licenses/asm-LICENSE.txt new file mode 100644 index 0000000000000..afb064f2f2666 --- /dev/null +++ b/x-pack/plugin/security/licenses/asm-LICENSE.txt @@ -0,0 +1,26 @@ +Copyright (c) 2012 France Télécom +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. diff --git a/x-pack/plugin/security/licenses/asm-NOTICE.txt b/x-pack/plugin/security/licenses/asm-NOTICE.txt new file mode 100644 index 0000000000000..8d1c8b69c3fce --- /dev/null +++ b/x-pack/plugin/security/licenses/asm-NOTICE.txt @@ -0,0 +1 @@ + diff --git a/x-pack/plugin/security/licenses/jakarta.mail-1.6.3.jar.sha1 b/x-pack/plugin/security/licenses/jakarta.mail-1.6.3.jar.sha1 new file mode 100644 index 0000000000000..12d5021ee3752 --- /dev/null +++ b/x-pack/plugin/security/licenses/jakarta.mail-1.6.3.jar.sha1 @@ -0,0 +1 @@ +787e007e377223bba85a33599d3da416c135f99b \ No newline at end of file diff --git a/x-pack/plugin/security/licenses/jakarta.mail-LICENSE.txt b/x-pack/plugin/security/licenses/jakarta.mail-LICENSE.txt new file mode 100644 index 0000000000000..5de3d1b40c199 --- /dev/null +++ b/x-pack/plugin/security/licenses/jakarta.mail-LICENSE.txt @@ -0,0 +1,637 @@ +# Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + + 1. DEFINITIONS + + "Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + + "Contributor" means any person or entity that Distributes the Program. + + "Licensed Patents" mean patent claims licensable by a Contributor which + are necessarily infringed by the use or sale of its Contribution alone + or when combined with the Program. + + "Program" means the Contributions Distributed in accordance with this + Agreement. + + "Recipient" means anyone who receives the Program under this Agreement + or any Secondary License (as applicable), including Contributors. + + "Derivative Works" shall mean any work, whether in Source Code or other + form, that is based on (or derived from) the Program and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. + + "Modified Works" shall mean any work in Source Code or other form that + results from an addition to, deletion from, or modification of the + contents of the Program, including, for purposes of clarity any new file + in Source Code form that contains any contents of the Program. Modified + Works shall not include works that contain only declarations, + interfaces, types, classes, structures, or files of the Program solely + in each case in order to link to, bind by name, or subclass the Program + or Modified Works thereof. + + "Distribute" means the acts of a) distributing or b) making available + in any manner that enables the transfer of a copy. + + "Source Code" means the form of a Program preferred for making + modifications, including but not limited to software source code, + documentation source, and configuration files. + + "Secondary License" means either the GNU General Public License, + Version 2.0, or any later versions of that license, including any + exceptions or additional permissions as identified by the initial + Contributor. + + 2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + + 3. REQUIREMENTS + + 3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + + 3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + + 3.3 Contributors may not remove or alter any copyright, patent, + trademark, attribution notices, disclaimers of warranty, or limitations + of liability ("notices") contained within the Program from any copy of + the Program which they Distribute, provided that Contributors may add + their own appropriate notices. + + 4. COMMERCIAL DISTRIBUTION + + Commercial distributors of software may accept certain responsibilities + with respect to end users, business partners and the like. While this + license is intended to facilitate the commercial use of the Program, + the Contributor who includes the Program in a commercial product + offering should do so in a manner which does not create potential + liability for other Contributors. Therefore, if a Contributor includes + the Program in a commercial product offering, such Contributor + ("Commercial Contributor") hereby agrees to defend and indemnify every + other Contributor ("Indemnified Contributor") against any losses, + damages and costs (collectively "Losses") arising from claims, lawsuits + and other legal actions brought by a third party against the Indemnified + Contributor to the extent caused by the acts or omissions of such + Commercial Contributor in connection with its distribution of the Program + in a commercial product offering. The obligations in this section do not + apply to any claims or Losses relating to any actual or alleged + intellectual property infringement. In order to qualify, an Indemnified + Contributor must: a) promptly notify the Commercial Contributor in + writing of such claim, and b) allow the Commercial Contributor to control, + and cooperate with the Commercial Contributor in, the defense and any + related settlement negotiations. The Indemnified Contributor may + participate in any such claim at its own expense. + + For example, a Contributor might include the Program in a commercial + product offering, Product X. That Contributor is then a Commercial + Contributor. If that Commercial Contributor then makes performance + claims, or offers warranties related to Product X, those performance + claims and warranties are such Commercial Contributor's responsibility + alone. Under this section, the Commercial Contributor would have to + defend claims against the other Contributors related to those performance + claims and warranties, and if a court requires any other Contributor to + pay any damages as a result, the Commercial Contributor must pay + those damages. + + 5. NO WARRANTY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT + PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" + BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR + IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF + TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR + PURPOSE. Each Recipient is solely responsible for determining the + appropriateness of using and distributing the Program and assumes all + risks associated with its exercise of rights under this Agreement, + including but not limited to the risks and costs of program errors, + compliance with applicable laws, damage to or loss of data, programs + or equipment, and unavailability or interruption of operations. + + 6. DISCLAIMER OF LIABILITY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT + PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS + SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST + PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE + EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGES. + + 7. GENERAL + + If any provision of this Agreement is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this Agreement, and without further + action by the parties hereto, such provision shall be reformed to the + minimum extent necessary to make such provision valid and enforceable. + + If Recipient institutes patent litigation against any entity + (including a cross-claim or counterclaim in a lawsuit) alleging that the + Program itself (excluding combinations of the Program with other software + or hardware) infringes such Recipient's patent(s), then such Recipient's + rights granted under Section 2(b) shall terminate as of the date such + litigation is filed. + + All Recipient's rights under this Agreement shall terminate if it + fails to comply with any of the material terms or conditions of this + Agreement and does not cure such failure in a reasonable period of + time after becoming aware of such noncompliance. If all Recipient's + rights under this Agreement terminate, Recipient agrees to cease use + and distribution of the Program as soon as reasonably practicable. + However, Recipient's obligations under this Agreement and any licenses + granted by Recipient relating to the Program shall continue and survive. + + Everyone is permitted to copy and distribute copies of this Agreement, + but in order to avoid inconsistency the Agreement is copyrighted and + may only be modified in the following manner. The Agreement Steward + reserves the right to publish new versions (including revisions) of + this Agreement from time to time. No one other than the Agreement + Steward has the right to modify this Agreement. The Eclipse Foundation + is the initial Agreement Steward. The Eclipse Foundation may assign the + responsibility to serve as the Agreement Steward to a suitable separate + entity. Each new version of the Agreement will be given a distinguishing + version number. The Program (including Contributions) may always be + Distributed subject to the version of the Agreement under which it was + received. In addition, after a new version of the Agreement is published, + Contributor may elect to Distribute the Program (including its + Contributions) under the new version. + + Except as expressly stated in Sections 2(a) and 2(b) above, Recipient + receives no rights or licenses to the intellectual property of any + Contributor under this Agreement, whether expressly, by implication, + estoppel or otherwise. All rights in the Program not expressly granted + under this Agreement are reserved. Nothing in this Agreement is intended + to be enforceable by any entity that is not a Contributor or Recipient. + No third-party beneficiary rights are created under this Agreement. + + Exhibit A - Form of Secondary Licenses Notice + + "This Source Code may also be made available under the following + Secondary Licenses when the conditions for such availability set forth + in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), + version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. + +--- + +## The GNU General Public License (GPL) Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor + Boston, MA 02110-1335 + USA + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your freedom to + share and change it. By contrast, the GNU General Public License is + intended to guarantee your freedom to share and change free software--to + make sure the software is free for all its users. This General Public + License applies to most of the Free Software Foundation's software and + to any other program whose authors commit to using it. (Some other Free + Software Foundation software is covered by the GNU Library General + Public License instead.) You can apply it to your programs, too. + + When we speak of free software, we are referring to freedom, not price. + Our General Public Licenses are designed to make sure that you have the + freedom to distribute copies of free software (and charge for this + service if you wish), that you receive source code or can get it if you + want it, that you can change the software or use pieces of it in new + free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid anyone + to deny you these rights or to ask you to surrender the rights. These + restrictions translate to certain responsibilities for you if you + distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether gratis + or for a fee, you must give the recipients all the rights that you have. + You must make sure that they, too, receive or can get the source code. + And you must show them these terms so they know their rights. + + We protect your rights with two steps: (1) copyright the software, and + (2) offer you this license which gives you legal permission to copy, + distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain + that everyone understands that there is no warranty for this free + software. If the software is modified by someone else and passed on, we + want its recipients to know that what they have is not the original, so + that any problems introduced by others will not reflect on the original + authors' reputations. + + Finally, any free program is threatened constantly by software patents. + We wish to avoid the danger that redistributors of a free program will + individually obtain patent licenses, in effect making the program + proprietary. To prevent this, we have made it clear that any patent must + be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and + modification follow. + + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains a + notice placed by the copyright holder saying it may be distributed under + the terms of this General Public License. The "Program", below, refers + to any such program or work, and a "work based on the Program" means + either the Program or any derivative work under copyright law: that is + to say, a work containing the Program or a portion of it, either + verbatim or with modifications and/or translated into another language. + (Hereinafter, translation is included without limitation in the term + "modification".) Each licensee is addressed as "you". + + Activities other than copying, distribution and modification are not + covered by this License; they are outside its scope. The act of running + the Program is not restricted, and the output from the Program is + covered only if its contents constitute a work based on the Program + (independent of having been made by running the Program). Whether that + is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's source + code as you receive it, in any medium, provided that you conspicuously + and appropriately publish on each copy an appropriate copyright notice + and disclaimer of warranty; keep intact all the notices that refer to + this License and to the absence of any warranty; and give any other + recipients of the Program a copy of this License along with the Program. + + You may charge a fee for the physical act of transferring a copy, and + you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion of + it, thus forming a work based on the Program, and copy and distribute + such modifications or work under the terms of Section 1 above, provided + that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any part + thereof, to be licensed as a whole at no charge to all third parties + under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a notice + that there is no warranty (or else, saying that you provide a + warranty) and that users may redistribute the program under these + conditions, and telling the user how to view a copy of this License. + (Exception: if the Program itself is interactive but does not + normally print such an announcement, your work based on the Program + is not required to print an announcement.) + + These requirements apply to the modified work as a whole. If + identifiable sections of that work are not derived from the Program, and + can be reasonably considered independent and separate works in + themselves, then this License, and its terms, do not apply to those + sections when you distribute them as separate works. But when you + distribute the same sections as part of a whole which is a work based on + the Program, the distribution of the whole must be on the terms of this + License, whose permissions for other licensees extend to the entire + whole, and thus to each and every part regardless of who wrote it. + + Thus, it is not the intent of this section to claim rights or contest + your rights to work written entirely by you; rather, the intent is to + exercise the right to control the distribution of derivative or + collective works based on the Program. + + In addition, mere aggregation of another work not based on the Program + with the Program (or with a work based on the Program) on a volume of a + storage or distribution medium does not bring the other work under the + scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, + under Section 2) in object code or executable form under the terms of + Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections 1 + and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your cost + of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to + distribute corresponding source code. (This alternative is allowed + only for noncommercial distribution and only if you received the + program in object code or executable form with such an offer, in + accord with Subsection b above.) + + The source code for a work means the preferred form of the work for + making modifications to it. For an executable work, complete source code + means all the source code for all modules it contains, plus any + associated interface definition files, plus the scripts used to control + compilation and installation of the executable. However, as a special + exception, the source code distributed need not include anything that is + normally distributed (in either source or binary form) with the major + components (compiler, kernel, and so on) of the operating system on + which the executable runs, unless that component itself accompanies the + executable. + + If distribution of executable or object code is made by offering access + to copy from a designated place, then offering equivalent access to copy + the source code from the same place counts as distribution of the source + code, even though third parties are not compelled to copy the source + along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program + except as expressly provided under this License. Any attempt otherwise + to copy, modify, sublicense or distribute the Program is void, and will + automatically terminate your rights under this License. However, parties + who have received copies, or rights, from you under this License will + not have their licenses terminated so long as such parties remain in + full compliance. + + 5. You are not required to accept this License, since you have not + signed it. However, nothing else grants you permission to modify or + distribute the Program or its derivative works. These actions are + prohibited by law if you do not accept this License. Therefore, by + modifying or distributing the Program (or any work based on the + Program), you indicate your acceptance of this License to do so, and all + its terms and conditions for copying, distributing or modifying the + Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the + Program), the recipient automatically receives a license from the + original licensor to copy, distribute or modify the Program subject to + these terms and conditions. You may not impose any further restrictions + on the recipients' exercise of the rights granted herein. You are not + responsible for enforcing compliance by third parties to this License. + + 7. If, as a consequence of a court judgment or allegation of patent + infringement or for any other reason (not limited to patent issues), + conditions are imposed on you (whether by court order, agreement or + otherwise) that contradict the conditions of this License, they do not + excuse you from the conditions of this License. If you cannot distribute + so as to satisfy simultaneously your obligations under this License and + any other pertinent obligations, then as a consequence you may not + distribute the Program at all. For example, if a patent license would + not permit royalty-free redistribution of the Program by all those who + receive copies directly or indirectly through you, then the only way you + could satisfy both it and this License would be to refrain entirely from + distribution of the Program. + + If any portion of this section is held invalid or unenforceable under + any particular circumstance, the balance of the section is intended to + apply and the section as a whole is intended to apply in other + circumstances. + + It is not the purpose of this section to induce you to infringe any + patents or other property right claims or to contest validity of any + such claims; this section has the sole purpose of protecting the + integrity of the free software distribution system, which is implemented + by public license practices. Many people have made generous + contributions to the wide range of software distributed through that + system in reliance on consistent application of that system; it is up to + the author/donor to decide if he or she is willing to distribute + software through any other system and a licensee cannot impose that choice. + + This section is intended to make thoroughly clear what is believed to be + a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in + certain countries either by patents or by copyrighted interfaces, the + original copyright holder who places the Program under this License may + add an explicit geographical distribution limitation excluding those + countries, so that distribution is permitted only in or among countries + not thus excluded. In such case, this License incorporates the + limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new + versions of the General Public License from time to time. Such new + versions will be similar in spirit to the present version, but may + differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the Program + specifies a version number of this License which applies to it and "any + later version", you have the option of following the terms and + conditions either of that version or of any later version published by + the Free Software Foundation. If the Program does not specify a version + number of this License, you may choose any version ever published by the + Free Software Foundation. + + 10. If you wish to incorporate parts of the Program into other free + programs whose distribution conditions are different, write to the + author to ask for permission. For software which is copyrighted by the + Free Software Foundation, write to the Free Software Foundation; we + sometimes make exceptions for this. Our decision will be guided by the + two goals of preserving the free status of all derivatives of our free + software and of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO + WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. + EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR + OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, + EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE + ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH + YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL + NECESSARY SERVICING, REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN + WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY + AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR + DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL + DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM + (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED + INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF + THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR + OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest + possible use to the public, the best way to achieve this is to make it + free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest to + attach them to the start of each source file to most effectively convey + the exclusion of warranty; and each file should have at least the + "copyright" line and a pointer to where the full notice is found. + + One line to give the program's name and a brief idea of what it does. + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA + + Also add information on how to contact you by electronic and paper mail. + + If the program is interactive, make it output a short notice like this + when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type + `show w'. This is free software, and you are welcome to redistribute + it under certain conditions; type `show c' for details. + + The hypothetical commands `show w' and `show c' should show the + appropriate parts of the General Public License. Of course, the commands + you use may be called something other than `show w' and `show c'; they + could even be mouse-clicks or menu items--whatever suits your program. + + You should also get your employer (if you work as a programmer) or your + school, if any, to sign a "copyright disclaimer" for the program, if + necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + program `Gnomovision' (which makes passes at compilers) written by + James Hacker. + + signature of Ty Coon, 1 April 1989 + Ty Coon, President of Vice + + This General Public License does not permit incorporating your program + into proprietary programs. If your program is a subroutine library, you + may consider it more useful to permit linking proprietary applications + with the library. If this is what you want to do, use the GNU Library + General Public License instead of this License. + +--- + +## CLASSPATH EXCEPTION + + Linking this library statically or dynamically with other modules is + making a combined work based on this library. Thus, the terms and + conditions of the GNU General Public License version 2 cover the whole + combination. + + As a special exception, the copyright holders of this library give you + permission to link this library with independent modules to produce an + executable, regardless of the license terms of these independent + modules, and to copy and distribute the resulting executable under + terms of your choice, provided that you also meet, for each linked + independent module, the terms and conditions of the license of that + module. An independent module is a module which is not derived from or + based on this library. If you modify this library, you may extend this + exception to your version of the library, but you are not obligated to + do so. If you do not wish to do so, delete this exception statement + from your version. diff --git a/x-pack/plugin/security/licenses/jakarta.mail-NOTICE.txt b/x-pack/plugin/security/licenses/jakarta.mail-NOTICE.txt new file mode 100644 index 0000000000000..9a5159e29c9e3 --- /dev/null +++ b/x-pack/plugin/security/licenses/jakarta.mail-NOTICE.txt @@ -0,0 +1,50 @@ +# Notices for Eclipse Project for JavaMail + +This content is produced and maintained by the Eclipse Project for JavaMail +project. + +* Project home: https://projects.eclipse.org/projects/ee4j.javamail + +## Trademarks + +Eclipse Project for JavaMail is a trademark of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v. 2.0 which is available at +http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made +available under the following Secondary Licenses when the conditions for such +availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU +General Public License, version 2 with the GNU Classpath Exception which is +available at https://www.gnu.org/software/classpath/license.html. + +SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +## Source Code + +The project maintains the following source code repositories: + +* https://github.com/eclipse-ee4j/javamail + +## Third-party Content + +This project leverages the following third party content. + +None + +## Cryptography + +Content may contain encryption software. The country in which you are currently +may have restrictions on the import, possession, and use, and/or re-export to +another country, of encryption software. BEFORE using any encryption software, +please check the country's laws, regulations and policies concerning the import, +possession, or use, and re-export of encryption software, to see if this is +permitted. + diff --git a/x-pack/plugin/security/licenses/jcip-annotations-1.0.jar.sha1 b/x-pack/plugin/security/licenses/jcip-annotations-1.0.jar.sha1 new file mode 100644 index 0000000000000..9eaed5270992b --- /dev/null +++ b/x-pack/plugin/security/licenses/jcip-annotations-1.0.jar.sha1 @@ -0,0 +1 @@ +afba4942caaeaf46aab0b976afd57cc7c181467e \ No newline at end of file diff --git a/x-pack/plugin/security/licenses/jcip-annotations-LICENSE.txt b/x-pack/plugin/security/licenses/jcip-annotations-LICENSE.txt new file mode 100644 index 0000000000000..d645695673349 --- /dev/null +++ b/x-pack/plugin/security/licenses/jcip-annotations-LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/x-pack/plugin/security/licenses/jcip-annotations-NOTICE.txt b/x-pack/plugin/security/licenses/jcip-annotations-NOTICE.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/x-pack/plugin/security/licenses/json-smart-2.3.jar.sha1 b/x-pack/plugin/security/licenses/json-smart-2.3.jar.sha1 new file mode 100644 index 0000000000000..8c5c1588c150f --- /dev/null +++ b/x-pack/plugin/security/licenses/json-smart-2.3.jar.sha1 @@ -0,0 +1 @@ +007396407491352ce4fa30de92efb158adb76b5b \ No newline at end of file diff --git a/x-pack/plugin/security/licenses/json-smart-LICENSE.txt b/x-pack/plugin/security/licenses/json-smart-LICENSE.txt new file mode 100644 index 0000000000000..d645695673349 --- /dev/null +++ b/x-pack/plugin/security/licenses/json-smart-LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/x-pack/plugin/security/licenses/json-smart-NOTICE.txt b/x-pack/plugin/security/licenses/json-smart-NOTICE.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/x-pack/plugin/security/licenses/lang-tag-1.4.4.jar.sha1 b/x-pack/plugin/security/licenses/lang-tag-1.4.4.jar.sha1 new file mode 100644 index 0000000000000..9f21e84c8af3f --- /dev/null +++ b/x-pack/plugin/security/licenses/lang-tag-1.4.4.jar.sha1 @@ -0,0 +1 @@ +1db9a709239ae473a69b5424c7e78d0b7108229d \ No newline at end of file diff --git a/x-pack/plugin/security/licenses/lang-tag-LICENSE.txt b/x-pack/plugin/security/licenses/lang-tag-LICENSE.txt new file mode 100644 index 0000000000000..d645695673349 --- /dev/null +++ b/x-pack/plugin/security/licenses/lang-tag-LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/x-pack/plugin/security/licenses/lang-tag-NOTICE.txt b/x-pack/plugin/security/licenses/lang-tag-NOTICE.txt new file mode 100644 index 0000000000000..37a85f6850d57 --- /dev/null +++ b/x-pack/plugin/security/licenses/lang-tag-NOTICE.txt @@ -0,0 +1,14 @@ +Nimbus Language Tags + +Copyright 2012-2016, Connect2id Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/x-pack/plugin/security/licenses/nimbus-jose-jwt-4.41.2.jar.sha1 b/x-pack/plugin/security/licenses/nimbus-jose-jwt-4.41.2.jar.sha1 new file mode 100644 index 0000000000000..7713379f35a6c --- /dev/null +++ b/x-pack/plugin/security/licenses/nimbus-jose-jwt-4.41.2.jar.sha1 @@ -0,0 +1 @@ +3981d32ddfa2919a7af46eb5e484f8dc064da665 \ No newline at end of file diff --git a/x-pack/plugin/security/licenses/nimbus-jose-jwt-LICENSE.txt b/x-pack/plugin/security/licenses/nimbus-jose-jwt-LICENSE.txt new file mode 100644 index 0000000000000..d645695673349 --- /dev/null +++ b/x-pack/plugin/security/licenses/nimbus-jose-jwt-LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/x-pack/plugin/security/licenses/nimbus-jose-jwt-NOTICE.txt b/x-pack/plugin/security/licenses/nimbus-jose-jwt-NOTICE.txt new file mode 100644 index 0000000000000..cb9ad94f662a6 --- /dev/null +++ b/x-pack/plugin/security/licenses/nimbus-jose-jwt-NOTICE.txt @@ -0,0 +1,14 @@ +Nimbus JOSE + JWT + +Copyright 2012 - 2018, Connect2id Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/x-pack/plugin/security/licenses/oauth2-oidc-sdk-6.5.jar.sha1 b/x-pack/plugin/security/licenses/oauth2-oidc-sdk-6.5.jar.sha1 new file mode 100644 index 0000000000000..12e6376c4db32 --- /dev/null +++ b/x-pack/plugin/security/licenses/oauth2-oidc-sdk-6.5.jar.sha1 @@ -0,0 +1 @@ +422759fc195f65345e8da3265c69dea3c6cf56a5 \ No newline at end of file diff --git a/x-pack/plugin/security/licenses/oauth2-oidc-sdk-LICENSE.txt b/x-pack/plugin/security/licenses/oauth2-oidc-sdk-LICENSE.txt new file mode 100644 index 0000000000000..d645695673349 --- /dev/null +++ b/x-pack/plugin/security/licenses/oauth2-oidc-sdk-LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/x-pack/plugin/security/licenses/oauth2-oidc-sdk-NOTICE.txt b/x-pack/plugin/security/licenses/oauth2-oidc-sdk-NOTICE.txt new file mode 100644 index 0000000000000..5e111b04cfc45 --- /dev/null +++ b/x-pack/plugin/security/licenses/oauth2-oidc-sdk-NOTICE.txt @@ -0,0 +1,14 @@ +Nimbus OAuth 2.0 SDK with OpenID Connect extensions + +Copyright 2012-2018, Connect2id Ltd and contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 7b7e72fdd6b98..6ddcb6c4af81a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -80,6 +80,9 @@ import org.elasticsearch.xpack.core.security.action.CreateApiKeyAction; import org.elasticsearch.xpack.core.security.action.GetApiKeyAction; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateAction; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutAction; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationAction; import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesAction; import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesAction; import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesAction; @@ -135,6 +138,9 @@ import org.elasticsearch.xpack.security.action.TransportGetApiKeyAction; import org.elasticsearch.xpack.security.action.TransportInvalidateApiKeyAction; import org.elasticsearch.xpack.security.action.filter.SecurityActionFilter; +import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectAuthenticateAction; +import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectLogoutAction; +import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectPrepareAuthenticationAction; import org.elasticsearch.xpack.security.action.privilege.TransportDeletePrivilegesAction; import org.elasticsearch.xpack.security.action.privilege.TransportGetPrivilegesAction; import org.elasticsearch.xpack.security.action.privilege.TransportPutPrivilegesAction; @@ -193,6 +199,7 @@ import org.elasticsearch.xpack.security.rest.action.RestInvalidateApiKeyAction; import org.elasticsearch.xpack.security.rest.action.oauth2.RestGetTokenAction; import org.elasticsearch.xpack.security.rest.action.oauth2.RestInvalidateTokenAction; +import org.elasticsearch.xpack.security.rest.action.oidc.RestOpenIdConnectLogoutAction; import org.elasticsearch.xpack.security.rest.action.privilege.RestDeletePrivilegesAction; import org.elasticsearch.xpack.security.rest.action.privilege.RestGetPrivilegesAction; import org.elasticsearch.xpack.security.rest.action.privilege.RestPutPrivilegesAction; @@ -204,6 +211,8 @@ import org.elasticsearch.xpack.security.rest.action.rolemapping.RestDeleteRoleMappingAction; import org.elasticsearch.xpack.security.rest.action.rolemapping.RestGetRoleMappingsAction; import org.elasticsearch.xpack.security.rest.action.rolemapping.RestPutRoleMappingAction; +import org.elasticsearch.xpack.security.rest.action.oidc.RestOpenIdConnectAuthenticateAction; +import org.elasticsearch.xpack.security.rest.action.oidc.RestOpenIdConnectPrepareAuthenticationAction; import org.elasticsearch.xpack.security.rest.action.saml.RestSamlAuthenticateAction; import org.elasticsearch.xpack.security.rest.action.saml.RestSamlInvalidateSessionAction; import org.elasticsearch.xpack.security.rest.action.saml.RestSamlLogoutAction; @@ -737,6 +746,10 @@ public void onIndexModule(IndexModule module) { new ActionHandler<>(SamlAuthenticateAction.INSTANCE, TransportSamlAuthenticateAction.class), new ActionHandler<>(SamlLogoutAction.INSTANCE, TransportSamlLogoutAction.class), new ActionHandler<>(SamlInvalidateSessionAction.INSTANCE, TransportSamlInvalidateSessionAction.class), + new ActionHandler<>(OpenIdConnectPrepareAuthenticationAction.INSTANCE, + TransportOpenIdConnectPrepareAuthenticationAction.class), + new ActionHandler<>(OpenIdConnectAuthenticateAction.INSTANCE, TransportOpenIdConnectAuthenticateAction.class), + new ActionHandler<>(OpenIdConnectLogoutAction.INSTANCE, TransportOpenIdConnectLogoutAction.class), new ActionHandler<>(GetPrivilegesAction.INSTANCE, TransportGetPrivilegesAction.class), new ActionHandler<>(PutPrivilegesAction.INSTANCE, TransportPutPrivilegesAction.class), new ActionHandler<>(DeletePrivilegesAction.INSTANCE, TransportDeletePrivilegesAction.class), @@ -789,6 +802,9 @@ public List getRestHandlers(Settings settings, RestController restC new RestSamlAuthenticateAction(settings, restController, getLicenseState()), new RestSamlLogoutAction(settings, restController, getLicenseState()), new RestSamlInvalidateSessionAction(settings, restController, getLicenseState()), + new RestOpenIdConnectPrepareAuthenticationAction(settings, restController, getLicenseState()), + new RestOpenIdConnectAuthenticateAction(settings, restController, getLicenseState()), + new RestOpenIdConnectLogoutAction(settings, restController, getLicenseState()), new RestGetPrivilegesAction(settings, restController, getLicenseState()), new RestPutPrivilegesAction(settings, restController, getLicenseState()), new RestDeletePrivilegesAction(settings, restController, getLicenseState()), diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectAuthenticateAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectAuthenticateAction.java new file mode 100644 index 0000000000000..1b4aff064a0c3 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectAuthenticateAction.java @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.action.oidc; + +import com.nimbusds.oauth2.sdk.id.State; +import com.nimbusds.openid.connect.sdk.Nonce; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateRequest; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateResponse; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateAction; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.security.authc.AuthenticationService; +import org.elasticsearch.xpack.security.authc.TokenService; +import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm; +import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectToken; + +import java.util.Map; + +public class TransportOpenIdConnectAuthenticateAction + extends HandledTransportAction { + + private final ThreadPool threadPool; + private final AuthenticationService authenticationService; + private final TokenService tokenService; + + @Inject + public TransportOpenIdConnectAuthenticateAction(ThreadPool threadPool, TransportService transportService, + ActionFilters actionFilters, AuthenticationService authenticationService, + TokenService tokenService) { + super(OpenIdConnectAuthenticateAction.NAME, transportService, actionFilters, + (Writeable.Reader) OpenIdConnectAuthenticateRequest::new); + this.threadPool = threadPool; + this.authenticationService = authenticationService; + this.tokenService = tokenService; + } + + @Override + protected void doExecute(Task task, OpenIdConnectAuthenticateRequest request, + ActionListener listener) { + final OpenIdConnectToken token = new OpenIdConnectToken(request.getRedirectUri(), new State(request.getState()), + new Nonce(request.getNonce())); + final ThreadContext threadContext = threadPool.getThreadContext(); + Authentication originatingAuthentication = Authentication.getAuthentication(threadContext); + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { + authenticationService.authenticate(OpenIdConnectAuthenticateAction.NAME, request, token, ActionListener.wrap( + authentication -> { + AuthenticationResult result = threadContext.getTransient(AuthenticationResult.THREAD_CONTEXT_KEY); + if (result == null) { + listener.onFailure(new IllegalStateException("Cannot find AuthenticationResult on thread context")); + return; + } + @SuppressWarnings("unchecked") final Map tokenMetadata = (Map) result.getMetadata() + .get(OpenIdConnectRealm.CONTEXT_TOKEN_DATA); + tokenService.createOAuth2Tokens(authentication, originatingAuthentication, tokenMetadata, true, + ActionListener.wrap(tuple -> { + final String tokenString = tokenService.getAccessTokenAsString(tuple.v1()); + final TimeValue expiresIn = tokenService.getExpirationDelay(); + listener.onResponse(new OpenIdConnectAuthenticateResponse(authentication.getUser().principal(), tokenString, + tuple.v2(), expiresIn)); + }, listener::onFailure)); + }, e -> { + logger.debug(() -> new ParameterizedMessage("OpenIDConnectToken [{}] could not be authenticated", token), e); + listener.onFailure(e); + } + )); + } + } +} + diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectLogoutAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectLogoutAction.java new file mode 100644 index 0000000000000..fb1969f4fb06a --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectLogoutAction.java @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.action.oidc; + +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutAction; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutRequest; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutResponse; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.Realm; +import org.elasticsearch.xpack.core.security.authc.support.TokensInvalidationResult; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.authc.Realms; +import org.elasticsearch.xpack.security.authc.TokenService; +import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm; + +import java.text.ParseException; +import java.util.Map; + +/** + * Transport action responsible for generating an OpenID connect logout request to be sent to an OpenID Connect Provider + */ +public class TransportOpenIdConnectLogoutAction extends HandledTransportAction { + + private final Realms realms; + private final TokenService tokenService; + private static final Logger logger = LogManager.getLogger(TransportOpenIdConnectLogoutAction.class); + + @Inject + public TransportOpenIdConnectLogoutAction(TransportService transportService, ActionFilters actionFilters, Realms realms, + TokenService tokenService) { + super(OpenIdConnectLogoutAction.NAME, transportService, actionFilters, + (Writeable.Reader) OpenIdConnectLogoutRequest::new); + this.realms = realms; + this.tokenService = tokenService; + } + + @Override + protected void doExecute(Task task, OpenIdConnectLogoutRequest request, ActionListener listener) { + invalidateRefreshToken(request.getRefreshToken(), ActionListener.wrap(ignore -> { + final String token = request.getToken(); + tokenService.getAuthenticationAndMetaData(token, ActionListener.wrap( + tuple -> { + final Authentication authentication = tuple.v1(); + final Map tokenMetadata = tuple.v2(); + validateAuthenticationAndMetadata(authentication, tokenMetadata); + tokenService.invalidateAccessToken(token, ActionListener.wrap( + result -> { + if (logger.isTraceEnabled()) { + logger.trace("OpenID Connect Logout for user [{}] and token [{}...{}]", + authentication.getUser().principal(), + token.substring(0, 8), + token.substring(token.length() - 8)); + } + OpenIdConnectLogoutResponse response = buildResponse(authentication, tokenMetadata); + listener.onResponse(response); + }, listener::onFailure) + ); + }, listener::onFailure)); + }, listener::onFailure)); + } + + private OpenIdConnectLogoutResponse buildResponse(Authentication authentication, Map tokenMetadata) { + final String idTokenHint = (String) getFromMetadata(tokenMetadata, "id_token_hint"); + final Realm realm = this.realms.realm(authentication.getAuthenticatedBy().getName()); + final JWT idToken; + try { + idToken = JWTParser.parse(idTokenHint); + } catch (ParseException e) { + throw new ElasticsearchSecurityException("Token Metadata did not contain a valid IdToken", e); + } + return ((OpenIdConnectRealm) realm).buildLogoutResponse(idToken); + } + + private void validateAuthenticationAndMetadata(Authentication authentication, Map tokenMetadata) { + if (tokenMetadata == null) { + throw new ElasticsearchSecurityException("Authentication did not contain metadata"); + } + if (authentication == null) { + throw new ElasticsearchSecurityException("No active authentication"); + } + final User user = authentication.getUser(); + if (user == null) { + throw new ElasticsearchSecurityException("No active user"); + } + + final Authentication.RealmRef ref = authentication.getAuthenticatedBy(); + if (ref == null || Strings.isNullOrEmpty(ref.getName())) { + throw new ElasticsearchSecurityException("Authentication {} has no authenticating realm", + authentication); + } + final Realm realm = this.realms.realm(authentication.getAuthenticatedBy().getName()); + if (realm == null) { + throw new ElasticsearchSecurityException("Authenticating realm {} does not exist", ref.getName()); + } + if (realm instanceof OpenIdConnectRealm == false) { + throw new IllegalArgumentException("Access token is not valid for an OpenID Connect realm"); + } + } + + private Object getFromMetadata(Map metadata, String key) { + if (metadata.containsKey(key) == false) { + throw new ElasticsearchSecurityException("Authentication token does not have OpenID Connect metadata [{}]", key); + } + Object value = metadata.get(key); + if (null != value && value instanceof String == false) { + throw new ElasticsearchSecurityException("In authentication token, OpenID Connect metadata [{}] is [{}] rather than " + + "String", key, value.getClass()); + } + return value; + + } + private void invalidateRefreshToken(String refreshToken, ActionListener listener) { + if (refreshToken == null) { + listener.onResponse(null); + } else { + tokenService.invalidateRefreshToken(refreshToken, listener); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectPrepareAuthenticationAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectPrepareAuthenticationAction.java new file mode 100644 index 0000000000000..652daf18f5342 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectPrepareAuthenticationAction.java @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.action.oidc; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationAction; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationRequest; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationResponse; +import org.elasticsearch.xpack.core.security.authc.Realm; +import org.elasticsearch.xpack.security.authc.Realms; +import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm; + +import java.util.List; +import java.util.stream.Collectors; + +public class TransportOpenIdConnectPrepareAuthenticationAction extends HandledTransportAction { + + private final Realms realms; + + @Inject + public TransportOpenIdConnectPrepareAuthenticationAction(TransportService transportService, + ActionFilters actionFilters, Realms realms) { + super(OpenIdConnectPrepareAuthenticationAction.NAME, transportService, actionFilters, + (Writeable.Reader) OpenIdConnectPrepareAuthenticationRequest::new); + this.realms = realms; + } + + @Override + protected void doExecute(Task task, OpenIdConnectPrepareAuthenticationRequest request, + ActionListener listener) { + Realm realm = null; + if (Strings.hasText(request.getIssuer())) { + List matchingRealms = this.realms.stream() + .filter(r -> r instanceof OpenIdConnectRealm && ((OpenIdConnectRealm) r).isIssuerValid(request.getIssuer())) + .map(r -> (OpenIdConnectRealm) r) + .collect(Collectors.toList()); + if (matchingRealms.isEmpty()) { + listener.onFailure( + new ElasticsearchSecurityException("Cannot find OpenID Connect realm with issuer [{}]", request.getIssuer())); + } else if (matchingRealms.size() > 1) { + listener.onFailure( + new ElasticsearchSecurityException("Found multiple OpenID Connect realm with issuer [{}]", request.getIssuer())); + } else { + realm = matchingRealms.get(0); + } + } else if (Strings.hasText(request.getRealmName())) { + realm = this.realms.realm(request.getRealmName()); + } + + if (realm instanceof OpenIdConnectRealm) { + prepareAuthenticationResponse((OpenIdConnectRealm) realm, request.getState(), request.getNonce(), request.getLoginHint(), + listener); + } else { + listener.onFailure( + new ElasticsearchSecurityException("Cannot find OpenID Connect realm with name [{}]", request.getRealmName())); + } + } + + private void prepareAuthenticationResponse(OpenIdConnectRealm realm, String state, String nonce, String loginHint, + ActionListener listener) { + try { + final OpenIdConnectPrepareAuthenticationResponse authenticationResponse = + realm.buildAuthenticationRequestUri(state, nonce, loginHint); + listener.onResponse(authenticationResponse); + } catch (ElasticsearchException e) { + listener.onFailure(e); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java index 54bffd8a21566..66206d5013713 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java @@ -18,6 +18,7 @@ import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings; +import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings; import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; import org.elasticsearch.xpack.core.ssl.SSLService; @@ -27,6 +28,7 @@ import org.elasticsearch.xpack.security.authc.file.FileRealm; import org.elasticsearch.xpack.security.authc.kerberos.KerberosRealm; import org.elasticsearch.xpack.security.authc.ldap.LdapRealm; +import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm; import org.elasticsearch.xpack.security.authc.pki.PkiRealm; import org.elasticsearch.xpack.security.authc.saml.SamlRealm; import org.elasticsearch.xpack.security.authc.support.RoleMappingFileBootstrapCheck; @@ -45,6 +47,7 @@ /** * Provides a single entry point into dealing with all standard XPack security {@link Realm realms}. * This class does not handle extensions. + * * @see Realms for the component that manages configured realms (including custom extension realms) */ public final class InternalRealms { @@ -53,15 +56,16 @@ public final class InternalRealms { * The list of all internal realm types, excluding {@link ReservedRealm#TYPE}. */ private static final Set XPACK_TYPES = Collections - .unmodifiableSet(Sets.newHashSet(NativeRealmSettings.TYPE, FileRealmSettings.TYPE, LdapRealmSettings.AD_TYPE, - LdapRealmSettings.LDAP_TYPE, PkiRealmSettings.TYPE, SamlRealmSettings.TYPE, KerberosRealmSettings.TYPE)); + .unmodifiableSet(Sets.newHashSet(NativeRealmSettings.TYPE, FileRealmSettings.TYPE, LdapRealmSettings.AD_TYPE, + LdapRealmSettings.LDAP_TYPE, PkiRealmSettings.TYPE, SamlRealmSettings.TYPE, KerberosRealmSettings.TYPE, + OpenIdConnectRealmSettings.TYPE)); /** * The list of all standard realm types, which are those provided by x-pack and do not have extensive * interaction with third party sources */ private static final Set STANDARD_TYPES = Collections.unmodifiableSet(Sets.newHashSet(NativeRealmSettings.TYPE, - FileRealmSettings.TYPE, LdapRealmSettings.AD_TYPE, LdapRealmSettings.LDAP_TYPE, PkiRealmSettings.TYPE)); + FileRealmSettings.TYPE, LdapRealmSettings.AD_TYPE, LdapRealmSettings.LDAP_TYPE, PkiRealmSettings.TYPE)); /** * Determines whether type is an internal realm-type that is provided by x-pack, @@ -90,6 +94,7 @@ static boolean isStandardRealm(String type) { /** * Creates {@link Realm.Factory factories} for each internal realm type. * This excludes the {@link ReservedRealm}, as it cannot be created dynamically. + * * @return A map from realm-type to Factory */ public static Map getFactories(ThreadPool threadPool, ResourceWatcherService resourceWatcherService, @@ -105,12 +110,14 @@ public static Map getFactories(ThreadPool threadPool, Res return nativeRealm; }); map.put(LdapRealmSettings.AD_TYPE, config -> new LdapRealm(config, sslService, - resourceWatcherService, nativeRoleMappingStore, threadPool)); + resourceWatcherService, nativeRoleMappingStore, threadPool)); map.put(LdapRealmSettings.LDAP_TYPE, config -> new LdapRealm(config, - sslService, resourceWatcherService, nativeRoleMappingStore, threadPool)); + sslService, resourceWatcherService, nativeRoleMappingStore, threadPool)); map.put(PkiRealmSettings.TYPE, config -> new PkiRealm(config, resourceWatcherService, nativeRoleMappingStore)); map.put(SamlRealmSettings.TYPE, config -> SamlRealm.create(config, sslService, resourceWatcherService, nativeRoleMappingStore)); map.put(KerberosRealmSettings.TYPE, config -> new KerberosRealm(config, nativeRoleMappingStore, threadPool)); + map.put(OpenIdConnectRealmSettings.TYPE, config -> new OpenIdConnectRealm(config, sslService, nativeRoleMappingStore, + resourceWatcherService)); return Collections.unmodifiableMap(map); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticator.java new file mode 100644 index 0000000000000..32cffc80071c3 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticator.java @@ -0,0 +1,722 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authc.oidc; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jose.proc.JWSVerificationKeySelector; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jose.util.IOUtils; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.oauth2.sdk.AuthorizationCode; +import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant; +import com.nimbusds.oauth2.sdk.ErrorObject; +import com.nimbusds.oauth2.sdk.ResponseType; +import com.nimbusds.oauth2.sdk.TokenErrorResponse; +import com.nimbusds.oauth2.sdk.auth.Secret; +import com.nimbusds.oauth2.sdk.id.State; +import com.nimbusds.oauth2.sdk.token.AccessToken; +import com.nimbusds.oauth2.sdk.token.BearerTokenError; +import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; +import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse; +import com.nimbusds.openid.connect.sdk.AuthenticationResponse; +import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser; +import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse; +import com.nimbusds.openid.connect.sdk.Nonce; +import com.nimbusds.openid.connect.sdk.OIDCTokenResponse; +import com.nimbusds.openid.connect.sdk.claims.AccessTokenHash; +import com.nimbusds.openid.connect.sdk.token.OIDCTokens; +import com.nimbusds.openid.connect.sdk.validators.AccessTokenValidator; +import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator; +import net.minidev.json.JSONObject; +import org.apache.commons.codec.Charsets; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.auth.AuthenticationException; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.concurrent.FutureCallback; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.ssl.DefaultHostnameVerifier; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.entity.ContentType; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; +import org.apache.http.impl.nio.client.HttpAsyncClients; +import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager; +import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.nio.conn.NoopIOSessionStrategy; +import org.apache.http.nio.conn.SchemeIOSessionStrategy; +import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy; +import org.apache.http.nio.reactor.ConnectingIOReactor; +import org.apache.http.util.EntityUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.SpecialPermission; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.CheckedRunnable; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.SuppressForbidden; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.common.util.concurrent.ListenableFuture; +import org.elasticsearch.watcher.FileChangesListener; +import org.elasticsearch.watcher.FileWatcher; +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.ssl.SSLConfiguration; +import org.elasticsearch.xpack.core.ssl.SSLService; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.ALLOWED_CLOCK_SKEW; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.HTTP_CONNECT_TIMEOUT; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.HTTP_CONNECTION_READ_TIMEOUT; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.HTTP_MAX_CONNECTIONS; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.HTTP_MAX_ENDPOINT_CONNECTIONS; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.HTTP_SOCKET_TIMEOUT; + +/** + * Handles an OpenID Connect Authentication response as received by the facilitator. In the case of an implicit flow, validates + * the ID Token and extracts the elasticsearch user properties from it. In the case of an authorization code flow, it first + * exchanges the code in the authentication response for an ID Token at the token endpoint of the OpenID Connect Provider. + */ +public class OpenIdConnectAuthenticator { + + private final RealmConfig realmConfig; + private final OpenIdConnectProviderConfiguration opConfig; + private final RelyingPartyConfiguration rpConfig; + private final SSLService sslService; + private AtomicReference idTokenValidator = new AtomicReference<>(); + private final CloseableHttpAsyncClient httpClient; + private final ResourceWatcherService watcherService; + + private static final Logger LOGGER = LogManager.getLogger(OpenIdConnectAuthenticator.class); + + public OpenIdConnectAuthenticator(RealmConfig realmConfig, OpenIdConnectProviderConfiguration opConfig, + RelyingPartyConfiguration rpConfig, SSLService sslService, ResourceWatcherService watcherService) { + this.realmConfig = realmConfig; + this.opConfig = opConfig; + this.rpConfig = rpConfig; + this.sslService = sslService; + this.httpClient = createHttpClient(); + this.watcherService = watcherService; + this.idTokenValidator.set(createIdTokenValidator()); + } + + // For testing + OpenIdConnectAuthenticator(RealmConfig realmConfig, OpenIdConnectProviderConfiguration opConfig, RelyingPartyConfiguration rpConfig, + SSLService sslService, IDTokenValidator idTokenValidator, ResourceWatcherService watcherService) { + this.realmConfig = realmConfig; + this.opConfig = opConfig; + this.rpConfig = rpConfig; + this.sslService = sslService; + this.httpClient = createHttpClient(); + this.idTokenValidator.set(idTokenValidator); + this.watcherService = watcherService; + } + + /** + * Processes an OpenID Connect Response to an Authentication Request that comes in the form of a URL with the necessary parameters, + * that is contained in the provided Token. If the response is valid, it calls the provided listener with a set of OpenID Connect + * claims that identify the authenticated user. If the UserInfo endpoint is specified in the configuration, we attempt to make a + * UserInfo request and add the returned claims to the Id Token claims. + * + * @param token The OpenIdConnectToken to consume + * @param listener The listener to notify with the resolved {@link JWTClaimsSet} + */ + public void authenticate(OpenIdConnectToken token, final ActionListener listener) { + try { + AuthenticationResponse authenticationResponse = AuthenticationResponseParser.parse(new URI(token.getRedirectUrl())); + final Nonce expectedNonce = token.getNonce(); + State expectedState = token.getState(); + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("OpenID Connect Provider redirected user to [{}]. Expected Nonce is [{}] and expected State is [{}]", + token.getRedirectUrl(), expectedNonce, expectedState); + } + if (authenticationResponse instanceof AuthenticationErrorResponse) { + ErrorObject error = ((AuthenticationErrorResponse) authenticationResponse).getErrorObject(); + listener.onFailure(new ElasticsearchSecurityException("OpenID Connect Provider response indicates authentication failure" + + "Code=[{}], Description=[{}]", error.getCode(), error.getDescription())); + return; + } + final AuthenticationSuccessResponse response = authenticationResponse.toSuccessResponse(); + validateState(expectedState, response.getState()); + validateResponseType(response); + if (rpConfig.getResponseType().impliesCodeFlow()) { + final AuthorizationCode code = response.getAuthorizationCode(); + exchangeCodeForToken(code, ActionListener.wrap(tokens -> { + final AccessToken accessToken = tokens.v1(); + final JWT idToken = tokens.v2(); + validateAccessToken(accessToken, idToken); + getUserClaims(accessToken, idToken, expectedNonce, true, listener); + }, listener::onFailure)); + } else { + final JWT idToken = response.getIDToken(); + final AccessToken accessToken = response.getAccessToken(); + validateAccessToken(accessToken, idToken); + getUserClaims(accessToken, idToken, expectedNonce, true, listener); + } + } catch (ElasticsearchSecurityException e) { + // Don't wrap in a new ElasticsearchSecurityException + listener.onFailure(e); + } catch (Exception e) { + listener.onFailure(new ElasticsearchSecurityException("Failed to consume the OpenID connect response. ", e)); + } + } + + /** + * Collects all the user claims we can get for the authenticated user. This happens in two steps: + *
    + *
  • First we attempt to validate the Id Token we have received and get any claims it contains
  • + *
  • If we have received an Access Token and the UserInfo endpoint is configured, we also attempt to get the user info response + * from there and parse the returned claims, + * see {@link OpenIdConnectAuthenticator#getAndCombineUserInfoClaims(AccessToken, JWTClaimsSet, ActionListener)}
  • + *
+ * + * @param accessToken The {@link AccessToken} that the OP has issued for this user + * @param idToken The {@link JWT} Id Token that the OP has issued for this user + * @param expectedNonce The nonce value we sent in the authentication request and should be contained in the Id Token + * @param claimsListener The listener to notify with the resolved {@link JWTClaimsSet} + */ + private void getUserClaims(@Nullable AccessToken accessToken, JWT idToken, Nonce expectedNonce, boolean shouldRetry, + ActionListener claimsListener) { + try { + JWTClaimsSet verifiedIdTokenClaims = idTokenValidator.get().validate(idToken, expectedNonce).toJWTClaimsSet(); + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Received and validated the Id Token for the user: [{}]", verifiedIdTokenClaims); + } + // Add the Id Token string as a synthetic claim + final JSONObject verifiedIdTokenClaimsObject = verifiedIdTokenClaims.toJSONObject(); + final JWTClaimsSet idTokenClaim = new JWTClaimsSet.Builder().claim("id_token_hint", idToken.serialize()).build(); + verifiedIdTokenClaimsObject.merge(idTokenClaim.toJSONObject()); + final JWTClaimsSet enrichedVerifiedIdTokenClaims = JWTClaimsSet.parse(verifiedIdTokenClaimsObject); + if (accessToken != null && opConfig.getUserinfoEndpoint() != null) { + getAndCombineUserInfoClaims(accessToken, enrichedVerifiedIdTokenClaims, claimsListener); + } else { + if (accessToken == null && opConfig.getUserinfoEndpoint() != null) { + LOGGER.debug("UserInfo endpoint is configured but the OP didn't return an access token so we can't query it"); + } else if (accessToken != null && opConfig.getUserinfoEndpoint() == null) { + LOGGER.debug("OP returned an access token but the UserInfo endpoint is not configured."); + } + claimsListener.onResponse(enrichedVerifiedIdTokenClaims); + } + } catch (BadJOSEException e) { + // We only try to update the cached JWK set once if a remote source is used and + // RSA or ECDSA is used for signatures + if (shouldRetry + && JWSAlgorithm.Family.HMAC_SHA.contains(rpConfig.getSignatureAlgorithm()) == false + && opConfig.getJwkSetPath().startsWith("https://")) { + ((ReloadableJWKSource) ((JWSVerificationKeySelector) idTokenValidator.get().getJWSKeySelector()).getJWKSource()) + .triggerReload(ActionListener.wrap(v -> { + getUserClaims(accessToken, idToken, expectedNonce, false, claimsListener); + }, ex -> { + LOGGER.trace("Attempted and failed to refresh JWK cache upon token validation failure", e); + claimsListener.onFailure(ex); + })); + } else { + claimsListener.onFailure(new ElasticsearchSecurityException("Failed to parse or validate the ID Token", e)); + } + } catch (com.nimbusds.oauth2.sdk.ParseException | ParseException | JOSEException e) { + claimsListener.onFailure(new ElasticsearchSecurityException("Failed to parse or validate the ID Token", e)); + } + } + + /** + * Validates an access token according to the + * specification. + *

+ * When using the authorization code flow the OP might not provide the at_hash parameter in the + * Id Token as allowed in the specification. In such a case we can't validate the access token + * but this is considered safe as it was received in a back channel communication that was protected + * by TLS. Also when using the implicit flow with the response type set to "id_token", no Access + * Token will be returned from the OP + * + * @param accessToken The Access Token to validate. Can be null when the configured response type is "id_token" + * @param idToken The Id Token that was received in the same response + */ + private void validateAccessToken(AccessToken accessToken, JWT idToken) { + try { + if (rpConfig.getResponseType().equals(ResponseType.parse("id_token token")) || + rpConfig.getResponseType().equals(ResponseType.parse("code"))) { + assert (accessToken != null) : "Access Token cannot be null for Response Type " + rpConfig.getResponseType().toString(); + final boolean isValidationOptional = rpConfig.getResponseType().equals(ResponseType.parse("code")); + // only "Bearer" is defined in the specification but check just in case + if (accessToken.getType().toString().equals("Bearer") == false) { + throw new ElasticsearchSecurityException("Invalid access token type [{}], while [Bearer] was expected", + accessToken.getType()); + } + String atHashValue = idToken.getJWTClaimsSet().getStringClaim("at_hash"); + if (Strings.hasText(atHashValue) == false) { + if (isValidationOptional == false) { + throw new ElasticsearchSecurityException("Failed to verify access token. ID Token doesn't contain at_hash claim "); + } + } else { + AccessTokenHash atHash = new AccessTokenHash(atHashValue); + JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(idToken.getHeader().getAlgorithm().getName()); + AccessTokenValidator.validate(accessToken, jwsAlgorithm, atHash); + } + } else if (rpConfig.getResponseType().equals(ResponseType.parse("id_token")) && accessToken != null) { + // This should NOT happen and indicates a misconfigured OP. Warn the user but do not fail + LOGGER.warn("Access Token incorrectly returned from the OpenId Connect Provider while using \"id_token\" response type."); + } + } catch (Exception e) { + throw new ElasticsearchSecurityException("Failed to verify access token.", e); + } + } + + /** + * Reads and parses a JWKSet from a file + * + * @param jwkSetPath The path to the file that contains the JWKs as a string. + * @return the parsed {@link JWKSet} + * @throws ParseException if the file cannot be parsed + * @throws IOException if the file cannot be read + */ + @SuppressForbidden(reason = "uses toFile") + private JWKSet readJwkSetFromFile(String jwkSetPath) throws IOException, ParseException { + final Path path = realmConfig.env().configFile().resolve(jwkSetPath); + return JWKSet.load(path.toFile()); + } + + /** + * Validate that the response we received corresponds to the response type we requested + * + * @param response The {@link AuthenticationSuccessResponse} we received + * @throws ElasticsearchSecurityException if the response is not the expected one for the configured response type + */ + private void validateResponseType(AuthenticationSuccessResponse response) { + if (rpConfig.getResponseType().equals(response.impliedResponseType()) == false) { + throw new ElasticsearchSecurityException("Unexpected response type [{}], while [{}] is configured", + response.impliedResponseType(), rpConfig.getResponseType()); + } + } + + /** + * Validate that the state parameter the response contained corresponds to the one that we generated in the + * beginning of this authentication attempt and was stored with the user's session at the facilitator + * + * @param expectedState The state that was originally generated + * @param state The state that was contained in the response + */ + private void validateState(State expectedState, State state) { + if (null == state) { + throw new ElasticsearchSecurityException("Failed to validate the response, the response did not contain a state parameter"); + } else if (null == expectedState) { + throw new ElasticsearchSecurityException("Failed to validate the response, the user's session did not contain a state " + + "parameter"); + } else if (state.equals(expectedState) == false) { + throw new ElasticsearchSecurityException("Invalid state parameter [{}], while [{}] was expected", state, expectedState); + } + } + + /** + * Attempts to make a request to the UserInfo Endpoint of the OpenID Connect provider + */ + private void getAndCombineUserInfoClaims(AccessToken accessToken, JWTClaimsSet verifiedIdTokenClaims, + ActionListener claimsListener) { + try { + final HttpGet httpGet = new HttpGet(opConfig.getUserinfoEndpoint()); + httpGet.setHeader("Authorization", "Bearer " + accessToken.getValue()); + AccessController.doPrivileged((PrivilegedAction) () -> { + httpClient.execute(httpGet, new FutureCallback() { + @Override + public void completed(HttpResponse result) { + handleUserinfoResponse(result, verifiedIdTokenClaims, claimsListener); + } + + @Override + public void failed(Exception ex) { + claimsListener.onFailure(new ElasticsearchSecurityException("Failed to get claims from the Userinfo Endpoint.", + ex)); + } + + @Override + public void cancelled() { + claimsListener.onFailure( + new ElasticsearchSecurityException("Failed to get claims from the Userinfo Endpoint. Request was cancelled")); + } + }); + return null; + }); + } catch (Exception e) { + claimsListener.onFailure(new ElasticsearchSecurityException("Failed to get user information from the UserInfo endpoint.", e)); + } + } + + /** + * Handle the UserInfo Response from the OpenID Connect Provider. If successful, merge the returned claims with the claims + * of the Id Token and call the provided listener. + */ + private void handleUserinfoResponse(HttpResponse httpResponse, JWTClaimsSet verifiedIdTokenClaims, + ActionListener claimsListener) { + try { + final HttpEntity entity = httpResponse.getEntity(); + final Header encodingHeader = entity.getContentEncoding(); + final Charset encoding = encodingHeader == null ? StandardCharsets.UTF_8 : Charsets.toCharset(encodingHeader.getValue()); + final Header contentHeader = entity.getContentType(); + final String contentAsString = EntityUtils.toString(entity, encoding); + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Received UserInfo Response from OP with status [{}] and content [{}] ", + httpResponse.getStatusLine().getStatusCode(), contentAsString); + } + if (httpResponse.getStatusLine().getStatusCode() == 200) { + if (ContentType.parse(contentHeader.getValue()).getMimeType().equals("application/json")) { + final JWTClaimsSet userInfoClaims = JWTClaimsSet.parse(contentAsString); + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Successfully retrieved user information: [{}]", userInfoClaims.toJSONObject().toJSONString()); + } + final JSONObject combinedClaims = verifiedIdTokenClaims.toJSONObject(); + combinedClaims.merge(userInfoClaims.toJSONObject()); + claimsListener.onResponse(JWTClaimsSet.parse(combinedClaims)); + } else if (ContentType.parse(contentHeader.getValue()).getMimeType().equals("application/jwt")) { + //TODO Handle validating possibly signed responses + claimsListener.onFailure(new IllegalStateException("Unable to parse Userinfo Response. Signed/encryopted JWTs are" + + "not currently supported")); + } else { + claimsListener.onFailure(new IllegalStateException("Unable to parse Userinfo Response. Content type was expected to " + + "be [application/json] or [appliation/jwt] but was [" + contentHeader.getValue() + "]")); + } + } else { + final Header wwwAuthenticateHeader = httpResponse.getFirstHeader("WWW-Authenticate"); + if (Strings.hasText(wwwAuthenticateHeader.getValue())) { + BearerTokenError error = BearerTokenError.parse(wwwAuthenticateHeader.getValue()); + claimsListener.onFailure( + new ElasticsearchSecurityException("Failed to get user information from the UserInfo endpoint. Code=[{}], " + + "Description=[{}]", error.getCode(), error.getDescription())); + } else { + claimsListener.onFailure( + new ElasticsearchSecurityException("Failed to get user information from the UserInfo endpoint. Code=[{}], " + + "Description=[{}]", httpResponse.getStatusLine().getStatusCode(), + httpResponse.getStatusLine().getReasonPhrase())); + } + } + } catch (IOException | com.nimbusds.oauth2.sdk.ParseException | ParseException e) { + claimsListener.onFailure(new ElasticsearchSecurityException("Failed to get user information from the UserInfo endpoint.", + e)); + } + } + + /** + * Attempts to make a request to the Token Endpoint of the OpenID Connect provider in order to exchange an + * authorization code for an Id Token (and potentially an Access Token) + */ + private void exchangeCodeForToken(AuthorizationCode code, ActionListener> tokensListener) { + try { + final AuthorizationCodeGrant codeGrant = new AuthorizationCodeGrant(code, rpConfig.getRedirectUri()); + final HttpPost httpPost = new HttpPost(opConfig.getTokenEndpoint()); + final List params = new ArrayList<>(); + for (Map.Entry> entry : codeGrant.toParameters().entrySet()) { + // All parameters of AuthorizationCodeGrant are singleton lists + params.add(new BasicNameValuePair(entry.getKey(), entry.getValue().get(0))); + } + httpPost.setEntity(new UrlEncodedFormEntity(params)); + httpPost.setHeader("Content-type", "application/x-www-form-urlencoded"); + UsernamePasswordCredentials creds = new UsernamePasswordCredentials(rpConfig.getClientId().getValue(), + rpConfig.getClientSecret().toString()); + httpPost.addHeader(new BasicScheme().authenticate(creds, httpPost, null)); + SpecialPermission.check(); + AccessController.doPrivileged((PrivilegedAction) () -> { + httpClient.execute(httpPost, new FutureCallback() { + @Override + public void completed(HttpResponse result) { + handleTokenResponse(result, tokensListener); + } + + @Override + public void failed(Exception ex) { + tokensListener.onFailure( + new ElasticsearchSecurityException("Failed to exchange code for Id Token using the Token Endpoint.", ex)); + } + + @Override + public void cancelled() { + final String message = "Failed to exchange code for Id Token using the Token Endpoint. Request was cancelled"; + tokensListener.onFailure(new ElasticsearchSecurityException(message)); + } + }); + return null; + }); + } catch (AuthenticationException | UnsupportedEncodingException e) { + tokensListener.onFailure( + new ElasticsearchSecurityException("Failed to exchange code for Id Token using the Token Endpoint.", e)); + } + } + + /** + * Handle the Token Response from the OpenID Connect Provider. If successful, extract the (yet not validated) Id Token + * and access token and call the provided listener. + */ + private void handleTokenResponse(HttpResponse httpResponse, ActionListener> tokensListener) { + try { + final HttpEntity entity = httpResponse.getEntity(); + final Header encodingHeader = entity.getContentEncoding(); + final Header contentHeader = entity.getContentType(); + if (ContentType.parse(contentHeader.getValue()).getMimeType().equals("application/json") == false) { + tokensListener.onFailure(new IllegalStateException("Unable to parse Token Response. Content type was expected to be " + + "[application/json] but was [" + contentHeader.getValue() + "]")); + return; + } + final Charset encoding = encodingHeader == null ? StandardCharsets.UTF_8 : Charsets.toCharset(encodingHeader.getValue()); + final String json = EntityUtils.toString(entity, encoding); + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Received Token Response from OP with status [{}] and content [{}] ", + httpResponse.getStatusLine().getStatusCode(), json); + } + final OIDCTokenResponse oidcTokenResponse = OIDCTokenResponse.parse(JSONObjectUtils.parse(json)); + if (oidcTokenResponse.indicatesSuccess() == false) { + TokenErrorResponse errorResponse = oidcTokenResponse.toErrorResponse(); + tokensListener.onFailure( + new ElasticsearchSecurityException("Failed to exchange code for Id Token. Code=[{}], Description=[{}]", + errorResponse.getErrorObject().getCode(), errorResponse.getErrorObject().getDescription())); + } else { + OIDCTokenResponse successResponse = oidcTokenResponse.toSuccessResponse(); + final OIDCTokens oidcTokens = successResponse.getOIDCTokens(); + final AccessToken accessToken = oidcTokens.getAccessToken(); + final JWT idToken = oidcTokens.getIDToken(); + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Successfully exchanged code for ID Token: [{}] and Access Token [{}]", + idToken, accessToken); + } + if (idToken == null) { + tokensListener.onFailure(new ElasticsearchSecurityException("Token Response did not contain an ID Token or parsing of" + + " the JWT failed.")); + return; + } + tokensListener.onResponse(new Tuple<>(accessToken, idToken)); + } + } catch (IOException | com.nimbusds.oauth2.sdk.ParseException e) { + tokensListener.onFailure( + new ElasticsearchSecurityException("Failed to exchange code for Id Token using the Token Endpoint. " + + "Unable to parse Token Response", e)); + } + } + + /** + * Creates a {@link CloseableHttpAsyncClient} that uses a {@link PoolingNHttpClientConnectionManager} + */ + private CloseableHttpAsyncClient createHttpClient() { + try { + SpecialPermission.check(); + return AccessController.doPrivileged( + (PrivilegedExceptionAction) () -> { + ConnectingIOReactor ioReactor = new DefaultConnectingIOReactor(); + final String sslKey = RealmSettings.realmSslPrefix(realmConfig.identifier()); + final SSLConfiguration sslConfiguration = sslService.getSSLConfiguration(sslKey); + final SSLContext clientContext = sslService.sslContext(sslConfiguration); + boolean isHostnameVerificationEnabled = sslConfiguration.verificationMode().isHostnameVerificationEnabled(); + final HostnameVerifier verifier = isHostnameVerificationEnabled ? + new DefaultHostnameVerifier() : NoopHostnameVerifier.INSTANCE; + Registry registry = RegistryBuilder.create() + .register("http", NoopIOSessionStrategy.INSTANCE) + .register("https", new SSLIOSessionStrategy(clientContext, verifier)) + .build(); + PoolingNHttpClientConnectionManager connectionManager = new PoolingNHttpClientConnectionManager(ioReactor, registry); + connectionManager.setDefaultMaxPerRoute(realmConfig.getSetting(HTTP_MAX_ENDPOINT_CONNECTIONS)); + connectionManager.setMaxTotal(realmConfig.getSetting(HTTP_MAX_CONNECTIONS)); + final RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(Math.toIntExact(realmConfig.getSetting(HTTP_CONNECT_TIMEOUT).getMillis())) + .setConnectionRequestTimeout(Math.toIntExact(realmConfig.getSetting(HTTP_CONNECTION_READ_TIMEOUT).getSeconds())) + .setSocketTimeout(Math.toIntExact(realmConfig.getSetting(HTTP_SOCKET_TIMEOUT).getMillis())).build(); + CloseableHttpAsyncClient httpAsyncClient = HttpAsyncClients.custom() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(requestConfig) + .build(); + httpAsyncClient.start(); + return httpAsyncClient; + }); + } catch (PrivilegedActionException e) { + throw new IllegalStateException("Unable to create a HttpAsyncClient instance", e); + } + } + + /* + * Creates an {@link IDTokenValidator} based on the current Relying Party configuration + */ + IDTokenValidator createIdTokenValidator() { + try { + final JWSAlgorithm requestedAlgorithm = rpConfig.getSignatureAlgorithm(); + final int allowedClockSkew = Math.toIntExact(realmConfig.getSetting(ALLOWED_CLOCK_SKEW).getMillis()); + final IDTokenValidator idTokenValidator; + if (JWSAlgorithm.Family.HMAC_SHA.contains(requestedAlgorithm)) { + final Secret clientSecret = new Secret(rpConfig.getClientSecret().toString()); + idTokenValidator = + new IDTokenValidator(opConfig.getIssuer(), rpConfig.getClientId(), requestedAlgorithm, clientSecret); + } else { + String jwkSetPath = opConfig.getJwkSetPath(); + if (jwkSetPath.startsWith("https://")) { + final JWSVerificationKeySelector keySelector = new JWSVerificationKeySelector(requestedAlgorithm, + new ReloadableJWKSource(new URL(jwkSetPath))); + idTokenValidator = new IDTokenValidator(opConfig.getIssuer(), rpConfig.getClientId(), keySelector, null); + } else { + setMetadataFileWatcher(jwkSetPath); + final JWKSet jwkSet = readJwkSetFromFile(jwkSetPath); + idTokenValidator = new IDTokenValidator(opConfig.getIssuer(), rpConfig.getClientId(), requestedAlgorithm, jwkSet); + } + } + idTokenValidator.setMaxClockSkew(allowedClockSkew); + return idTokenValidator; + } catch (IOException | ParseException e) { + throw new IllegalStateException("Unable to create a IDTokenValidator instance", e); + } + } + + private void setMetadataFileWatcher(String jwkSetPath) throws IOException { + final Path path = realmConfig.env().configFile().resolve(jwkSetPath); + FileWatcher watcher = new FileWatcher(path); + watcher.addListener(new FileListener(LOGGER, () -> this.idTokenValidator.set(createIdTokenValidator()))); + watcherService.add(watcher, ResourceWatcherService.Frequency.MEDIUM); + } + + protected void close() { + try { + this.httpClient.close(); + } catch (IOException e) { + LOGGER.debug("Unable to close the HttpAsyncClient", e); + } + } + + private static class FileListener implements FileChangesListener { + + private final Logger logger; + private final CheckedRunnable onChange; + + private FileListener(Logger logger, CheckedRunnable onChange) { + this.logger = logger; + this.onChange = onChange; + } + + @Override + public void onFileCreated(Path file) { + onFileChanged(file); + } + + @Override + public void onFileDeleted(Path file) { + onFileChanged(file); + } + + @Override + public void onFileChanged(Path file) { + try { + onChange.run(); + } catch (Exception e) { + logger.warn(new ParameterizedMessage("An error occurred while reloading file {}", file), e); + } + } + } + + /** + * Remote JSON Web Key source specified by a JWKSet URL. The retrieved JWK set is cached to + * avoid unnecessary http requests. A single attempt to update the cached set is made + * (with {@ling ReloadableJWKSource#triggerReload}) when the {@link IDTokenValidator} fails + * to validate an ID Token (because of an unknown key) as this might mean that the OpenID + * Connect Provider has rotated the signing keys. + */ + class ReloadableJWKSource implements JWKSource { + + private volatile JWKSet cachedJwkSet = new JWKSet(); + private final AtomicReference> reloadFutureRef = new AtomicReference<>(); + private final URL jwkSetPath; + + private ReloadableJWKSource(URL jwkSetPath) { + this.jwkSetPath = jwkSetPath; + triggerReload(ActionListener.wrap(success -> LOGGER.trace("Successfully loaded and cached remote JWKSet on startup"), + failure -> LOGGER.trace("Failed to load and cache remote JWKSet on startup", failure))); + } + + @Override + public List get(JWKSelector jwkSelector, C context) { + return jwkSelector.select(cachedJwkSet); + } + + void triggerReload(ActionListener toNotify) { + ListenableFuture future = reloadFutureRef.get(); + while (future == null) { + future = new ListenableFuture<>(); + if (reloadFutureRef.compareAndSet(null, future)) { + reloadAsync(future); + } else { + future = reloadFutureRef.get(); + } + } + future.addListener(toNotify, EsExecutors.newDirectExecutorService(), null); + } + + void reloadAsync(final ListenableFuture future) { + try { + final HttpGet httpGet = new HttpGet(jwkSetPath.toURI()); + AccessController.doPrivileged((PrivilegedAction) () -> { + httpClient.execute(httpGet, new FutureCallback() { + @Override + public void completed(HttpResponse result) { + try { + cachedJwkSet = JWKSet.parse(IOUtils.readInputStreamToString(result.getEntity().getContent(), + StandardCharsets.UTF_8)); + reloadFutureRef.set(null); + LOGGER.trace("Successfully refreshed and cached remote JWKSet"); + } catch (IOException | ParseException e) { + failed(e); + } + } + + @Override + public void failed(Exception ex) { + future.onFailure(new ElasticsearchSecurityException("Failed to retrieve remote JWK set.", ex)); + reloadFutureRef.set(null); + } + + @Override + public void cancelled() { + future.onFailure( + new ElasticsearchSecurityException("Failed to retrieve remote JWK set. Request was cancelled.")); + reloadFutureRef.set(null); + } + }); + return null; + }); + } catch (URISyntaxException e) { + future.onFailure(e); + reloadFutureRef.set(null); + } + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectProviderConfiguration.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectProviderConfiguration.java new file mode 100644 index 0000000000000..272ab283c75be --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectProviderConfiguration.java @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authc.oidc; + +import com.nimbusds.oauth2.sdk.id.Issuer; +import org.elasticsearch.common.Nullable; + +import java.net.URI; +import java.util.Objects; + +/** + * A Class that contains all the OpenID Connect Provider configuration + */ +public class OpenIdConnectProviderConfiguration { + private final String providerName; + private final URI authorizationEndpoint; + private final URI tokenEndpoint; + private final URI userinfoEndpoint; + private final URI endsessionEndpoint; + private final Issuer issuer; + private final String jwkSetPath; + + public OpenIdConnectProviderConfiguration(String providerName, Issuer issuer, String jwkSetPath, URI authorizationEndpoint, + URI tokenEndpoint, @Nullable URI userinfoEndpoint, @Nullable URI endsessionEndpoint) { + this.providerName = Objects.requireNonNull(providerName, "OP Name must be provided"); + this.authorizationEndpoint = Objects.requireNonNull(authorizationEndpoint, "Authorization Endpoint must be provided"); + this.tokenEndpoint = Objects.requireNonNull(tokenEndpoint, "Token Endpoint must be provided"); + this.userinfoEndpoint = userinfoEndpoint; + this.endsessionEndpoint = endsessionEndpoint; + this.issuer = Objects.requireNonNull(issuer, "OP Issuer must be provided"); + this.jwkSetPath = Objects.requireNonNull(jwkSetPath, "jwkSetUrl must be provided"); + } + + public String getProviderName() { + return providerName; + } + + public URI getAuthorizationEndpoint() { + return authorizationEndpoint; + } + + public URI getTokenEndpoint() { + return tokenEndpoint; + } + + public URI getUserinfoEndpoint() { + return userinfoEndpoint; + } + + public URI getEndsessionEndpoint() { + return endsessionEndpoint; + } + + public Issuer getIssuer() { + return issuer; + } + + public String getJwkSetPath() { + return jwkSetPath; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java new file mode 100644 index 0000000000000..72b04951a9121 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java @@ -0,0 +1,473 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authc.oidc; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; + +import com.nimbusds.oauth2.sdk.ParseException; +import com.nimbusds.oauth2.sdk.ResponseType; +import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.oauth2.sdk.id.ClientID; +import com.nimbusds.oauth2.sdk.id.Issuer; +import com.nimbusds.oauth2.sdk.id.State; +import com.nimbusds.openid.connect.sdk.AuthenticationRequest; +import com.nimbusds.openid.connect.sdk.LogoutRequest; +import com.nimbusds.openid.connect.sdk.Nonce; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchSecurityException; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.lease.Releasable; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.SettingsException; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutResponse; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationResponse; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; +import org.elasticsearch.xpack.core.security.authc.Realm; +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.security.user.User; +import org.elasticsearch.xpack.core.ssl.SSLService; +import org.elasticsearch.xpack.security.authc.TokenService; +import org.elasticsearch.xpack.security.authc.support.DelegatedAuthorizationSupport; +import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; + +import java.net.URI; +import java.net.URISyntaxException; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + + +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.DN_CLAIM; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.GROUPS_CLAIM; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.MAIL_CLAIM; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.NAME_CLAIM; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.OP_ENDSESSION_ENDPOINT; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.OP_ISSUER; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.OP_JWKSET_PATH; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.OP_NAME; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT; +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_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; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_REDIRECT_URI; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_RESPONSE_TYPE; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_REQUESTED_SCOPES; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.RP_SIGNATURE_ALGORITHM; + +public class OpenIdConnectRealm extends Realm implements Releasable { + + public static final String CONTEXT_TOKEN_DATA = "_oidc_tokendata"; + private final OpenIdConnectProviderConfiguration opConfiguration; + private final RelyingPartyConfiguration rpConfiguration; + private final OpenIdConnectAuthenticator openIdConnectAuthenticator; + private final ClaimParser principalAttribute; + private final ClaimParser groupsAttribute; + private final ClaimParser dnAttribute; + private final ClaimParser nameAttribute; + private final ClaimParser mailAttribute; + private final Boolean populateUserMetadata; + private final UserRoleMapper roleMapper; + + private DelegatedAuthorizationSupport delegatedRealms; + + public OpenIdConnectRealm(RealmConfig config, SSLService sslService, UserRoleMapper roleMapper, + ResourceWatcherService watcherService) { + super(config); + this.roleMapper = roleMapper; + this.rpConfiguration = buildRelyingPartyConfiguration(config); + this.opConfiguration = buildOpenIdConnectProviderConfiguration(config); + this.principalAttribute = ClaimParser.forSetting(logger, PRINCIPAL_CLAIM, config, true); + this.groupsAttribute = ClaimParser.forSetting(logger, GROUPS_CLAIM, config, false); + this.dnAttribute = ClaimParser.forSetting(logger, DN_CLAIM, config, false); + this.nameAttribute = ClaimParser.forSetting(logger, NAME_CLAIM, config, false); + this.mailAttribute = ClaimParser.forSetting(logger, MAIL_CLAIM, config, false); + this.populateUserMetadata = config.getSetting(POPULATE_USER_METADATA); + if (TokenService.isTokenServiceEnabled(config.settings()) == false) { + throw new IllegalStateException("OpenID Connect Realm requires that the token service be enabled (" + + XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey() + ")"); + } + this.openIdConnectAuthenticator = + new OpenIdConnectAuthenticator(config, opConfiguration, rpConfiguration, sslService, watcherService); + } + + // For testing + OpenIdConnectRealm(RealmConfig config, OpenIdConnectAuthenticator authenticator, UserRoleMapper roleMapper) { + super(config); + this.roleMapper = roleMapper; + this.rpConfiguration = buildRelyingPartyConfiguration(config); + this.opConfiguration = buildOpenIdConnectProviderConfiguration(config); + this.openIdConnectAuthenticator = authenticator; + this.principalAttribute = ClaimParser.forSetting(logger, PRINCIPAL_CLAIM, config, true); + this.groupsAttribute = ClaimParser.forSetting(logger, GROUPS_CLAIM, config, false); + this.dnAttribute = ClaimParser.forSetting(logger, DN_CLAIM, config, false); + this.nameAttribute = ClaimParser.forSetting(logger, NAME_CLAIM, config, false); + this.mailAttribute = ClaimParser.forSetting(logger, MAIL_CLAIM, config, false); + this.populateUserMetadata = config.getSetting(POPULATE_USER_METADATA); + } + + @Override + public void initialize(Iterable realms, XPackLicenseState licenseState) { + if (delegatedRealms != null) { + throw new IllegalStateException("Realm has already been initialized"); + } + delegatedRealms = new DelegatedAuthorizationSupport(realms, config, licenseState); + } + + @Override + public boolean supports(AuthenticationToken token) { + return token instanceof OpenIdConnectToken; + } + + @Override + public AuthenticationToken token(ThreadContext context) { + return null; + } + + @Override + public void authenticate(AuthenticationToken token, ActionListener listener) { + if (token instanceof OpenIdConnectToken) { + OpenIdConnectToken oidcToken = (OpenIdConnectToken) token; + openIdConnectAuthenticator.authenticate(oidcToken, ActionListener.wrap( + jwtClaimsSet -> { + buildUserFromClaims(jwtClaimsSet, listener); + }, + e -> { + logger.debug("Failed to consume the OpenIdConnectToken ", e); + if (e instanceof ElasticsearchSecurityException) { + listener.onResponse(AuthenticationResult.unsuccessful("Failed to authenticate user with OpenID Connect", e)); + } else { + listener.onFailure(e); + } + })); + } else { + listener.onResponse(AuthenticationResult.notHandled()); + } + } + + @Override + public void lookupUser(String username, ActionListener listener) { + listener.onResponse(null); + } + + + private void buildUserFromClaims(JWTClaimsSet claims, ActionListener authResultListener) { + final String principal = principalAttribute.getClaimValue(claims); + if (Strings.isNullOrEmpty(principal)) { + authResultListener.onResponse(AuthenticationResult.unsuccessful( + principalAttribute + "not found in " + claims.toJSONObject(), null)); + return; + } + + final Map tokenMetadata = new HashMap<>(); + tokenMetadata.put("id_token_hint", claims.getClaim("id_token_hint")); + ActionListener wrappedAuthResultListener = ActionListener.wrap(auth -> { + if (auth.isAuthenticated()) { + // Add the ID Token as metadata on the authentication, so that it can be used for logout requests + Map metadata = new HashMap<>(auth.getMetadata()); + metadata.put(CONTEXT_TOKEN_DATA, tokenMetadata); + auth = AuthenticationResult.success(auth.getUser(), metadata); + } + authResultListener.onResponse(auth); + }, authResultListener::onFailure); + + if (delegatedRealms.hasDelegation()) { + delegatedRealms.resolve(principal, wrappedAuthResultListener); + return; + } + + final Map userMetadata = new HashMap<>(); + if (populateUserMetadata) { + Map claimsMap = claims.getClaims(); + /* + * We whitelist the Types that we want to parse as metadata from the Claims, explicitly filtering out {@link Date}s + */ + Set allowedEntries = claimsMap.entrySet().stream().filter(entry -> { + Object v = entry.getValue(); + return (v instanceof String || v instanceof Boolean || v instanceof Number || v instanceof Collections); + }).collect(Collectors.toSet()); + for (Map.Entry entry : allowedEntries) { + userMetadata.put("oidc(" + entry.getKey() + ")", entry.getValue()); + } + } + final List groups = groupsAttribute.getClaimValues(claims); + final String dn = dnAttribute.getClaimValue(claims); + final String mail = mailAttribute.getClaimValue(claims); + final String name = nameAttribute.getClaimValue(claims); + UserRoleMapper.UserData userData = new UserRoleMapper.UserData(principal, dn, groups, userMetadata, config); + roleMapper.resolveRoles(userData, ActionListener.wrap(roles -> { + final User user = new User(principal, roles.toArray(Strings.EMPTY_ARRAY), name, mail, userMetadata, true); + wrappedAuthResultListener.onResponse(AuthenticationResult.success(user)); + }, wrappedAuthResultListener::onFailure)); + + } + + private RelyingPartyConfiguration buildRelyingPartyConfiguration(RealmConfig config) { + final String redirectUriString = require(config, RP_REDIRECT_URI); + final URI redirectUri; + try { + redirectUri = new URI(redirectUriString); + } catch (URISyntaxException e) { + // This should never happen as it's already validated in the settings + throw new SettingsException("Invalid URI:" + RP_REDIRECT_URI.getKey(), e); + } + final String postLogoutRedirectUriString = config.getSetting(RP_POST_LOGOUT_REDIRECT_URI); + final URI postLogoutRedirectUri; + try { + postLogoutRedirectUri = new URI(postLogoutRedirectUriString); + } catch (URISyntaxException e) { + // This should never happen as it's already validated in the settings + throw new SettingsException("Invalid URI:" + RP_POST_LOGOUT_REDIRECT_URI.getKey(), e); + } + final ClientID clientId = new ClientID(require(config, RP_CLIENT_ID)); + final SecureString clientSecret = config.getSetting(RP_CLIENT_SECRET); + final ResponseType responseType; + try { + // This should never happen as it's already validated in the settings + responseType = ResponseType.parse(require(config, RP_RESPONSE_TYPE)); + } catch (ParseException e) { + throw new SettingsException("Invalid value for " + RP_RESPONSE_TYPE.getKey(), e); + } + + final Scope requestedScope = new Scope(config.getSetting(RP_REQUESTED_SCOPES).toArray(Strings.EMPTY_ARRAY)); + if (requestedScope.contains("openid") == false) { + requestedScope.add("openid"); + } + final JWSAlgorithm signatureAlgorithm = JWSAlgorithm.parse(require(config, RP_SIGNATURE_ALGORITHM)); + + return new RelyingPartyConfiguration(clientId, clientSecret, redirectUri, responseType, requestedScope, + signatureAlgorithm, postLogoutRedirectUri); + } + + private OpenIdConnectProviderConfiguration buildOpenIdConnectProviderConfiguration(RealmConfig config) { + String providerName = require(config, OP_NAME); + Issuer issuer = new Issuer(require(config, OP_ISSUER)); + + String jwkSetUrl = require(config, OP_JWKSET_PATH); + + URI authorizationEndpoint; + try { + authorizationEndpoint = new URI(require(config, OP_AUTHORIZATION_ENDPOINT)); + } catch (URISyntaxException e) { + // This should never happen as it's already validated in the settings + throw new SettingsException("Invalid URI: " + OP_AUTHORIZATION_ENDPOINT.getKey(), e); + } + URI tokenEndpoint; + try { + tokenEndpoint = new URI(require(config, OP_TOKEN_ENDPOINT)); + } catch (URISyntaxException e) { + // This should never happen as it's already validated in the settings + throw new SettingsException("Invalid URL: " + OP_TOKEN_ENDPOINT.getKey(), e); + } + URI userinfoEndpoint; + try { + userinfoEndpoint = (config.getSetting(OP_USERINFO_ENDPOINT, () -> null) == null) ? null : + new URI(config.getSetting(OP_USERINFO_ENDPOINT, () -> null)); + } catch (URISyntaxException e) { + // This should never happen as it's already validated in the settings + throw new SettingsException("Invalid URI: " + OP_USERINFO_ENDPOINT.getKey(), e); + } + URI endsessionEndpoint; + try { + endsessionEndpoint = (config.getSetting(OP_ENDSESSION_ENDPOINT, () -> null) == null) ? null : + new URI(config.getSetting(OP_ENDSESSION_ENDPOINT, () -> null)); + } catch (URISyntaxException e) { + // This should never happen as it's already validated in the settings + throw new SettingsException("Invalid URI: " + OP_ENDSESSION_ENDPOINT.getKey(), e); + } + + return new OpenIdConnectProviderConfiguration(providerName, issuer, jwkSetUrl, authorizationEndpoint, tokenEndpoint, + userinfoEndpoint, endsessionEndpoint); + } + + private static String require(RealmConfig config, Setting.AffixSetting setting) { + final String value = config.getSetting(setting); + if (value.isEmpty()) { + throw new SettingsException("The configuration setting [" + RealmSettings.getFullSettingKey(config, setting) + + "] is required"); + } + return value; + } + + /** + * Creates the URI for an OIDC Authentication Request from the realm configuration using URI Query String Serialization and + * possibly generates a state parameter and a nonce. It then returns the URI, state and nonce encapsulated in a + * {@link OpenIdConnectPrepareAuthenticationResponse}. A facilitator can provide a state and a nonce parameter in two cases: + *

    + *
  • In case of Kibana, it allows for a better UX by ensuring that all requests to an OpenID Connect Provider within + * the same browser context (even across tabs) will use the same state and nonce values.
  • + *
  • In case of custom facilitators, the implementer might require/support generating the state parameter in order + * to tie this to an anti-XSRF token.
  • + *
+ * + * + * @param existingState An existing state that can be reused or null if we need to generate one + * @param existingNonce An existing nonce that can be reused or null if we need to generate one + * @param loginHint A String with a login hint to add to the authentication request in case of a 3rd party initiated login + * + * @return an {@link OpenIdConnectPrepareAuthenticationResponse} + */ + public OpenIdConnectPrepareAuthenticationResponse buildAuthenticationRequestUri(@Nullable String existingState, + @Nullable String existingNonce, + @Nullable String loginHint) { + final State state = existingState != null ? new State(existingState) : new State(); + final Nonce nonce = existingNonce != null ? new Nonce(existingNonce) : new Nonce(); + final AuthenticationRequest.Builder builder = new AuthenticationRequest.Builder(rpConfiguration.getResponseType(), + rpConfiguration.getRequestedScope(), + rpConfiguration.getClientId(), + rpConfiguration.getRedirectUri()) + .endpointURI(opConfiguration.getAuthorizationEndpoint()) + .state(state) + .nonce(nonce); + if (Strings.hasText(loginHint)) { + builder.loginHint(loginHint); + } + return new OpenIdConnectPrepareAuthenticationResponse(builder.build().toURI().toString(), + state.getValue(), nonce.getValue()); + } + + public boolean isIssuerValid(String issuer) { + return this.opConfiguration.getIssuer().getValue().equals(issuer); + } + + public OpenIdConnectLogoutResponse buildLogoutResponse(JWT idTokenHint) { + if (opConfiguration.getEndsessionEndpoint() != null) { + final State state = new State(); + final LogoutRequest logoutRequest = new LogoutRequest(opConfiguration.getEndsessionEndpoint(), idTokenHint, + rpConfiguration.getPostLogoutRedirectUri(), state); + return new OpenIdConnectLogoutResponse(logoutRequest.toURI().toString()); + } else { + return new OpenIdConnectLogoutResponse((String) null); + } + } + + @Override + public void close() { + openIdConnectAuthenticator.close(); + } + + static final class ClaimParser { + private final String name; + private final Function> parser; + + ClaimParser(String name, Function> parser) { + this.name = name; + this.parser = parser; + } + + List getClaimValues(JWTClaimsSet claims) { + return parser.apply(claims); + } + + String getClaimValue(JWTClaimsSet claims) { + List claimValues = parser.apply(claims); + if (claimValues == null || claimValues.isEmpty()) { + return null; + } else { + return claimValues.get(0); + } + } + + @Override + public String toString() { + return name; + } + + static ClaimParser forSetting(Logger logger, OpenIdConnectRealmSettings.ClaimSetting setting, RealmConfig realmConfig, + boolean required) { + + if (realmConfig.hasSetting(setting.getClaim())) { + String claimName = realmConfig.getSetting(setting.getClaim()); + if (realmConfig.hasSetting(setting.getPattern())) { + Pattern regex = Pattern.compile(realmConfig.getSetting(setting.getPattern())); + return new ClaimParser( + "OpenID Connect Claim [" + claimName + "] with pattern [" + regex.pattern() + "] for [" + + setting.name(realmConfig) + "]", + claims -> { + Object claimValueObject = claims.getClaim(claimName); + List values; + if (claimValueObject == null) { + values = Collections.emptyList(); + } else if (claimValueObject instanceof String) { + values = Collections.singletonList((String) claimValueObject); + } else if (claimValueObject instanceof List) { + values = (List) claimValueObject; + } else { + throw new SettingsException("Setting [" + RealmSettings.getFullSettingKey(realmConfig, setting.getClaim()) + + " expects a claim with String or a String Array value but found a " + + claimValueObject.getClass().getName()); + } + return values.stream().map(s -> { + final Matcher matcher = regex.matcher(s); + if (matcher.find() == false) { + logger.debug("OpenID Connect Claim [{}] is [{}], which does not match [{}]", + claimName, s, regex.pattern()); + return null; + } + final String value = matcher.group(1); + if (Strings.isNullOrEmpty(value)) { + logger.debug("OpenID Connect Claim [{}] is [{}], which does match [{}] but group(1) is empty", + claimName, s, regex.pattern()); + return null; + } + return value; + }).filter(Objects::nonNull).collect(Collectors.toList()); + }); + } else { + return new ClaimParser( + "OpenID Connect Claim [" + claimName + "] for [" + setting.name(realmConfig) + "]", + claims -> { + Object claimValueObject = claims.getClaim(claimName); + if (claimValueObject == null) { + return Collections.emptyList(); + } else if (claimValueObject instanceof String) { + return Collections.singletonList((String) claimValueObject); + } else if (claimValueObject instanceof List == false) { + throw new SettingsException("Setting [" + RealmSettings.getFullSettingKey(realmConfig, setting.getClaim()) + + " expects a claim with String or a String Array value but found a " + + claimValueObject.getClass().getName()); + } + return (List) claimValueObject; + }); + } + } else if (required) { + throw new SettingsException("Setting [" + RealmSettings.getFullSettingKey(realmConfig, setting.getClaim()) + + "] is required"); + } else if (realmConfig.hasSetting(setting.getPattern())) { + throw new SettingsException("Setting [" + RealmSettings.getFullSettingKey(realmConfig, setting.getPattern()) + + "] cannot be set unless [" + RealmSettings.getFullSettingKey(realmConfig, setting.getClaim()) + + "] is also set"); + } else { + return new ClaimParser("No OpenID Connect Claim for [" + setting.name(realmConfig) + "]", + attributes -> Collections.emptyList()); + } + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectToken.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectToken.java new file mode 100644 index 0000000000000..ab61fd8fb9d5f --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectToken.java @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authc.oidc; + +import com.nimbusds.oauth2.sdk.id.State; +import com.nimbusds.openid.connect.sdk.Nonce; +import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; + +/** + * A {@link AuthenticationToken} to hold OpenID Connect related content. + * Depending on the flow the token can contain only a code ( oAuth2 authorization code + * grant flow ) or even an Identity Token ( oAuth2 implicit flow ) + */ +public class OpenIdConnectToken implements AuthenticationToken { + + private String redirectUrl; + private State state; + private Nonce nonce; + + /** + * @param redirectUrl The URI where the OP redirected the browser after the authentication event at the OP. This is passed as is from + * the facilitator entity (i.e. Kibana), so it is URL Encoded. It contains either the code or the id_token itself + * depending on the flow used + * @param state The state value that we generated or the facilitator provided for this specific flow and should be stored at the + * user's session with the facilitator. + * @param nonce The nonce value that we generated or the facilitator provided for this specific flow and should be stored at the + * user's session with the facilitator. + */ + public OpenIdConnectToken(String redirectUrl, State state, Nonce nonce) { + this.redirectUrl = redirectUrl; + this.state = state; + this.nonce = nonce; + } + + @Override + public String principal() { + return ""; + } + + @Override + public Object credentials() { + return redirectUrl; + } + + @Override + public void clearCredentials() { + this.redirectUrl = null; + } + + public State getState() { + return state; + } + + public Nonce getNonce() { + return nonce; + } + + public String getRedirectUrl() { + return redirectUrl; + } + + public String toString() { + return getClass().getSimpleName() + "{ redirectUrl=" + redirectUrl + ", state=" + state + ", nonce=" + nonce + "}"; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/RelyingPartyConfiguration.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/RelyingPartyConfiguration.java new file mode 100644 index 0000000000000..ed67974c0b0d2 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/RelyingPartyConfiguration.java @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authc.oidc; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.oauth2.sdk.ResponseType; +import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.oauth2.sdk.id.ClientID; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.settings.SecureString; + +import java.net.URI; +import java.util.Objects; + +/** + * A Class that contains all the OpenID Connect Relying Party configuration + */ +public class RelyingPartyConfiguration { + private final ClientID clientId; + private final SecureString clientSecret; + private final URI redirectUri; + private final ResponseType responseType; + private final Scope requestedScope; + private final JWSAlgorithm signatureAlgorithm; + private final URI postLogoutRedirectUri; + + public RelyingPartyConfiguration(ClientID clientId, SecureString clientSecret, URI redirectUri, ResponseType responseType, + Scope requestedScope, JWSAlgorithm algorithm, @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.postLogoutRedirectUri = postLogoutRedirectUri; + } + + public ClientID getClientId() { + return clientId; + } + + public SecureString getClientSecret() { + return clientSecret; + } + + public URI getRedirectUri() { + return redirectUri; + } + + public ResponseType getResponseType() { + return responseType; + } + + public Scope getRequestedScope() { + return requestedScope; + } + + public JWSAlgorithm getSignatureAlgorithm() { + return signatureAlgorithm; + } + + public URI getPostLogoutRedirectUri() { + return postLogoutRedirectUri; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/OpenIdConnectBaseRestHandler.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/OpenIdConnectBaseRestHandler.java new file mode 100644 index 0000000000000..008b5d0676e2c --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/OpenIdConnectBaseRestHandler.java @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.rest.action.oidc; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.LicenseUtils; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings; +import org.elasticsearch.xpack.security.authc.Realms; +import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; + +public abstract class OpenIdConnectBaseRestHandler extends SecurityBaseRestHandler { + + private static final String OIDC_REALM_TYPE = OpenIdConnectRealmSettings.TYPE; + + /** + * @param settings the node's settings + * @param licenseState the license state that will be used to determine if security is licensed + */ + protected OpenIdConnectBaseRestHandler(Settings settings, XPackLicenseState licenseState) { + super(settings, licenseState); + } + + @Override + protected Exception checkFeatureAvailable(RestRequest request) { + Exception failedFeature = super.checkFeatureAvailable(request); + if (failedFeature != null) { + return failedFeature; + } else if (Realms.isRealmTypeAvailable(licenseState.allowedRealmType(), OIDC_REALM_TYPE)) { + return null; + } else { + logger.info("The '{}' realm is not available under the current license", OIDC_REALM_TYPE); + return LicenseUtils.newComplianceException(OIDC_REALM_TYPE); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectAuthenticateAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectAuthenticateAction.java new file mode 100644 index 0000000000000..2ac75872b7c8a --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectAuthenticateAction.java @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.rest.action.oidc; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestBuilderListener; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateAction; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateRequest; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateResponse; + +import java.io.IOException; + +import static org.elasticsearch.rest.RestRequest.Method.POST; + +/** + * Rest handler that authenticates the user based on the information provided as parameters of the redirect_uri + */ +public class RestOpenIdConnectAuthenticateAction extends OpenIdConnectBaseRestHandler { + + static final ObjectParser PARSER = new ObjectParser<>("oidc_authn", + OpenIdConnectAuthenticateRequest::new); + + static { + PARSER.declareString(OpenIdConnectAuthenticateRequest::setRedirectUri, new ParseField("redirect_uri")); + PARSER.declareString(OpenIdConnectAuthenticateRequest::setState, new ParseField("state")); + PARSER.declareString(OpenIdConnectAuthenticateRequest::setNonce, new ParseField("nonce")); + } + + public RestOpenIdConnectAuthenticateAction(Settings settings, RestController controller, XPackLicenseState licenseState) { + super(settings, licenseState); + controller.registerHandler(POST, "/_security/oidc/authenticate", this); + } + + @Override + protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + try (XContentParser parser = request.contentParser()) { + final OpenIdConnectAuthenticateRequest authenticateRequest = PARSER.parse(parser, null); + logger.trace("OIDC Authenticate: " + authenticateRequest); + return channel -> client.execute(OpenIdConnectAuthenticateAction.INSTANCE, authenticateRequest, + new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(OpenIdConnectAuthenticateResponse response, XContentBuilder builder) + throws Exception { + builder.startObject() + .field("username", response.getPrincipal()) + .field("access_token", response.getAccessTokenString()) + .field("refresh_token", response.getRefreshTokenString()) + .field("expires_in", response.getExpiresIn().seconds()) + .endObject(); + return new BytesRestResponse(RestStatus.OK, builder); + } + }); + } + } + + @Override + public String getName() { + return "security_oidc_authenticate_action"; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectLogoutAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectLogoutAction.java new file mode 100644 index 0000000000000..e098e14c423b8 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectLogoutAction.java @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.rest.action.oidc; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestBuilderListener; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutAction; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutRequest; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutResponse; + +import java.io.IOException; + +import static org.elasticsearch.rest.RestRequest.Method.POST; + +/** + * Rest handler that invalidates a security token for the given OpenID Connect realm and if the configuration of + * the realm supports it, generates a redirect to the `end_session_endpoint` of the OpenID Connect Provider. + */ +public class RestOpenIdConnectLogoutAction extends OpenIdConnectBaseRestHandler { + + static final ObjectParser PARSER = new ObjectParser<>("oidc_logout", + OpenIdConnectLogoutRequest::new); + + static { + PARSER.declareString(OpenIdConnectLogoutRequest::setToken, new ParseField("token")); + PARSER.declareString(OpenIdConnectLogoutRequest::setRefreshToken, new ParseField("refresh_token")); + } + + public RestOpenIdConnectLogoutAction(Settings settings, RestController controller, XPackLicenseState licenseState) { + super(settings, licenseState); + controller.registerHandler(POST, "/_security/oidc/logout", this); + } + + @Override + protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + try (XContentParser parser = request.contentParser()) { + final OpenIdConnectLogoutRequest logoutRequest = PARSER.parse(parser, null); + return channel -> client.execute(OpenIdConnectLogoutAction.INSTANCE, logoutRequest, + new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(OpenIdConnectLogoutResponse response, XContentBuilder builder) throws Exception { + builder.startObject(); + builder.field("redirect", response.getEndSessionUrl()); + builder.endObject(); + return new BytesRestResponse(RestStatus.OK, builder); + } + }); + } + } + + @Override + public String getName() { + return "security_oidc_logout_action"; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectPrepareAuthenticationAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectPrepareAuthenticationAction.java new file mode 100644 index 0000000000000..60786c82b56ef --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectPrepareAuthenticationAction.java @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.rest.action.oidc; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestBuilderListener; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationAction; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationRequest; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationResponse; + +import java.io.IOException; + +import static org.elasticsearch.rest.RestRequest.Method.POST; + +/** + * Generates an oAuth 2.0 authentication request as a URL string and returns it to the REST client. + */ +public class RestOpenIdConnectPrepareAuthenticationAction extends OpenIdConnectBaseRestHandler { + + static final ObjectParser PARSER = new ObjectParser<>("oidc_prepare_authentication", + OpenIdConnectPrepareAuthenticationRequest::new); + + static { + PARSER.declareString(OpenIdConnectPrepareAuthenticationRequest::setRealmName, new ParseField("realm")); + PARSER.declareString(OpenIdConnectPrepareAuthenticationRequest::setIssuer, new ParseField("iss")); + PARSER.declareString(OpenIdConnectPrepareAuthenticationRequest::setLoginHint, new ParseField("login_hint")); + PARSER.declareString(OpenIdConnectPrepareAuthenticationRequest::setState, new ParseField("state")); + PARSER.declareString(OpenIdConnectPrepareAuthenticationRequest::setNonce, new ParseField("nonce")); + } + + public RestOpenIdConnectPrepareAuthenticationAction(Settings settings, RestController controller, XPackLicenseState licenseState) { + super(settings, licenseState); + controller.registerHandler(POST, "/_security/oidc/prepare", this); + } + + @Override + protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + try (XContentParser parser = request.contentParser()) { + final OpenIdConnectPrepareAuthenticationRequest prepareAuthenticationRequest = PARSER.parse(parser, null); + logger.trace("OIDC Prepare Authentication: " + prepareAuthenticationRequest); + return channel -> client.execute(OpenIdConnectPrepareAuthenticationAction.INSTANCE, prepareAuthenticationRequest, + new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(OpenIdConnectPrepareAuthenticationResponse response, XContentBuilder builder) + throws Exception { + logger.trace("OIDC Prepare Authentication Response: " + response); + return new BytesRestResponse(RestStatus.OK, response.toXContent(builder, request)); + } + }); + } + } + + @Override + public String getName() { + return "security_oidc_prepare_authentication_action"; + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/OpenIdConnectAuthenticateRequestTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/OpenIdConnectAuthenticateRequestTests.java new file mode 100644 index 0000000000000..08bc96c40b4e4 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/OpenIdConnectAuthenticateRequestTests.java @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.action.oidc; + + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateRequest; + +import java.io.IOException; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +public class OpenIdConnectAuthenticateRequestTests extends ESTestCase { + + public void testSerialization() throws IOException { + final OpenIdConnectAuthenticateRequest request = new OpenIdConnectAuthenticateRequest(); + final String nonce = randomAlphaOfLengthBetween(8, 12); + final String state = randomAlphaOfLengthBetween(8, 12); + final String redirectUri = "https://rp.com/cb?code=thisisacode&state=" + state; + request.setRedirectUri(redirectUri); + request.setState(state); + request.setNonce(nonce); + final BytesStreamOutput out = new BytesStreamOutput(); + request.writeTo(out); + + final OpenIdConnectAuthenticateRequest unserialized = new OpenIdConnectAuthenticateRequest(out.bytes().streamInput()); + assertThat(unserialized.getRedirectUri(), equalTo(redirectUri)); + assertThat(unserialized.getState(), equalTo(state)); + assertThat(unserialized.getNonce(), equalTo(nonce)); + } + + public void testValidation() { + final OpenIdConnectAuthenticateRequest request = new OpenIdConnectAuthenticateRequest(); + final ActionRequestValidationException validation = request.validate(); + assertNotNull(validation); + assertThat(validation.validationErrors().size(), equalTo(3)); + assertThat(validation.validationErrors().get(0), containsString("state parameter is missing")); + assertThat(validation.validationErrors().get(1), containsString("nonce parameter is missing")); + assertThat(validation.validationErrors().get(2), containsString("redirect_uri parameter is missing")); + + final OpenIdConnectAuthenticateRequest request2 = new OpenIdConnectAuthenticateRequest(); + request2.setRedirectUri("https://rp.company.com/cb?code=abc"); + request2.setState(randomAlphaOfLengthBetween(8, 12)); + final ActionRequestValidationException validation2 = request2.validate(); + assertNotNull(validation2); + assertThat(validation2.validationErrors().size(), equalTo(1)); + assertThat(validation2.validationErrors().get(0), containsString("nonce parameter is missing")); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/OpenIdConnectPrepareAuthenticationRequestTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/OpenIdConnectPrepareAuthenticationRequestTests.java new file mode 100644 index 0000000000000..e668008deb901 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/OpenIdConnectPrepareAuthenticationRequestTests.java @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.action.oidc; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationRequest; + +import java.io.IOException; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +public class OpenIdConnectPrepareAuthenticationRequestTests extends ESTestCase { + + public void testSerialization() throws IOException { + final OpenIdConnectPrepareAuthenticationRequest request = new OpenIdConnectPrepareAuthenticationRequest(); + request.setRealmName("oidc-realm1"); + final BytesStreamOutput out = new BytesStreamOutput(); + request.writeTo(out); + + final OpenIdConnectPrepareAuthenticationRequest deserialized = + new OpenIdConnectPrepareAuthenticationRequest(out.bytes().streamInput()); + assertThat(deserialized.getRealmName(), equalTo("oidc-realm1")); + + final OpenIdConnectPrepareAuthenticationRequest request2 = new OpenIdConnectPrepareAuthenticationRequest(); + request2.setIssuer("https://op.company.org/"); + final BytesStreamOutput out2 = new BytesStreamOutput(); + request2.writeTo(out2); + + final OpenIdConnectPrepareAuthenticationRequest deserialized2 = + new OpenIdConnectPrepareAuthenticationRequest(out2.bytes().streamInput()); + assertThat(deserialized2.getIssuer(), equalTo("https://op.company.org/")); + } + + public void testSerializationWithStateAndNonce() throws IOException { + final OpenIdConnectPrepareAuthenticationRequest request = new OpenIdConnectPrepareAuthenticationRequest(); + final String nonce = randomAlphaOfLengthBetween(8, 12); + final String state = randomAlphaOfLengthBetween(8, 12); + request.setRealmName("oidc-realm1"); + request.setNonce(nonce); + request.setState(state); + final BytesStreamOutput out = new BytesStreamOutput(); + request.writeTo(out); + + final OpenIdConnectPrepareAuthenticationRequest deserialized = + new OpenIdConnectPrepareAuthenticationRequest(out.bytes().streamInput()); + assertThat(deserialized.getRealmName(), equalTo("oidc-realm1")); + assertThat(deserialized.getState(), equalTo(state)); + assertThat(deserialized.getNonce(), equalTo(nonce)); + } + + public void testValidation() { + final OpenIdConnectPrepareAuthenticationRequest request = new OpenIdConnectPrepareAuthenticationRequest(); + final ActionRequestValidationException validation = request.validate(); + assertNotNull(validation); + assertThat(validation.validationErrors().size(), equalTo(1)); + assertThat(validation.validationErrors().get(0), containsString("one of [realm, issuer] must be provided")); + + final OpenIdConnectPrepareAuthenticationRequest request2 = new OpenIdConnectPrepareAuthenticationRequest(); + request2.setRealmName("oidc-realm1"); + request2.setIssuer("https://op.company.org/"); + final ActionRequestValidationException validation2 = request2.validate(); + assertNotNull(validation2); + assertThat(validation2.validationErrors().size(), equalTo(1)); + assertThat(validation2.validationErrors().get(0), + containsString("only one of [realm, issuer] can be provided in the same request")); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectLogoutActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectLogoutActionTests.java new file mode 100644 index 0000000000000..ddf1742109915 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectLogoutActionTests.java @@ -0,0 +1,230 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.action.oidc; + +import com.nimbusds.jwt.JWT; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.bulk.BulkAction; +import org.elasticsearch.action.bulk.BulkItemResponse; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.bulk.BulkRequestBuilder; +import org.elasticsearch.action.bulk.BulkResponse; +import org.elasticsearch.action.get.GetAction; +import org.elasticsearch.action.get.GetRequestBuilder; +import org.elasticsearch.action.index.IndexAction; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.index.IndexRequestBuilder; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.action.update.UpdateAction; +import org.elasticsearch.action.update.UpdateRequest; +import org.elasticsearch.action.update.UpdateRequestBuilder; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.test.ClusterServiceUtils; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.Transport; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutRequest; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutResponse; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.core.ssl.SSLService; +import org.elasticsearch.xpack.security.authc.Realms; +import org.elasticsearch.xpack.security.authc.TokenService; +import org.elasticsearch.xpack.security.authc.UserToken; +import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm; +import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectTestCase; +import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; +import org.elasticsearch.xpack.security.support.SecurityIndexManager; +import org.junit.After; +import org.junit.Before; + +import java.time.Clock; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import static org.elasticsearch.xpack.security.authc.TokenServiceTests.mockGetTokenFromId; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.startsWith; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TransportOpenIdConnectLogoutActionTests extends OpenIdConnectTestCase { + + private OpenIdConnectRealm oidcRealm; + private TokenService tokenService; + private List indexRequests; + private List bulkRequests; + private Client client; + private TransportOpenIdConnectLogoutAction action; + + @Before + public void setup() throws Exception { + final Settings settings = getBasicRealmSettings() + .put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), true) + .put("path.home", createTempDir()) + .build(); + final Settings sslSettings = Settings.builder() + .put("xpack.security.authc.realms.oidc.oidc-realm.ssl.verification_mode", "certificate") + .put("path.home", createTempDir()) + .build(); + final ThreadContext threadContext = new ThreadContext(settings); + final ThreadPool threadPool = mock(ThreadPool.class); + when(threadPool.getThreadContext()).thenReturn(threadContext); + new Authentication(new User("kibana"), new Authentication.RealmRef("realm", "type", "node"), null).writeToContext(threadContext); + indexRequests = new ArrayList<>(); + bulkRequests = new ArrayList<>(); + client = mock(Client.class); + when(client.threadPool()).thenReturn(threadPool); + when(client.settings()).thenReturn(settings); + doAnswer(invocationOnMock -> { + GetRequestBuilder builder = new GetRequestBuilder(client, GetAction.INSTANCE); + builder.setIndex((String) invocationOnMock.getArguments()[0]) + .setType((String) invocationOnMock.getArguments()[1]) + .setId((String) invocationOnMock.getArguments()[2]); + return builder; + }).when(client).prepareGet(anyString(), anyString(), anyString()); + doAnswer(invocationOnMock -> { + IndexRequestBuilder builder = new IndexRequestBuilder(client, IndexAction.INSTANCE); + builder.setIndex((String) invocationOnMock.getArguments()[0]) + .setType((String) invocationOnMock.getArguments()[1]) + .setId((String) invocationOnMock.getArguments()[2]); + return builder; + }).when(client).prepareIndex(anyString(), anyString(), anyString()); + doAnswer(invocationOnMock -> { + UpdateRequestBuilder builder = new UpdateRequestBuilder(client, UpdateAction.INSTANCE); + builder.setIndex((String) invocationOnMock.getArguments()[0]) + .setType((String) invocationOnMock.getArguments()[1]) + .setId((String) invocationOnMock.getArguments()[2]); + return builder; + }).when(client).prepareUpdate(anyString(), anyString(), anyString()); + doAnswer(invocationOnMock -> { + BulkRequestBuilder builder = new BulkRequestBuilder(client, BulkAction.INSTANCE); + return builder; + }).when(client).prepareBulk(); + doAnswer(invocationOnMock -> { + IndexRequest indexRequest = (IndexRequest) invocationOnMock.getArguments()[0]; + ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; + indexRequests.add(indexRequest); + final IndexResponse response = new IndexResponse( + indexRequest.shardId(), indexRequest.type(), indexRequest.id(), 1, 1, 1, true); + listener.onResponse(response); + return Void.TYPE; + }).when(client).index(any(IndexRequest.class), any(ActionListener.class)); + doAnswer(invocationOnMock -> { + IndexRequest indexRequest = (IndexRequest) invocationOnMock.getArguments()[1]; + ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; + indexRequests.add(indexRequest); + final IndexResponse response = new IndexResponse( + indexRequest.shardId(), indexRequest.type(), indexRequest.id(), 1, 1, 1, true); + listener.onResponse(response); + return Void.TYPE; + }).when(client).execute(eq(IndexAction.INSTANCE), any(IndexRequest.class), any(ActionListener.class)); + doAnswer(invocationOnMock -> { + BulkRequest bulkRequest = (BulkRequest) invocationOnMock.getArguments()[0]; + ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; + bulkRequests.add(bulkRequest); + final BulkResponse response = new BulkResponse(new BulkItemResponse[0], 1); + listener.onResponse(response); + return Void.TYPE; + }).when(client).bulk(any(BulkRequest.class), any(ActionListener.class)); + + final SecurityIndexManager securityIndex = mock(SecurityIndexManager.class); + doAnswer(inv -> { + ((Runnable) inv.getArguments()[1]).run(); + return null; + }).when(securityIndex).prepareIndexIfNeededThenExecute(any(Consumer.class), any(Runnable.class)); + doAnswer(inv -> { + ((Runnable) inv.getArguments()[1]).run(); + return null; + }).when(securityIndex).checkIndexVersionThenExecute(any(Consumer.class), any(Runnable.class)); + when(securityIndex.isAvailable()).thenReturn(true); + + final ClusterService clusterService = ClusterServiceUtils.createClusterService(threadPool); + tokenService = new TokenService(settings, Clock.systemUTC(), client, securityIndex, clusterService); + + final TransportService transportService = new TransportService(Settings.EMPTY, mock(Transport.class), null, + TransportService.NOOP_TRANSPORT_INTERCEPTOR, x -> null, null, Collections.emptySet()); + final Realms realms = mock(Realms.class); + action = new TransportOpenIdConnectLogoutAction(transportService, mock(ActionFilters.class), realms, tokenService); + + final Environment env = TestEnvironment.newEnvironment(settings); + + final RealmConfig.RealmIdentifier realmIdentifier = new RealmConfig.RealmIdentifier("oidc", REALM_NAME); + + final RealmConfig realmConfig = new RealmConfig(realmIdentifier, settings, env, threadContext); + oidcRealm = new OpenIdConnectRealm(realmConfig, new SSLService(sslSettings, env), mock(UserRoleMapper.class), + mock(ResourceWatcherService.class)); + when(realms.realm(realmConfig.name())).thenReturn(oidcRealm); + } + + public void testLogoutInvalidatesTokens() throws Exception { + final String subject = randomAlphaOfLength(8); + final JWT signedIdToken = generateIdToken(subject, randomAlphaOfLength(8), randomAlphaOfLength(8)); + final User user = new User("oidc-user", new String[]{"superuser"}, null, null, null, true); + final Authentication.RealmRef realmRef = new Authentication.RealmRef(oidcRealm.name(), OpenIdConnectRealmSettings.TYPE, "node01"); + final Authentication authentication = new Authentication(user, realmRef, null); + + final Map tokenMetadata = new HashMap<>(); + tokenMetadata.put("id_token_hint", signedIdToken.serialize()); + tokenMetadata.put("oidc_realm", REALM_NAME); + + final PlainActionFuture> future = new PlainActionFuture<>(); + tokenService.createOAuth2Tokens(authentication, authentication, tokenMetadata, true, future); + final UserToken userToken = future.actionGet().v1(); + mockGetTokenFromId(userToken, false, client); + final String tokenString = tokenService.getAccessTokenAsString(userToken); + + final OpenIdConnectLogoutRequest request = new OpenIdConnectLogoutRequest(); + request.setToken(tokenString); + + final PlainActionFuture listener = new PlainActionFuture<>(); + action.doExecute(mock(Task.class), request, listener); + final OpenIdConnectLogoutResponse response = listener.get(); + assertNotNull(response); + assertThat(response.getEndSessionUrl(), notNullValue()); + // One index request to create the token + assertThat(indexRequests.size(), equalTo(1)); + final IndexRequest indexRequest = indexRequests.get(0); + assertThat(indexRequest, notNullValue()); + assertThat(indexRequest.id(), startsWith("token")); + // One bulk request (containing one update request) to invalidate the token + assertThat(bulkRequests.size(), equalTo(1)); + final BulkRequest bulkRequest = bulkRequests.get(0); + assertThat(bulkRequest.requests().size(), equalTo(1)); + assertThat(bulkRequest.requests().get(0), instanceOf(UpdateRequest.class)); + assertThat(bulkRequest.requests().get(0).id(), startsWith("token_")); + assertThat(bulkRequest.requests().get(0).toString(), containsString("\"access_token\":{\"invalidated\":true")); + } + + @After + public void cleanup() { + oidcRealm.close(); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/InternalRealmsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/InternalRealmsTests.java index f9007583c2ca1..e3298e5103772 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/InternalRealmsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/InternalRealmsTests.java @@ -18,6 +18,7 @@ import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings; +import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings; import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; import org.elasticsearch.xpack.core.ssl.SSLService; @@ -61,7 +62,7 @@ public void testIsStandardType() { String type = randomFrom(NativeRealmSettings.TYPE, FileRealmSettings.TYPE, LdapRealmSettings.AD_TYPE, LdapRealmSettings.LDAP_TYPE, PkiRealmSettings.TYPE); assertThat(InternalRealms.isStandardRealm(type), is(true)); - type = randomFrom(SamlRealmSettings.TYPE, KerberosRealmSettings.TYPE); + type = randomFrom(SamlRealmSettings.TYPE, KerberosRealmSettings.TYPE, OpenIdConnectRealmSettings.TYPE); assertThat(InternalRealms.isStandardRealm(type), is(false)); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/SecurityRealmSettingsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/SecurityRealmSettingsTests.java index 202467147cddf..3bc89d29f8df7 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/SecurityRealmSettingsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/SecurityRealmSettingsTests.java @@ -17,6 +17,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.security.authc.kerberos.KerberosRealmTestCase; +import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectTestCase; import org.elasticsearch.xpack.security.authc.saml.SamlRealmTestHelper; import org.hamcrest.Matchers; import org.junit.AfterClass; @@ -48,6 +49,9 @@ protected Settings nodeSettings(int nodeOrdinal) { final Path kerbKeyTab = createTempFile("es", "keytab"); KerberosRealmTestCase.writeKeyTab(kerbKeyTab, null); + final Path jwkSet = createTempFile("jwkset", "json"); + OpenIdConnectTestCase.writeJwkSetToFile(jwkSet); + settings = Settings.builder() .put(super.nodeSettings(nodeOrdinal).filter(s -> s.startsWith("xpack.security.authc.realms.") == false)) .put("xpack.security.authc.token.enabled", true) @@ -67,6 +71,16 @@ protected Settings nodeSettings(int nodeOrdinal) { .put("xpack.security.authc.realms.saml.saml1.attributes.principal", "uid") .put("xpack.security.authc.realms.kerberos.kerb1.order", 7) .put("xpack.security.authc.realms.kerberos.kerb1.keytab.path", kerbKeyTab.toAbsolutePath()) + .put("xpack.security.authc.realms.oidc.oidc1.order", 8) + .put("xpack.security.authc.realms.oidc.oidc1.op.name", "myprovider") + .put("xpack.security.authc.realms.oidc.oidc1.op.issuer", "https://the.issuer.com:8090") + .put("xpack.security.authc.realms.oidc.oidc1.op.jwkset_path", jwkSet.toAbsolutePath()) + .put("xpack.security.authc.realms.oidc.oidc1.op.authorization_endpoint", "https://the.issuer.com:8090/login") + .put("xpack.security.authc.realms.oidc.oidc1.op.token_endpoint", "https://the.issuer.com:8090/token") + .put("xpack.security.authc.realms.oidc.oidc1.rp.redirect_uri", "https://localhost/cb") + .put("xpack.security.authc.realms.oidc.oidc1.rp.client_id", "my_client") + .put("xpack.security.authc.realms.oidc.oidc1.rp.response_type", "code") + .put("xpack.security.authc.realms.oidc.oidc1.claims.principal", "sub") .build(); } catch (IOException e) { throw new RuntimeException(e); @@ -84,7 +98,7 @@ protected Settings nodeSettings(int nodeOrdinal) { } /** - * Some realms (currently only SAML, but maybe more in the future) hold on to resources that may need to be explicitly closed. + * Some realms (SAML and OIDC at the moment) hold on to resources that may need to be explicitly closed. */ @AfterClass public static void closeRealms() throws IOException { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java new file mode 100644 index 0000000000000..e7fdbfe558ad2 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java @@ -0,0 +1,808 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authc.oidc; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jose.crypto.MACSigner; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.OctetSequenceKey; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jose.proc.BadJWSException; +import com.nimbusds.jose.proc.JWSVerificationKeySelector; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.PlainJWT; +import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.jwt.proc.BadJWTException; +import com.nimbusds.oauth2.sdk.ResponseType; +import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.oauth2.sdk.auth.Secret; +import com.nimbusds.oauth2.sdk.id.ClientID; +import com.nimbusds.oauth2.sdk.id.Issuer; +import com.nimbusds.oauth2.sdk.id.State; +import com.nimbusds.oauth2.sdk.token.AccessToken; +import com.nimbusds.oauth2.sdk.token.BearerAccessToken; +import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse; +import com.nimbusds.openid.connect.sdk.Nonce; +import com.nimbusds.openid.connect.sdk.claims.AccessTokenHash; +import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator; +import com.nimbusds.openid.connect.sdk.validators.InvalidHashException; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.ssl.SSLService; +import org.junit.After; +import org.junit.Before; +import org.mockito.Mockito; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Base64; +import java.util.Collections; +import java.util.Date; +import java.util.UUID; + +import static java.time.Instant.now; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase { + + private OpenIdConnectAuthenticator authenticator; + private Settings globalSettings; + private Environment env; + private ThreadContext threadContext; + + @Before + public void setup() { + globalSettings = Settings.builder().put("path.home", createTempDir()) + .put("xpack.security.authc.realms.oidc.oidc-realm.ssl.verification_mode", "certificate").build(); + env = TestEnvironment.newEnvironment(globalSettings); + threadContext = new ThreadContext(globalSettings); + } + + @After + public void cleanup() { + authenticator.close(); + } + + private OpenIdConnectAuthenticator buildAuthenticator() throws URISyntaxException { + final RealmConfig config = buildConfig(getBasicRealmSettings().build(), threadContext); + return new OpenIdConnectAuthenticator(config, getOpConfig(), getDefaultRpConfig(), new SSLService(globalSettings, env), null); + } + + private OpenIdConnectAuthenticator buildAuthenticator(OpenIdConnectProviderConfiguration opConfig, RelyingPartyConfiguration rpConfig, + OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource) { + final RealmConfig config = buildConfig(getBasicRealmSettings().build(), threadContext); + final JWSVerificationKeySelector keySelector = new JWSVerificationKeySelector(rpConfig.getSignatureAlgorithm(), jwkSource); + final IDTokenValidator validator = new IDTokenValidator(opConfig.getIssuer(), rpConfig.getClientId(), keySelector, null); + return new OpenIdConnectAuthenticator(config, opConfig, rpConfig, new SSLService(globalSettings, env), validator, + null); + } + + private OpenIdConnectAuthenticator buildAuthenticator(OpenIdConnectProviderConfiguration opConfig, + RelyingPartyConfiguration rpConfig) { + final RealmConfig config = buildConfig(getBasicRealmSettings().build(), threadContext); + final IDTokenValidator validator = new IDTokenValidator(opConfig.getIssuer(), rpConfig.getClientId(), + rpConfig.getSignatureAlgorithm(), new Secret(rpConfig.getClientSecret().toString())); + return new OpenIdConnectAuthenticator(config, opConfig, rpConfig, new SSLService(globalSettings, env), validator, + null); + } + + public void testEmptyRedirectUrlIsRejected() throws Exception { + authenticator = buildAuthenticator(); + OpenIdConnectToken token = new OpenIdConnectToken(null, new State(), new Nonce()); + final PlainActionFuture future = new PlainActionFuture<>(); + authenticator.authenticate(token, future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("Failed to consume the OpenID connect response")); + } + + public void testInvalidStateIsRejected() throws URISyntaxException { + authenticator = buildAuthenticator(); + final String code = randomAlphaOfLengthBetween(8, 12); + final String state = randomAlphaOfLengthBetween(8, 12); + final String invalidState = state.concat(randomAlphaOfLength(2)); + final String redirectUrl = "https://rp.elastic.co/cb?code=" + code + "&state=" + state; + OpenIdConnectToken token = new OpenIdConnectToken(redirectUrl, new State(invalidState), new Nonce()); + final PlainActionFuture future = new PlainActionFuture<>(); + authenticator.authenticate(token, future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("Invalid state parameter")); + } + + public void testInvalidNonceIsRejected() throws Exception { + final Tuple keyMaterial = getRandomJwkForType(randomFrom("HS", "ES", "RS")); + final JWK jwk = keyMaterial.v2().getKeys().get(0); + final Key key = keyMaterial.v1(); + RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName()); + OpenIdConnectProviderConfiguration opConfig = getOpConfig(); + if (jwk.getAlgorithm().getName().startsWith("HS")) { + authenticator = buildAuthenticator(opConfig, rpConfig); + } else { + OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk); + authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource); + } + + final State state = new State(); + final Nonce nonce = new Nonce(); + final Nonce invalidNonce = new Nonce(); + final String subject = "janedoe"; + final String keyId = (jwk.getAlgorithm().getName().startsWith("HS")) ? null : jwk.getKeyID(); + final Tuple tokens = buildTokens(invalidNonce, key, jwk.getAlgorithm().getName(), keyId, subject, true, false); + final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri()); + final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce); + final PlainActionFuture future = new PlainActionFuture<>(); + authenticator.authenticate(token, future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token")); + assertThat(e.getCause(), instanceOf(BadJWTException.class)); + assertThat(e.getCause().getMessage(), containsString("Unexpected JWT nonce")); + } + + public void testAuthenticateImplicitFlowWithRsa() throws Exception { + final Tuple keyMaterial = getRandomJwkForType("RS"); + final JWK jwk = keyMaterial.v2().getKeys().get(0); + final Key key = keyMaterial.v1(); + RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName()); + OpenIdConnectProviderConfiguration opConfig = getOpConfig(); + OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk); + authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource); + + final State state = new State(); + final Nonce nonce = new Nonce(); + final String subject = "janedoe"; + final Tuple tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), jwk.getKeyID(), subject, true, false); + final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri()); + final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce); + final PlainActionFuture future = new PlainActionFuture<>(); + authenticator.authenticate(token, future); + JWTClaimsSet claimsSet = future.actionGet(); + assertThat(claimsSet.getSubject(), equalTo(subject)); + } + + public void testAuthenticateImplicitFlowWithEcdsa() throws Exception { + final Tuple keyMaterial = getRandomJwkForType("RS"); + final JWK jwk = keyMaterial.v2().getKeys().get(0); + final Key key = keyMaterial.v1(); + RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName()); + OpenIdConnectProviderConfiguration opConfig = getOpConfig(); + OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk); + authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource); + + final State state = new State(); + final Nonce nonce = new Nonce(); + final String subject = "janedoe"; + final Tuple tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), jwk.getKeyID(), subject, true, false); + final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri()); + final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce); + final PlainActionFuture future = new PlainActionFuture<>(); + authenticator.authenticate(token, future); + JWTClaimsSet claimsSet = future.actionGet(); + assertThat(claimsSet.getSubject(), equalTo(subject)); + } + + public void testAuthenticateImplicitFlowWithHmac() throws Exception { + final Tuple keyMaterial = getRandomJwkForType("HS"); + final JWK jwk = keyMaterial.v2().getKeys().get(0); + final Key key = keyMaterial.v1(); + + RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName()); + OpenIdConnectProviderConfiguration opConfig = getOpConfig(); + authenticator = buildAuthenticator(opConfig, rpConfig); + + final State state = new State(); + final Nonce nonce = new Nonce(); + final String subject = "janedoe"; + final Tuple tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), null, subject, true, false); + final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri()); + final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce); + final PlainActionFuture future = new PlainActionFuture<>(); + authenticator.authenticate(token, future); + JWTClaimsSet claimsSet = future.actionGet(); + assertThat(claimsSet.getSubject(), equalTo(subject)); + } + + public void testClockSkewIsHonored() throws Exception { + final Tuple keyMaterial = getRandomJwkForType(randomFrom("HS", "ES", "RS")); + final JWK jwk = keyMaterial.v2().getKeys().get(0); + final Key key = keyMaterial.v1(); + RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName()); + OpenIdConnectProviderConfiguration opConfig = getOpConfig(); + if (jwk.getAlgorithm().getName().startsWith("HS")) { + authenticator = buildAuthenticator(opConfig, rpConfig); + } else { + OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk); + authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource); + } + final State state = new State(); + final Nonce nonce = new Nonce(); + final String subject = "janedoe"; + final String keyId = (jwk.getAlgorithm().getName().startsWith("HS")) ? null : jwk.getKeyID(); + JWTClaimsSet.Builder idTokenBuilder = new JWTClaimsSet.Builder() + .jwtID(randomAlphaOfLength(8)) + .audience(rpConfig.getClientId().getValue()) + // Expired 55 seconds ago with an allowed clock skew of 60 seconds + .expirationTime(Date.from(now().minusSeconds(55))) + .issuer(opConfig.getIssuer().getValue()) + .issueTime(Date.from(now().minusSeconds(200))) + .notBeforeTime(Date.from(now().minusSeconds(200))) + .claim("nonce", nonce) + .subject(subject); + final Tuple tokens = buildTokens(idTokenBuilder.build(), key, jwk.getAlgorithm().getName(), keyId, subject, + true, false); + final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri()); + final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce); + final PlainActionFuture future = new PlainActionFuture<>(); + authenticator.authenticate(token, future); + JWTClaimsSet claimsSet = future.actionGet(); + assertThat(claimsSet.getSubject(), equalTo(subject)); + } + + public void testImplicitFlowFailsWithExpiredToken() throws Exception { + final Tuple keyMaterial = getRandomJwkForType(randomFrom("HS", "ES", "RS")); + final JWK jwk = keyMaterial.v2().getKeys().get(0); + final Key key = keyMaterial.v1(); + RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName()); + OpenIdConnectProviderConfiguration opConfig = getOpConfig(); + if (jwk.getAlgorithm().getName().startsWith("HS")) { + authenticator = buildAuthenticator(opConfig, rpConfig); + } else { + OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk); + authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource); + } + final State state = new State(); + final Nonce nonce = new Nonce(); + final String subject = "janedoe"; + final String keyId = (jwk.getAlgorithm().getName().startsWith("HS")) ? null : jwk.getKeyID(); + JWTClaimsSet.Builder idTokenBuilder = new JWTClaimsSet.Builder() + .jwtID(randomAlphaOfLength(8)) + .audience(rpConfig.getClientId().getValue()) + // Expired 65 seconds ago with an allowed clock skew of 60 seconds + .expirationTime(Date.from(now().minusSeconds(65))) + .issuer(opConfig.getIssuer().getValue()) + .issueTime(Date.from(now().minusSeconds(200))) + .notBeforeTime(Date.from(now().minusSeconds(200))) + .claim("nonce", nonce) + .subject(subject); + final Tuple tokens = buildTokens(idTokenBuilder.build(), key, jwk.getAlgorithm().getName(), keyId, + subject, true, false); + final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri()); + final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce); + final PlainActionFuture future = new PlainActionFuture<>(); + authenticator.authenticate(token, future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token")); + assertThat(e.getCause(), instanceOf(BadJWTException.class)); + assertThat(e.getCause().getMessage(), containsString("Expired JWT")); + } + + public void testImplicitFlowFailsNotYetIssuedToken() throws Exception { + final Tuple keyMaterial = getRandomJwkForType(randomFrom("HS", "ES", "RS")); + final JWK jwk = keyMaterial.v2().getKeys().get(0); + final Key key = keyMaterial.v1(); + RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName()); + OpenIdConnectProviderConfiguration opConfig = getOpConfig(); + if (jwk.getAlgorithm().getName().startsWith("HS")) { + authenticator = buildAuthenticator(opConfig, rpConfig); + } else { + OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk); + authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource); + } + final State state = new State(); + final Nonce nonce = new Nonce(); + final String subject = "janedoe"; + final String keyId = (jwk.getAlgorithm().getName().startsWith("HS")) ? null : jwk.getKeyID(); + JWTClaimsSet.Builder idTokenBuilder = new JWTClaimsSet.Builder() + .jwtID(randomAlphaOfLength(8)) + .audience(rpConfig.getClientId().getValue()) + .expirationTime(Date.from(now().plusSeconds(3600))) + .issuer(opConfig.getIssuer().getValue()) + // Issued 80 seconds in the future with max allowed clock skew of 60 + .issueTime(Date.from(now().plusSeconds(80))) + .notBeforeTime(Date.from(now().minusSeconds(80))) + .claim("nonce", nonce) + .subject(subject); + final Tuple tokens = buildTokens(idTokenBuilder.build(), key, jwk.getAlgorithm().getName(), keyId, + subject, true, false); + final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri()); + final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce); + final PlainActionFuture future = new PlainActionFuture<>(); + authenticator.authenticate(token, future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token")); + assertThat(e.getCause(), instanceOf(BadJWTException.class)); + assertThat(e.getCause().getMessage(), containsString("JWT issue time ahead of current time")); + } + + public void testImplicitFlowFailsInvalidIssuer() throws Exception { + final Tuple keyMaterial = getRandomJwkForType(randomFrom("HS", "ES", "RS")); + final JWK jwk = keyMaterial.v2().getKeys().get(0); + final Key key = keyMaterial.v1(); + RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName()); + OpenIdConnectProviderConfiguration opConfig = getOpConfig(); + if (jwk.getAlgorithm().getName().startsWith("HS")) { + authenticator = buildAuthenticator(opConfig, rpConfig); + } else { + OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk); + authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource); + } + final State state = new State(); + final Nonce nonce = new Nonce(); + final String subject = "janedoe"; + final String keyId = (jwk.getAlgorithm().getName().startsWith("HS")) ? null : jwk.getKeyID(); + JWTClaimsSet.Builder idTokenBuilder = new JWTClaimsSet.Builder() + .jwtID(randomAlphaOfLength(8)) + .audience(rpConfig.getClientId().getValue()) + .expirationTime(Date.from(now().plusSeconds(3600))) + .issuer("https://another.op.org") + .issueTime(Date.from(now().minusSeconds(200))) + .notBeforeTime(Date.from(now().minusSeconds(200))) + .claim("nonce", nonce) + .subject(subject); + final Tuple tokens = buildTokens(idTokenBuilder.build(), key, jwk.getAlgorithm().getName(), keyId, + subject, true, false); + final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri()); + final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce); + final PlainActionFuture future = new PlainActionFuture<>(); + authenticator.authenticate(token, future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token")); + assertThat(e.getCause(), instanceOf(BadJWTException.class)); + assertThat(e.getCause().getMessage(), containsString("Unexpected JWT issuer")); + } + + public void testImplicitFlowFailsInvalidAudience() throws Exception { + final Tuple keyMaterial = getRandomJwkForType(randomFrom("HS", "ES", "RS")); + final JWK jwk = keyMaterial.v2().getKeys().get(0); + final Key key = keyMaterial.v1(); + RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName()); + OpenIdConnectProviderConfiguration opConfig = getOpConfig(); + if (jwk.getAlgorithm().getName().startsWith("HS")) { + authenticator = buildAuthenticator(opConfig, rpConfig); + } else { + OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk); + authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource); + } + final State state = new State(); + final Nonce nonce = new Nonce(); + final String subject = "janedoe"; + final String keyId = (jwk.getAlgorithm().getName().startsWith("HS")) ? null : jwk.getKeyID(); + JWTClaimsSet.Builder idTokenBuilder = new JWTClaimsSet.Builder() + .jwtID(randomAlphaOfLength(8)) + .audience("some-other-RP") + .expirationTime(Date.from(now().plusSeconds(3600))) + .issuer(opConfig.getIssuer().getValue()) + .issueTime(Date.from(now().minusSeconds(200))) + .notBeforeTime(Date.from(now().minusSeconds(80))) + .claim("nonce", nonce) + .subject(subject); + final Tuple tokens = buildTokens(idTokenBuilder.build(), key, jwk.getAlgorithm().getName(), keyId, + subject, true, false); + final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri()); + final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce); + final PlainActionFuture future = new PlainActionFuture<>(); + authenticator.authenticate(token, future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token")); + assertThat(e.getCause(), instanceOf(BadJWTException.class)); + assertThat(e.getCause().getMessage(), containsString("Unexpected JWT audience")); + } + + public void testAuthenticateImplicitFlowFailsWithForgedRsaIdToken() throws Exception { + final Tuple keyMaterial = getRandomJwkForType("RS"); + final JWK jwk = keyMaterial.v2().getKeys().get(0); + final Key key = keyMaterial.v1(); + RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName()); + OpenIdConnectProviderConfiguration opConfig = getOpConfig(); + OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk); + authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource); + + final State state = new State(); + final Nonce nonce = new Nonce(); + final String subject = "janedoe"; + final Tuple tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), jwk.getKeyID(), subject, true, true); + final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri()); + final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce); + final PlainActionFuture future = new PlainActionFuture<>(); + authenticator.authenticate(token, future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token")); + assertThat(e.getCause(), instanceOf(BadJWSException.class)); + assertThat(e.getCause().getMessage(), containsString("Signed JWT rejected: Invalid signature")); + } + + public void testAuthenticateImplicitFlowFailsWithForgedEcsdsaIdToken() throws Exception { + final Tuple keyMaterial = getRandomJwkForType("ES"); + final JWK jwk = keyMaterial.v2().getKeys().get(0); + final Key key = keyMaterial.v1(); + RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName()); + OpenIdConnectProviderConfiguration opConfig = getOpConfig(); + OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk); + authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource); + + final State state = new State(); + final Nonce nonce = new Nonce(); + final String subject = "janedoe"; + final Tuple tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), jwk.getKeyID(), subject, true, true); + final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri()); + final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce); + final PlainActionFuture future = new PlainActionFuture<>(); + authenticator.authenticate(token, future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token")); + assertThat(e.getCause(), instanceOf(BadJWSException.class)); + assertThat(e.getCause().getMessage(), containsString("Signed JWT rejected: Invalid signature")); + } + + public void testAuthenticateImplicitFlowFailsWithForgedHmacIdToken() throws Exception { + final Tuple keyMaterial = getRandomJwkForType("HS"); + final JWK jwk = keyMaterial.v2().getKeys().get(0); + final Key key = keyMaterial.v1(); + RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName()); + OpenIdConnectProviderConfiguration opConfig = getOpConfig(); + authenticator = buildAuthenticator(opConfig, rpConfig); + + final State state = new State(); + final Nonce nonce = new Nonce(); + final String subject = "janedoe"; + final Tuple tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), null, subject, true, true); + final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri()); + final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce); + final PlainActionFuture future = new PlainActionFuture<>(); + authenticator.authenticate(token, future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token")); + assertThat(e.getCause(), instanceOf(BadJWSException.class)); + assertThat(e.getCause().getMessage(), containsString("Signed JWT rejected: Invalid signature")); + } + + public void testAuthenticateImplicitFlowFailsWithForgedAccessToken() throws Exception { + final Tuple keyMaterial = getRandomJwkForType(randomFrom("HS", "ES", "RS")); + final JWK jwk = keyMaterial.v2().getKeys().get(0); + final Key key = keyMaterial.v1(); + RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName()); + OpenIdConnectProviderConfiguration opConfig = getOpConfig(); + if (jwk.getAlgorithm().getName().startsWith("HS")) { + authenticator = buildAuthenticator(opConfig, rpConfig); + } else { + OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk); + authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource); + } + final State state = new State(); + final Nonce nonce = new Nonce(); + final String subject = "janedoe"; + final String keyId = (jwk.getAlgorithm().getName().startsWith("HS")) ? null : jwk.getKeyID(); + final Tuple tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), keyId, subject, true, false); + final String responseUrl = buildAuthResponse(tokens.v2(), new BearerAccessToken("someforgedAccessToken"), state, + rpConfig.getRedirectUri()); + final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce); + final PlainActionFuture future = new PlainActionFuture<>(); + authenticator.authenticate(token, future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("Failed to verify access token")); + assertThat(e.getCause(), instanceOf(InvalidHashException.class)); + assertThat(e.getCause().getMessage(), containsString("Access token hash (at_hash) mismatch")); + } + + public void testImplicitFlowFailsWithNoneAlgorithm() throws Exception { + final Tuple keyMaterial = getRandomJwkForType(randomFrom("HS")); + final JWK jwk = keyMaterial.v2().getKeys().get(0); + final Key key = keyMaterial.v1(); + RelyingPartyConfiguration rpConfig = getRpConfigNoAccessToken(jwk.getAlgorithm().getName()); + OpenIdConnectProviderConfiguration opConfig = getOpConfig(); + if (jwk.getAlgorithm().getName().startsWith("HS")) { + authenticator = buildAuthenticator(opConfig, rpConfig); + } else { + OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk); + authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource); + } + final State state = new State(); + final Nonce nonce = new Nonce(); + final String subject = "janedoe"; + final String keyId = (jwk.getAlgorithm().getName().startsWith("HS")) ? null : jwk.getKeyID(); + final Tuple tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), keyId, subject, false, false); + JWT idToken = tokens.v2(); + // Change the algorithm of the signed JWT to NONE + String[] serializedParts = idToken.serialize().split("\\."); + String legitimateHeader = new String(Base64.getUrlDecoder().decode(serializedParts[0]), StandardCharsets.UTF_8); + String forgedHeader = legitimateHeader.replace(jwk.getAlgorithm().getName(), "NONE"); + String encodedForgedHeader = + Base64.getUrlEncoder().withoutPadding().encodeToString(forgedHeader.getBytes(StandardCharsets.UTF_8)); + String fordedTokenString = encodedForgedHeader + "." + serializedParts[1] + "." + serializedParts[2]; + idToken = SignedJWT.parse(fordedTokenString); + final String responseUrl = buildAuthResponse(idToken, tokens.v1(), state, rpConfig.getRedirectUri()); + final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce); + final PlainActionFuture future = new PlainActionFuture<>(); + authenticator.authenticate(token, future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token")); + assertThat(e.getCause(), instanceOf(BadJOSEException.class)); + assertThat(e.getCause().getMessage(), containsString("Another algorithm expected, or no matching key(s) found")); + } + + /** + * The premise of this attack is that an RP that expects a JWT signed with an asymmetric algorithm (RSA, ECDSA) + * receives a JWT signed with an HMAC. Trusting the received JWT's alg claim more than it's own configuration, + * it attempts to validate the HMAC with the provider's {RSA,EC} public key as a secret key + */ + public void testImplicitFlowFailsWithAlgorithmMixupAttack() throws Exception { + final Tuple keyMaterial = getRandomJwkForType(randomFrom("ES", "RS")); + final JWK jwk = keyMaterial.v2().getKeys().get(0); + RelyingPartyConfiguration rpConfig = getRpConfig(jwk.getAlgorithm().getName()); + OpenIdConnectProviderConfiguration opConfig = getOpConfig(); + OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk); + authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource); + final State state = new State(); + final Nonce nonce = new Nonce(); + final String subject = "janedoe"; + SecretKeySpec hmacKey = new SecretKeySpec("thisismysupersupersupersupersupersuperlongsecret".getBytes(StandardCharsets.UTF_8), + "HmacSha384"); + final Tuple tokens = buildTokens(nonce, hmacKey, "HS384", null, subject, + true, false); + final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri()); + final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce); + final PlainActionFuture future = new PlainActionFuture<>(); + authenticator.authenticate(token, future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token")); + assertThat(e.getCause(), instanceOf(BadJOSEException.class)); + assertThat(e.getCause().getMessage(), containsString("Another algorithm expected, or no matching key(s) found")); + } + + public void testImplicitFlowFailsWithUnsignedJwt() throws Exception { + final Tuple keyMaterial = getRandomJwkForType(randomFrom("HS", "ES", "RS")); + final JWK jwk = keyMaterial.v2().getKeys().get(0); + RelyingPartyConfiguration rpConfig = getRpConfigNoAccessToken(jwk.getAlgorithm().getName()); + OpenIdConnectProviderConfiguration opConfig = getOpConfig(); + if (jwk.getAlgorithm().getName().startsWith("HS")) { + authenticator = buildAuthenticator(opConfig, rpConfig); + } else { + OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = mockSource(jwk); + authenticator = buildAuthenticator(opConfig, rpConfig, jwkSource); + } + final State state = new State(); + final Nonce nonce = new Nonce(); + final String subject = "janedoe"; + JWTClaimsSet.Builder idTokenBuilder = new JWTClaimsSet.Builder() + .jwtID(randomAlphaOfLength(8)) + .audience(rpConfig.getClientId().getValue()) + .expirationTime(Date.from(now().plusSeconds(3600))) + .issuer(opConfig.getIssuer().getValue()) + .issueTime(Date.from(now().minusSeconds(200))) + .notBeforeTime(Date.from(now().minusSeconds(200))) + .claim("nonce", nonce) + .subject(subject); + + final String responseUrl = buildAuthResponse(new PlainJWT(idTokenBuilder.build()), null, state, + rpConfig.getRedirectUri()); + final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce); + final PlainActionFuture future = new PlainActionFuture<>(); + authenticator.authenticate(token, future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token")); + assertThat(e.getCause(), instanceOf(BadJWTException.class)); + assertThat(e.getCause().getMessage(), containsString("Signed ID token expected")); + } + + private OpenIdConnectProviderConfiguration getOpConfig() throws URISyntaxException { + return new OpenIdConnectProviderConfiguration("op_name", + new Issuer("https://op.example.com"), + "https://op.example.org/jwks.json", + new URI("https://op.example.org/login"), + new URI("https://op.example.org/token"), + null, + new URI("https://op.example.org/logout")); + } + + private RelyingPartyConfiguration getDefaultRpConfig() throws URISyntaxException { + return new RelyingPartyConfiguration( + new ClientID("rp-my"), + new SecureString("thisismysupersupersupersupersupersuperlongsecret".toCharArray()), + new URI("https://rp.elastic.co/cb"), + new ResponseType("id_token", "token"), + new Scope("openid"), + JWSAlgorithm.RS384, + new URI("https://rp.elastic.co/successfull_logout")); + } + private RelyingPartyConfiguration getRpConfig(String alg) throws URISyntaxException { + return new RelyingPartyConfiguration( + new ClientID("rp-my"), + new SecureString("thisismysupersupersupersupersupersuperlongsecret".toCharArray()), + new URI("https://rp.elastic.co/cb"), + new ResponseType("id_token", "token"), + new Scope("openid"), + JWSAlgorithm.parse(alg), + new URI("https://rp.elastic.co/successfull_logout")); + } + + private RelyingPartyConfiguration getRpConfigNoAccessToken(String alg) throws URISyntaxException { + return new RelyingPartyConfiguration( + new ClientID("rp-my"), + new SecureString("thisismysupersupersupersupersupersuperlongsecret".toCharArray()), + new URI("https://rp.elastic.co/cb"), + new ResponseType("id_token"), + new Scope("openid"), + JWSAlgorithm.parse(alg), + new URI("https://rp.elastic.co/successfull_logout")); + } + + private String buildAuthResponse(JWT idToken, @Nullable AccessToken accessToken, State state, URI redirectUri) { + AuthenticationSuccessResponse response = new AuthenticationSuccessResponse( + redirectUri, + null, + idToken, + accessToken, + state, + null, + null); + return response.toURI().toString(); + } + + private OpenIdConnectAuthenticator.ReloadableJWKSource mockSource(JWK jwk) { + OpenIdConnectAuthenticator.ReloadableJWKSource jwkSource = + mock(OpenIdConnectAuthenticator.ReloadableJWKSource.class); + when(jwkSource.get(any(), any())).thenReturn(Collections.singletonList(jwk)); + Mockito.doAnswer(invocation -> { + @SuppressWarnings("unchecked") + ActionListener listener = (ActionListener) invocation.getArguments()[0]; + listener.onResponse(null); + return null; + }).when(jwkSource).triggerReload(any(ActionListener.class)); + return jwkSource; + } + + private Tuple buildTokens(JWTClaimsSet idToken, Key key, String alg, String keyId, + String subject, boolean withAccessToken, boolean forged) throws Exception { + AccessToken accessToken = null; + if (withAccessToken) { + accessToken = new BearerAccessToken(Base64.getUrlEncoder().encodeToString(randomByteArrayOfLength(32))); + AccessTokenHash expectedHash = AccessTokenHash.compute(accessToken, JWSAlgorithm.parse(alg)); + idToken = JWTClaimsSet.parse(idToken.toJSONObject().appendField("at_hash", expectedHash.getValue())); + } + SignedJWT jwt = new SignedJWT( + new JWSHeader.Builder(JWSAlgorithm.parse(alg)).keyID(keyId).build(), + idToken); + + if (key instanceof RSAPrivateKey) { + jwt.sign(new RSASSASigner((PrivateKey) key)); + } else if (key instanceof ECPrivateKey) { + jwt.sign(new ECDSASigner((ECPrivateKey) key)); + } else if (key instanceof SecretKey) { + jwt.sign(new MACSigner((SecretKey) key)); + } + if (forged) { + // Change the sub claim to "attacker" + String[] serializedParts = jwt.serialize().split("\\."); + String legitimatePayload = new String(Base64.getUrlDecoder().decode(serializedParts[1]), StandardCharsets.UTF_8); + String forgedPayload = legitimatePayload.replace(subject, "attacker"); + String encodedForgedPayload = + Base64.getUrlEncoder().withoutPadding().encodeToString(forgedPayload.getBytes(StandardCharsets.UTF_8)); + String fordedTokenString = serializedParts[0] + "." + encodedForgedPayload + "." + serializedParts[2]; + jwt = SignedJWT.parse(fordedTokenString); + } + return new Tuple<>(accessToken, jwt); + } + + private Tuple buildTokens(Nonce nonce, Key key, String alg, String keyId, String subject, boolean withAccessToken, + boolean forged) throws Exception { + RelyingPartyConfiguration rpConfig = getRpConfig(alg); + OpenIdConnectProviderConfiguration opConfig = getOpConfig(); + JWTClaimsSet.Builder idTokenBuilder = new JWTClaimsSet.Builder() + .jwtID(randomAlphaOfLength(8)) + .audience(rpConfig.getClientId().getValue()) + .expirationTime(Date.from(now().plusSeconds(3600))) + .issuer(opConfig.getIssuer().getValue()) + .issueTime(Date.from(now().minusSeconds(4))) + .notBeforeTime(Date.from(now().minusSeconds(4))) + .claim("nonce", nonce) + .subject(subject); + + return buildTokens(idTokenBuilder.build(), key, alg, keyId, subject, withAccessToken, forged); + } + + private Tuple getRandomJwkForType(String type) throws Exception { + JWK jwk; + Key key; + int hashSize; + if (type.equals("RS")) { + hashSize = randomFrom(256, 384, 512); + int keySize = randomFrom(2048, 4096); + KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA"); + gen.initialize(keySize); + KeyPair keyPair = gen.generateKeyPair(); + key = keyPair.getPrivate(); + jwk = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic()) + .privateKey((RSAPrivateKey) keyPair.getPrivate()) + .keyUse(KeyUse.SIGNATURE) + .keyID(UUID.randomUUID().toString()) + .algorithm(JWSAlgorithm.parse(type + hashSize)) + .build(); + + } else if (type.equals("HS")) { + hashSize = randomFrom(256, 384); + SecretKeySpec hmacKey = new SecretKeySpec("thisismysupersupersupersupersupersuperlongsecret".getBytes(StandardCharsets.UTF_8), + "HmacSha" + hashSize); + //SecretKey hmacKey = KeyGenerator.getInstance("HmacSha" + hashSize).generateKey(); + key = hmacKey; + jwk = new OctetSequenceKey.Builder(hmacKey) + .keyID(UUID.randomUUID().toString()) + .algorithm(JWSAlgorithm.parse(type + hashSize)) + .build(); + + } else if (type.equals("ES")) { + hashSize = randomFrom(256, 384, 512); + ECKey.Curve curve = curveFromHashSize(hashSize); + KeyPairGenerator gen = KeyPairGenerator.getInstance("EC"); + gen.initialize(curve.toECParameterSpec()); + KeyPair keyPair = gen.generateKeyPair(); + key = keyPair.getPrivate(); + jwk = new ECKey.Builder(curve, (ECPublicKey) keyPair.getPublic()) + .privateKey((ECPrivateKey) keyPair.getPrivate()) + .algorithm(JWSAlgorithm.parse(type + hashSize)) + .build(); + } else { + throw new IllegalArgumentException("Invalid key type :" + type); + } + return new Tuple(key, new JWKSet(jwk)); + } + + private ECKey.Curve curveFromHashSize(int size) { + if (size == 256) { + return ECKey.Curve.P_256; + } else if (size == 384) { + return ECKey.Curve.P_384; + } else if (size == 512) { + return ECKey.Curve.P_521; + } else { + throw new IllegalArgumentException("Invalid hash size:" + size); + } + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmSettingsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmSettingsTests.java new file mode 100644 index 0000000000000..cd92168b3aa95 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmSettingsTests.java @@ -0,0 +1,256 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authc.oidc; + + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsException; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings; +import org.hamcrest.Matchers; +import org.junit.Before; + +import java.util.Arrays; + +import static org.elasticsearch.xpack.core.security.authc.RealmSettings.getFullSettingKey; + +public class OpenIdConnectRealmSettingsTests extends ESTestCase { + + private static final String REALM_NAME = "oidc1-realm"; + private ThreadContext threadContext; + + @Before + public void setupEnv() { + Settings globalSettings = Settings.builder().put("path.home", createTempDir()).build(); + threadContext = new ThreadContext(globalSettings); + } + + public void testIncorrectResponseTypeThrowsError() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "hybrid"); + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> { + new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null); + }); + assertThat(exception.getMessage(), Matchers.containsString(getFullSettingKey(REALM_NAME, + OpenIdConnectRealmSettings.RP_RESPONSE_TYPE))); + } + + public void testMissingAuthorizationEndpointThrowsError() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code"); + SettingsException exception = expectThrows(SettingsException.class, () -> { + new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null); + }); + assertThat(exception.getMessage(), + Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT))); + } + + public void testInvalidAuthorizationEndpointThrowsError() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "this is not a URI") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code"); + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> { + new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null); + }); + assertThat(exception.getMessage(), + Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT))); + } + + public void testMissingTokenEndpointThrowsError() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code"); + SettingsException exception = expectThrows(SettingsException.class, () -> { + new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null); + }); + assertThat(exception.getMessage(), + Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT))); + } + + public void testInvalidTokenEndpointThrowsError() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "This is not a uri") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code"); + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> { + new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null); + }); + assertThat(exception.getMessage(), + Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT))); + } + + public void testMissingJwksUrlThrowsError() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code"); + SettingsException exception = expectThrows(SettingsException.class, () -> { + new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null); + }); + assertThat(exception.getMessage(), + Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH))); + } + + public void testMissingIssuerThrowsError() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code"); + SettingsException exception = expectThrows(SettingsException.class, () -> { + new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null); + }); + assertThat(exception.getMessage(), + Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER))); + } + + public void testMissingNameTypeThrowsError() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code"); + SettingsException exception = expectThrows(SettingsException.class, () -> { + new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null); + }); + assertThat(exception.getMessage(), + Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME))); + } + + public void testMissingRedirectUriThrowsError() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code"); + SettingsException exception = expectThrows(SettingsException.class, () -> { + new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null); + }); + assertThat(exception.getMessage(), + Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI))); + } + + public void testMissingClientIdThrowsError() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code"); + SettingsException exception = expectThrows(SettingsException.class, () -> { + new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null); + }); + assertThat(exception.getMessage(), + Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID))); + } + + public void testMissingPrincipalClaimThrowsError() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com/cb") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code") + .putList(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REQUESTED_SCOPES), + Arrays.asList("openid", "scope1", "scope2")); + SettingsException exception = expectThrows(SettingsException.class, () -> { + new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null); + }); + assertThat(exception.getMessage(), + Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()))); + } + + public void testPatternWithoutSettingThrowsError() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.NAME_CLAIM.getPattern()), "^(.*)$") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com/cb") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code") + .putList(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REQUESTED_SCOPES), + Arrays.asList("openid", "scope1", "scope2")); + SettingsException exception = expectThrows(SettingsException.class, () -> { + new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null, null); + }); + assertThat(exception.getMessage(), + Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.NAME_CLAIM.getClaim()))); + assertThat(exception.getMessage(), + Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.NAME_CLAIM.getPattern()))); + } + + private RealmConfig buildConfig(Settings realmSettings) { + final Settings settings = Settings.builder() + .put("path.home", createTempDir()) + .put(realmSettings).build(); + final Environment env = TestEnvironment.newEnvironment(settings); + return new RealmConfig(new RealmConfig.RealmIdentifier("oidc", REALM_NAME), settings, env, threadContext); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmTests.java new file mode 100644 index 0000000000000..0d26c0b442c88 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmTests.java @@ -0,0 +1,341 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authc.oidc; + +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.oauth2.sdk.id.State; +import com.nimbusds.openid.connect.sdk.Nonce; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.license.XPackLicenseState; + +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutResponse; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationResponse; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.Realm; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings; +import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; +import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.authc.support.MockLookupRealm; +import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; +import org.hamcrest.Matchers; +import org.junit.Before; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +import static java.time.Instant.now; +import static org.elasticsearch.xpack.core.security.authc.RealmSettings.getFullSettingKey; +import static org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm.CONTEXT_TOKEN_DATA; +import static org.hamcrest.Matchers.arrayContainingInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class OpenIdConnectRealmTests extends OpenIdConnectTestCase { + + private Settings globalSettings; + private Environment env; + private ThreadContext threadContext; + + @Before + public void setupEnv() { + globalSettings = Settings.builder().put("path.home", createTempDir()).build(); + env = TestEnvironment.newEnvironment(globalSettings); + threadContext = new ThreadContext(globalSettings); + } + + public void testAuthentication() throws Exception { + final UserRoleMapper roleMapper = mock(UserRoleMapper.class); + AtomicReference userData = new AtomicReference<>(); + doAnswer(invocation -> { + assert invocation.getArguments().length == 2; + userData.set((UserRoleMapper.UserData) invocation.getArguments()[0]); + ActionListener> listener = (ActionListener>) invocation.getArguments()[1]; + listener.onResponse(new HashSet<>(Arrays.asList("kibana_user", "role1"))); + return null; + }).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class)); + + final boolean notPopulateMetadata = randomBoolean(); + + AuthenticationResult result = authenticateWithOidc(roleMapper, notPopulateMetadata, false); + assertThat(result.getUser().roles(), arrayContainingInAnyOrder("kibana_user", "role1")); + if (notPopulateMetadata == false) { + assertThat(result.getUser().metadata().get("oidc(iss)"), equalTo("https://op.company.org")); + assertThat(result.getUser().metadata().get("oidc(name)"), equalTo("Clinton Barton")); + } + } + + public void testWithAuthorizingRealm() throws Exception { + final UserRoleMapper roleMapper = mock(UserRoleMapper.class); + doAnswer(invocation -> { + assert invocation.getArguments().length == 2; + ActionListener> listener = (ActionListener>) invocation.getArguments()[1]; + listener.onFailure(new RuntimeException("Role mapping should not be called")); + return null; + }).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class)); + + AuthenticationResult result = authenticateWithOidc(roleMapper, randomBoolean(), true); + assertThat(result.getUser().roles(), arrayContainingInAnyOrder("lookup_user_role")); + assertThat(result.getUser().fullName(), equalTo("Clinton Barton")); + assertThat(result.getUser().metadata().entrySet(), Matchers.iterableWithSize(1)); + assertThat(result.getUser().metadata().get("is_lookup"), Matchers.equalTo(true)); + assertNotNull(result.getMetadata().get(CONTEXT_TOKEN_DATA)); + assertThat(result.getMetadata().get(CONTEXT_TOKEN_DATA), instanceOf(Map.class)); + Map tokenMetadata = (Map) result.getMetadata().get(CONTEXT_TOKEN_DATA); + assertThat(tokenMetadata.get("id_token_hint"), equalTo("thisis.aserialized.jwt")); + } + + public void testClaimPatternParsing() throws Exception { + final Settings.Builder builder = getBasicRealmSettings(); + builder.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getPattern()), "^OIDC-(.+)"); + final RealmConfig config = buildConfig(builder.build(), threadContext); + final OpenIdConnectRealmSettings.ClaimSetting principalSetting = new OpenIdConnectRealmSettings.ClaimSetting("principal"); + final OpenIdConnectRealm.ClaimParser parser = OpenIdConnectRealm.ClaimParser.forSetting(logger, principalSetting, config, true); + final JWTClaimsSet claims = new JWTClaimsSet.Builder() + .subject("OIDC-cbarton") + .audience("https://rp.elastic.co/cb") + .expirationTime(Date.from(now().plusSeconds(3600))) + .issueTime(Date.from(now().minusSeconds(5))) + .jwtID(randomAlphaOfLength(8)) + .issuer("https://op.company.org") + .build(); + assertThat(parser.getClaimValue(claims), equalTo("cbarton")); + } + + public void testInvalidPrincipalClaimPatternParsing() { + final OpenIdConnectAuthenticator authenticator = mock(OpenIdConnectAuthenticator.class); + final OpenIdConnectToken token = new OpenIdConnectToken("", new State(), new Nonce()); + final Settings.Builder builder = getBasicRealmSettings(); + builder.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getPattern()), "^OIDC-(.+)"); + final RealmConfig config = buildConfig(builder.build(), threadContext); + final OpenIdConnectRealm realm = new OpenIdConnectRealm(config, authenticator, null); + final JWTClaimsSet claims = new JWTClaimsSet.Builder() + .subject("cbarton@avengers.com") + .audience("https://rp.elastic.co/cb") + .expirationTime(Date.from(now().plusSeconds(3600))) + .issueTime(Date.from(now().minusSeconds(5))) + .jwtID(randomAlphaOfLength(8)) + .issuer("https://op.company.org") + .build(); + doAnswer((i) -> { + ActionListener listener = (ActionListener) i.getArguments()[1]; + listener.onResponse(claims); + return null; + }).when(authenticator).authenticate(any(OpenIdConnectToken.class), any(ActionListener.class)); + + final PlainActionFuture future = new PlainActionFuture<>(); + realm.authenticate(token, future); + final AuthenticationResult result = future.actionGet(); + assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.CONTINUE)); + assertThat(result.getMessage(), containsString("claims.principal")); + assertThat(result.getMessage(), containsString("sub")); + assertThat(result.getMessage(), containsString("^OIDC-(.+)")); + } + + public void testBuildRelyingPartyConfigWithoutOpenIdScope() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com/cb") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code") + .putList(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REQUESTED_SCOPES), + Arrays.asList("scope1", "scope2")); + final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(settingsBuilder.build(), threadContext), null, + null); + final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(null, null, null); + final String state = response.getState(); + final String nonce = response.getNonce(); + assertThat(response.getAuthenticationRequestUrl(), + equalTo("https://op.example.com/login?scope=scope1+scope2+openid&response_type=code" + + "&redirect_uri=https%3A%2F%2Frp.my.com%2Fcb&state=" + state + "&nonce=" + nonce + "&client_id=rp-my")); + } + + public void testBuildingAuthenticationRequest() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com/cb") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code") + .putList(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REQUESTED_SCOPES), + Arrays.asList("openid", "scope1", "scope2")); + final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(settingsBuilder.build(), threadContext), null, + null); + final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(null, null, null); + final String state = response.getState(); + final String nonce = response.getNonce(); + assertThat(response.getAuthenticationRequestUrl(), + equalTo("https://op.example.com/login?scope=openid+scope1+scope2&response_type=code" + + "&redirect_uri=https%3A%2F%2Frp.my.com%2Fcb&state=" + state + "&nonce=" + nonce + "&client_id=rp-my")); + } + + public void testBuilidingAuthenticationRequestWithDefaultScope() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com/cb") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code"); + final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(settingsBuilder.build(), threadContext), null, + null); + final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(null, null, null); + final String state = response.getState(); + final String nonce = response.getNonce(); + assertThat(response.getAuthenticationRequestUrl(), equalTo("https://op.example.com/login?scope=openid&response_type=code" + + "&redirect_uri=https%3A%2F%2Frp.my.com%2Fcb&state=" + state + "&nonce=" + nonce + "&client_id=rp-my")); + } + + public void testBuildLogoutResponse() throws Exception { + final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(getBasicRealmSettings().build(), threadContext), null, + null); + // Random strings, as we will not validate the token here + final JWT idToken = generateIdToken(randomAlphaOfLength(8), randomAlphaOfLength(8), randomAlphaOfLength(8)); + final OpenIdConnectLogoutResponse logoutResponse = realm.buildLogoutResponse(idToken); + assertThat(logoutResponse.getEndSessionUrl(), containsString("https://op.example.org/logout?id_token_hint=")); + assertThat(logoutResponse.getEndSessionUrl(), + containsString("&post_logout_redirect_uri=https%3A%2F%2Frp.elastic.co%2Fsucc_logout&state=")); + } + + public void testBuildingAuthenticationRequestWithExistingStateAndNonce() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com/cb") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code"); + final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(settingsBuilder.build(), threadContext), null, + null); + final String state = new State().getValue(); + final String nonce = new Nonce().getValue(); + final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(state, nonce, null); + + assertThat(response.getAuthenticationRequestUrl(), equalTo("https://op.example.com/login?scope=openid&response_type=code" + + "&redirect_uri=https%3A%2F%2Frp.my.com%2Fcb&state=" + state + "&nonce=" + nonce + "&client_id=rp-my")); + } + + public void testBuildingAuthenticationRequestWithLoginHint() { + final Settings.Builder settingsBuilder = Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com/cb") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code"); + final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(settingsBuilder.build(), threadContext), null, + null); + final String state = new State().getValue(); + final String nonce = new Nonce().getValue(); + final String thehint = randomAlphaOfLength(8); + final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(state, nonce, thehint); + + assertThat(response.getAuthenticationRequestUrl(), equalTo("https://op.example.com/login?login_hint=" + thehint + + "&scope=openid&response_type=code&redirect_uri=https%3A%2F%2Frp.my.com%2Fcb&state=" + + state + "&nonce=" + nonce + "&client_id=rp-my")); + } + + private AuthenticationResult authenticateWithOidc(UserRoleMapper roleMapper, boolean notPopulateMetadata, boolean useAuthorizingRealm) + throws Exception { + + final String principal = "324235435454"; + final MockLookupRealm lookupRealm = new MockLookupRealm( + new RealmConfig(new RealmConfig.RealmIdentifier("mock", "mock_lookup"), globalSettings, env, threadContext)); + final OpenIdConnectAuthenticator authenticator = mock(OpenIdConnectAuthenticator.class); + + final Settings.Builder builder = getBasicRealmSettings(); + if (notPopulateMetadata) { + builder.put(getFullSettingKey(REALM_NAME, SamlRealmSettings.POPULATE_USER_METADATA), + false); + } + if (useAuthorizingRealm) { + builder.putList(getFullSettingKey(new RealmConfig.RealmIdentifier("oidc", REALM_NAME), + DelegatedAuthorizationSettings.AUTHZ_REALMS), lookupRealm.name()); + lookupRealm.registerUser(new User(principal, new String[]{"lookup_user_role"}, "Clinton Barton", "cbarton@shield.gov", + Collections.singletonMap("is_lookup", true), true)); + } + final RealmConfig config = buildConfig(builder.build(), threadContext); + final OpenIdConnectRealm realm = new OpenIdConnectRealm(config, authenticator, roleMapper); + initializeRealms(realm, lookupRealm); + final OpenIdConnectToken token = new OpenIdConnectToken("", new State(), new Nonce()); + final JWTClaimsSet claims = new JWTClaimsSet.Builder() + .subject(principal) + .audience("https://rp.elastic.co/cb") + .expirationTime(Date.from(now().plusSeconds(3600))) + .issueTime(Date.from(now().minusSeconds(5))) + .jwtID(randomAlphaOfLength(8)) + .issuer("https://op.company.org") + .claim("groups", Arrays.asList("group1", "group2", "groups3")) + .claim("mail", "cbarton@shield.gov") + .claim("name", "Clinton Barton") + .claim("id_token_hint", "thisis.aserialized.jwt") + .build(); + + doAnswer((i) -> { + ActionListener listener = (ActionListener) i.getArguments()[1]; + listener.onResponse(claims); + return null; + }).when(authenticator).authenticate(any(OpenIdConnectToken.class), any(ActionListener.class)); + + final PlainActionFuture future = new PlainActionFuture<>(); + realm.authenticate(token, future); + final AuthenticationResult result = future.get(); + assertThat(result, notNullValue()); + assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS)); + assertThat(result.getUser().principal(), equalTo(principal)); + assertThat(result.getUser().email(), equalTo("cbarton@shield.gov")); + assertThat(result.getUser().fullName(), equalTo("Clinton Barton")); + + return result; + } + + private void initializeRealms(Realm... realms) { + XPackLicenseState licenseState = mock(XPackLicenseState.class); + when(licenseState.isAuthorizationRealmAllowed()).thenReturn(true); + + final List realmList = Arrays.asList(realms); + for (Realm realm : realms) { + realm.initialize(realmList, licenseState); + } + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectTestCase.java new file mode 100644 index 0000000000000..df5acb0c3a72c --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectTestCase.java @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authc.oidc; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.openid.connect.sdk.Nonce; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.Arrays; +import java.util.Date; + +import static java.time.Instant.now; +import static org.elasticsearch.xpack.core.security.authc.RealmSettings.getFullSettingKey; + +public abstract class OpenIdConnectTestCase extends ESTestCase { + + protected static final String REALM_NAME = "oidc-realm"; + + protected static Settings.Builder getBasicRealmSettings() { + return Settings.builder() + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.org/login") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.org/token") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ENDSESSION_ENDPOINT), "https://op.example.org/logout") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.org/jwks.json") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.elastic.co/cb") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_POST_LOGOUT_REDIRECT_URI), "https://rp.elastic.co/succ_logout") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), randomFrom("code", "id_token")) + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.GROUPS_CLAIM.getClaim()), "groups") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.MAIL_CLAIM.getClaim()), "mail") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.NAME_CLAIM.getClaim()), "name"); + } + + protected JWT generateIdToken(String subject, String audience, String issuer) throws Exception { + int hashSize = randomFrom(256, 384, 512); + int keySize = randomFrom(2048, 4096); + KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA"); + gen.initialize(keySize); + KeyPair keyPair = gen.generateKeyPair(); + JWTClaimsSet idTokenClaims = new JWTClaimsSet.Builder() + .jwtID(randomAlphaOfLength(8)) + .audience(audience) + .expirationTime(Date.from(now().plusSeconds(3600))) + .issuer(issuer) + .issueTime(Date.from(now().minusSeconds(4))) + .notBeforeTime(Date.from(now().minusSeconds(4))) + .claim("nonce", new Nonce()) + .subject(subject) + .build(); + + SignedJWT jwt = new SignedJWT( + new JWSHeader.Builder(JWSAlgorithm.parse("RS" + hashSize)).build(), + idTokenClaims); + jwt.sign(new RSASSASigner(keyPair.getPrivate())); + return jwt; + } + + protected RealmConfig buildConfig(Settings realmSettings, ThreadContext threadContext) { + final Settings settings = Settings.builder() + .put("path.home", createTempDir()) + .put(realmSettings).build(); + final Environment env = TestEnvironment.newEnvironment(settings); + return new RealmConfig(new RealmConfig.RealmIdentifier("oidc", REALM_NAME), settings, env, threadContext); + } + + public static void writeJwkSetToFile(Path file) throws IOException { + Files.write(file, Arrays.asList( + "{\n" + + " \"keys\": [\n" + + " {\n" + + " \"kty\": \"RSA\",\n" + + " \"d\": \"lT2V49RNsu0eTroQDqFCiHY-CkPWdKfKAf66sJrWPNpSX8URa6pTCruFQMsb9ZSqQ8eIvqys9I9rq6Wpaxn1aGRahVzxp7nsBPZYw" + + "SY09LRzhvAxJwWdwtF-ogrV5-p99W9mhEa0khot3myzzfWNnGzcf1IudqvkqE9zrlUJg-kvA3icbs6HgaZVAevb_mx-bgbtJdnUxyPGwXLyQ7g6hlntQ" + + "R_vpzTnK7XFU6fvkrojh7UPJkanKAH0gf3qPrB-Y2gQML7RSlKo-ZfJNHa83G4NRLHKuWTI6dSKJlqmS9zWGmyC3dx5kGjgqD6YgwtWlip8q-U839zxt" + + "z25yeslsQ\",\n" + + " \"e\": \"AQAB\",\n" + + " \"use\": \"sig\",\n" + + " \"kid\": \"testkey\",\n" + + " \"alg\": \"RS256\",\n" + + " \"n\": \"lXBe4UngWJiUfbqbeOvwbH04kYLCpeH4k0o3ngScZDo6ydc_gBDEVwPLQpi8D930aIzr3XHP3RCj0hnpxUun7MNMhWxJZVOd1eg5u" + + "uO-nPIhkqr9iGKV5srJk0Dvw0wBaGZuXMBheY2ViNaKTR9EEtjNwU2d2-I5U3YlrnFR6nj-Pn_hWaiCbb_pSFM4w9QpoLDmuwMRanHY_YK7Td2WMICSG" + + "P3IRGmbecRZCqgkWVZk396EMoMLNxi8WcErYknyY9r-QeJMruRkr27kgx78L7KZ9uBmu9oKXRQl15ZDYe7Bnt9E5wSdOCV9R9h5VRVUur-_129XkDeAX" + + "-6re63_Mw\"\n" + + " }\n" + + " ]\n" + + "}" + )); + } +} diff --git a/x-pack/qa/oidc-op-tests/build.gradle b/x-pack/qa/oidc-op-tests/build.gradle new file mode 100644 index 0000000000000..72fd21c993278 --- /dev/null +++ b/x-pack/qa/oidc-op-tests/build.gradle @@ -0,0 +1,84 @@ +Project idpFixtureProject = xpackProject("test:idp-fixture") + +apply plugin: 'elasticsearch.standalone-rest-test' +apply plugin: 'elasticsearch.rest-test' +apply plugin: 'elasticsearch.test.fixtures' + +dependencies { + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + testCompile project(path: xpackModule('core'), configuration: 'default') + testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') + testCompile project(path: xpackModule('security'), configuration: 'testArtifacts') +} +testFixtures.useFixture ":x-pack:test:idp-fixture" + +String ephemeralPort; +task setupPorts { + // Don't attempt to get ephemeral ports when Docker is not available + onlyIf { idpFixtureProject.postProcessFixture.enabled } + dependsOn idpFixtureProject.postProcessFixture + doLast { + ephemeralPort = idpFixtureProject.postProcessFixture.ext."test.fixtures.oidc-provider.tcp.8080" + } +} + +integTestCluster { + dependsOn setupPorts + setting 'xpack.license.self_generated.type', 'trial' + setting 'xpack.security.enabled', 'true' + setting 'xpack.security.http.ssl.enabled', 'false' + setting 'xpack.security.authc.token.enabled', 'true' + setting 'xpack.security.authc.realms.file.file.order', '0' + setting 'xpack.security.authc.realms.native.native.order', '1' + // OpenID Connect Realm 1 configured for authorization grant flow + setting 'xpack.security.authc.realms.oidc.c2id.order', '2' + setting 'xpack.security.authc.realms.oidc.c2id.op.name', 'c2id-op' + setting 'xpack.security.authc.realms.oidc.c2id.op.issuer', 'http://localhost:8080' + setting 'xpack.security.authc.realms.oidc.c2id.op.authorization_endpoint', "http://127.0.0.1:${-> ephemeralPort}/c2id-login" + setting 'xpack.security.authc.realms.oidc.c2id.op.token_endpoint', "http://127.0.0.1:${-> ephemeralPort}/c2id/token" + setting 'xpack.security.authc.realms.oidc.c2id.op.userinfo_endpoint', "http://127.0.0.1:${-> ephemeralPort}/c2id/userinfo" + setting 'xpack.security.authc.realms.oidc.c2id.op.jwkset_path', 'op-jwks.json' + setting 'xpack.security.authc.realms.oidc.c2id.rp.redirect_uri', 'https://my.fantastic.rp/cb' + setting 'xpack.security.authc.realms.oidc.c2id.rp.client_id', 'elasticsearch-rp' + keystoreSetting 'xpack.security.authc.realms.oidc.c2id.rp.client_secret', 'b07efb7a1cf6ec9462afe7b6d3ab55c6c7880262aa61ac28dded292aca47c9a2' + setting 'xpack.security.authc.realms.oidc.c2id.rp.response_type', 'code' + setting 'xpack.security.authc.realms.oidc.c2id.claims.principal', 'sub' + setting 'xpack.security.authc.realms.oidc.c2id.claims.name', 'name' + setting 'xpack.security.authc.realms.oidc.c2id.claims.mail', 'email' + setting 'xpack.security.authc.realms.oidc.c2id.claims.groups', 'groups' + // OpenID Connect Realm 2 configured for implicit flow + setting 'xpack.security.authc.realms.oidc.c2id-implicit.order', '3' + setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.name', 'c2id-implicit' + setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.issuer', 'http://localhost:8080' + setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.authorization_endpoint', "http://127.0.0.1:${-> ephemeralPort}/c2id-login" + setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.token_endpoint', "http://127.0.0.1:${-> ephemeralPort}/c2id/token" + setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.userinfo_endpoint', "http://127.0.0.1:${-> ephemeralPort}/c2id/userinfo" + setting 'xpack.security.authc.realms.oidc.c2id-implicit.op.jwkset_path', 'op-jwks.json' + setting 'xpack.security.authc.realms.oidc.c2id-implicit.rp.redirect_uri', 'https://my.fantastic.rp/cb' + setting 'xpack.security.authc.realms.oidc.c2id-implicit.rp.client_id', 'elasticsearch-rp' + keystoreSetting 'xpack.security.authc.realms.oidc.c2id-implicit.rp.client_secret', 'b07efb7a1cf6ec9462afe7b6d3ab55c6c7880262aa61ac28dded292aca47c9a2' + setting 'xpack.security.authc.realms.oidc.c2id-implicit.rp.response_type', 'id_token token' + setting 'xpack.security.authc.realms.oidc.c2id-implicit.claims.principal', 'sub' + setting 'xpack.security.authc.realms.oidc.c2id-implicit.claims.name', 'name' + setting 'xpack.security.authc.realms.oidc.c2id-implicit.claims.mail', 'email' + setting 'xpack.security.authc.realms.oidc.c2id-implicit.claims.groups', 'groups' + setting 'xpack.ml.enabled', 'false' + + extraConfigFile 'op-jwks.json', idpFixtureProject.file("oidc/op-jwks.json") + + setupCommand 'setupTestAdmin', + 'bin/elasticsearch-users', 'useradd', "test_admin", '-p', 'x-pack-test-password', '-r', "superuser" + + waitCondition = { node, ant -> + File tmpFile = new File(node.cwd, 'wait.success') + ant.get(src: "http://${node.httpUri()}/_cluster/health?wait_for_nodes=>=${numNodes}&wait_for_status=yellow", + dest: tmpFile.toString(), + username: 'test_admin', + password: 'x-pack-test-password', + ignoreerrors: true, + retries: 10) + return tmpFile.exists() + } +} + +thirdPartyAudit.enabled = false \ No newline at end of file diff --git a/x-pack/qa/oidc-op-tests/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthIT.java b/x-pack/qa/oidc-op-tests/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthIT.java new file mode 100644 index 0000000000000..7835b236ed84d --- /dev/null +++ b/x-pack/qa/oidc-op-tests/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthIT.java @@ -0,0 +1,394 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authc.oidc; + +import net.minidev.json.JSONObject; +import net.minidev.json.parser.JSONParser; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicHeader; +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.Response; +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.elasticsearch.xpack.core.common.socket.SocketAccess; +import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.BeforeClass; + +import java.io.IOException; +import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.common.xcontent.XContentHelper.convertToMap; +import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; + +public class OpenIdConnectAuthIT extends ESRestTestCase { + + private static final String REALM_NAME = "c2id"; + private static final String REALM_NAME_IMPLICIT = "c2id-implicit"; + private static final String FACILITATOR_PASSWORD = "f@cilit@t0r"; + private static final String REGISTRATION_URL = "http://127.0.0.1:" + getEphemeralPortFromProperty("8080") + "/c2id/clients"; + private static final String LOGIN_API = "http://127.0.0.1:" + getEphemeralPortFromProperty("8080") + "/c2id-login/api/"; + + @Before + public void setupUserAndRoles() throws IOException { + setFacilitatorUser(); + setRoleMappings(); + } + + /** + * C2id server only supports dynamic registration, so we can't pre-seed it's config with our client data. Execute only once + */ + @BeforeClass + public static void registerClient() throws Exception { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpPost httpPost = new HttpPost(REGISTRATION_URL); + final BasicHttpContext context = new BasicHttpContext(); + String json = "{" + + "\"grant_types\": [\"implicit\", \"authorization_code\"]," + + "\"response_types\": [\"code\", \"token id_token\"]," + + "\"preferred_client_id\":\"elasticsearch-rp\"," + + "\"preferred_client_secret\":\"b07efb7a1cf6ec9462afe7b6d3ab55c6c7880262aa61ac28dded292aca47c9a2\"," + + "\"redirect_uris\": [\"https://my.fantastic.rp/cb\"]" + + "}"; + httpPost.setEntity(new StringEntity(json, ContentType.APPLICATION_JSON)); + httpPost.setHeader("Accept", "application/json"); + httpPost.setHeader("Content-type", "application/json"); + httpPost.setHeader("Authorization", "Bearer 811fa888f3e0fdc9e01d4201bfeee46a"); + CloseableHttpResponse response = SocketAccess.doPrivileged(() -> httpClient.execute(httpPost, context)); + assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); + } + } + + @Override + protected Settings restAdminSettings() { + String token = basicAuthHeaderValue("test_admin", new SecureString("x-pack-test-password".toCharArray())); + return Settings.builder() + .put(ThreadContext.PREFIX + ".Authorization", token) + .build(); + } + + private String authenticateAtOP(URI opAuthUri) throws Exception { + // C2ID doesn't have a non JS login page :/, so use their API directly + // see https://connect2id.com/products/server/docs/guides/login-page + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + final BasicHttpContext context = new BasicHttpContext(); + // Initiate the authentication process + HttpPost httpPost = new HttpPost(LOGIN_API + "initAuthRequest"); + String initJson = "{" + + " \"qs\":\"" + opAuthUri.getRawQuery() + "\"" + + "}"; + configureJsonRequest(httpPost, initJson); + JSONObject initResponse = execute(httpClient, httpPost, context, response -> { + assertHttpOk(response.getStatusLine()); + return parseJsonResponse(response); + }); + assertThat(initResponse.getAsString("type"), equalTo("auth")); + final String sid = initResponse.getAsString("sid"); + // Actually authenticate the user with ldapAuth + HttpPost loginHttpPost = new HttpPost(LOGIN_API + "authenticateSubject?cacheBuster=" + randomAlphaOfLength(8)); + String loginJson = "{" + + "\"username\":\"alice\"," + + "\"password\":\"secret\"" + + "}"; + configureJsonRequest(loginHttpPost, loginJson); + JSONObject loginJsonResponse = execute(httpClient, loginHttpPost, context, response -> { + assertHttpOk(response.getStatusLine()); + return parseJsonResponse(response); + }); + // Get the consent screen + HttpPut consentFetchHttpPut = + new HttpPut(LOGIN_API + "updateAuthRequest" + "/" + sid + "?cacheBuster=" + randomAlphaOfLength(8)); + String consentFetchJson = "{" + + "\"sub\": \"" + loginJsonResponse.getAsString("id") + "\"," + + "\"acr\": \"http://loa.c2id.com/basic\"," + + "\"amr\": [\"pwd\"]," + + "\"data\": {" + + "\"email\": \"" + loginJsonResponse.getAsString("email") + "\"," + + "\"name\": \"" + loginJsonResponse.getAsString("name") + "\"" + + "}" + + "}"; + configureJsonRequest(consentFetchHttpPut, consentFetchJson); + JSONObject consentFetchResponse = execute(httpClient, consentFetchHttpPut, context, response -> { + assertHttpOk(response.getStatusLine()); + return parseJsonResponse(response); + }); + if (consentFetchResponse.getAsString("type").equals("consent")) { + // If needed, submit the consent + HttpPut consentHttpPut = + new HttpPut(LOGIN_API + "updateAuthRequest" + "/" + sid + "?cacheBuster=" + randomAlphaOfLength(8)); + String consentJson = "{" + + "\"claims\":[\"name\", \"email\"]," + + "\"scope\":[\"openid\"]" + + "}"; + configureJsonRequest(consentHttpPut, consentJson); + JSONObject jsonConsentResponse = execute(httpClient, consentHttpPut, context, response -> { + assertHttpOk(response.getStatusLine()); + return parseJsonResponse(response); + }); + assertThat(jsonConsentResponse.getAsString("type"), equalTo("response")); + JSONObject parameters = (JSONObject) jsonConsentResponse.get("parameters"); + return parameters.getAsString("uri"); + } else if (consentFetchResponse.getAsString("type").equals("response")) { + JSONObject parameters = (JSONObject) consentFetchResponse.get("parameters"); + return parameters.getAsString("uri"); + } else { + fail("Received an invalid response from the OP"); + return null; + } + } + } + + private static String getEphemeralPortFromProperty(String port) { + String key = "test.fixtures.oidc-provider.tcp." + port; + final String value = System.getProperty(key); + assertNotNull("Expected the actual value for port " + port + " to be in system property " + key, value); + return value; + } + + private Map callAuthenticateApiUsingAccessToken(String accessToken) throws IOException { + Request request = new Request("GET", "/_security/_authenticate"); + RequestOptions.Builder options = request.getOptions().toBuilder(); + options.addHeader("Authorization", "Bearer " + accessToken); + request.setOptions(options); + return entityAsMap(client().performRequest(request)); + } + + private T execute(CloseableHttpClient client, HttpEntityEnclosingRequestBase request, + HttpContext context, CheckedFunction body) + throws Exception { + final int timeout = (int) TimeValue.timeValueSeconds(90).millis(); + RequestConfig requestConfig = RequestConfig.custom() + .setConnectionRequestTimeout(timeout) + .setConnectTimeout(timeout) + .setSocketTimeout(timeout) + .build(); + request.setConfig(requestConfig); + logger.info("Execute HTTP " + request.getMethod() + " " + request.getURI() + + " with payload " + EntityUtils.toString(request.getEntity())); + try (CloseableHttpResponse response = SocketAccess.doPrivileged(() -> client.execute(request, context))) { + return body.apply(response); + } catch (Exception e) { + logger.warn(new ParameterizedMessage("HTTP Request [{}] failed", request.getURI()), e); + throw e; + } + } + + private JSONObject parseJsonResponse(HttpResponse response) throws Exception { + JSONParser parser = new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE); + String entity = EntityUtils.toString(response.getEntity()); + logger.info("Response entity as string: " + entity); + return (JSONObject) parser.parse(entity); + } + + private void configureJsonRequest(HttpEntityEnclosingRequestBase request, String jsonBody) { + StringEntity entity = new StringEntity(jsonBody, ContentType.APPLICATION_JSON); + request.setEntity(entity); + request.setHeader("Accept", "application/json"); + request.setHeader("Content-type", "application/json"); + } + + public void testAuthenticateWithCodeFlow() throws Exception { + final PrepareAuthResponse prepareAuthResponse = getRedirectedFromFacilitator(REALM_NAME); + final String redirectUri = authenticateAtOP(prepareAuthResponse.getAuthUri()); + Tuple tokens = completeAuthentication(redirectUri, prepareAuthResponse.getState(), + prepareAuthResponse.getNonce()); + verifyElasticsearchAccessTokenForCodeFlow(tokens.v1()); + } + + public void testAuthenticateWithImplicitFlow() throws Exception { + final PrepareAuthResponse prepareAuthResponse = getRedirectedFromFacilitator(REALM_NAME_IMPLICIT); + final String redirectUri = authenticateAtOP(prepareAuthResponse.getAuthUri()); + Tuple tokens = completeAuthentication(redirectUri, prepareAuthResponse.getState(), + prepareAuthResponse.getNonce()); + verifyElasticsearchAccessTokenForImplicitFlow(tokens.v1()); + } + + private void verifyElasticsearchAccessTokenForCodeFlow(String accessToken) throws IOException { + final Map map = callAuthenticateApiUsingAccessToken(accessToken); + logger.info("Authentication with token Response: " + map); + assertThat(map.get("username"), equalTo("alice")); + assertThat((List) map.get("roles"), containsInAnyOrder("kibana_user", "auditor")); + + assertThat(map.get("metadata"), instanceOf(Map.class)); + final Map metadata = (Map) map.get("metadata"); + assertThat(metadata.get("oidc(sub)"), equalTo("alice")); + assertThat(metadata.get("oidc(iss)"), equalTo("http://localhost:8080")); + } + + private void verifyElasticsearchAccessTokenForImplicitFlow(String accessToken) throws IOException { + final Map map = callAuthenticateApiUsingAccessToken(accessToken); + logger.info("Authentication with token Response: " + map); + assertThat(map.get("username"), equalTo("alice")); + assertThat((List) map.get("roles"), containsInAnyOrder("limited_user", "auditor")); + + assertThat(map.get("metadata"), instanceOf(Map.class)); + final Map metadata = (Map) map.get("metadata"); + assertThat(metadata.get("oidc(sub)"), equalTo("alice")); + assertThat(metadata.get("oidc(iss)"), equalTo("http://localhost:8080")); + } + + + private PrepareAuthResponse getRedirectedFromFacilitator(String realmName) throws Exception { + final Map body = Collections.singletonMap("realm", realmName); + Request request = buildRequest("POST", "/_security/oidc/prepare", body, facilitatorAuth()); + final Response prepare = client().performRequest(request); + assertOK(prepare); + final Map responseBody = parseResponseAsMap(prepare.getEntity()); + logger.info("Created OpenIDConnect authentication request {}", responseBody); + final String state = (String) responseBody.get("state"); + final String nonce = (String) responseBody.get("nonce"); + final String authUri = (String) responseBody.get("redirect"); + return new PrepareAuthResponse(new URI(authUri), state, nonce); + } + + private Tuple completeAuthentication(String redirectUri, String state, String nonce) throws Exception { + final Map body = new HashMap<>(); + body.put("redirect_uri", redirectUri); + body.put("state", state); + body.put("nonce", nonce); + Request request = buildRequest("POST", "/_security/oidc/authenticate", body, facilitatorAuth()); + final Response authenticate = client().performRequest(request); + assertOK(authenticate); + final Map responseBody = parseResponseAsMap(authenticate.getEntity()); + logger.info(" OpenIDConnect authentication response {}", responseBody); + assertNotNull(responseBody.get("access_token")); + assertNotNull(responseBody.get("refresh_token")); + return new Tuple(responseBody.get("access_token"), responseBody.get("refresh_token")); + } + + private Request buildRequest(String method, String endpoint, Map body, Header... headers) throws IOException { + Request request = new Request(method, endpoint); + XContentBuilder builder = XContentFactory.jsonBuilder().map(body); + if (body != null) { + request.setJsonEntity(BytesReference.bytes(builder).utf8ToString()); + } + final RequestOptions.Builder options = request.getOptions().toBuilder(); + for (Header header : headers) { + options.addHeader(header.getName(), header.getValue()); + } + request.setOptions(options); + return request; + } + + private static BasicHeader facilitatorAuth() { + final String auth = + UsernamePasswordToken.basicAuthHeaderValue("facilitator", new SecureString(FACILITATOR_PASSWORD.toCharArray())); + return new BasicHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, auth); + } + + private Map parseResponseAsMap(HttpEntity entity) throws IOException { + return convertToMap(XContentType.JSON.xContent(), entity.getContent(), false); + } + + + private void assertHttpOk(StatusLine status) { + assertThat("Unexpected HTTP Response status: " + status, status.getStatusCode(), Matchers.equalTo(200)); + } + + /** + * We create a user named `facilitator` with the appropriate privileges ( `manage_oidc` ). A facilitator web app + * would need to create one also, in order to access the OIDC related APIs on behalf of the user. + */ + private void setFacilitatorUser() throws IOException { + Request createRoleRequest = new Request("PUT", "/_security/role/facilitator"); + createRoleRequest.setJsonEntity("{ \"cluster\" : [\"manage_oidc\", \"manage_token\"] }"); + adminClient().performRequest(createRoleRequest); + Request createUserRequest = new Request("PUT", "/_security/user/facilitator"); + createUserRequest.setJsonEntity("{ \"password\" : \"" + FACILITATOR_PASSWORD + "\", \"roles\" : [\"facilitator\"] }"); + adminClient().performRequest(createUserRequest); + } + + private void setRoleMappings() throws IOException { + Request createRoleMappingRequest = new Request("PUT", "/_security/role_mapping/oidc_kibana"); + createRoleMappingRequest.setJsonEntity("{ \"roles\" : [\"kibana_user\"]," + + "\"enabled\": true," + + "\"rules\": {" + + "\"field\": { \"realm.name\": \"" + REALM_NAME + "\"}" + + "}" + + "}"); + adminClient().performRequest(createRoleMappingRequest); + + createRoleMappingRequest = new Request("PUT", "/_security/role_mapping/oidc_limited"); + createRoleMappingRequest.setJsonEntity("{ \"roles\" : [\"limited_user\"]," + + "\"enabled\": true," + + "\"rules\": {" + + "\"field\": { \"realm.name\": \"" + REALM_NAME_IMPLICIT + "\"}" + + "}" + + "}"); + adminClient().performRequest(createRoleMappingRequest); + + createRoleMappingRequest = new Request("PUT", "/_security/role_mapping/oidc_auditor"); + createRoleMappingRequest.setJsonEntity("{ \"roles\" : [\"auditor\"]," + + "\"enabled\": true," + + "\"rules\": {" + + "\"field\": { \"groups\": \"audit\"}" + + "}" + + "}"); + adminClient().performRequest(createRoleMappingRequest); + } + + + /** + * Simple POJO encapsulating a response to calling /_security/oidc/prepare + */ + class PrepareAuthResponse { + private URI authUri; + private String state; + private String nonce; + + PrepareAuthResponse(URI authUri, String state, String nonce) { + this.authUri = authUri; + this.state = state; + this.nonce = nonce; + } + + URI getAuthUri() { + return authUri; + } + + String getState() { + return state; + } + + String getNonce() { + return nonce; + } + } +} diff --git a/x-pack/test/idp-fixture/docker-compose.yml b/x-pack/test/idp-fixture/docker-compose.yml index 53fb62855164d..c549fbbfa5dd7 100644 --- a/x-pack/test/idp-fixture/docker-compose.yml +++ b/x-pack/test/idp-fixture/docker-compose.yml @@ -38,3 +38,10 @@ services: - ./idp/shibboleth-idp/conf:/opt/shibboleth-idp/conf - ./idp/shibboleth-idp/credentials:/opt/shibboleth-idp/credentials - ./idp/shib-jetty-base/start.d/ssl.ini:/opt/shib-jetty-base/start.d/ssl.ini + + oidc-provider: + image: "c2id/c2id-server:7.8" + ports: + - "8080" + volumes: + - ./oidc/override.properties:/etc/c2id/override.properties \ No newline at end of file diff --git a/x-pack/test/idp-fixture/oidc/op-jwks.json b/x-pack/test/idp-fixture/oidc/op-jwks.json new file mode 100644 index 0000000000000..7a26fb7714c25 --- /dev/null +++ b/x-pack/test/idp-fixture/oidc/op-jwks.json @@ -0,0 +1 @@ +{"keys":[{"kty":"RSA","e":"AQAB","use":"sig","kid":"CXup","n":"hrwD-lc-IwzwidCANmy4qsiZk11yp9kHykOuP0yOnwi36VomYTQVEzZXgh2sDJpGgAutdQudgwLoV8tVSsTG9SQHgJjH9Pd_9V4Ab6PANyZNG6DSeiq1QfiFlEP6Obt0JbRB3W7X2vkxOVaNoWrYskZodxU2V0ogeVL_LkcCGAyNu2jdx3j0DjJatNVk7ystNxb9RfHhJGgpiIkO5S3QiSIVhbBKaJHcZHPF1vq9g0JMGuUCI-OTSVg6XBkTLEGw1C_R73WD_oVEBfdXbXnLukoLHBS11p3OxU7f4rfxA_f_72_UwmWGJnsqS3iahbms3FkvqoL9x_Vj3GhuJSf97Q"},{"kty":"EC","use":"sig","crv":"P-256","kid":"yGvt","x":"pvgdqM3RCshljmuCF1D2Ez1w5ei5k7-bpimWLPNeEHI","y":"JSmUhbUTqiFclVLEdw6dz038F7Whw4URobjXbAReDuM"},{"kty":"EC","use":"sig","crv":"P-384","kid":"9nHY","x":"JPKhjhE0Bj579Mgj3Cn3ERGA8fKVYoGOaV9BPKhtnEobphf8w4GSeigMesL-038W","y":"UbJa1QRX7fo9LxSlh7FOH5ABT5lEtiQeQUcX9BW0bpJFlEVGqwec80tYLdOIl59M"},{"kty":"EC","use":"sig","crv":"P-521","kid":"tVzS","x":"AZgkRHlIyNQJlPIwTWdHqouw41k9dS3GJO04BDEnJnd_Dd1owlCn9SMXA-JuXINn4slwbG4wcECbctXb2cvdGtmn","y":"AdBC6N9lpupzfzcIY3JLIuc8y8MnzV-ItmzHQcC5lYWMTbuM9NU_FlvINeVo8g6i4YZms2xFB-B0VVdaoF9kUswC"}]} \ No newline at end of file diff --git a/x-pack/test/idp-fixture/oidc/override.properties b/x-pack/test/idp-fixture/oidc/override.properties new file mode 100644 index 0000000000000..888bde9acb48e --- /dev/null +++ b/x-pack/test/idp-fixture/oidc/override.properties @@ -0,0 +1,4 @@ +op.issuer=http://localhost:8080 +op.authz.endpoint=http://localhost:8080/c2id-login/ +op.reg.apiAccessTokenSHA256=d1c4fa70d9ee708d13cfa01daa0e060a05a2075a53c5cc1ad79e460e96ab5363 +jose.jwkSer=RnVsbCBrZXk6CnsKICAia2V5cyI6IFsKICAgIHsKICAgICAgInAiOiAiLXhhN2d2aW5tY3N3QXU3Vm1mV2loZ2o3U3gzUzhmd2dFSTdMZEVveW5FU1RzcElaeUY5aHc0NVhQZmI5VHlpbzZsOHZTS0F5RmU4T2lOalpkNE1Ra0ttYlJzTmxxR1Y5VlBoWF84UG1JSm5mcGVhb3E5YnZfU0k1blZHUl9zYUUzZE9sTEE2VWpaS0lsRVBNb0ZuRlZCMUFaUU9qQlhRRzZPTDg2eDZ2NHMwIiwKICAgICAgImt0eSI6ICJSU0EiLAogICAgICAicSI6ICJ2Q3pDQUlpdHV0MGx1V0djQloyLUFabURLc1RxNkkxcUp0RmlEYkIyZFBNQVlBNldOWTdaWEZoVWxsSjJrT2ZELWdlYjlkYkN2ODBxNEwyajVZSjZoOTBUc1NRWWVHRlljN1lZMGdCMU5VR3l5cXctb29QN0EtYlJmMGI3b3I4ajZJb0hzQTZKa2JranN6c3otbkJ2U2RmUURlZkRNSVc3Ni1ZWjN0c2hsY2MiLAogICAgICAiZCI6ICJtbFBOcm1zVVM5UmJtX1I5SElyeHdmeFYzZnJ2QzlaQktFZzRzc1ZZaThfY09lSjV2U1hyQV9laEtwa2g4QVhYaUdWUGpQbVlyd29xQzFVUksxUkZmLVg0dG10emV2OUVHaU12Z0JCaEF5RkdTSUd0VUNla2x4Q2dhb3BpMXdZSU1Bd0M0STZwMUtaZURxTVNCWVZGeHA5ZWlJZ2pwb05JbV9lR3hXUUs5VHNnYmk5T3lyc1VqaE9KLVczN2JVMEJWUU56UXpxODhCcGxmNzM3VmV1dy1FeDZaMk1iWXR3SWdfZ0JVb0JEZ0NrZkhoOVE4MElYcEZRV0x1RzgwenFrdkVwTHZ0RWxLbDRvQ3BHVnBjcmFUOFNsOGpYc3FDT1k0dnVRT19LRVUzS2VPNUNJbHd4eEhJYXZjQTE5cHFpSWJ5cm1LbThxS0ZEWHluUFJMSGFNZ1EiLAogICAgICAiZSI6ICJBUUFCIiwKICAgICAgImtpZCI6ICJyc2EzODRfMjA0OCIsCiAgICAgICJxaSI6ICJzMldTamVrVDl3S2JPbk9neGNoaDJPY3VubzE2Y20wS281Z3hoUWJTdVMyMldfUjJBR2ZVdkRieGF0cTRLakQ3THo3X1k2TjdTUkwzUVpudVhoZ1djeXgyNGhrUGppQUZLNmlkYVZKQzJqQmgycEZTUDVTNXZxZ0lsME12eWY4NjlwdkN4S0NzaGRKMGdlRWhveE93VkRPYXJqdTl2Zm9IQV90LWJoRlZrUnciLAogICAgICAiZHAiOiAiQlJhQTFqYVRydG9mTHZBSUJBYW1OSEVhSm51RU9zTVJJMFRCZXFuR1BNUm0tY2RjSG1OUVo5WUtqb2JpdXlmbnhGZ0piVDlSeElBRG0ySkpoZEp5RTN4Y1dTSzhmSjBSM1Jick1aT1dwako0QmJTVzFtU1VtRnlKTGxib3puRFhZR2RaZ1hzS0o1UkFrRUNQZFBCY3YwZVlkbk9NYWhfZndfaFZoNjRuZ2tFIiwKICAgICAgImFsZyI6ICJSU0EzODQiLAogICAgICAiZHEiOiAiUFJoVERKVlR3cDNXaDZfWFZrTjIwMUlpTWhxcElrUDN1UTYyUlRlTDNrQ2ZXSkNqMkZPLTRxcVRIQk0tQjZJWUVPLXpoVWZyQnhiMzJ1djNjS2JDWGFZN3BJSFJxQlFEQWQ2WGhHYzlwc0xqNThXd3VGY2RncERJYUFpRjNyc3NUMjJ4UFVvYkJFTVdBalV3bFJrNEtNTjItMnpLQk5FR3lIcDIzOUpKdnpVIiwKICAgICAgIm4iOiAidUpDWDVDbEZpM0JnTXBvOWhRSVZ2SDh0Vi1jLTVFdG5OeUZxVm91R3NlNWwyUG92MWJGb0tsRllsU25YTzNWUE9KRWR3azNDdl9VT0UtQzlqZERYRHpvS3Z4RURaTVM1TDZWMFpIVEJoNndIOV9iN3JHSlBxLV9RdlNkejczSzZxbHpGaUtQamRvdTF6VlFYTmZfblBZbnRnQkdNRUtBc1pRNGp0cWJCdE5lV0h0MF9UM001cEktTV9KNGVlRWpCTW95TkZuU2ExTEZDVmZRNl9YVnpjelp1TlRGMlh6UmdRWkFmcmJGRXZ6eXR1TzVMZTNTTXFrUUFJeDhFQmkwYXVlRUNqNEQ4cDNVNXFVRG92NEF2VnRJbUZlbFJvb1pBMHJtVW1KRHJ4WExrVkhuVUpzaUF6ZW9TLTNBSnV1bHJkMGpuNjJ5VjZHV2dFWklZMVNlZVd3IgogICAgfQogIF0KfQo \ No newline at end of file