Skip to content

Commit 54a0448

Browse files
authored
1 parent 4beced5 commit 54a0448

15 files changed

+820
-98
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.core.security.action.oidc;
7+
8+
import org.elasticsearch.action.Action;
9+
import org.elasticsearch.common.io.stream.Writeable;
10+
11+
public class OpenIdConnectLogoutAction extends Action<OpenIdConnectLogoutResponse> {
12+
13+
public static final OpenIdConnectLogoutAction INSTANCE = new OpenIdConnectLogoutAction();
14+
public static final String NAME = "cluster:admin/xpack/security/oidc/logout";
15+
16+
private OpenIdConnectLogoutAction() {
17+
super(NAME);
18+
}
19+
20+
@Override
21+
public OpenIdConnectLogoutResponse newResponse() {
22+
throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
23+
}
24+
25+
@Override
26+
public Writeable.Reader<OpenIdConnectLogoutResponse> getResponseReader() {
27+
return OpenIdConnectLogoutResponse::new;
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.core.security.action.oidc;
7+
8+
import org.elasticsearch.action.ActionRequest;
9+
import org.elasticsearch.action.ActionRequestValidationException;
10+
import org.elasticsearch.common.Nullable;
11+
import org.elasticsearch.common.Strings;
12+
import org.elasticsearch.common.io.stream.StreamInput;
13+
import org.elasticsearch.common.io.stream.StreamOutput;
14+
15+
import java.io.IOException;
16+
17+
import static org.elasticsearch.action.ValidateActions.addValidationError;
18+
19+
public final class OpenIdConnectLogoutRequest extends ActionRequest {
20+
21+
private String token;
22+
@Nullable
23+
private String refreshToken;
24+
25+
public OpenIdConnectLogoutRequest() {
26+
27+
}
28+
29+
public OpenIdConnectLogoutRequest(StreamInput in) throws IOException {
30+
super.readFrom(in);
31+
token = in.readString();
32+
refreshToken = in.readOptionalString();
33+
}
34+
35+
@Override
36+
public ActionRequestValidationException validate() {
37+
ActionRequestValidationException validationException = null;
38+
if (Strings.isNullOrEmpty(token)) {
39+
validationException = addValidationError("token is missing", validationException);
40+
}
41+
return validationException;
42+
}
43+
44+
public String getToken() {
45+
return token;
46+
}
47+
48+
public void setToken(String token) {
49+
this.token = token;
50+
}
51+
52+
public String getRefreshToken() {
53+
return refreshToken;
54+
}
55+
56+
public void setRefreshToken(String refreshToken) {
57+
this.refreshToken = refreshToken;
58+
}
59+
60+
@Override
61+
public void writeTo(StreamOutput out) throws IOException {
62+
super.writeTo(out);
63+
out.writeString(token);
64+
out.writeOptionalString(refreshToken);
65+
}
66+
67+
@Override
68+
public void readFrom(StreamInput in) {
69+
throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.core.security.action.oidc;
7+
8+
import org.elasticsearch.action.ActionResponse;
9+
import org.elasticsearch.common.io.stream.StreamInput;
10+
import org.elasticsearch.common.io.stream.StreamOutput;
11+
12+
import java.io.IOException;
13+
14+
public final class OpenIdConnectLogoutResponse extends ActionResponse {
15+
16+
private String endSessionUrl;
17+
18+
public OpenIdConnectLogoutResponse(StreamInput in) throws IOException {
19+
super.readFrom(in);
20+
this.endSessionUrl = in.readString();
21+
}
22+
23+
public OpenIdConnectLogoutResponse(String endSessionUrl) {
24+
this.endSessionUrl = endSessionUrl;
25+
}
26+
27+
@Override
28+
public void readFrom(StreamInput in) {
29+
throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
30+
}
31+
32+
@Override
33+
public void writeTo(StreamOutput out) throws IOException {
34+
super.writeTo(out);
35+
out.writeString(endSessionUrl);
36+
}
37+
38+
public String toString() {
39+
return "{endSessionUrl=" + endSessionUrl + "}";
40+
}
41+
42+
public String getEndSessionUrl() {
43+
return endSessionUrl;
44+
}
45+
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/oidc/OpenIdConnectRealmSettings.java

+21-3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ private OpenIdConnectRealmSettings() {
4747
throw new IllegalArgumentException("Invalid value [" + v + "] for [" + key + "]. Not a valid URI.", e);
4848
}
4949
}, Setting.Property.NodeScope));
50+
public static final Setting.AffixSetting<String> RP_POST_LOGOUT_REDIRECT_URI
51+
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "rp.post_logout_redirect_uri",
52+
key -> Setting.simpleString(key, v -> {
53+
try {
54+
new URI(v);
55+
} catch (URISyntaxException e) {
56+
throw new IllegalArgumentException("Invalid value [" + v + "] for [" + key + "]. Not a valid URI.", e);
57+
}
58+
}, Setting.Property.NodeScope));
5059
public static final Setting.AffixSetting<String> RP_RESPONSE_TYPE
5160
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "rp.response_type",
5261
key -> Setting.simpleString(key, v -> {
@@ -95,6 +104,15 @@ private OpenIdConnectRealmSettings() {
95104
throw new IllegalArgumentException("Invalid value [" + v + "] for [" + key + "]. Not a valid URI.", e);
96105
}
97106
}, Setting.Property.NodeScope));
107+
public static final Setting.AffixSetting<String> OP_ENDSESSION_ENDPOINT
108+
= Setting.affixKeySetting(RealmSettings.realmSettingPrefix(TYPE), "op.endsession_endpoint",
109+
key -> Setting.simpleString(key, v -> {
110+
try {
111+
new URI(v);
112+
} catch (URISyntaxException e) {
113+
throw new IllegalArgumentException("Invalid value [" + v + "] for [" + key + "]. Not a valid URI.", e);
114+
}
115+
}, Setting.Property.NodeScope));
98116
public static final Setting.AffixSetting<String> OP_ISSUER
99117
= RealmSettings.simpleString(TYPE, "op.issuer", Setting.Property.NodeScope);
100118
public static final Setting.AffixSetting<String> OP_JWKSET_PATH
@@ -132,9 +150,9 @@ private OpenIdConnectRealmSettings() {
132150
public static Set<Setting.AffixSetting<?>> getSettings() {
133151
final Set<Setting.AffixSetting<?>> set = Sets.newHashSet(
134152
RP_CLIENT_ID, RP_REDIRECT_URI, RP_RESPONSE_TYPE, RP_REQUESTED_SCOPES, RP_CLIENT_SECRET, RP_SIGNATURE_ALGORITHM,
135-
OP_NAME, OP_AUTHORIZATION_ENDPOINT, OP_TOKEN_ENDPOINT, OP_USERINFO_ENDPOINT, OP_ISSUER, OP_JWKSET_PATH,
136-
HTTP_CONNECT_TIMEOUT, HTTP_CONNECTION_READ_TIMEOUT, HTTP_SOCKET_TIMEOUT, HTTP_MAX_CONNECTIONS, HTTP_MAX_ENDPOINT_CONNECTIONS,
137-
ALLOWED_CLOCK_SKEW);
153+
RP_POST_LOGOUT_REDIRECT_URI, OP_NAME, OP_AUTHORIZATION_ENDPOINT, OP_TOKEN_ENDPOINT, OP_USERINFO_ENDPOINT,
154+
OP_ENDSESSION_ENDPOINT, OP_ISSUER, OP_JWKSET_PATH, HTTP_CONNECT_TIMEOUT, HTTP_CONNECTION_READ_TIMEOUT, HTTP_SOCKET_TIMEOUT,
155+
HTTP_MAX_CONNECTIONS, HTTP_MAX_ENDPOINT_CONNECTIONS, ALLOWED_CLOCK_SKEW);
138156
set.addAll(DelegatedAuthorizationSettings.getSettings(TYPE));
139157
set.addAll(RealmSettings.getStandardSettings(TYPE));
140158
set.addAll(SSLConfigurationSettings.getRealmSettings(TYPE));

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java

+5
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
import org.elasticsearch.xpack.core.security.action.GetApiKeyAction;
8282
import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction;
8383
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateAction;
84+
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutAction;
8485
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationAction;
8586
import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesAction;
8687
import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesAction;
@@ -138,6 +139,7 @@
138139
import org.elasticsearch.xpack.security.action.TransportInvalidateApiKeyAction;
139140
import org.elasticsearch.xpack.security.action.filter.SecurityActionFilter;
140141
import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectAuthenticateAction;
142+
import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectLogoutAction;
141143
import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectPrepareAuthenticationAction;
142144
import org.elasticsearch.xpack.security.action.privilege.TransportDeletePrivilegesAction;
143145
import org.elasticsearch.xpack.security.action.privilege.TransportGetPrivilegesAction;
@@ -197,6 +199,7 @@
197199
import org.elasticsearch.xpack.security.rest.action.RestInvalidateApiKeyAction;
198200
import org.elasticsearch.xpack.security.rest.action.oauth2.RestGetTokenAction;
199201
import org.elasticsearch.xpack.security.rest.action.oauth2.RestInvalidateTokenAction;
202+
import org.elasticsearch.xpack.security.rest.action.oidc.RestOpenIdConnectLogoutAction;
200203
import org.elasticsearch.xpack.security.rest.action.privilege.RestDeletePrivilegesAction;
201204
import org.elasticsearch.xpack.security.rest.action.privilege.RestGetPrivilegesAction;
202205
import org.elasticsearch.xpack.security.rest.action.privilege.RestPutPrivilegesAction;
@@ -745,6 +748,7 @@ public void onIndexModule(IndexModule module) {
745748
new ActionHandler<>(OpenIdConnectPrepareAuthenticationAction.INSTANCE,
746749
TransportOpenIdConnectPrepareAuthenticationAction.class),
747750
new ActionHandler<>(OpenIdConnectAuthenticateAction.INSTANCE, TransportOpenIdConnectAuthenticateAction.class),
751+
new ActionHandler<>(OpenIdConnectLogoutAction.INSTANCE, TransportOpenIdConnectLogoutAction.class),
748752
new ActionHandler<>(GetPrivilegesAction.INSTANCE, TransportGetPrivilegesAction.class),
749753
new ActionHandler<>(PutPrivilegesAction.INSTANCE, TransportPutPrivilegesAction.class),
750754
new ActionHandler<>(DeletePrivilegesAction.INSTANCE, TransportDeletePrivilegesAction.class),
@@ -799,6 +803,7 @@ public List<RestHandler> getRestHandlers(Settings settings, RestController restC
799803
new RestSamlInvalidateSessionAction(settings, restController, getLicenseState()),
800804
new RestOpenIdConnectPrepareAuthenticationAction(settings, restController, getLicenseState()),
801805
new RestOpenIdConnectAuthenticateAction(settings, restController, getLicenseState()),
806+
new RestOpenIdConnectLogoutAction(settings, restController, getLicenseState()),
802807
new RestGetPrivilegesAction(settings, restController, getLicenseState()),
803808
new RestPutPrivilegesAction(settings, restController, getLicenseState()),
804809
new RestDeletePrivilegesAction(settings, restController, getLicenseState()),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.security.action.oidc;
7+
8+
import com.nimbusds.jwt.JWT;
9+
import com.nimbusds.jwt.JWTParser;
10+
import org.apache.logging.log4j.LogManager;
11+
import org.apache.logging.log4j.Logger;
12+
import org.elasticsearch.ElasticsearchSecurityException;
13+
import org.elasticsearch.action.ActionListener;
14+
import org.elasticsearch.action.support.ActionFilters;
15+
import org.elasticsearch.action.support.HandledTransportAction;
16+
import org.elasticsearch.common.Strings;
17+
import org.elasticsearch.common.inject.Inject;
18+
import org.elasticsearch.common.io.stream.Writeable;
19+
import org.elasticsearch.tasks.Task;
20+
import org.elasticsearch.transport.TransportService;
21+
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutAction;
22+
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutRequest;
23+
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutResponse;
24+
import org.elasticsearch.xpack.core.security.authc.Authentication;
25+
import org.elasticsearch.xpack.core.security.authc.Realm;
26+
import org.elasticsearch.xpack.core.security.authc.support.TokensInvalidationResult;
27+
import org.elasticsearch.xpack.core.security.user.User;
28+
import org.elasticsearch.xpack.security.authc.Realms;
29+
import org.elasticsearch.xpack.security.authc.TokenService;
30+
import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm;
31+
32+
import java.io.IOException;
33+
import java.text.ParseException;
34+
import java.util.Map;
35+
36+
/**
37+
* Transport action responsible for generating an OpenID connect logout request to be sent to an OpenID Connect Provider
38+
*/
39+
public class TransportOpenIdConnectLogoutAction extends HandledTransportAction<OpenIdConnectLogoutRequest, OpenIdConnectLogoutResponse> {
40+
41+
private final Realms realms;
42+
private final TokenService tokenService;
43+
private static final Logger logger = LogManager.getLogger(TransportOpenIdConnectLogoutAction.class);
44+
45+
@Inject
46+
public TransportOpenIdConnectLogoutAction(TransportService transportService, ActionFilters actionFilters, Realms realms,
47+
TokenService tokenService) {
48+
super(OpenIdConnectLogoutAction.NAME, transportService, actionFilters,
49+
(Writeable.Reader<OpenIdConnectLogoutRequest>) OpenIdConnectLogoutRequest::new);
50+
this.realms = realms;
51+
this.tokenService = tokenService;
52+
}
53+
54+
@Override
55+
protected void doExecute(Task task, OpenIdConnectLogoutRequest request, ActionListener<OpenIdConnectLogoutResponse> listener) {
56+
invalidateRefreshToken(request.getRefreshToken(), ActionListener.wrap(ignore -> {
57+
try {
58+
final String token = request.getToken();
59+
tokenService.getAuthenticationAndMetaData(token, ActionListener.wrap(
60+
tuple -> {
61+
final Authentication authentication = tuple.v1();
62+
final Map<String, Object> tokenMetadata = tuple.v2();
63+
validateAuthenticationAndMetadata(authentication, tokenMetadata);
64+
tokenService.invalidateAccessToken(token, ActionListener.wrap(
65+
result -> {
66+
if (logger.isTraceEnabled()) {
67+
logger.trace("OpenID Connect Logout for user [{}] and token [{}...{}]",
68+
authentication.getUser().principal(),
69+
token.substring(0, 8),
70+
token.substring(token.length() - 8));
71+
}
72+
OpenIdConnectLogoutResponse response = buildResponse(authentication, tokenMetadata);
73+
listener.onResponse(response);
74+
}, listener::onFailure)
75+
);
76+
}, listener::onFailure));
77+
} catch (IOException e) {
78+
listener.onFailure(e);
79+
}
80+
}, listener::onFailure));
81+
}
82+
83+
private OpenIdConnectLogoutResponse buildResponse(Authentication authentication, Map<String, Object> tokenMetadata) {
84+
final String idTokenHint = (String) getFromMetadata(tokenMetadata, "id_token_hint");
85+
final Realm realm = this.realms.realm(authentication.getAuthenticatedBy().getName());
86+
final JWT idToken;
87+
try {
88+
idToken = JWTParser.parse(idTokenHint);
89+
} catch (ParseException e) {
90+
throw new ElasticsearchSecurityException("Token Metadata did not contain a valid IdToken", e);
91+
}
92+
return ((OpenIdConnectRealm) realm).buildLogoutResponse(idToken);
93+
}
94+
95+
private void validateAuthenticationAndMetadata(Authentication authentication, Map<String, Object> tokenMetadata) {
96+
if (tokenMetadata == null) {
97+
throw new ElasticsearchSecurityException("Authentication did not contain metadata");
98+
}
99+
if (authentication == null) {
100+
throw new ElasticsearchSecurityException("No active authentication");
101+
}
102+
final User user = authentication.getUser();
103+
if (user == null) {
104+
throw new ElasticsearchSecurityException("No active user");
105+
}
106+
107+
final Authentication.RealmRef ref = authentication.getAuthenticatedBy();
108+
if (ref == null || Strings.isNullOrEmpty(ref.getName())) {
109+
throw new ElasticsearchSecurityException("Authentication {} has no authenticating realm",
110+
authentication);
111+
}
112+
final Realm realm = this.realms.realm(authentication.getAuthenticatedBy().getName());
113+
if (realm == null) {
114+
throw new ElasticsearchSecurityException("Authenticating realm {} does not exist", ref.getName());
115+
}
116+
if (realm instanceof OpenIdConnectRealm == false) {
117+
throw new IllegalArgumentException("Access token is not valid for an OpenID Connect realm");
118+
}
119+
}
120+
121+
private Object getFromMetadata(Map<String, Object> metadata, String key) {
122+
if (metadata.containsKey(key) == false) {
123+
throw new ElasticsearchSecurityException("Authentication token does not have OpenID Connect metadata [{}]", key);
124+
}
125+
Object value = metadata.get(key);
126+
if (null != value && value instanceof String == false) {
127+
throw new ElasticsearchSecurityException("In authentication token, OpenID Connect metadata [{}] is [{}] rather than " +
128+
"String", key, value.getClass());
129+
}
130+
return value;
131+
132+
}
133+
private void invalidateRefreshToken(String refreshToken, ActionListener<TokensInvalidationResult> listener) {
134+
if (refreshToken == null) {
135+
listener.onResponse(null);
136+
} else {
137+
tokenService.invalidateRefreshToken(refreshToken, listener);
138+
}
139+
}
140+
}

0 commit comments

Comments
 (0)