diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateAction.java new file mode 100644 index 0000000000000..b27a71e202e55 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateAction.java @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.oidc; + +import org.elasticsearch.action.Action; +import org.elasticsearch.common.io.stream.Writeable; + +/** + * Action for initiating an authentication process using OpenID Connect + */ +public final class OpenIdConnectAuthenticateAction extends Action { + + public static final OpenIdConnectAuthenticateAction INSTANCE = new OpenIdConnectAuthenticateAction(); + public static final String NAME = "cluster:admin/xpack/security/oidc/authenticate"; + + private OpenIdConnectAuthenticateAction() { + super(NAME); + } + + @Override + public OpenIdConnectAuthenticateResponse newResponse() { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + @Override + public Writeable.Reader getResponseReader() { + return OpenIdConnectAuthenticateResponse::new; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateRequest.java new file mode 100644 index 0000000000000..3605e182ca460 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateRequest.java @@ -0,0 +1,95 @@ +/* + * 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.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +/** + * 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 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 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.readOptionalString(); + } + + 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() { + return null; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(redirectUri); + out.writeString(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 "{redirectUri=" + redirectUri + ", state=" + state + ", nonce=" + nonce + "}"; + } +} + diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateRequestBuilder.java new file mode 100644 index 0000000000000..cbdd13aec0463 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateRequestBuilder.java @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.oidc; + +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.client.ElasticsearchClient; + +/** + * Request builder for populating a {@link OpenIdConnectAuthenticateRequest} + */ +public class OpenIdConnectAuthenticateRequestBuilder + extends ActionRequestBuilder { + + public OpenIdConnectAuthenticateRequestBuilder(ElasticsearchClient client) { + super(client, OpenIdConnectAuthenticateAction.INSTANCE, new OpenIdConnectAuthenticateRequest()); + } + + public OpenIdConnectAuthenticateRequestBuilder redirectUri(String redirectUri) { + request.setRedirectUri(redirectUri); + return this; + } + + public OpenIdConnectAuthenticateRequestBuilder state(String state) { + request.setState(state); + return this; + } + + public OpenIdConnectAuthenticateRequestBuilder nonce(String nonce) { + request.setNonce(nonce); + return this; + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateResponse.java new file mode 100644 index 0000000000000..93b7c6b292ae9 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateResponse.java @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.oidc; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.unit.TimeValue; + +import java.io.IOException; + +public class OpenIdConnectAuthenticateResponse extends ActionResponse { + private String principal; + private String accessTokenString; + private String refreshTokenString; + private TimeValue expiresIn; + + public OpenIdConnectAuthenticateResponse(String principal, String accessTokenString, String refreshTokenString, TimeValue expiresIn) { + this.principal = principal; + this.accessTokenString = accessTokenString; + this.refreshTokenString = refreshTokenString; + this.expiresIn = expiresIn; + } + + public OpenIdConnectAuthenticateResponse(StreamInput in) throws IOException { + super.readFrom(in); + principal = in.readString(); + accessTokenString = in.readString(); + refreshTokenString = in.readString(); + expiresIn = in.readTimeValue(); + } + + public String getPrincipal() { + return principal; + } + + public String getAccessTokenString() { + return accessTokenString; + } + + public String getRefreshTokenString() { + return refreshTokenString; + } + + public TimeValue getExpiresIn() { + return expiresIn; + } + + @Override + public void readFrom(StreamInput in) { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(principal); + out.writeString(accessTokenString); + out.writeString(refreshTokenString); + out.writeTimeValue(expiresIn); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationAction.java new file mode 100644 index 0000000000000..2aa82c7286cec --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationAction.java @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.oidc; + +import org.elasticsearch.action.Action; +import org.elasticsearch.common.io.stream.Writeable; + +public class OpenIdConnectPrepareAuthenticationAction extends Action { + + public static final OpenIdConnectPrepareAuthenticationAction INSTANCE = new OpenIdConnectPrepareAuthenticationAction(); + public static final String NAME = "cluster:admin/xpack/security/oidc/prepare"; + + private OpenIdConnectPrepareAuthenticationAction() { + super(NAME); + } + + @Override + public OpenIdConnectPrepareAuthenticationResponse newResponse() { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + @Override + public Writeable.Reader getResponseReader() { + return OpenIdConnectPrepareAuthenticationResponse::new; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationRequest.java new file mode 100644 index 0000000000000..af690b606feb3 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationRequest.java @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.oidc; + +import org.elasticsearch.action.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 { + + private String realmName; + + public String getRealmName() { + return realmName; + } + + public void setRealmName(String realmName) { + this.realmName = realmName; + } + + public OpenIdConnectPrepareAuthenticationRequest() { + } + + public OpenIdConnectPrepareAuthenticationRequest(StreamInput in) throws IOException { + super.readFrom(in); + realmName = in.readString(); + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (Strings.hasText(realmName) == false) { + validationException = addValidationError("realm name must be provided", null); + } + return validationException; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(realmName); + } + + @Override + public void readFrom(StreamInput in) { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + public String toString() { + return "{realmName=" + realmName + "}"; + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationRequestBuilder.java new file mode 100644 index 0000000000000..b7992345a105a --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationRequestBuilder.java @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.oidc; + +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.client.ElasticsearchClient; + +/** + * Request builder for populating a {@link OpenIdConnectPrepareAuthenticationRequest} + */ +public class OpenIdConnectPrepareAuthenticationRequestBuilder + extends ActionRequestBuilder { + + public OpenIdConnectPrepareAuthenticationRequestBuilder(ElasticsearchClient client) { + super(client, OpenIdConnectPrepareAuthenticationAction.INSTANCE, new OpenIdConnectPrepareAuthenticationRequest()); + } + + public OpenIdConnectPrepareAuthenticationRequestBuilder realmName(String name) { + request.setRealmName(name); + return this; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationResponse.java new file mode 100644 index 0000000000000..cf8bce6896882 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectPrepareAuthenticationResponse.java @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.oidc; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +/** + * A response object that contains the OpenID Connect Authentication Request as a URL and the state and nonce values that were + * generated for this request. + */ +public class OpenIdConnectPrepareAuthenticationResponse extends ActionResponse implements ToXContentObject { + + private String authenticationRequestUrl; + /* + * The oAuth2 state parameter used for CSRF protection. + */ + private String state; + /* + * String value used to associate a Client session with an ID Token, and to mitigate replay attacks. + */ + private String nonce; + + public OpenIdConnectPrepareAuthenticationResponse(String authorizationEndpointUrl, String state, String nonce) { + this.authenticationRequestUrl = authorizationEndpointUrl; + this.state = state; + this.nonce = nonce; + } + + public OpenIdConnectPrepareAuthenticationResponse(StreamInput in) throws IOException { + super.readFrom(in); + authenticationRequestUrl = in.readString(); + state = in.readString(); + nonce = in.readString(); + } + + public String getAuthenticationRequestUrl() { + return authenticationRequestUrl; + } + + public String getState() { + return state; + } + + public String getNonce() { + return nonce; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(authenticationRequestUrl); + out.writeString(state); + out.writeString(nonce); + } + + public String toString() { + return "{authenticationRequestUrl=" + authenticationRequestUrl + ", state=" + state + ", nonce=" + nonce + "}"; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("authentication_request_url", authenticationRequestUrl); + builder.field("state", state); + builder.field("nonce", nonce); + builder.endObject(); + return builder; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/InternalRealmsSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/InternalRealmsSettings.java index 8b2ef18406830..dd4a843345298 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/InternalRealmsSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/InternalRealmsSettings.java @@ -10,6 +10,7 @@ import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings; +import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings; import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; @@ -34,6 +35,7 @@ public static Set> getSettings() { set.addAll(PkiRealmSettings.getSettings()); set.addAll(SamlRealmSettings.getSettings()); set.addAll(KerberosRealmSettings.getSettings()); + set.addAll(OpenIdConnectRealmSettings.getSettings()); return Collections.unmodifiableSet(set); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmSettings.java index 913fcba3d33c8..0c35525f1debb 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmSettings.java @@ -6,6 +6,8 @@ package org.elasticsearch.xpack.core.security.authc; import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.SecureSetting; +import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; @@ -55,6 +57,17 @@ public static Setting.AffixSetting simpleString(String realmType, String return Setting.affixKeySetting(realmSettingPrefix(realmType), suffix, key -> Setting.simpleString(key, properties)); } + /** + * Create a {@link SecureSetting#secureString secure string} {@link Setting} object of a realm of + * with the provided type and setting suffix. + * + * @param realmType The type of the realm, used within the setting prefix + * @param suffix The suffix of the setting (everything following the realm name in the affix setting) + */ + public static Setting.AffixSetting secureString(String realmType, String suffix) { + return Setting.affixKeySetting(realmSettingPrefix(realmType), suffix, key -> SecureSetting.secureString(key, null)); + } + /** * Create a {@link Function} that acts as a factory an {@link org.elasticsearch.common.settings.Setting.AffixSetting}. * The {@code Function} takes the realm-type as an argument. diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/oidc/OpenIdConnectRealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/oidc/OpenIdConnectRealmSettings.java new file mode 100644 index 0000000000000..5d51d23c3c69a --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/oidc/OpenIdConnectRealmSettings.java @@ -0,0 +1,57 @@ +/* + * 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.util.set.Sets; +import org.elasticsearch.xpack.core.security.authc.RealmSettings; +import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + + +public class OpenIdConnectRealmSettings { + + private OpenIdConnectRealmSettings() { + } + + public static final String TYPE = "oidc"; + + public static final Setting.AffixSetting OP_NAME + = RealmSettings.simpleString(TYPE, "op.name", Setting.Property.NodeScope); + public static final Setting.AffixSetting RP_CLIENT_ID + = RealmSettings.simpleString(TYPE, "rp.client_id", Setting.Property.NodeScope); + public static final Setting.AffixSetting RP_CLIENT_SECRET + = RealmSettings.secureString(TYPE, "rp.client_secret"); + public static final Setting.AffixSetting RP_REDIRECT_URI + = RealmSettings.simpleString(TYPE, "rp.redirect_uri", Setting.Property.NodeScope); + public static final Setting.AffixSetting RP_RESPONSE_TYPE + = RealmSettings.simpleString(TYPE, "rp.response_type", Setting.Property.NodeScope); + public static final Setting.AffixSetting OP_AUTHORIZATION_ENDPOINT + = RealmSettings.simpleString(TYPE, "op.authorization_endpoint", Setting.Property.NodeScope); + public static final Setting.AffixSetting OP_TOKEN_ENDPOINT + = RealmSettings.simpleString(TYPE, "op.token_endpoint", Setting.Property.NodeScope); + public static final Setting.AffixSetting OP_USERINFO_ENDPOINT + = RealmSettings.simpleString(TYPE, "op.userinfo_endpoint", Setting.Property.NodeScope); + public static final Setting.AffixSetting OP_ISSUER + = RealmSettings.simpleString(TYPE, "op.issuer", Setting.Property.NodeScope); + public static final Setting.AffixSetting> RP_REQUESTED_SCOPES = Setting.affixKeySetting( + RealmSettings.realmSettingPrefix(TYPE), "rp.requested_scopes", + key -> Setting.listSetting(key, Collections.singletonList("openid"), Function.identity(), Setting.Property.NodeScope)); + + public static Set> getSettings() { + final Set> set = Sets.newHashSet( + OP_NAME, RP_CLIENT_ID, RP_REDIRECT_URI, RP_RESPONSE_TYPE, RP_REQUESTED_SCOPES, RP_CLIENT_SECRET, + OP_AUTHORIZATION_ENDPOINT, OP_TOKEN_ENDPOINT, OP_USERINFO_ENDPOINT, OP_ISSUER); + set.addAll(DelegatedAuthorizationSettings.getSettings(TYPE)); + set.addAll(RealmSettings.getStandardSettings(TYPE)); + return set; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java index fba595e7a09e4..76e621bf5b6a2 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java @@ -33,6 +33,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/*"); @@ -70,6 +71,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); @@ -94,6 +96,7 @@ public final class ClusterPrivilege extends Privilege { .put("transport_client", TRANSPORT_CLIENT) .put("manage_security", MANAGE_SECURITY) .put("manage_saml", MANAGE_SAML) + .put("manage_oidc", MANAGE_OIDC) .put("manage_pipeline", MANAGE_PIPELINE) .put("manage_rollup", MANAGE_ROLLUP) .put("manage_ccr", MANAGE_CCR) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 714da7cf11c35..f83e31b3a01d7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -84,6 +84,8 @@ import org.elasticsearch.xpack.core.security.SecurityExtension; import org.elasticsearch.xpack.core.security.SecurityField; import org.elasticsearch.xpack.core.security.SecuritySettings; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateAction; +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; @@ -143,6 +145,8 @@ import org.elasticsearch.xpack.security.action.interceptor.ResizeRequestInterceptor; import org.elasticsearch.xpack.security.action.interceptor.SearchRequestInterceptor; import org.elasticsearch.xpack.security.action.interceptor.UpdateRequestInterceptor; +import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectAuthenticateAction; +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; @@ -204,6 +208,8 @@ import org.elasticsearch.xpack.security.rest.action.rolemapping.RestDeleteRoleMappingAction; import org.elasticsearch.xpack.security.rest.action.rolemapping.RestGetRoleMappingsAction; import org.elasticsearch.xpack.security.rest.action.rolemapping.RestPutRoleMappingAction; +import org.elasticsearch.xpack.security.rest.action.oidc.RestOpenIdConnectAuthenticateAction; +import org.elasticsearch.xpack.security.rest.action.oidc.RestOpenIdConnectPrepareAuthenticationAction; import org.elasticsearch.xpack.security.rest.action.saml.RestSamlAuthenticateAction; import org.elasticsearch.xpack.security.rest.action.saml.RestSamlInvalidateSessionAction; import org.elasticsearch.xpack.security.rest.action.saml.RestSamlLogoutAction; @@ -731,6 +737,9 @@ public void onIndexModule(IndexModule module) { new ActionHandler<>(SamlAuthenticateAction.INSTANCE, TransportSamlAuthenticateAction.class), new ActionHandler<>(SamlLogoutAction.INSTANCE, TransportSamlLogoutAction.class), new ActionHandler<>(SamlInvalidateSessionAction.INSTANCE, TransportSamlInvalidateSessionAction.class), + new ActionHandler<>(OpenIdConnectPrepareAuthenticationAction.INSTANCE, + TransportOpenIdConnectPrepareAuthenticationAction.class), + new ActionHandler<>(OpenIdConnectAuthenticateAction.INSTANCE, TransportOpenIdConnectAuthenticateAction.class), new ActionHandler<>(GetPrivilegesAction.INSTANCE, TransportGetPrivilegesAction.class), new ActionHandler<>(PutPrivilegesAction.INSTANCE, TransportPutPrivilegesAction.class), new ActionHandler<>(DeletePrivilegesAction.INSTANCE, TransportDeletePrivilegesAction.class) @@ -780,6 +789,8 @@ public List getRestHandlers(Settings settings, RestController restC new RestSamlAuthenticateAction(settings, restController, getLicenseState()), new RestSamlLogoutAction(settings, restController, getLicenseState()), new RestSamlInvalidateSessionAction(settings, restController, getLicenseState()), + new RestOpenIdConnectPrepareAuthenticationAction(settings, restController, getLicenseState()), + new RestOpenIdConnectAuthenticateAction(settings, restController, getLicenseState()), new RestGetPrivilegesAction(settings, restController, getLicenseState()), new RestPutPrivilegesAction(settings, restController, getLicenseState()), new RestDeletePrivilegesAction(settings, restController, getLicenseState()) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectAuthenticateAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectAuthenticateAction.java new file mode 100644 index 0000000000000..4f58fa7c6f72c --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectAuthenticateAction.java @@ -0,0 +1,80 @@ +/* + * 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.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateRequest; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateResponse; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateAction; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.security.authc.AuthenticationService; +import org.elasticsearch.xpack.security.authc.TokenService; +import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm; +import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectToken; + +import java.util.Map; + +public class TransportOpenIdConnectAuthenticateAction + extends HandledTransportAction { + + private final ThreadPool threadPool; + private final AuthenticationService authenticationService; + private final TokenService tokenService; + + @Inject + public TransportOpenIdConnectAuthenticateAction(ThreadPool threadPool, TransportService transportService, + ActionFilters actionFilters, AuthenticationService authenticationService, + TokenService tokenService) { + super(OpenIdConnectAuthenticateAction.NAME, transportService, actionFilters, + (Writeable.Reader) OpenIdConnectAuthenticateRequest::new); + this.threadPool = threadPool; + this.authenticationService = authenticationService; + this.tokenService = tokenService; + } + + @Override + protected void doExecute(Task task, OpenIdConnectAuthenticateRequest request, + ActionListener listener) { + final OpenIdConnectToken token = new OpenIdConnectToken(request.getRedirectUri(), request.getState(), request.getNonce()); + final ThreadContext threadContext = threadPool.getThreadContext(); + Authentication originatingAuthentication = Authentication.getAuthentication(threadContext); + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { + authenticationService.authenticate(OpenIdConnectAuthenticateAction.NAME, request, token, ActionListener.wrap( + authentication -> { + AuthenticationResult result = threadContext.getTransient(AuthenticationResult.THREAD_CONTEXT_KEY); + if (result == null) { + listener.onFailure(new IllegalStateException("Cannot find AuthenticationResult on thread context")); + return; + } + @SuppressWarnings("unchecked") final Map tokenMetadata = (Map) result.getMetadata() + .get(OpenIdConnectRealm.CONTEXT_TOKEN_DATA); + tokenService.createUserToken(authentication, originatingAuthentication, + ActionListener.wrap(tuple -> { + final String tokenString = tokenService.getUserTokenString(tuple.v1()); + final TimeValue expiresIn = tokenService.getExpirationDelay(); + listener.onResponse(new OpenIdConnectAuthenticateResponse(authentication.getUser().principal(), tokenString, + tuple.v2(), expiresIn)); + }, listener::onFailure), tokenMetadata, true); + }, e -> { + logger.debug(() -> new ParameterizedMessage("OpenIDConnectToken [{}] could not be authenticated", token), e); + listener.onFailure(e); + } + )); + } + } +} + diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectPrepareAuthenticationAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectPrepareAuthenticationAction.java new file mode 100644 index 0000000000000..5d3930c791982 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectPrepareAuthenticationAction.java @@ -0,0 +1,59 @@ +/* + * 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.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; + + +public class TransportOpenIdConnectPrepareAuthenticationAction extends HandledTransportAction { + + private final Realms realms; + + @Inject + public TransportOpenIdConnectPrepareAuthenticationAction(TransportService transportService, + ActionFilters actionFilters, Realms realms) { + super(OpenIdConnectPrepareAuthenticationAction.NAME, transportService, actionFilters, + (Writeable.Reader) OpenIdConnectPrepareAuthenticationRequest::new); + this.realms = realms; + } + + @Override + protected void doExecute(Task task, OpenIdConnectPrepareAuthenticationRequest request, + ActionListener listener) { + final Realm realm = this.realms.realm(request.getRealmName()); + if (null == realm || realm instanceof OpenIdConnectRealm == false) { + listener.onFailure( + new ElasticsearchSecurityException("Cannot find OpenID Connect realm with name [{}]", request.getRealmName())); + } else { + prepareAuthenticationResponse((OpenIdConnectRealm) realm, listener); + } + } + + private void prepareAuthenticationResponse(OpenIdConnectRealm realm, + ActionListener listener) { + try { + final OpenIdConnectPrepareAuthenticationResponse authenticationResponse = realm.buildAuthenticationRequestUri(); + listener.onResponse(authenticationResponse); + } catch (ElasticsearchException e) { + listener.onFailure(e); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java index 54bffd8a21566..6d1087e1b95ee 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java @@ -18,6 +18,7 @@ import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings; +import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings; import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; import org.elasticsearch.xpack.core.ssl.SSLService; @@ -27,6 +28,7 @@ import org.elasticsearch.xpack.security.authc.file.FileRealm; import org.elasticsearch.xpack.security.authc.kerberos.KerberosRealm; import org.elasticsearch.xpack.security.authc.ldap.LdapRealm; +import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm; import org.elasticsearch.xpack.security.authc.pki.PkiRealm; import org.elasticsearch.xpack.security.authc.saml.SamlRealm; import org.elasticsearch.xpack.security.authc.support.RoleMappingFileBootstrapCheck; @@ -111,6 +113,7 @@ public static Map getFactories(ThreadPool threadPool, Res 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)); return Collections.unmodifiableMap(map); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectProviderConfiguration.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectProviderConfiguration.java new file mode 100644 index 0000000000000..0bfab29e626f2 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectProviderConfiguration.java @@ -0,0 +1,50 @@ +/* + * 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.Nullable; + +import java.util.Objects; + +/** + * A Class that contains all the OpenID Connect Provider configuration + */ +public class OpenIdConnectProviderConfiguration { + private final String providerName; + private final String authorizationEndpoint; + private final String tokenEndpoint; + private final String userinfoEndpoint; + private final String issuer; + + public OpenIdConnectProviderConfiguration(String providerName, String issuer, String authorizationEndpoint, + @Nullable String tokenEndpoint, @Nullable String userinfoEndpoint) { + this.providerName = Objects.requireNonNull(providerName, "OP Name must be provided"); + this.authorizationEndpoint = Objects.requireNonNull(authorizationEndpoint, "Authorization Endpoint must be provided"); + this.tokenEndpoint = tokenEndpoint; + this.userinfoEndpoint = userinfoEndpoint; + this.issuer = Objects.requireNonNull(issuer, "OP Issuer must be provided"); + } + + public String getProviderName() { + return providerName; + } + + public String getAuthorizationEndpoint() { + return authorizationEndpoint; + } + + public String getTokenEndpoint() { + return tokenEndpoint; + } + + public String getUserinfoEndpoint() { + return userinfoEndpoint; + } + + public String getIssuer() { + return issuer; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java new file mode 100644 index 0000000000000..0e6c35456cf9a --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java @@ -0,0 +1,154 @@ +/* + * 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.ElasticsearchException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.SettingsException; +import org.elasticsearch.common.util.concurrent.ThreadContext; +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.user.User; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.List; + +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT; +import static org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings.OP_ISSUER; +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.RP_CLIENT_ID; +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; + +public class OpenIdConnectRealm extends Realm { + + public static final String CONTEXT_TOKEN_DATA = "_oidc_tokendata"; + private static final SecureRandom RANDOM_INSTANCE = new SecureRandom(); + private final OpenIdConnectProviderConfiguration opConfiguration; + private final RelyingPartyConfiguration rpConfiguration; + + public OpenIdConnectRealm(RealmConfig config) { + super(config); + this.rpConfiguration = buildRelyingPartyConfiguration(config); + this.opConfiguration = buildOpenIdConnectProviderConfiguration(config); + } + + @Override + public boolean supports(AuthenticationToken token) { + return false; + } + + @Override + public AuthenticationToken token(ThreadContext context) { + return null; + } + + @Override + public void authenticate(AuthenticationToken token, ActionListener listener) { + + } + + @Override + public void lookupUser(String username, ActionListener listener) { + + } + + private RelyingPartyConfiguration buildRelyingPartyConfiguration(RealmConfig config) { + String redirectUri = require(config, RP_REDIRECT_URI); + String clientId = require(config, RP_CLIENT_ID); + String responseType = require(config, RP_RESPONSE_TYPE); + if (responseType.equals("id_token") == false && responseType.equals("code") == false) { + throw new SettingsException("The configuration setting [" + RealmSettings.getFullSettingKey(config, RP_RESPONSE_TYPE) + + "] value can only be code or id_token"); + } + List requestedScopes = config.getSetting(RP_REQUESTED_SCOPES); + + return new RelyingPartyConfiguration(clientId, redirectUri, responseType, requestedScopes); + } + + private OpenIdConnectProviderConfiguration buildOpenIdConnectProviderConfiguration(RealmConfig config) { + String providerName = require(config, OP_NAME); + String authorizationEndpoint = require(config, OP_AUTHORIZATION_ENDPOINT); + String issuer = require(config, OP_ISSUER); + String tokenEndpoint = config.getSetting(OP_TOKEN_ENDPOINT, () -> null); + String userinfoEndpoint = config.getSetting(OP_USERINFO_ENDPOINT, () -> null); + + return new OpenIdConnectProviderConfiguration(providerName, issuer, authorizationEndpoint, tokenEndpoint, userinfoEndpoint); + } + + static String require(RealmConfig config, Setting.AffixSetting setting) { + final String value = config.getSetting(setting); + if (value.isEmpty()) { + throw new SettingsException("The configuration setting [" + RealmSettings.getFullSettingKey(config, setting) + + "] is required"); + } + return value; + } + + /** + * Creates the URI for an OIDC Authentication Request from the realm configuration using URI Query String Serialization and + * generates a state parameter and a nonce. It then returns the URI, state and nonce encapsulated in a + * {@link OpenIdConnectPrepareAuthenticationResponse} + * + * @return an {@link OpenIdConnectPrepareAuthenticationResponse} + */ + public OpenIdConnectPrepareAuthenticationResponse buildAuthenticationRequestUri() throws ElasticsearchException { + try { + final String state = createNonceValue(); + final String nonce = createNonceValue(); + StringBuilder builder = new StringBuilder(); + builder.append(opConfiguration.getAuthorizationEndpoint()); + addParameter(builder, "response_type", rpConfiguration.getResponseType(), true); + addParameter(builder, "scope", Strings.collectionToDelimitedString(rpConfiguration.getRequestedScopes(), " ")); + addParameter(builder, "client_id", rpConfiguration.getClientId()); + addParameter(builder, "state", state); + if (Strings.hasText(nonce)) { + addParameter(builder, "nonce", nonce); + } + addParameter(builder, "redirect_uri", rpConfiguration.getRedirectUri()); + return new OpenIdConnectPrepareAuthenticationResponse(builder.toString(), state, nonce); + } catch (UnsupportedEncodingException e) { + throw new ElasticsearchException("Cannot build OpenID Connect Authentication Request", e); + } + } + + private void addParameter(StringBuilder builder, String parameter, String value, boolean isFirstParameter) + throws UnsupportedEncodingException { + char prefix = isFirstParameter ? '?' : '&'; + builder.append(prefix).append(parameter).append("="); + builder.append(URLEncoder.encode(value, StandardCharsets.UTF_8.name())); + } + + private void addParameter(StringBuilder builder, String parameter, String value) throws UnsupportedEncodingException { + addParameter(builder, parameter, value, false); + } + + /** + * Creates a cryptographically secure alphanumeric string to be used as a nonce or state. It adheres to the + * specification's requirements by using 180 bits for the random value. + * The random string is encoded in a URL safe manner. + * + * @return an alphanumeric string + */ + private static String createNonceValue() { + final byte[] randomBytes = new byte[20]; + RANDOM_INSTANCE.nextBytes(randomBytes); + return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectToken.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectToken.java new file mode 100644 index 0000000000000..9fa04090fec63 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectToken.java @@ -0,0 +1,63 @@ +/* + * 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.Nullable; +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 redirectUri; + private String state; + @Nullable + private String nonce; + + /** + * @param redirectUri 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. + * @param state The state value that we generated for this specific flow and should be stored at the user's session with the + * facilitator. + * @param nonce The nonce value that we generated for this specific flow and should be stored at the user's session with the + * facilitator. + */ + public OpenIdConnectToken(String redirectUri, String state, String nonce) { + this.redirectUri = redirectUri; + this.state = state; + this.nonce = nonce; + } + + @Override + public String principal() { + return ""; + } + + @Override + public Object credentials() { + return redirectUri; + } + + @Override + public void clearCredentials() { + this.redirectUri = null; + } + + public String getState() { + return state; + } + + public String getNonce() { + return nonce; + } + + public String toString() { + return getClass().getSimpleName() + "{ redirectUri=" + redirectUri + ", state=" + state + ", nonce=" + nonce + "}"; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/RelyingPartyConfiguration.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/RelyingPartyConfiguration.java new file mode 100644 index 0000000000000..516f787d0efeb --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/RelyingPartyConfiguration.java @@ -0,0 +1,42 @@ +/* + * 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 java.util.List; +import java.util.Objects; + +/** + * A Class that contains all the OpenID Connect Relying Party configuration + */ +public class RelyingPartyConfiguration { + private final String clientId; + private final String redirectUri; + private final String responseType; + private final List requestedScopes; + + public RelyingPartyConfiguration(String clientId, String redirectUri, String responseType, List requestedScopes) { + this.clientId = Objects.requireNonNull(clientId, "clientId must be provided"); + this.redirectUri = Objects.requireNonNull(redirectUri, "redirectUri must be provided"); + this.responseType = Objects.requireNonNull(responseType, "responseType must be provided"); + this.requestedScopes = Objects.requireNonNull(requestedScopes, "responseType must be provided"); + } + + public String getClientId() { + return clientId; + } + + public String getRedirectUri() { + return redirectUri; + } + + public String getResponseType() { + return responseType; + } + + public List getRequestedScopes() { + return requestedScopes; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/OpenIdConnectBaseRestHandler.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/OpenIdConnectBaseRestHandler.java new file mode 100644 index 0000000000000..008b5d0676e2c --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/OpenIdConnectBaseRestHandler.java @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.rest.action.oidc; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.LicenseUtils; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings; +import org.elasticsearch.xpack.security.authc.Realms; +import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; + +public abstract class OpenIdConnectBaseRestHandler extends SecurityBaseRestHandler { + + private static final String OIDC_REALM_TYPE = OpenIdConnectRealmSettings.TYPE; + + /** + * @param settings the node's settings + * @param licenseState the license state that will be used to determine if security is licensed + */ + protected OpenIdConnectBaseRestHandler(Settings settings, XPackLicenseState licenseState) { + super(settings, licenseState); + } + + @Override + protected Exception checkFeatureAvailable(RestRequest request) { + Exception failedFeature = super.checkFeatureAvailable(request); + if (failedFeature != null) { + return failedFeature; + } else if (Realms.isRealmTypeAvailable(licenseState.allowedRealmType(), OIDC_REALM_TYPE)) { + return null; + } else { + logger.info("The '{}' realm is not available under the current license", OIDC_REALM_TYPE); + return LicenseUtils.newComplianceException(OIDC_REALM_TYPE); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectAuthenticateAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectAuthenticateAction.java new file mode 100644 index 0000000000000..d7130da64530a --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectAuthenticateAction.java @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.rest.action.oidc; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestBuilderListener; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateAction; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateRequest; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateResponse; + +import java.io.IOException; + +import static org.elasticsearch.rest.RestRequest.Method.POST; + +/** + * Rest handler that authenticates the user based on the information provided as parameters of the redirect_uri + */ +public class RestOpenIdConnectAuthenticateAction extends OpenIdConnectBaseRestHandler { + + static final ObjectParser PARSER = new ObjectParser<>("oidc_authn", + OpenIdConnectAuthenticateRequest::new); + + static { + PARSER.declareString(OpenIdConnectAuthenticateRequest::setRedirectUri, new ParseField("redirect_uri")); + PARSER.declareString(OpenIdConnectAuthenticateRequest::setState, new ParseField("state")); + PARSER.declareString(OpenIdConnectAuthenticateRequest::setNonce, new ParseField("nonce")); + } + + public RestOpenIdConnectAuthenticateAction(Settings settings, RestController controller, XPackLicenseState licenseState) { + super(settings, licenseState); + controller.registerHandler(POST, "/_security/oidc/authenticate", this); + } + + @Override + protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + try (XContentParser parser = request.contentParser()) { + final OpenIdConnectAuthenticateRequest authenticateRequest = PARSER.parse(parser, null); + logger.trace("OIDC Authenticate: " + authenticateRequest); + return channel -> client.execute(OpenIdConnectAuthenticateAction.INSTANCE, authenticateRequest, + new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(OpenIdConnectAuthenticateResponse response, XContentBuilder builder) + throws Exception { + builder.startObject(); + builder.startObject() + .field("username", response.getPrincipal()) + .field("access_token", response.getAccessTokenString()) + .field("refresh_token", response.getRefreshTokenString()) + .field("expires_in", response.getExpiresIn().seconds()) + .endObject(); + return new BytesRestResponse(RestStatus.OK, builder); + } + }); + } + } + + @Override + public String getName() { + return "security_oidc_authenticate_action"; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectPrepareAuthenticationAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectPrepareAuthenticationAction.java new file mode 100644 index 0000000000000..a8775271a879a --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectPrepareAuthenticationAction.java @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.rest.action.oidc; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestBuilderListener; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationAction; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationRequest; +import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationResponse; + +import java.io.IOException; + +import static org.elasticsearch.rest.RestRequest.Method.POST; + +/** + * Generates an oAuth 2.0 authentication request as a URL string and returns it to the REST client. + */ +public class RestOpenIdConnectPrepareAuthenticationAction extends OpenIdConnectBaseRestHandler { + + static final ObjectParser PARSER = new ObjectParser<>("oidc_prepare_authentication", + OpenIdConnectPrepareAuthenticationRequest::new); + + static { + PARSER.declareString(OpenIdConnectPrepareAuthenticationRequest::setRealmName, new ParseField("realm")); + } + + public RestOpenIdConnectPrepareAuthenticationAction(Settings settings, RestController controller, XPackLicenseState licenseState) { + super(settings, licenseState); + controller.registerHandler(POST, "/_security/oidc/prepare", this); + } + + @Override + protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + try (XContentParser parser = request.contentParser()) { + final OpenIdConnectPrepareAuthenticationRequest prepareAuthenticationRequest = PARSER.parse(parser, null); + logger.trace("OIDC Prepare Authentication: " + prepareAuthenticationRequest); + return channel -> client.execute(OpenIdConnectPrepareAuthenticationAction.INSTANCE, prepareAuthenticationRequest, + new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(OpenIdConnectPrepareAuthenticationResponse response, XContentBuilder builder) + throws Exception { + logger.trace("OIDC Prepare Authentication Response: " + response); + return new BytesRestResponse(RestStatus.OK, response.toXContent(builder, request)); + } + }); + } + } + + @Override + public String getName() { + return "security_oidc_prepare_authentication_action"; + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/OpenIdConnectAuthenticateRequestTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/OpenIdConnectAuthenticateRequestTests.java new file mode 100644 index 0000000000000..fe3e5c5dcc5f1 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/OpenIdConnectAuthenticateRequestTests.java @@ -0,0 +1,35 @@ +/* + * 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.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.equalTo; + +public class OpenIdConnectAuthenticateRequestTests extends ESTestCase { + + public void testSerialization() throws IOException { + final OpenIdConnectAuthenticateRequest request = new OpenIdConnectAuthenticateRequest(); + final String nonce = randomBoolean() ? null : 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)); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/OpenIdConnectPrepareAuthenticationRequestTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/OpenIdConnectPrepareAuthenticationRequestTests.java new file mode 100644 index 0000000000000..bfff933e2c7ad --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/OpenIdConnectPrepareAuthenticationRequestTests.java @@ -0,0 +1,38 @@ +/* + * 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 unserialized = + new OpenIdConnectPrepareAuthenticationRequest(out.bytes().streamInput()); + assertThat(unserialized.getRealmName(), equalTo("oidc-realm1")); + } + + 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("realm name must be provided")); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/InternalRealmsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/InternalRealmsTests.java index f9007583c2ca1..e3298e5103772 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/InternalRealmsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/InternalRealmsTests.java @@ -18,6 +18,7 @@ import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings; +import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings; import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; import org.elasticsearch.xpack.core.ssl.SSLService; @@ -61,7 +62,7 @@ public void testIsStandardType() { String type = randomFrom(NativeRealmSettings.TYPE, FileRealmSettings.TYPE, LdapRealmSettings.AD_TYPE, LdapRealmSettings.LDAP_TYPE, PkiRealmSettings.TYPE); assertThat(InternalRealms.isStandardRealm(type), is(true)); - type = randomFrom(SamlRealmSettings.TYPE, KerberosRealmSettings.TYPE); + type = randomFrom(SamlRealmSettings.TYPE, KerberosRealmSettings.TYPE, OpenIdConnectRealmSettings.TYPE); assertThat(InternalRealms.isStandardRealm(type), is(false)); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmTests.java new file mode 100644 index 0000000000000..1b8cbea8dde53 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmTests.java @@ -0,0 +1,167 @@ +/* + * 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.action.oidc.OpenIdConnectPrepareAuthenticationResponse; +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; +import static org.hamcrest.Matchers.equalTo; + +public class OpenIdConnectRealmTests extends ESTestCase { + + private static final String REALM_NAME = "oidc1-realm"; + 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 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_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .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"); + SettingsException exception = expectThrows(SettingsException.class, () -> { + new OpenIdConnectRealm(buildConfig(settingsBuilder.build())); + }); + assertThat(exception.getMessage(), Matchers.containsString("value can only be code or id_token")); + } + + 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.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())); + }); + assertThat(exception.getMessage(), + Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT))); + } + + 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_NAME), "the op") + .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())); + }); + 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_ISSUER), "https://op.example.com") + .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())); + }); + 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_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .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())); + }); + 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_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .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())); + }); + assertThat(exception.getMessage(), + Matchers.containsString(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID))); + } + + public void testBuilidingAuthenticationRequest() { + 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.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())); + final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(); + final String state = response.getState(); + final String nonce = response.getNonce(); + assertThat(response.getAuthenticationRequestUrl(), + equalTo("https://op.example.com/login?response_type=code&scope=openid+scope1+scope2&client_id=rp-my&state=" + state + + "&nonce=" + nonce + "&redirect_uri=https%3A%2F%2Frp.my.com%2Fcb")); + } + + + 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_ISSUER), "https://op.example.com") + .put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op") + .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())); + final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(); + final String state = response.getState(); + final String nonce = response.getNonce(); + assertThat(response.getAuthenticationRequestUrl(), equalTo("https://op.example.com/login?response_type=code&scope=openid" + + "&client_id=rp-my&state=" + state + "&nonce=" + nonce + "&redirect_uri=https%3A%2F%2Frp.my.com%2Fcb")); + } + + 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); + } +}