Skip to content

Commit 0fe2853

Browse files
Add in-response-to field to saml successful response (#137599)
Return in_response_to field in successful SAML authentication attempts, as this information is useful to the client closes of #128179
1 parent 8f5e51a commit 0fe2853

File tree

13 files changed

+98
-11
lines changed

13 files changed

+98
-11
lines changed

docs/changelog/137599.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 137599
2+
summary: In-response-to in saml successful response
3+
area: Authentication
4+
type: enhancement
5+
issues:
6+
- 128179

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import org.elasticsearch.action.ActionResponse;
1010
import org.elasticsearch.common.io.stream.StreamOutput;
11+
import org.elasticsearch.core.Nullable;
1112
import org.elasticsearch.core.TimeValue;
1213
import org.elasticsearch.xpack.core.security.authc.Authentication;
1314

@@ -25,14 +26,27 @@ public final class SamlAuthenticateResponse extends ActionResponse {
2526
private final String realm;
2627
private final TimeValue expiresIn;
2728
private final Authentication authentication;
29+
private final String inResponseTo;
2830

31+
// Constructor used in Serverless
2932
public SamlAuthenticateResponse(Authentication authentication, String tokenString, String refreshToken, TimeValue expiresIn) {
33+
this(authentication, tokenString, refreshToken, expiresIn, null);
34+
}
35+
36+
public SamlAuthenticateResponse(
37+
Authentication authentication,
38+
String tokenString,
39+
String refreshToken,
40+
TimeValue expiresIn,
41+
@Nullable String inResponseTo
42+
) {
3043
this.principal = authentication.getEffectiveSubject().getUser().principal();
3144
this.realm = authentication.getEffectiveSubject().getRealm().getName();
3245
this.tokenString = tokenString;
3346
this.refreshToken = refreshToken;
3447
this.expiresIn = expiresIn;
3548
this.authentication = authentication;
49+
this.inResponseTo = inResponseTo;
3650
}
3751

3852
public String getPrincipal() {
@@ -59,6 +73,13 @@ public Authentication getAuthentication() {
5973
return authentication;
6074
}
6175

76+
public String getInResponseTo() {
77+
return inResponseTo;
78+
}
79+
80+
// note that this method is not used in any current code path,
81+
// but is left here for compatibility with old versions, and as such
82+
// is not up to date, i.e. it does not write 'inResponseTo'
6283
@Override
6384
public void writeTo(StreamOutput out) throws IOException {
6485
out.writeString(principal);
@@ -67,5 +88,6 @@ public void writeTo(StreamOutput out) throws IOException {
6788
out.writeString(refreshToken);
6889
out.writeTimeValue(expiresIn);
6990
authentication.writeTo(out);
91+
// intentionally missing inResponseTo
7092
}
7193
}

x-pack/plugin/security/qa/saml-rest-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/saml/SamlInResponseToIT.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ public void testInResponseTo_matchingValues() throws Exception {
3535
String requestId = generateRandomRequestId();
3636
var response = authUser(username, requestId, requestId);
3737
assertThat(response, hasKey("access_token"));
38+
assertThat(response, hasKey("in_response_to"));
39+
assertThat(response.get("in_response_to"), equalTo(requestId));
3840
}
3941

4042
public void testInResponseTo_requestAndTokenHaveDifferentValues() throws Exception {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ private static String buildLogoutResponseUrl(SamlRealm realm, SamlLogoutRequestH
120120
}
121121

122122
private void findAndInvalidateTokens(SamlRealm realm, SamlLogoutRequestHandler.Result result, ActionListener<Integer> listener) {
123-
final Map<String, Object> tokenMetadata = realm.createTokenMetadata(result.getNameId(), result.getSession());
123+
final Map<String, Object> tokenMetadata = realm.createTokenMetadata(result.getNameId(), result.getSession(), null);
124124
if (Strings.isNullOrEmpty((String) tokenMetadata.get(SamlRealm.TOKEN_METADATA_NAMEID_VALUE))) {
125125
// If we don't have a valid name-id to match against, don't do anything
126126
LOGGER.debug("Logout request [{}] has no NameID value, so cannot invalidate any sessions", result);

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,17 @@ public void handleTokenResponse(
3636
) {
3737
@SuppressWarnings("unchecked")
3838
final Map<String, Object> tokenMeta = (Map<String, Object>) authenticationResult.getMetadata().get(SamlRealm.CONTEXT_TOKEN_DATA);
39+
final String inResponseTo = (String) tokenMeta.get(SamlRealm.TOKEN_METADATA_IN_RESPONSE_TO);
3940
tokenService.createOAuth2Tokens(authentication, originatingAuthentication, tokenMeta, true, ActionListener.wrap(tokenResult -> {
4041
final TimeValue expiresIn = tokenService.getExpirationDelay();
4142
listener.onResponse(
42-
new SamlAuthenticateResponse(authentication, tokenResult.getAccessToken(), tokenResult.getRefreshToken(), expiresIn)
43+
new SamlAuthenticateResponse(
44+
authentication,
45+
tokenResult.getAccessToken(),
46+
tokenResult.getRefreshToken(),
47+
expiresIn,
48+
inResponseTo
49+
)
4350
);
4451
}, listener::onFailure));
4552
}

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

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,20 @@ public class SamlAttributes implements Releasable {
2727

2828
private final SamlNameId name;
2929
private final String session;
30+
private final String inResponseTo;
3031
private final List<SamlAttribute> attributes;
3132
private final List<SamlPrivateAttribute> privateAttributes;
3233

33-
SamlAttributes(SamlNameId name, String session, List<SamlAttribute> attributes, List<SamlPrivateAttribute> privateAttributes) {
34+
SamlAttributes(
35+
SamlNameId name,
36+
String session,
37+
String inResponseTo,
38+
List<SamlAttribute> attributes,
39+
List<SamlPrivateAttribute> privateAttributes
40+
) {
3441
this.name = name;
3542
this.session = session;
43+
this.inResponseTo = inResponseTo;
3644
this.attributes = attributes;
3745
this.privateAttributes = privateAttributes;
3846
}
@@ -89,9 +97,24 @@ String session() {
8997
return session;
9098
}
9199

100+
String inResponseTo() {
101+
return inResponseTo;
102+
}
103+
92104
@Override
93105
public String toString() {
94-
return getClass().getSimpleName() + "(" + name + ")[" + session + "]{" + attributes + "}{" + privateAttributes + "}";
106+
return getClass().getSimpleName()
107+
+ "("
108+
+ name
109+
+ ")["
110+
+ session
111+
+ "]["
112+
+ inResponseTo
113+
+ "]{"
114+
+ attributes
115+
+ "}{"
116+
+ privateAttributes
117+
+ "}";
95118
}
96119

97120
@Override

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ private SamlAttributes authenticateResponse(Element element, Collection<String>
117117
final Assertion assertion = details.v1();
118118
final SamlNameId nameId = SamlNameId.forSubject(assertion.getSubject());
119119
final String session = getSessionIndex(assertion);
120-
final SamlAttributes samlAttributes = buildSamlAttributes(nameId, session, details.v2());
120+
final SamlAttributes samlAttributes = buildSamlAttributes(nameId, session, response.getInResponseTo(), details.v2());
121121
if (logger.isTraceEnabled()) {
122122
StringBuilder sb = new StringBuilder();
123123
sb.append("The SAML Assertion contained the following attributes: \n");
@@ -141,7 +141,7 @@ private SamlAttributes authenticateResponse(Element element, Collection<String>
141141
return samlAttributes;
142142
}
143143

144-
private SamlAttributes buildSamlAttributes(SamlNameId nameId, String session, List<Attribute> attributes) {
144+
private SamlAttributes buildSamlAttributes(SamlNameId nameId, String session, String inResponseTo, List<Attribute> attributes) {
145145
List<SamlAttributes.SamlAttribute> samlAttributes = new ArrayList<>();
146146
List<SamlAttributes.SamlPrivateAttribute> samlPrivateAttributes = new ArrayList<>();
147147
for (Attribute attribute : attributes) {
@@ -151,7 +151,7 @@ private SamlAttributes buildSamlAttributes(SamlNameId nameId, String session, Li
151151
samlAttributes.add(new SamlAttributes.SamlAttribute(attribute));
152152
}
153153
}
154-
return new SamlAttributes(nameId, session, samlAttributes, samlPrivateAttributes);
154+
return new SamlAttributes(nameId, session, inResponseTo, samlAttributes, samlPrivateAttributes);
155155
}
156156

157157
private static String getSessionIndex(Assertion assertion) {

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.elasticsearch.common.util.concurrent.ThreadContext;
3131
import org.elasticsearch.common.util.set.Sets;
3232
import org.elasticsearch.core.CheckedRunnable;
33+
import org.elasticsearch.core.Nullable;
3334
import org.elasticsearch.core.Releasable;
3435
import org.elasticsearch.core.Releasables;
3536
import org.elasticsearch.core.SuppressForbidden;
@@ -159,6 +160,7 @@ public final class SamlRealm extends Realm implements Releasable {
159160
public static final String TOKEN_METADATA_NAMEID_SP_QUALIFIER = "saml_nameid_sp_qual";
160161
public static final String TOKEN_METADATA_NAMEID_SP_PROVIDED_ID = "saml_nameid_sp_id";
161162
public static final String TOKEN_METADATA_SESSION = "saml_session";
163+
public static final String TOKEN_METADATA_IN_RESPONSE_TO = "saml_in_response_to";
162164
public static final String TOKEN_METADATA_REALM = "saml_realm";
163165

164166
public static final String PRIVATE_ATTRIBUTES_METADATA = "saml_private_attributes";
@@ -588,7 +590,7 @@ private void buildUser(SamlAttributes attributes, ActionListener<AuthenticationR
588590
return;
589591
}
590592

591-
final Map<String, Object> tokenMetadata = createTokenMetadata(attributes.name(), attributes.session());
593+
final Map<String, Object> tokenMetadata = createTokenMetadata(attributes.name(), attributes.session(), attributes.inResponseTo());
592594
final Map<String, List<SecureString>> privateAttributesMetadata = attributes.privateAttributes()
593595
.stream()
594596
.collect(Collectors.toMap(SamlPrivateAttribute::name, SamlPrivateAttribute::values));
@@ -639,7 +641,7 @@ private void buildUser(SamlAttributes attributes, ActionListener<AuthenticationR
639641
}));
640642
}
641643

642-
public Map<String, Object> createTokenMetadata(SamlNameId nameId, String session) {
644+
public Map<String, Object> createTokenMetadata(@Nullable SamlNameId nameId, String session, @Nullable String inResponseTo) {
643645
final Map<String, Object> tokenMeta = new HashMap<>();
644646
if (nameId != null) {
645647
tokenMeta.put(TOKEN_METADATA_NAMEID_VALUE, nameId.value);
@@ -655,6 +657,9 @@ public Map<String, Object> createTokenMetadata(SamlNameId nameId, String session
655657
tokenMeta.put(TOKEN_METADATA_NAMEID_SP_PROVIDED_ID, null);
656658
}
657659
tokenMeta.put(TOKEN_METADATA_SESSION, session);
660+
if (inResponseTo != null) {
661+
tokenMeta.put(TOKEN_METADATA_IN_RESPONSE_TO, inResponseTo);
662+
}
658663
tokenMeta.put(TOKEN_METADATA_REALM, name());
659664
return tokenMeta;
660665
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ public RestResponse buildResponse(SamlAuthenticateResponse response, XContentBui
102102
if (response.getAuthentication() != null) {
103103
builder.field("authentication", response.getAuthentication());
104104
}
105+
if (response.getInResponseTo() != null) {
106+
builder.field("in_response_to", response.getInResponseTo());
107+
}
105108
builder.endObject();
106109
return new RestResponse(RestStatus.OK, builder);
107110
}

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionActionTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ private TokenService.CreateTokenResult storeToken(byte[] userTokenBytes, byte[]
463463
.user(new User("bob"))
464464
.realmRef(new RealmRef("native", NativeRealmSettings.TYPE, "node01"))
465465
.build(false);
466-
final Map<String, Object> metadata = samlRealm.createTokenMetadata(nameId, session);
466+
final Map<String, Object> metadata = samlRealm.createTokenMetadata(nameId, session, null);
467467
final PlainActionFuture<TokenService.CreateTokenResult> future = new PlainActionFuture<>();
468468
tokenService.createOAuth2Tokens(userTokenBytes, refreshTokenBytes, authentication, authentication, metadata, future);
469469
return future.actionGet();

0 commit comments

Comments
 (0)