Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Transport Actions to indicate authN realm #45767

Merged
merged 7 commits into from
Aug 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,8 @@ task verifyVersions {
* after the backport of the backcompat code is complete.
*/

boolean bwc_tests_enabled = true
final String bwc_tests_disabled_issue = "" /* place a PR link here when committing bwc changes */
boolean bwc_tests_enabled = false
final String bwc_tests_disabled_issue = "https://github.com/elastic/elasticsearch/pull/45767"
if (bwc_tests_enabled == false) {
if (bwc_tests_disabled_issue.isEmpty()) {
throw new GradleException("bwc_tests_disabled_issue must be set when bwc_tests_enabled == false")
Expand Down
13 changes: 9 additions & 4 deletions x-pack/docs/en/rest-api/security/oidc-authenticate-api.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,28 @@ and <<security-api-oidc-logout,OpenID Connect logout API>>
==== {api-request-body-title}

`redirect_uri`::
The URL to which the OpenID Connect Provider redirected the User Agent in
(Required, string) The URL to which the OpenID Connect Provider redirected the User Agent in
response to an authentication request, after a successful authentication. This
URL is expected to be provided as-is (URL encoded), taken from the body of the
response or as the value of a `Location` header in the response from the OpenID
Connect Provider.

`state`::
String value used to maintain state between the authentication request and the
(Required, string) Used to maintain state between the authentication request and the
response. This value needs to be the same as the one that was provided to the
call to `/_security/oidc/prepare` earlier, or the one that was generated by {es}
and included in the response to that call.

`nonce`::
String value used to associate a Client session with an ID Token and to mitigate
(Required, string) Used to associate a Client session with an ID Token and to mitigate
replay attacks. This value needs to be the same as the one that was provided to
the call to `/_security/oidc/prepare` earlier, or the one that was generated by
{es} and included in the response to that call.

`realm`::
(Optional, string) Used to identify the name of the OpenID Connect realm that should
be used to authenticate this. Useful when multiple realms have been defined.

[[security-api-oidc-authenticate-example]]
==== {api-examples-title}

Expand All @@ -63,7 +67,8 @@ POST /_security/oidc/authenticate
{
"redirect_uri" : "https://oidc-kibana.elastic.co:5603/api/security/v1/oidc?code=jtI3Ntt8v3_XvcLzCFGq&state=4dbrihtIAt3wBTwo6DxK-vdk-sSyDBV8Yf0AjdkdT5I",
"state" : "4dbrihtIAt3wBTwo6DxK-vdk-sSyDBV8Yf0AjdkdT5I",
"nonce" : "WaBPH0KqPVdG5HHdSxPRjfoZbXMCicm5v1OiAj0DUFM"
"nonce" : "WaBPH0KqPVdG5HHdSxPRjfoZbXMCicm5v1OiAj0DUFM",
"realm" : "oidc1"
}
--------------------------------------------------
// CONSOLE
Expand Down
4 changes: 2 additions & 2 deletions x-pack/docs/en/rest-api/security/oidc-logout-api.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ and
==== {api-request-body-title}

`access_token`::
The value of the access token to be invalidated as part of the logout.
(Required, string) The value of the access token to be invalidated as part of the logout.

`refresh_token`::
(Optional) The value of the refresh token to be invalidated as part of the logout.
(Optional, string) The value of the refresh token to be invalidated as part of the logout.


[[security-api-oidc-logout-example]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,28 @@ and <<security-api-oidc-logout,OpenID Connect logout API>>.
The following parameters can be specified in the body of the request:

`realm`::
The name of the OpenID Connect realm in {es} the configuration of which should
(Optional, string) The name of the OpenID Connect realm in {es} the configuration of which should
be used in order to generate the authentication request. Cannot be specified
when `iss` is specified.
when `iss` is specified. One of `realm`, `iss` is required.

`state`::
String value used to maintain state between the authentication request and the
(Optional, string) Value used to maintain state between the authentication request and the
response, typically used as a Cross-Site Request Forgery mitigation. If the
caller of the API doesn't provide a value, {es} will generate one with
sufficient entropy itself and return it in the response.

`nonce`::
String value used to associate a Client session with an ID Token and to mitigate
(Optional, string) Value used to associate a Client session with an ID Token and to mitigate
replay attacks. If the caller of the API doesn't provide a value, {es} will
generate one with sufficient entropy itself and return it in the response.

`issuer`::
In the case of a 3rd Party initiated Single Sign On, this is the Issuer
`iss`::
(Optional, string) In the case of a 3rd Party initiated Single Sign On, this is the Issuer
Identifier for the OP that the RP is to send the Authentication Request to.
Cannot be specified when `realm` is specified.
Cannot be specified when `realm` is specified. One of `realm`, `iss` is required.

`login_hint`::
In the case of a 3rd Party initiated Single Sign On, a string value to be
(Optional, string) In the case of a 3rd Party initiated Single Sign On, a string value to be
included in the authentication request, as the `login_hint` parameter. This
parameter is not valid when `realm` is specified

Expand Down
7 changes: 5 additions & 2 deletions x-pack/docs/en/security/authentication/oidc-guide.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,9 @@ POST /_security/oidc/prepare
this HTTP GET request, the custom web app will need to make an HTTP POST request to
`_security/oidc/authenticate`, again - authenticating as the `facilitator` user - passing the URL
where the user's browser was redirected to, as a parameter, along with the
values for `nonce` and `state` it had saved in the user's session previously.
values for `nonce` and `state` it had saved in the user's session previously. If more than one
OpenID Connect realms are configured, the custom web app can specify the name of the realm to be
used for handling this, but this parameter is optional.
See {ref}/security-api-oidc-authenticate.html[OIDC Authenticate API] for more details
+
[source,js]
Expand All @@ -658,7 +660,8 @@ POST /_security/oidc/authenticate
{
"redirect_uri" : "https://oidc-kibana.elastic.co:5603/api/security/v1/oidc?code=jtI3Ntt8v3_XvcLzCFGq&state=4dbrihtIAt3wBTwo6DxK-vdk-sSyDBV8Yf0AjdkdT5I",
"state" : "4dbrihtIAt3wBTwo6DxK-vdk-sSyDBV8Yf0AjdkdT5I",
"nonce" : "WaBPH0KqPVdG5HHdSxPRjfoZbXMCicm5v1OiAj0DUFM"
"nonce" : "WaBPH0KqPVdG5HHdSxPRjfoZbXMCicm5v1OiAj0DUFM",
"realm" : "oidc1"
}
-----------------------------------------------------------------------
// CONSOLE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
package org.elasticsearch.xpack.core.security.action.oidc;

import org.elasticsearch.Version;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.Strings;
Expand Down Expand Up @@ -38,6 +39,11 @@ public class OpenIdConnectAuthenticateRequest extends ActionRequest {
*/
private String nonce;

/**
* The name of the OIDC Realm that should consume the authentication request
*/
private String realm;

public OpenIdConnectAuthenticateRequest() {

}
Expand All @@ -47,6 +53,10 @@ public OpenIdConnectAuthenticateRequest(StreamInput in) throws IOException {
redirectUri = in.readString();
state = in.readString();
nonce = in.readString();
if (in.getVersion().onOrAfter(Version.V_7_4_0)) {
realm = in.readOptionalString();
}

}

public String getRedirectUri() {
Expand All @@ -73,6 +83,14 @@ public void setNonce(String nonce) {
this.nonce = nonce;
}

public String getRealm() {
return realm;
}

public void setRealm(String realm) {
this.realm = realm;
}

@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
Expand All @@ -94,10 +112,13 @@ public void writeTo(StreamOutput out) throws IOException {
out.writeString(redirectUri);
out.writeString(state);
out.writeString(nonce);
if (out.getVersion().onOrAfter(Version.V_7_4_0)) {
out.writeOptionalString(realm);
}
}

public String toString() {
return "{redirectUri=" + redirectUri + ", state=" + state + ", nonce=" + nonce + "}";
return "{redirectUri=" + redirectUri + ", state=" + state + ", nonce=" + nonce + ", realm=" +realm+"}";
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.io.stream.StreamInput;

import java.io.IOException;
Expand All @@ -19,6 +20,8 @@ public final class SamlAuthenticateRequest extends ActionRequest {

private byte[] saml;
private List<String> validRequestIds;
@Nullable
private String realm;

public SamlAuthenticateRequest(StreamInput in) throws IOException {
super(in);
Expand Down Expand Up @@ -47,4 +50,12 @@ public List<String> getValidRequestIds() {
public void setValidRequestIds(List<String> validRequestIds) {
this.validRequestIds = validRequestIds;
}

public String getRealm() {
return realm;
}

public void setRealm(String realm) {
this.realm = realm;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Erk! This class really ought to have serialization logic in it. Can we add that as a followup PR?
It's probably not the only SAML request/response that's missing it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ public SamlAuthenticateRequestBuilder validRequestIds(List<String> validRequestI
request.setValidRequestIds(validRequestIds);
return this;
}

public SamlAuthenticateRequestBuilder authenticatingRealm(String realm) {
request.setRealm(realm);
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public TransportOpenIdConnectAuthenticateAction(ThreadPool threadPool, Transport
protected void doExecute(Task task, OpenIdConnectAuthenticateRequest request,
ActionListener<OpenIdConnectAuthenticateResponse> listener) {
final OpenIdConnectToken token = new OpenIdConnectToken(request.getRedirectUri(), new State(request.getState()),
new Nonce(request.getNonce()));
new Nonce(request.getNonce()), request.getRealm());
final ThreadContext threadContext = threadPool.getThreadContext();
Authentication originatingAuthentication = Authentication.getAuthentication(threadContext);
try (ThreadContext.StoredContext ignore = threadContext.stashContext()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public TransportSamlAuthenticateAction(ThreadPool threadPool, TransportService t

@Override
protected void doExecute(Task task, SamlAuthenticateRequest request, ActionListener<SamlAuthenticateResponse> listener) {
final SamlToken saml = new SamlToken(request.getSaml(), request.getValidRequestIds());
final SamlToken saml = new SamlToken(request.getSaml(), request.getValidRequestIds(), request.getRealm());
logger.trace("Attempting to authenticate SamlToken [{}]", saml);
final ThreadContext threadContext = threadPool.getThreadContext();
Authentication originatingAuthentication = Authentication.getAuthentication(threadContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ public void authenticate(String action, TransportMessage message, User fallbackU
}

/**
* Authenticates the username and password that are provided as parameters. This will not look
* at the values in the ThreadContext for Authentication.
* Authenticates the user based on the contents of the token that is provided as parameter. This will not look at the values in the
* ThreadContext for Authentication.
*
* @param action The action of the message
* @param message The message that resulted in this authenticate call
Expand Down Expand Up @@ -347,9 +347,10 @@ void extractToken(Consumer<AuthenticationToken> consumer) {

/**
* Consumes the {@link AuthenticationToken} provided by the caller. In the case of a {@code null} token, {@link #handleNullToken()}
* is called. In the case of a {@code non-null} token, the realms are iterated over and the first realm that returns a non-null
* {@link User} is the authenticating realm and iteration is stopped. This user is then passed to {@link #consumeUser(User, Map)}
* if no exception was caught while trying to authenticate the token
* is called. In the case of a {@code non-null} token, the realms are iterated over in the order defined in the configuration
* while possibly also taking into consideration the last realm that authenticated this principal. When consulting multiple realms,
* the first realm that returns a non-null {@link User} is the authenticating realm and iteration is stopped. This user is then
* passed to {@link #consumeUser(User, Map)} if no exception was caught while trying to authenticate the token
*/
private void consumeToken(AuthenticationToken token) {
if (token == null) {
Expand Down Expand Up @@ -411,6 +412,12 @@ private void consumeToken(AuthenticationToken token) {
}
}

/**
* Possibly reorders the realm list depending on whether this principal has been recently authenticated by a specific realm
*
* @param principal The principal of the {@link AuthenticationToken} to be authenticated by a realm
* @return a list of realms ordered based on which realm should authenticate the current {@link AuthenticationToken}
*/
private List<Realm> getRealmList(String principal) {
final List<Realm> orderedRealmList = this.defaultOrderedRealmList;
if (lastSuccessfulAuthCache != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,22 @@ public boolean supports(AuthenticationToken token) {
return token instanceof OpenIdConnectToken;
}

private boolean isTokenForRealm(OpenIdConnectToken oidcToken) {
if (oidcToken.getAuthenticatingRealm() == null) {
return true;
} else {
return oidcToken.getAuthenticatingRealm().equals(this.name());
}
}

@Override
public AuthenticationToken token(ThreadContext context) {
return null;
}

@Override
public void authenticate(AuthenticationToken token, ActionListener<AuthenticationResult> listener) {
if (token instanceof OpenIdConnectToken) {
if (token instanceof OpenIdConnectToken && isTokenForRealm((OpenIdConnectToken) token)) {
OpenIdConnectToken oidcToken = (OpenIdConnectToken) token;
openIdConnectAuthenticator.authenticate(oidcToken, ActionListener.wrap(
jwtClaimsSet -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import com.nimbusds.oauth2.sdk.id.State;
import com.nimbusds.openid.connect.sdk.Nonce;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;

/**
Expand All @@ -19,6 +20,7 @@ public class OpenIdConnectToken implements AuthenticationToken {
private String redirectUrl;
private State state;
private Nonce nonce;
private String authenticatingRealm;

/**
* @param redirectUrl The URI where the OP redirected the browser after the authentication event at the OP. This is passed as is from
Expand All @@ -28,11 +30,13 @@ public class OpenIdConnectToken implements AuthenticationToken {
* user's session with the facilitator.
* @param nonce The nonce value that we generated or the facilitator provided for this specific flow and should be stored at the
* user's session with the facilitator.
* @param authenticatingRealm The realm that should authenticate this OpenId Connect Authentication Response
*/
public OpenIdConnectToken(String redirectUrl, State state, Nonce nonce) {
public OpenIdConnectToken(String redirectUrl, State state, Nonce nonce, @Nullable String authenticatingRealm) {
this.redirectUrl = redirectUrl;
this.state = state;
this.nonce = nonce;
this.authenticatingRealm = authenticatingRealm;
}

@Override
Expand Down Expand Up @@ -62,7 +66,10 @@ public String getRedirectUrl() {
return redirectUrl;
}

public String getAuthenticatingRealm() { return authenticatingRealm; }

public String toString() {
return getClass().getSimpleName() + "{ redirectUrl=" + redirectUrl + ", state=" + state + ", nonce=" + nonce + "}";
return getClass().getSimpleName() + "{ redirectUrl=" + redirectUrl + ", state=" + state + ", nonce=" + nonce + ", " +
"authenticatingRealm="+ authenticatingRealm +"}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,14 @@ public boolean supports(AuthenticationToken token) {
return token instanceof SamlToken;
}

private boolean isTokenForRealm(SamlToken samlToken) {
if (samlToken.getAuthenticatingRealm() == null) {
return true;
} else {
return samlToken.getAuthenticatingRealm().equals(this.name());
}
}

/**
* Always returns {@code null} as there is no support for reading a SAML token out of a request
*
Expand All @@ -396,7 +404,7 @@ public AuthenticationToken token(ThreadContext threadContext) {

@Override
public void authenticate(AuthenticationToken authenticationToken, ActionListener<AuthenticationResult> listener) {
if (authenticationToken instanceof SamlToken) {
if (authenticationToken instanceof SamlToken && isTokenForRealm((SamlToken) authenticationToken)) {
try {
final SamlToken token = (SamlToken) authenticationToken;
final SamlAttributes attributes = authenticator.authenticate(token);
Expand Down
Loading