From 60eead9cebefdf876ed0e3258ea868214e64afe2 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Wed, 26 Jan 2022 15:17:24 -0700 Subject: [PATCH] Add Session Index Support Closes gh-10613 --- .../DefaultSaml2AuthenticatedPrincipal.java | 18 ++++++++++++++++-- .../Saml2AuthenticatedPrincipal.java | 6 +++++- .../logout/OpenSamlLogoutRequestResolver.java | 17 ++++++++++++++++- .../OpenSaml4AuthenticationProvider.java | 15 +++++++++++++-- .../OpenSaml4AuthenticationProviderTests.java | 3 ++- .../authentication/TestOpenSamlObjects.java | 6 +++++- .../OpenSamlLogoutRequestResolverTests.java | 8 ++++++-- 7 files changed, 63 insertions(+), 10 deletions(-) diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipal.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipal.java index 22c12e1ebf..db478be3ab 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipal.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipal.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.security.saml2.provider.service.authentication; import java.io.Serializable; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -34,14 +35,22 @@ public class DefaultSaml2AuthenticatedPrincipal implements Saml2AuthenticatedPri private final Map> attributes; + private final List sessionIndexes; + private String registrationId; public DefaultSaml2AuthenticatedPrincipal(String name, Map> attributes) { + this(name, attributes, Collections.emptyList()); + } + + public DefaultSaml2AuthenticatedPrincipal(String name, Map> attributes, + List sessionIndexes) { Assert.notNull(name, "name cannot be null"); Assert.notNull(attributes, "attributes cannot be null"); + Assert.notNull(sessionIndexes, "sessionIndexes cannot be null"); this.name = name; this.attributes = attributes; - this.registrationId = null; + this.sessionIndexes = sessionIndexes; } @Override @@ -54,6 +63,11 @@ public class DefaultSaml2AuthenticatedPrincipal implements Saml2AuthenticatedPri return this.attributes; } + @Override + public List getSessionIndexes() { + return this.sessionIndexes; + } + @Override public String getRelyingPartyRegistrationId() { return this.registrationId; diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java index c40015f94f..205c5f8941 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,4 +76,8 @@ public interface Saml2AuthenticatedPrincipal extends AuthenticatedPrincipal { return null; } + default List getSessionIndexes() { + return Collections.emptyList(); + } + } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java index a4b39ce84d..14d95b9161 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,10 +31,12 @@ import org.opensaml.core.xml.io.MarshallingException; import org.opensaml.saml.saml2.core.Issuer; import org.opensaml.saml.saml2.core.LogoutRequest; import org.opensaml.saml.saml2.core.NameID; +import org.opensaml.saml.saml2.core.SessionIndex; import org.opensaml.saml.saml2.core.impl.IssuerBuilder; import org.opensaml.saml.saml2.core.impl.LogoutRequestBuilder; import org.opensaml.saml.saml2.core.impl.LogoutRequestMarshaller; import org.opensaml.saml.saml2.core.impl.NameIDBuilder; +import org.opensaml.saml.saml2.core.impl.SessionIndexBuilder; import org.w3c.dom.Element; import org.springframework.security.core.Authentication; @@ -67,6 +69,8 @@ final class OpenSamlLogoutRequestResolver { private final NameIDBuilder nameIdBuilder; + private final SessionIndexBuilder sessionIndexBuilder; + private final LogoutRequestBuilder logoutRequestBuilder; private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver; @@ -87,6 +91,9 @@ final class OpenSamlLogoutRequestResolver { Assert.notNull(this.issuerBuilder, "issuerBuilder must be configured in OpenSAML"); this.nameIdBuilder = (NameIDBuilder) registry.getBuilderFactory().getBuilder(NameID.DEFAULT_ELEMENT_NAME); Assert.notNull(this.nameIdBuilder, "nameIdBuilder must be configured in OpenSAML"); + this.sessionIndexBuilder = (SessionIndexBuilder) registry.getBuilderFactory() + .getBuilder(SessionIndex.DEFAULT_ELEMENT_NAME); + Assert.notNull(this.sessionIndexBuilder, "sessionIndexBuilder must be configured in OpenSAML"); } /** @@ -122,6 +129,14 @@ final class OpenSamlLogoutRequestResolver { NameID nameId = this.nameIdBuilder.buildObject(); nameId.setValue(authentication.getName()); logoutRequest.setNameID(nameId); + if (authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal) { + Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal(); + for (String index : principal.getSessionIndexes()) { + SessionIndex sessionIndex = this.sessionIndexBuilder.buildObject(); + sessionIndex.setSessionIndex(index); + logoutRequest.getSessionIndexes().add(sessionIndex); + } + } logoutRequestConsumer.accept(registration, logoutRequest); if (logoutRequest.getID() == null) { logoutRequest.setID("LR" + UUID.randomUUID()); diff --git a/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java b/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java index cac0c2a3ed..13671526bc 100644 --- a/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java +++ b/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,6 +57,7 @@ import org.opensaml.saml.saml2.assertion.impl.DelegationRestrictionConditionVali import org.opensaml.saml.saml2.core.Assertion; import org.opensaml.saml.saml2.core.Attribute; import org.opensaml.saml.saml2.core.AttributeStatement; +import org.opensaml.saml.saml2.core.AuthnStatement; import org.opensaml.saml.saml2.core.Condition; import org.opensaml.saml.saml2.core.EncryptedAssertion; import org.opensaml.saml.saml2.core.OneTimeUse; @@ -425,7 +426,9 @@ public final class OpenSaml4AuthenticationProvider implements AuthenticationProv Assertion assertion = CollectionUtils.firstElement(response.getAssertions()); String username = assertion.getSubject().getNameID().getValue(); Map> attributes = getAssertionAttributes(assertion); - DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal(username, attributes); + List sessionIndexes = getSessionIndexes(assertion); + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal(username, attributes, + sessionIndexes); String registrationId = responseToken.token.getRelyingPartyRegistration().getRegistrationId(); principal.setRelyingPartyRegistrationId(registrationId); return new Saml2Authentication(principal, token.getSaml2Response(), @@ -617,6 +620,14 @@ public final class OpenSaml4AuthenticationProvider implements AuthenticationProv return attributeMap; } + private static List getSessionIndexes(Assertion assertion) { + List sessionIndexes = new ArrayList<>(); + for (AuthnStatement statement : assertion.getAuthnStatements()) { + sessionIndexes.add(statement.getSessionIndex()); + } + return sessionIndexes; + } + private static Object getXmlObjectValue(XMLObject xmlObject) { if (xmlObject instanceof XSAny) { return ((XSAny) xmlObject).getTextContent(); diff --git a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java index 400d196b72..f05c3b0523 100644 --- a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java +++ b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -247,6 +247,7 @@ public class OpenSaml4AuthenticationProviderTests { expected.put("registeredDate", Collections.singletonList(registeredDate)); assertThat((String) principal.getFirstAttribute("name")).isEqualTo("John Doe"); assertThat(principal.getAttributes()).isEqualTo(expected); + assertThat(principal.getSessionIndexes()).contains("session-index"); } @Test diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java index 643df7e685..1c7b023314 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,6 +49,7 @@ import org.opensaml.saml.saml2.core.Attribute; import org.opensaml.saml.saml2.core.AttributeStatement; import org.opensaml.saml.saml2.core.AttributeValue; import org.opensaml.saml.saml2.core.AuthnRequest; +import org.opensaml.saml.saml2.core.AuthnStatement; import org.opensaml.saml.saml2.core.Conditions; import org.opensaml.saml.saml2.core.EncryptedAssertion; import org.opensaml.saml.saml2.core.EncryptedAttribute; @@ -153,6 +154,9 @@ public final class TestOpenSamlObjects { confirmationData.setRecipient(recipientUri); subjectConfirmation.setSubjectConfirmationData(confirmationData); assertion.getSubject().getSubjectConfirmations().add(subjectConfirmation); + AuthnStatement statement = build(AuthnStatement.DEFAULT_ELEMENT_NAME); + statement.setSessionIndex("session-index"); + assertion.getAuthnStatements().add(statement); return assertion; } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolverTests.java index d386fefddc..5c831e8264 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolverTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package org.springframework.security.saml2.provider.service.web.authentication.l import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import jakarta.servlet.http.HttpServletRequest; @@ -86,10 +87,13 @@ public class OpenSamlLogoutRequestResolverTests { Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding(); LogoutRequest logoutRequest = getLogoutRequest(saml2LogoutRequest.getSamlRequest(), binding); assertThat(logoutRequest.getNameID().getValue()).isEqualTo(authentication.getName()); + assertThat(logoutRequest.getSessionIndexes()).hasSize(1); + assertThat(logoutRequest.getSessionIndexes().get(0).getSessionIndex()).isEqualTo("session-index"); } private Saml2Authentication authentication(RelyingPartyRegistration registration) { - DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>()); + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>(), + Arrays.asList("session-index")); principal.setRelyingPartyRegistrationId(registration.getRegistrationId()); return new Saml2Authentication(principal, "response", new ArrayList<>()); }