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 {