Skip to content

Commit 3c97a6a

Browse files
Allow cofiguring SAML secure attributes
This PR is twofold: - it adds a new `secure_attributes` setting to the SAML realm, and - introduces extension point that allows providing a custom `SamlAuthenticateResponseHandler` The `secure_attributes` setting can be used to define which SAML attributes should be treated as secure. This implies that these attributes should not be logged, or returned as part of user's metadata when `populate_user_metadata` is set to `true`.
1 parent 8ff02e2 commit 3c97a6a

File tree

10 files changed

+322
-40
lines changed

10 files changed

+322
-40
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,16 @@ public class SamlRealmSettings {
139139
key -> Setting.positiveTimeSetting(key, TimeValue.timeValueMinutes(3), Setting.Property.NodeScope)
140140
);
141141

142+
/**
143+
* The names of attributes that should be treated as secure and never populated as part of user metadata
144+
* (even when {@code #POPULATE_USER_METADATA} is configured).
145+
*/
146+
public static final Function<String, Setting.AffixSetting<List<String>>> SECURE_ATTRIBUTES = (type) -> Setting.affixKeySetting(
147+
RealmSettings.realmSettingPrefix(type),
148+
"secure_attributes",
149+
(key) -> Setting.stringListSetting(key, attributes -> verifyNonNullNotEmpty(key, attributes), Setting.Property.NodeScope)
150+
);
151+
142152
public static final Function<String, Setting.AffixSetting<List<String>>> EXCLUDE_ROLES = (type) -> Setting.affixKeySetting(
143153
RealmSettings.realmSettingPrefix(type),
144154
"exclude_roles",
@@ -201,7 +211,8 @@ public static Set<Setting.AffixSetting<?>> getSettings(String type) {
201211
ENCRYPTION_KEY_ALIAS.apply(type),
202212
SIGNING_KEY_ALIAS.apply(type),
203213
SIGNING_MESSAGE_TYPES.apply(type),
204-
REQUESTED_AUTHN_CONTEXT_CLASS_REF.apply(type)
214+
REQUESTED_AUTHN_CONTEXT_CLASS_REF.apply(type),
215+
SECURE_ATTRIBUTES.apply(type)
205216
);
206217

207218
set.addAll(X509KeyPairSettings.affix(RealmSettings.realmSettingPrefix(type), ENCRYPTION_SETTING_KEY, false));

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@
310310
import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore;
311311
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
312312
import org.elasticsearch.xpack.security.authc.jwt.JwtRealm;
313+
import org.elasticsearch.xpack.security.authc.saml.SamlAuthenticateResponseHandler;
313314
import org.elasticsearch.xpack.security.authc.service.CachingServiceAccountTokenStore;
314315
import org.elasticsearch.xpack.security.authc.service.CompositeServiceAccountTokenStore;
315316
import org.elasticsearch.xpack.security.authc.service.FileServiceAccountTokenStore;
@@ -641,6 +642,7 @@ public class Security extends Plugin
641642
private final SetOnce<SecondaryAuthActions> secondaryAuthActions = new SetOnce<>();
642643
private final SetOnce<QueryableBuiltInRolesProviderFactory> queryableRolesProviderFactory = new SetOnce<>();
643644
private final SetOnce<SecurityMigrationExecutor> securityMigrationExecutor = new SetOnce<>();
645+
private final SetOnce<SamlAuthenticateResponseHandler.Factory> samlAuthenticateResponseHandlerFactory = new SetOnce<>();
644646

645647
// Node local retry count for migration jobs that's checked only on the master node to make sure
646648
// submit migration jobs doesn't get out of hand and retries forever if they fail. Reset by a
@@ -990,6 +992,15 @@ Collection<Object> createComponents(
990992
if (fileRoleValidator.get() == null) {
991993
fileRoleValidator.set(new FileRoleValidator.Default());
992994
}
995+
if (samlAuthenticateResponseHandlerFactory.get() == null) {
996+
samlAuthenticateResponseHandlerFactory.set(new SamlAuthenticateResponseHandler.DefaultFactory());
997+
}
998+
components.add(
999+
new PluginComponentBinding<>(
1000+
SamlAuthenticateResponseHandler.class,
1001+
samlAuthenticateResponseHandlerFactory.get().create(tokenService, getClock())
1002+
)
1003+
);
9931004
this.fileRolesStore.set(
9941005
new FileRolesStore(settings, environment, resourceWatcherService, getLicenseState(), xContentRegistry, fileRoleValidator.get())
9951006
);
@@ -2477,6 +2488,7 @@ public void loadExtensions(ExtensionLoader loader) {
24772488
loadSingletonExtensionAndSetOnce(loader, fileRoleValidator, FileRoleValidator.class);
24782489
loadSingletonExtensionAndSetOnce(loader, secondaryAuthActions, SecondaryAuthActions.class);
24792490
loadSingletonExtensionAndSetOnce(loader, queryableRolesProviderFactory, QueryableBuiltInRolesProviderFactory.class);
2491+
loadSingletonExtensionAndSetOnce(loader, samlAuthenticateResponseHandlerFactory, SamlAuthenticateResponseHandler.Factory.class);
24802492
}
24812493

24822494
private <T> void loadSingletonExtensionAndSetOnce(ExtensionLoader loader, SetOnce<T> setOnce, Class<T> clazz) {

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlAuthenticateAction.java

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import org.elasticsearch.action.support.HandledTransportAction;
1313
import org.elasticsearch.common.util.concurrent.EsExecutors;
1414
import org.elasticsearch.common.util.concurrent.ThreadContext;
15-
import org.elasticsearch.core.TimeValue;
1615
import org.elasticsearch.injection.guice.Inject;
1716
import org.elasticsearch.tasks.Task;
1817
import org.elasticsearch.threadpool.ThreadPool;
@@ -25,11 +24,9 @@
2524
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
2625
import org.elasticsearch.xpack.core.security.user.User;
2726
import org.elasticsearch.xpack.security.authc.AuthenticationService;
28-
import org.elasticsearch.xpack.security.authc.TokenService;
29-
import org.elasticsearch.xpack.security.authc.saml.SamlRealm;
27+
import org.elasticsearch.xpack.security.authc.saml.SamlAuthenticateResponseHandler;
3028
import org.elasticsearch.xpack.security.authc.saml.SamlToken;
3129

32-
import java.util.Map;
3330
import java.util.concurrent.Executor;
3431

3532
/**
@@ -39,7 +36,7 @@ public final class TransportSamlAuthenticateAction extends HandledTransportActio
3936

4037
private final ThreadPool threadPool;
4138
private final AuthenticationService authenticationService;
42-
private final TokenService tokenService;
39+
private final SamlAuthenticateResponseHandler tokenHandler;
4340
private final SecurityContext securityContext;
4441
private final Executor genericExecutor;
4542

@@ -49,7 +46,7 @@ public TransportSamlAuthenticateAction(
4946
TransportService transportService,
5047
ActionFilters actionFilters,
5148
AuthenticationService authenticationService,
52-
TokenService tokenService,
49+
SamlAuthenticateResponseHandler tokenHandler,
5350
SecurityContext securityContext
5451
) {
5552
// TODO replace DIRECT_EXECUTOR_SERVICE when removing workaround for https://github.com/elastic/elasticsearch/issues/97916
@@ -62,7 +59,7 @@ public TransportSamlAuthenticateAction(
6259
);
6360
this.threadPool = threadPool;
6461
this.authenticationService = authenticationService;
65-
this.tokenService = tokenService;
62+
this.tokenHandler = tokenHandler;
6663
this.securityContext = securityContext;
6764
this.genericExecutor = threadPool.generic();
6865
}
@@ -88,25 +85,9 @@ private void doExecuteForked(Task task, SamlAuthenticateRequest request, ActionL
8885
}
8986
assert authentication != null : "authentication should never be null at this point";
9087
assert false == authentication.isRunAs() : "saml realm authentication cannot have run-as";
91-
@SuppressWarnings("unchecked")
92-
final Map<String, Object> tokenMeta = (Map<String, Object>) result.getMetadata().get(SamlRealm.CONTEXT_TOKEN_DATA);
93-
tokenService.createOAuth2Tokens(
94-
authentication,
95-
originatingAuthentication,
96-
tokenMeta,
97-
true,
98-
ActionListener.wrap(tokenResult -> {
99-
final TimeValue expiresIn = tokenService.getExpirationDelay();
100-
listener.onResponse(
101-
new SamlAuthenticateResponse(
102-
authentication,
103-
tokenResult.getAccessToken(),
104-
tokenResult.getRefreshToken(),
105-
expiresIn
106-
)
107-
);
108-
}, listener::onFailure)
109-
);
88+
assert result.isAuthenticated();
89+
tokenHandler.handleTokenResponse(authentication, originatingAuthentication, result, listener);
90+
11091
}, e -> {
11192
logger.debug(() -> "SamlToken [" + saml + "] could not be authenticated", e);
11293
listener.onFailure(e);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
package org.elasticsearch.xpack.security.authc.saml;
8+
9+
import org.elasticsearch.action.ActionListener;
10+
import org.elasticsearch.core.TimeValue;
11+
import org.elasticsearch.xpack.core.security.action.saml.SamlAuthenticateResponse;
12+
import org.elasticsearch.xpack.core.security.authc.Authentication;
13+
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
14+
import org.elasticsearch.xpack.core.security.user.User;
15+
import org.elasticsearch.xpack.security.authc.TokenService;
16+
17+
import java.util.Map;
18+
19+
/**
20+
* Default implementation of {@link SamlAuthenticateResponseHandler} that returns tokens crested using the {@link TokenService}.
21+
*/
22+
public final class DefaultSamlAuthenticateResponseHandler implements SamlAuthenticateResponseHandler {
23+
24+
private final TokenService tokenService;
25+
26+
public DefaultSamlAuthenticateResponseHandler(TokenService tokenService) {
27+
this.tokenService = tokenService;
28+
}
29+
30+
@Override
31+
public void handleTokenResponse(
32+
Authentication authentication,
33+
Authentication originatingAuthentication,
34+
AuthenticationResult<User> authenticationResult,
35+
ActionListener<SamlAuthenticateResponse> listener
36+
) {
37+
@SuppressWarnings("unchecked")
38+
final Map<String, Object> tokenMeta = (Map<String, Object>) authenticationResult.getMetadata().get(SamlRealm.CONTEXT_TOKEN_DATA);
39+
tokenService.createOAuth2Tokens(authentication, originatingAuthentication, tokenMeta, true, ActionListener.wrap(tokenResult -> {
40+
final TimeValue expiresIn = tokenService.getExpirationDelay();
41+
listener.onResponse(
42+
new SamlAuthenticateResponse(authentication, tokenResult.getAccessToken(), tokenResult.getRefreshToken(), expiresIn)
43+
);
44+
}, listener::onFailure));
45+
}
46+
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlAttributes.java

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
package org.elasticsearch.xpack.security.authc.saml;
88

99
import org.elasticsearch.common.Strings;
10+
import org.elasticsearch.common.settings.SecureString;
11+
import org.elasticsearch.core.IOUtils;
1012
import org.elasticsearch.core.Nullable;
13+
import org.elasticsearch.core.Releasable;
1114
import org.opensaml.saml.saml2.core.Attribute;
1215
import org.opensaml.saml.saml2.core.NameIDType;
1316

@@ -17,19 +20,25 @@
1720
/**
1821
* An lightweight collection of SAML attributes
1922
*/
20-
public class SamlAttributes {
23+
public class SamlAttributes implements Releasable {
2124

2225
public static final String NAMEID_SYNTHENTIC_ATTRIBUTE = "nameid";
2326
public static final String PERSISTENT_NAMEID_SYNTHENTIC_ATTRIBUTE = "nameid:persistent";
2427

2528
private final SamlNameId name;
2629
private final String session;
2730
private final List<SamlAttribute> attributes;
31+
private final List<SamlSecureAttribute> secureAttributes;
2832

2933
SamlAttributes(SamlNameId name, String session, List<SamlAttribute> attributes) {
34+
this(name, session, attributes, List.of());
35+
}
36+
37+
SamlAttributes(SamlNameId name, String session, List<SamlAttribute> attributes, List<SamlSecureAttribute> secureAttributes) {
3038
this.name = name;
3139
this.session = session;
3240
this.attributes = attributes;
41+
this.secureAttributes = secureAttributes;
3342
}
3443

3544
/**
@@ -54,10 +63,28 @@ List<String> getAttributeValues(String attributeId) {
5463
.toList();
5564
}
5665

66+
List<SecureString> getSecureAttributeValues(String attributeId) {
67+
if (Strings.isNullOrEmpty(attributeId)) {
68+
return List.of();
69+
}
70+
return secureAttributes.stream()
71+
.filter(attr -> attributeId.equals(attr.name) || attributeId.equals(attr.friendlyName))
72+
.flatMap(attr -> attr.values.stream())
73+
.toList();
74+
}
75+
5776
List<SamlAttribute> attributes() {
5877
return attributes;
5978
}
6079

80+
List<SamlSecureAttribute> secureAttributes() {
81+
return secureAttributes;
82+
}
83+
84+
boolean isEmpty() {
85+
return attributes.isEmpty() && secureAttributes.isEmpty();
86+
}
87+
6188
SamlNameId name() {
6289
return name;
6390
}
@@ -68,7 +95,12 @@ String session() {
6895

6996
@Override
7097
public String toString() {
71-
return getClass().getSimpleName() + "(" + name + ")[" + session + "]{" + attributes + "}";
98+
return getClass().getSimpleName() + "(" + name + ")[" + session + "]{" + attributes + "}{" + secureAttributes + "}";
99+
}
100+
101+
@Override
102+
public void close() {
103+
IOUtils.closeWhileHandlingException(secureAttributes);
72104
}
73105

74106
static class SamlAttribute {
@@ -103,4 +135,59 @@ public String toString() {
103135
}
104136
}
105137

138+
static class SamlSecureAttribute implements Releasable {
139+
140+
final String name;
141+
final String friendlyName;
142+
final List<SecureString> values;
143+
144+
SamlSecureAttribute(Attribute attribute) {
145+
this(
146+
attribute.getName(),
147+
attribute.getFriendlyName(),
148+
attribute.getAttributeValues()
149+
.stream()
150+
.map(x -> x.getDOM().getTextContent())
151+
.filter(Objects::nonNull)
152+
.map(String::toCharArray)
153+
.map(SecureString::new)
154+
.toList()
155+
);
156+
}
157+
158+
SamlSecureAttribute(String name, @Nullable String friendlyName, List<SecureString> values) {
159+
this.name = Objects.requireNonNull(name, "Attribute name cannot be null");
160+
this.friendlyName = friendlyName;
161+
this.values = values;
162+
}
163+
164+
String name() {
165+
return name;
166+
}
167+
168+
String friendlyName() {
169+
return friendlyName;
170+
}
171+
172+
List<SecureString> values() {
173+
return values;
174+
}
175+
176+
@Override
177+
public String toString() {
178+
StringBuilder str = new StringBuilder();
179+
if (Strings.isNullOrEmpty(friendlyName)) {
180+
str.append(name);
181+
} else {
182+
str.append(friendlyName).append('(').append(name).append(')');
183+
}
184+
str.append("=").append("::es_redacted::").append("(len=").append(values.size()).append(')');
185+
return str.toString();
186+
}
187+
188+
@Override
189+
public void close() {
190+
IOUtils.closeWhileHandlingException(values);
191+
}
192+
}
106193
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
package org.elasticsearch.xpack.security.authc.saml;
8+
9+
import org.elasticsearch.action.ActionListener;
10+
import org.elasticsearch.xpack.core.security.action.saml.SamlAuthenticateResponse;
11+
import org.elasticsearch.xpack.core.security.authc.Authentication;
12+
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
13+
import org.elasticsearch.xpack.core.security.user.User;
14+
import org.elasticsearch.xpack.security.authc.TokenService;
15+
16+
import java.time.Clock;
17+
18+
/**
19+
* Interface for handling successful SAML authentications.
20+
*/
21+
public interface SamlAuthenticateResponseHandler {
22+
23+
/**
24+
* Called to handle and return a ({@link SamlAuthenticateResponse}) after successful SAML authentication.
25+
*/
26+
void handleTokenResponse(
27+
Authentication authentication,
28+
Authentication originatingAuthentication,
29+
AuthenticationResult<User> authenticationResult,
30+
ActionListener<SamlAuthenticateResponse> listener
31+
);
32+
33+
/**
34+
* The factory is used to make handler pluggable.
35+
*/
36+
interface Factory {
37+
SamlAuthenticateResponseHandler create(TokenService tokenService, Clock clock);
38+
}
39+
40+
/**
41+
* The default factory that creates {@link DefaultSamlAuthenticateResponseHandler}.
42+
*/
43+
class DefaultFactory implements Factory {
44+
45+
@Override
46+
public SamlAuthenticateResponseHandler create(TokenService tokenService, Clock clock) {
47+
return new DefaultSamlAuthenticateResponseHandler(tokenService);
48+
}
49+
}
50+
}

0 commit comments

Comments
 (0)