Add an OpenID Connect authentication realm (#40674) (#41178)

This commit adds an OpenID Connect authentication realm to
elasticsearch. Elasticsearch (with the assistance of kibana or
another web component) acts as an OpenID Connect Relying
Party and supports the Authorization Code Grant and Implicit
flows as described in http://ela.st/oidc-spec. It adds support
for consuming and verifying signed ID Tokens, both RP
initiated and 3rd party initiated Single Sign on and RP
initiated signle logout.
It also adds an OpenID Connect Provider in the idp-fixture to
be used for the associated integration tests.

This is a backport of #40674
This commit is contained in:
Ioannis Kakavas 2019-04-15 12:41:16 +03:00 committed by GitHub
parent 2980a6c70f
commit fe9442b05b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 7229 additions and 10 deletions

View File

@ -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<OpenIdConnectAuthenticateResponse> {
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<OpenIdConnectAuthenticateResponse> getResponseReader() {
return OpenIdConnectAuthenticateResponse::new;
}
}

View File

@ -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 + "}";
}
}

View File

@ -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<OpenIdConnectAuthenticateRequest, OpenIdConnectAuthenticateResponse> {
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;
}
}

View File

@ -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);
}
}

View File

@ -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<OpenIdConnectLogoutResponse> {
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<OpenIdConnectLogoutResponse> getResponseReader() {
return OpenIdConnectLogoutResponse::new;
}
}

View File

@ -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");
}
}

View File

@ -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;
}
}

View File

@ -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<OpenIdConnectPrepareAuthenticationResponse> {
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<OpenIdConnectPrepareAuthenticationResponse> getResponseReader() {
return OpenIdConnectPrepareAuthenticationResponse::new;
}
}

View File

@ -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
* <a href="https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin">3rd party initiated authentication</a>, 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 + "}";
}
}

View File

@ -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<OpenIdConnectPrepareAuthenticationRequest, OpenIdConnectPrepareAuthenticationResponse> {
public OpenIdConnectPrepareAuthenticationRequestBuilder(ElasticsearchClient client) {
super(client, OpenIdConnectPrepareAuthenticationAction.INSTANCE, new OpenIdConnectPrepareAuthenticationRequest());
}
public OpenIdConnectPrepareAuthenticationRequestBuilder realmName(String name) {
request.setRealmName(name);
return this;
}
}

View File

@ -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;
}
}

View File

@ -10,6 +10,7 @@ import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
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 final class InternalRealmsSettings {
set.addAll(PkiRealmSettings.getSettings());
set.addAll(SamlRealmSettings.getSettings());
set.addAll(KerberosRealmSettings.getSettings());
set.addAll(OpenIdConnectRealmSettings.getSettings());
return Collections.unmodifiableSet(set);
}
}

View File

@ -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 class RealmSettings {
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> 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 <em>realm-type</em> as an argument.

View File

@ -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<String> SUPPORTED_SIGNATURE_ALGORITHMS = Collections.unmodifiableList(
Arrays.asList("HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512"));
private static final List<String> RESPONSE_TYPES = Arrays.asList("code", "id_token", "id_token token");
public static final String TYPE = "oidc";
public static final Setting.AffixSetting<String> RP_CLIENT_ID
= RealmSettings.simpleString(TYPE, "rp.client_id", Setting.Property.NodeScope);
public static final Setting.AffixSetting<SecureString> RP_CLIENT_SECRET
= RealmSettings.secureString(TYPE, "rp.client_secret");
public static final Setting.AffixSetting<String> 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<String> 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<String> 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<String> 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<List<String>> RP_REQUESTED_SCOPES = Setting.affixKeySetting(
RealmSettings.realmSettingPrefix(TYPE), "rp.requested_scopes",
key -> Setting.listSetting(key, Collections.singletonList("openid"), Function.identity(), Setting.Property.NodeScope));
public static final Setting.AffixSetting<String> OP_NAME
= RealmSettings.simpleString(TYPE, "op.name", Setting.Property.NodeScope);
public static final Setting.AffixSetting<String> OP_AUTHORIZATION_ENDPOINT
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "op.authorization_endpoint",
key -> Setting.simpleString(key, v -> {
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<String> 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<String> 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<String> 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<String> OP_ISSUER
= RealmSettings.simpleString(TYPE, "op.issuer", Setting.Property.NodeScope);
public static final Setting.AffixSetting<String> OP_JWKSET_PATH
= RealmSettings.simpleString(TYPE, "op.jwkset_path", Setting.Property.NodeScope);
public static final Setting.AffixSetting<TimeValue> 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<Boolean> 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<TimeValue> 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<TimeValue> 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<TimeValue> 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<Integer> HTTP_MAX_CONNECTIONS
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "http.max_connections",
key -> Setting.intSetting(key, 200, Setting.Property.NodeScope));
public static final Setting.AffixSetting<Integer> 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<Setting.AffixSetting<?>> getSettings() {
final Set<Setting.AffixSetting<?>> set = Sets.newHashSet(
RP_CLIENT_ID, RP_REDIRECT_URI, RP_RESPONSE_TYPE, RP_REQUESTED_SCOPES, RP_CLIENT_SECRET, RP_SIGNATURE_ALGORITHM,
RP_POST_LOGOUT_REDIRECT_URI, OP_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:
* <ul>
* <li>The name of the OpenID Connect claim to use</li>
* <li>An optional java pattern (regex) to apply to that claim value in order to extract the substring that should be used.</li>
* </ul>
* 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<String> claim;
private final Setting.AffixSetting<String> 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<Setting.AffixSetting<?>> settings() {
return Arrays.asList(getClaim(), getPattern());
}
public String name(RealmConfig config) {
return getClaim().getConcreteSettingForNamespace(config.name()).getKey();
}
public Setting.AffixSetting<String> getClaim() {
return claim;
}
public Setting.AffixSetting<String> getPattern() {
return pattern;
}
}
}

View File

@ -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)

View File

@ -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}"
@ -162,7 +172,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
@ -259,7 +269,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 (
@ -280,7 +292,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'
)
}

View File

@ -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()

View File

@ -0,0 +1 @@
c592b500269bfde36096641b01238a8350f8aa31

View File

@ -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.

View File

@ -0,0 +1 @@
fa29aa438674ff19d5e1386d2c3527a0267f291e

View File

@ -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.

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@
787e007e377223bba85a33599d3da416c135f99b

View File

@ -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) <year> <name of author>
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.

View File

@ -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.

View File

@ -0,0 +1 @@
afba4942caaeaf46aab0b976afd57cc7c181467e

View File

@ -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.

View File

@ -0,0 +1 @@
007396407491352ce4fa30de92efb158adb76b5b

View File

@ -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.

View File

@ -0,0 +1 @@
1db9a709239ae473a69b5424c7e78d0b7108229d

View File

@ -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.

View File

@ -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.

View File

@ -0,0 +1 @@
3981d32ddfa2919a7af46eb5e484f8dc064da665

View File

@ -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.

View File

@ -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.

View File

@ -0,0 +1 @@
422759fc195f65345e8da3265c69dea3c6cf56a5

View File

@ -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.

View File

@ -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.

View File

@ -80,6 +80,9 @@ import org.elasticsearch.xpack.core.security.SecuritySettings;
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.TransportCreateApiKeyAction;
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;
@ -194,6 +200,7 @@ import org.elasticsearch.xpack.security.rest.action.RestGetApiKeyAction;
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;
@ -205,6 +212,8 @@ import org.elasticsearch.xpack.security.rest.action.role.RestPutRoleAction;
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;
@ -746,6 +755,10 @@ public class Security extends Plugin implements ActionPlugin, IngestPlugin, Netw
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),
@ -798,6 +811,9 @@ public class Security extends Plugin implements ActionPlugin, IngestPlugin, Netw
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()),

View File

@ -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<OpenIdConnectAuthenticateRequest, OpenIdConnectAuthenticateResponse> {
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>) OpenIdConnectAuthenticateRequest::new);
this.threadPool = threadPool;
this.authenticationService = authenticationService;
this.tokenService = tokenService;
}
@Override
protected void doExecute(Task task, OpenIdConnectAuthenticateRequest request,
ActionListener<OpenIdConnectAuthenticateResponse> 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<String, Object> tokenMetadata = (Map<String, Object>) 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);
}
));
}
}
}

View File

@ -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<OpenIdConnectLogoutRequest, OpenIdConnectLogoutResponse> {
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>) OpenIdConnectLogoutRequest::new);
this.realms = realms;
this.tokenService = tokenService;
}
@Override
protected void doExecute(Task task, OpenIdConnectLogoutRequest request, ActionListener<OpenIdConnectLogoutResponse> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<TokensInvalidationResult> listener) {
if (refreshToken == null) {
listener.onResponse(null);
} else {
tokenService.invalidateRefreshToken(refreshToken, listener);
}
}
}

View File

@ -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<OpenIdConnectPrepareAuthenticationRequest,
OpenIdConnectPrepareAuthenticationResponse> {
private final Realms realms;
@Inject
public TransportOpenIdConnectPrepareAuthenticationAction(TransportService transportService,
ActionFilters actionFilters, Realms realms) {
super(OpenIdConnectPrepareAuthenticationAction.NAME, transportService, actionFilters,
(Writeable.Reader<OpenIdConnectPrepareAuthenticationRequest>) OpenIdConnectPrepareAuthenticationRequest::new);
this.realms = realms;
}
@Override
protected void doExecute(Task task, OpenIdConnectPrepareAuthenticationRequest request,
ActionListener<OpenIdConnectPrepareAuthenticationResponse> listener) {
Realm realm = null;
if (Strings.hasText(request.getIssuer())) {
List<OpenIdConnectRealm> 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<OpenIdConnectPrepareAuthenticationResponse> listener) {
try {
final OpenIdConnectPrepareAuthenticationResponse authenticationResponse =
realm.buildAuthenticationRequestUri(state, nonce, loginHint);
listener.onResponse(authenticationResponse);
} catch (ElasticsearchException e) {
listener.onFailure(e);
}
}
}

View File

@ -18,6 +18,7 @@ import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
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.esnative.ReservedRealm;
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 @@ import java.util.stream.Collectors;
/**
* 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 <em>internal</em> realm types, excluding {@link ReservedRealm#TYPE}.
*/
private static final Set<String> 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<String> 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 <code>type</code> is an internal realm-type that is provided by x-pack,
@ -90,6 +94,7 @@ public final class InternalRealms {
/**
* Creates {@link Realm.Factory factories} for each <em>internal</em> realm type.
* This excludes the {@link ReservedRealm}, as it cannot be created dynamically.
*
* @return A map from <em>realm-type</em> to <code>Factory</code>
*/
public static Map<String, Realm.Factory> getFactories(ThreadPool threadPool, ResourceWatcherService resourceWatcherService,
@ -105,12 +110,14 @@ public final class InternalRealms {
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);
}

View File

@ -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> 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<JWTClaimsSet> 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:
* <ul>
* <li>First we attempt to validate the Id Token we have received and get any claims it contains</li>
* <li>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)}</li>
* </ul>
*
* @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<JWTClaimsSet> 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
* <a href="https://openid.net/specs/openid-connect-core-1_0.html#ImplicitTokenValidation">specification</a>.
* <p>
* 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<JWTClaimsSet> claimsListener) {
try {
final HttpGet httpGet = new HttpGet(opConfig.getUserinfoEndpoint());
httpGet.setHeader("Authorization", "Bearer " + accessToken.getValue());
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
httpClient.execute(httpGet, new FutureCallback<HttpResponse>() {
@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<JWTClaimsSet> 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<Tuple<AccessToken, JWT>> tokensListener) {
try {
final AuthorizationCodeGrant codeGrant = new AuthorizationCodeGrant(code, rpConfig.getRedirectUri());
final HttpPost httpPost = new HttpPost(opConfig.getTokenEndpoint());
final List<NameValuePair> params = new ArrayList<>();
for (Map.Entry<String, List<String>> entry : codeGrant.toParameters().entrySet()) {
// All parameters of AuthorizationCodeGrant are singleton lists
params.add(new BasicNameValuePair(entry.getKey(), entry.getValue().get(0)));
}
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<Void>) () -> {
httpClient.execute(httpPost, new FutureCallback<HttpResponse>() {
@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<Tuple<AccessToken, JWT>> 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<CloseableHttpAsyncClient>) () -> {
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<SchemeIOSessionStrategy> registry = RegistryBuilder.<SchemeIOSessionStrategy>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<Exception> onChange;
private FileListener(Logger logger, CheckedRunnable<Exception> 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<C extends SecurityContext> implements JWKSource<C> {
private volatile JWKSet cachedJwkSet = new JWKSet();
private final AtomicReference<ListenableFuture<Void>> 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<JWK> get(JWKSelector jwkSelector, C context) {
return jwkSelector.select(cachedJwkSet);
}
void triggerReload(ActionListener<Void> toNotify) {
ListenableFuture<Void> 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<Void> future) {
try {
final HttpGet httpGet = new HttpGet(jwkSetPath.toURI());
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
httpClient.execute(httpGet, new FutureCallback<HttpResponse>() {
@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);
}
}
}
}

View File

@ -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;
}
}

View File

@ -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<Realm> 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<AuthenticationResult> 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<User> listener) {
listener.onResponse(null);
}
private void buildUserFromClaims(JWTClaimsSet claims, ActionListener<AuthenticationResult> 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<String, Object> tokenMetadata = new HashMap<>();
tokenMetadata.put("id_token_hint", claims.getClaim("id_token_hint"));
ActionListener<AuthenticationResult> 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<String, Object> 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<String, Object> userMetadata = new HashMap<>();
if (populateUserMetadata) {
Map<String, Object> claimsMap = claims.getClaims();
/*
* We whitelist the Types that we want to parse as metadata from the Claims, explicitly filtering out {@link Date}s
*/
Set<Map.Entry> 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<String> 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<String> 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:
* <ul>
* <li>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.</li>
* <li>In case of custom facilitators, the implementer might require/support generating the state parameter in order
* to tie this to an anti-XSRF token.</li>
* </ul>
*
*
* @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<JWTClaimsSet, List<String>> parser;
ClaimParser(String name, Function<JWTClaimsSet, List<String>> parser) {
this.name = name;
this.parser = parser;
}
List<String> getClaimValues(JWTClaimsSet claims) {
return parser.apply(claims);
}
String getClaimValue(JWTClaimsSet claims) {
List<String> 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<String> values;
if (claimValueObject == null) {
values = Collections.emptyList();
} else if (claimValueObject instanceof String) {
values = Collections.singletonList((String) claimValueObject);
} else if (claimValueObject instanceof List) {
values = (List<String>) 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<String>) 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());
}
}
}
}

View File

@ -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 "<OIDC Token>";
}
@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 + "}";
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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<OpenIdConnectAuthenticateRequest, Void> 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<OpenIdConnectAuthenticateResponse>(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";
}
}

View File

@ -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<OpenIdConnectLogoutRequest, Void> 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<OpenIdConnectLogoutResponse>(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";
}
}

View File

@ -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<OpenIdConnectPrepareAuthenticationRequest, Void> 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<OpenIdConnectPrepareAuthenticationResponse>(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";
}
}

View File

@ -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"));
}
}

View File

@ -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"));
}
}

View File

@ -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<IndexRequest> indexRequests;
private List<BulkRequest> 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<IndexResponse> listener = (ActionListener<IndexResponse>) 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<IndexResponse> listener = (ActionListener<IndexResponse>) 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<BulkResponse> listener = (ActionListener<BulkResponse>) 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<String, Object> tokenMetadata = new HashMap<>();
tokenMetadata.put("id_token_hint", signedIdToken.serialize());
tokenMetadata.put("oidc_realm", REALM_NAME);
final PlainActionFuture<Tuple<UserToken, String>> 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<OpenIdConnectLogoutResponse> 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();
}
}

View File

@ -18,6 +18,7 @@ import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
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 class InternalRealmsTests extends ESTestCase {
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));
}
}

View File

@ -17,6 +17,7 @@ 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.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 @@ public class SecurityRealmSettingsTests extends SecurityIntegTestCase {
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 @@ public class SecurityRealmSettingsTests extends SecurityIntegTestCase {
.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 @@ public class SecurityRealmSettingsTests extends SecurityIntegTestCase {
}
/**
* 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 {

View File

@ -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<JWTClaimsSet> 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<JWTClaimsSet> 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<Key, JWKSet> 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<AccessToken, JWT> 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<JWTClaimsSet> 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<Key, JWKSet> 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<AccessToken, JWT> 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<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
JWTClaimsSet claimsSet = future.actionGet();
assertThat(claimsSet.getSubject(), equalTo(subject));
}
public void testAuthenticateImplicitFlowWithEcdsa() throws Exception {
final Tuple<Key, JWKSet> 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<AccessToken, JWT> 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<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
JWTClaimsSet claimsSet = future.actionGet();
assertThat(claimsSet.getSubject(), equalTo(subject));
}
public void testAuthenticateImplicitFlowWithHmac() throws Exception {
final Tuple<Key, JWKSet> 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<AccessToken, JWT> 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<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
JWTClaimsSet claimsSet = future.actionGet();
assertThat(claimsSet.getSubject(), equalTo(subject));
}
public void testClockSkewIsHonored() throws Exception {
final Tuple<Key, JWKSet> 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<AccessToken, JWT> 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<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
JWTClaimsSet claimsSet = future.actionGet();
assertThat(claimsSet.getSubject(), equalTo(subject));
}
public void testImplicitFlowFailsWithExpiredToken() throws Exception {
final Tuple<Key, JWKSet> 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<AccessToken, JWT> 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<JWTClaimsSet> 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<Key, JWKSet> 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<AccessToken, JWT> 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<JWTClaimsSet> 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<Key, JWKSet> 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<AccessToken, JWT> 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<JWTClaimsSet> 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<Key, JWKSet> 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<AccessToken, JWT> 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<JWTClaimsSet> 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<Key, JWKSet> 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<AccessToken, JWT> 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<JWTClaimsSet> 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<Key, JWKSet> 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<AccessToken, JWT> 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<JWTClaimsSet> 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<Key, JWKSet> 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<AccessToken, JWT> 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<JWTClaimsSet> 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<Key, JWKSet> 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<AccessToken, JWT> 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<JWTClaimsSet> 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<Key, JWKSet> 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<AccessToken, JWT> 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<JWTClaimsSet> 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<Key, JWKSet> 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<AccessToken, JWT> 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<JWTClaimsSet> 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<Key, JWKSet> 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<JWTClaimsSet> 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<Void> listener = (ActionListener<Void>) invocation.getArguments()[0];
listener.onResponse(null);
return null;
}).when(jwkSource).triggerReload(any(ActionListener.class));
return jwkSource;
}
private Tuple<AccessToken, JWT> 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<AccessToken, JWT> 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<Key, JWKSet> 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);
}
}
}

View File

@ -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);
}
}

View File

@ -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<UserRoleMapper.UserData> userData = new AtomicReference<>();
doAnswer(invocation -> {
assert invocation.getArguments().length == 2;
userData.set((UserRoleMapper.UserData) invocation.getArguments()[0]);
ActionListener<Set<String>> listener = (ActionListener<Set<String>>) 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<Set<String>> listener = (ActionListener<Set<String>>) 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<String, Object> 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<JWTClaimsSet> listener = (ActionListener<JWTClaimsSet>) i.getArguments()[1];
listener.onResponse(claims);
return null;
}).when(authenticator).authenticate(any(OpenIdConnectToken.class), any(ActionListener.class));
final PlainActionFuture<AuthenticationResult> 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<JWTClaimsSet> listener = (ActionListener<JWTClaimsSet>) i.getArguments()[1];
listener.onResponse(claims);
return null;
}).when(authenticator).authenticate(any(OpenIdConnectToken.class), any(ActionListener.class));
final PlainActionFuture<AuthenticationResult> 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<Realm> realmList = Arrays.asList(realms);
for (Realm realm : realms) {
realm.initialize(realmList, licenseState);
}
}
}

View File

@ -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" +
"}"
));
}
}

View File

@ -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

View File

@ -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<String, Object> 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> T execute(CloseableHttpClient client, HttpEntityEnclosingRequestBase request,
HttpContext context, CheckedFunction<HttpResponse, T, Exception> 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<String, String> 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<String, String> tokens = completeAuthentication(redirectUri, prepareAuthResponse.getState(),
prepareAuthResponse.getNonce());
verifyElasticsearchAccessTokenForImplicitFlow(tokens.v1());
}
private void verifyElasticsearchAccessTokenForCodeFlow(String accessToken) throws IOException {
final Map<String, Object> 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<String, Object> 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<String, String> body = Collections.singletonMap("realm", realmName);
Request request = buildRequest("POST", "/_security/oidc/prepare", body, facilitatorAuth());
final Response prepare = client().performRequest(request);
assertOK(prepare);
final Map<String, Object> 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<String, String> completeAuthentication(String redirectUri, String state, String nonce) throws Exception {
final Map<String, String> 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<String, Object> 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<String, ?> 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<String, Object> 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;
}
}
}

View File

@ -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

View File

@ -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"}]}

View File

@ -0,0 +1,4 @@
op.issuer=http://localhost:8080
op.authz.endpoint=http://localhost:8080/c2id-login/
op.reg.apiAccessTokenSHA256=d1c4fa70d9ee708d13cfa01daa0e060a05a2075a53c5cc1ad79e460e96ab5363
jose.jwkSer=RnVsbCBrZXk6CnsKICAia2V5cyI6IFsKICAgIHsKICAgICAgInAiOiAiLXhhN2d2aW5tY3N3QXU3Vm1mV2loZ2o3U3gzUzhmd2dFSTdMZEVveW5FU1RzcElaeUY5aHc0NVhQZmI5VHlpbzZsOHZTS0F5RmU4T2lOalpkNE1Ra0ttYlJzTmxxR1Y5VlBoWF84UG1JSm5mcGVhb3E5YnZfU0k1blZHUl9zYUUzZE9sTEE2VWpaS0lsRVBNb0ZuRlZCMUFaUU9qQlhRRzZPTDg2eDZ2NHMwIiwKICAgICAgImt0eSI6ICJSU0EiLAogICAgICAicSI6ICJ2Q3pDQUlpdHV0MGx1V0djQloyLUFabURLc1RxNkkxcUp0RmlEYkIyZFBNQVlBNldOWTdaWEZoVWxsSjJrT2ZELWdlYjlkYkN2ODBxNEwyajVZSjZoOTBUc1NRWWVHRlljN1lZMGdCMU5VR3l5cXctb29QN0EtYlJmMGI3b3I4ajZJb0hzQTZKa2JranN6c3otbkJ2U2RmUURlZkRNSVc3Ni1ZWjN0c2hsY2MiLAogICAgICAiZCI6ICJtbFBOcm1zVVM5UmJtX1I5SElyeHdmeFYzZnJ2QzlaQktFZzRzc1ZZaThfY09lSjV2U1hyQV9laEtwa2g4QVhYaUdWUGpQbVlyd29xQzFVUksxUkZmLVg0dG10emV2OUVHaU12Z0JCaEF5RkdTSUd0VUNla2x4Q2dhb3BpMXdZSU1Bd0M0STZwMUtaZURxTVNCWVZGeHA5ZWlJZ2pwb05JbV9lR3hXUUs5VHNnYmk5T3lyc1VqaE9KLVczN2JVMEJWUU56UXpxODhCcGxmNzM3VmV1dy1FeDZaMk1iWXR3SWdfZ0JVb0JEZ0NrZkhoOVE4MElYcEZRV0x1RzgwenFrdkVwTHZ0RWxLbDRvQ3BHVnBjcmFUOFNsOGpYc3FDT1k0dnVRT19LRVUzS2VPNUNJbHd4eEhJYXZjQTE5cHFpSWJ5cm1LbThxS0ZEWHluUFJMSGFNZ1EiLAogICAgICAiZSI6ICJBUUFCIiwKICAgICAgImtpZCI6ICJyc2EzODRfMjA0OCIsCiAgICAgICJxaSI6ICJzMldTamVrVDl3S2JPbk9neGNoaDJPY3VubzE2Y20wS281Z3hoUWJTdVMyMldfUjJBR2ZVdkRieGF0cTRLakQ3THo3X1k2TjdTUkwzUVpudVhoZ1djeXgyNGhrUGppQUZLNmlkYVZKQzJqQmgycEZTUDVTNXZxZ0lsME12eWY4NjlwdkN4S0NzaGRKMGdlRWhveE93VkRPYXJqdTl2Zm9IQV90LWJoRlZrUnciLAogICAgICAiZHAiOiAiQlJhQTFqYVRydG9mTHZBSUJBYW1OSEVhSm51RU9zTVJJMFRCZXFuR1BNUm0tY2RjSG1OUVo5WUtqb2JpdXlmbnhGZ0piVDlSeElBRG0ySkpoZEp5RTN4Y1dTSzhmSjBSM1Jick1aT1dwako0QmJTVzFtU1VtRnlKTGxib3puRFhZR2RaZ1hzS0o1UkFrRUNQZFBCY3YwZVlkbk9NYWhfZndfaFZoNjRuZ2tFIiwKICAgICAgImFsZyI6ICJSU0EzODQiLAogICAgICAiZHEiOiAiUFJoVERKVlR3cDNXaDZfWFZrTjIwMUlpTWhxcElrUDN1UTYyUlRlTDNrQ2ZXSkNqMkZPLTRxcVRIQk0tQjZJWUVPLXpoVWZyQnhiMzJ1djNjS2JDWGFZN3BJSFJxQlFEQWQ2WGhHYzlwc0xqNThXd3VGY2RncERJYUFpRjNyc3NUMjJ4UFVvYkJFTVdBalV3bFJrNEtNTjItMnpLQk5FR3lIcDIzOUpKdnpVIiwKICAgICAgIm4iOiAidUpDWDVDbEZpM0JnTXBvOWhRSVZ2SDh0Vi1jLTVFdG5OeUZxVm91R3NlNWwyUG92MWJGb0tsRllsU25YTzNWUE9KRWR3azNDdl9VT0UtQzlqZERYRHpvS3Z4RURaTVM1TDZWMFpIVEJoNndIOV9iN3JHSlBxLV9RdlNkejczSzZxbHpGaUtQamRvdTF6VlFYTmZfblBZbnRnQkdNRUtBc1pRNGp0cWJCdE5lV0h0MF9UM001cEktTV9KNGVlRWpCTW95TkZuU2ExTEZDVmZRNl9YVnpjelp1TlRGMlh6UmdRWkFmcmJGRXZ6eXR1TzVMZTNTTXFrUUFJeDhFQmkwYXVlRUNqNEQ4cDNVNXFVRG92NEF2VnRJbUZlbFJvb1pBMHJtVW1KRHJ4WExrVkhuVUpzaUF6ZW9TLTNBSnV1bHJkMGpuNjJ5VjZHV2dFWklZMVNlZVd3IgogICAgfQogIF0KfQo