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
index 88e9e82313dcb..8f6d616981b39 100644
--- 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
@@ -20,7 +20,17 @@
*/
public class OpenIdConnectPrepareAuthenticationRequest extends ActionRequest {
+ /**
+ * The name of the OpenID Connect realm in the configuration that should be used for authentication
+ */
private String realmName;
+ /**
+ * In case of a
+ * 3rd party initiated authentication, the
+ * issuer that the User Agent needs to be redirected to for authentication
+ */
+ private String issuer;
+ private String loginHint;
private String state;
private String nonce;
@@ -36,10 +46,22 @@ 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;
}
@@ -48,12 +70,18 @@ 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.readString();
+ realmName = in.readOptionalString();
+ issuer = in.readOptionalString();
+ loginHint = in.readOptionalString();
state = in.readOptionalString();
nonce = in.readOptionalString();
}
@@ -61,8 +89,11 @@ public OpenIdConnectPrepareAuthenticationRequest(StreamInput in) throws IOExcept
@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
- if (Strings.hasText(realmName) == false) {
- validationException = addValidationError("realm name must be provided", 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;
}
@@ -70,7 +101,9 @@ public ActionRequestValidationException validate() {
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
- out.writeString(realmName);
+ out.writeOptionalString(realmName);
+ out.writeOptionalString(issuer);
+ out.writeOptionalString(loginHint);
out.writeOptionalString(state);
out.writeOptionalString(nonce);
}
@@ -81,7 +114,8 @@ public void readFrom(StreamInput in) {
}
public String toString() {
- return "{realmName=" + realmName + ", state=" + state + ", nonce=" + nonce + "}";
+ return "{realmName=" + realmName + ", issuer=" + issuer + ", login_hint=" +
+ loginHint + ", state=" + state + ", nonce=" + nonce + "}";
}
}
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
index 47a75359b4e07..f1d3557f788a0 100644
--- 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
@@ -10,6 +10,7 @@
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;
@@ -21,6 +22,8 @@
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 {
@@ -38,19 +41,40 @@ public TransportOpenIdConnectPrepareAuthenticationAction(TransportService transp
@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) {
+ Realm realm = null;
+ if (Strings.hasText(request.getIssuer())) {
+ List matchingRealms = this.realms.stream()
+ .filter(r -> r instanceof OpenIdConnectRealm && ((OpenIdConnectRealm) r).isIssuerValid(request.getIssuer()))
+ .map(r -> (OpenIdConnectRealm) r)
+ .filter(r -> r.isIssuerValid(request.getIssuer()))
+ .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()));
- } else {
- prepareAuthenticationResponse((OpenIdConnectRealm) realm, request.getState(), request.getNonce(), listener);
}
}
- private void prepareAuthenticationResponse(OpenIdConnectRealm realm, String state, String nonce,
+ private void prepareAuthenticationResponse(OpenIdConnectRealm realm, String state, String nonce, String loginHint,
ActionListener listener) {
try {
- final OpenIdConnectPrepareAuthenticationResponse authenticationResponse = realm.buildAuthenticationRequestUri(state, nonce);
+ final OpenIdConnectPrepareAuthenticationResponse authenticationResponse =
+ realm.buildAuthenticationRequestUri(state, nonce, loginHint);
listener.onResponse(authenticationResponse);
} catch (ElasticsearchException e) {
listener.onFailure(e);
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java
index 6fec22f73afd5..6cf7f0a568b50 100644
--- 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
@@ -294,26 +294,33 @@ private static String require(RealmConfig config, Setting.AffixSetting s
*
* @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 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 authenticationRequest = new AuthenticationRequest(
- opConfiguration.getAuthorizationEndpoint(),
- rpConfiguration.getResponseType(),
+ final AuthenticationRequest.Builder builder = new AuthenticationRequest.Builder(rpConfiguration.getResponseType(),
rpConfiguration.getRequestedScope(),
rpConfiguration.getClientId(),
- rpConfiguration.getRedirectUri(),
- state,
- nonce);
-
- return new OpenIdConnectPrepareAuthenticationResponse(authenticationRequest.toURI().toString(),
+ 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);
+ }
+
@Override
public void close() {
openIdConnectAuthenticator.close();
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
index a813a772a8e82..60786c82b56ef 100644
--- 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
@@ -36,6 +36,8 @@ public class RestOpenIdConnectPrepareAuthenticationAction extends OpenIdConnectB
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"));
}
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
index a87ae85bfae56..e668008deb901 100644
--- 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
@@ -26,6 +26,15 @@ public void testSerialization() throws IOException {
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 {
@@ -50,6 +59,15 @@ public void testValidation() {
final ActionRequestValidationException validation = request.validate();
assertNotNull(validation);
assertThat(validation.validationErrors().size(), equalTo(1));
- assertThat(validation.validationErrors().get(0), containsString("realm name must be provided"));
+ assertThat(validation.validationErrors().get(0), containsString("one of [realm, issuer] must be provided"));
+
+ final OpenIdConnectPrepareAuthenticationRequest request2 = new OpenIdConnectPrepareAuthenticationRequest();
+ request2.setRealmName("oidc-realm1");
+ request2.setIssuer("https://op.company.org/");
+ final ActionRequestValidationException validation2 = request2.validate();
+ assertNotNull(validation2);
+ assertThat(validation2.validationErrors().size(), equalTo(1));
+ assertThat(validation2.validationErrors().get(0),
+ containsString("only one of [realm, issuer] can be provided in the same request"));
}
}
diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmTests.java
index 02b5820ee6219..fc3a26780b856 100644
--- 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
@@ -162,7 +162,7 @@ public void testBuildRelyingPartyConfigWithoutOpenIdScope() {
Arrays.asList("scope1", "scope2"));
final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null,
null);
- final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(null, null);
+ final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(null, null, null);
final String state = response.getState();
final String nonce = response.getNonce();
assertThat(response.getAuthenticationRequestUrl(),
@@ -170,7 +170,7 @@ public void testBuildRelyingPartyConfigWithoutOpenIdScope() {
"&redirect_uri=https%3A%2F%2Frp.my.com%2Fcb&state=" + state + "&nonce=" + nonce + "&client_id=rp-my"));
}
- public void testBuilidingAuthenticationRequest() {
+ 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")
@@ -185,7 +185,7 @@ public void testBuilidingAuthenticationRequest() {
Arrays.asList("openid", "scope1", "scope2"));
final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null,
null);
- final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(null, null);
+ final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(null, null, null);
final String state = response.getState();
final String nonce = response.getNonce();
assertThat(response.getAuthenticationRequestUrl(),
@@ -194,7 +194,7 @@ public void testBuilidingAuthenticationRequest() {
}
- public void testBuilidingAuthenticationRequestWithDefaultScope() {
+ public void testBuildingAuthenticationRequestWithDefaultScope() {
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")
@@ -207,14 +207,14 @@ public void testBuilidingAuthenticationRequestWithDefaultScope() {
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code");
final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null,
null);
- final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(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 testBuilidingAuthenticationRequestWithExistingStateAndNonce() {
+ 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")
@@ -229,12 +229,35 @@ public void testBuilidingAuthenticationRequestWithExistingStateAndNonce() {
null);
final String state = new State().getValue();
final String nonce = new Nonce().getValue();
- final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(state, nonce);
+ 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()), 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 {