66 */
77package org .elasticsearch .xpack .security .authc .saml ;
88
9+ import org .elasticsearch .core .Strings ;
10+ import org .hamcrest .Matcher ;
911import org .opensaml .saml .common .xml .SAMLConstants ;
1012import org .opensaml .saml .saml2 .core .Issuer ;
1113import org .opensaml .saml .saml2 .core .LogoutRequest ;
1214import org .opensaml .saml .saml2 .core .NameID ;
1315import org .opensaml .saml .saml2 .metadata .EntityDescriptor ;
1416import org .opensaml .security .x509 .X509Credential ;
1517
18+ import java .io .ByteArrayInputStream ;
19+ import java .io .IOException ;
20+ import java .io .UncheckedIOException ;
1621import java .net .URLDecoder ;
1722import java .nio .charset .StandardCharsets ;
1823import java .security .InvalidKeyException ;
2631import java .time .ZonedDateTime ;
2732import java .util .Base64 ;
2833import java .util .Collections ;
34+ import java .util .HashMap ;
35+ import java .util .Map ;
36+ import java .util .function .BiFunction ;
37+ import java .util .zip .Inflater ;
38+ import java .util .zip .InflaterInputStream ;
2939
3040import static java .util .Collections .emptySet ;
3141import static java .util .Collections .singleton ;
3242import static org .hamcrest .Matchers .equalTo ;
33- import static org .hamcrest .Matchers .startsWith ;
43+ import static org .hamcrest .Matchers .notNullValue ;
3444
3545public class SamlRedirectTests extends SamlTestCase {
3646
@@ -42,19 +52,22 @@ public class SamlRedirectTests extends SamlTestCase {
4252
4353 private static final SigningConfiguration NO_SIGNING = new SigningConfiguration (emptySet (), null );
4454
55+ /**
56+ * XML template of LogoutRequest.
57+ */
58+ private static final String EXPECTED_LOGOUT_REQUEST_TEMPLATE = """
59+ <?xml version="1.0" encoding="UTF-8"?>\
60+ <saml2p:LogoutRequest xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" \
61+ Destination="%s" \
62+ ID="_id123456789" IssueInstant="2018-01-14T22:47:00.000Z" Version="2.0">\
63+ <saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://idp.test/</saml2:Issuer>\
64+ <saml2:NameID xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">name-123456-7890</saml2:NameID>\
65+ </saml2p:LogoutRequest>""" ;
66+
4567 public void testRedirectUrlWithoutRelayStateOrSigning () {
4668 final SamlRedirect redirect = new SamlRedirect (buildLogoutRequest (LOGOUT_URL ), NO_SIGNING );
4769 final String url = redirect .getRedirectUrl ();
48- assertThat (
49- url ,
50- equalTo (
51- LOGOUT_URL
52- + "?SAMLRequest=nZFBa4QwFIT%2FSnh3Naa2ax%2FqsiAFYdtDu91DLyVo2AY0cX2x9Oc36gpLCz30mAwz3"
53- + "wwv2351LftUA2lrcohDDkyZ2jbanHJ4PTwEKWyLjGTXih739mRH96zOoyLHvNMQLlIO42DQStKERnaK0NX4snvcowg59oN1trYtsNIbtZFupn04"
54- + "1xNGkW760HkhmrKidoYAq8oc3nUTi5vk9m6T3vsfolFVhpw0LgfB4zTgcRAnByEw2SDnIef8DdhxnePZcCmPs3m4Lv13Z0mkhqknFL96ZtF15kp"
55- + "48hlV%2BS%2FCJAbL0sBP5StgiSwuzx8HKL4B"
56- )
57- );
70+ assertRedirectUrl (url , Map .of ("SAMLRequest" , equalTo (buildExpectedLogoutRequestString (LOGOUT_URL ))));
5871 }
5972
6073 public void testRedirectUrlWithRelayStateAndSigning () throws Exception {
@@ -64,51 +77,93 @@ public void testRedirectUrlWithRelayStateAndSigning() throws Exception {
6477 );
6578 final SamlRedirect redirect = new SamlRedirect (buildLogoutRequest (LOGOUT_URL ), signing );
6679 final String url = redirect .getRedirectUrl ("hello" );
67- assertThat (
80+ assertRedirectUrl (
6881 url ,
69- startsWith (
70- LOGOUT_URL
71- + "?SAMLRequest=nZFBa4QwFIT%2FSnh3Naa2ax%2FqsiAFYdtDu91DLyVo2AY0cX2x9Oc36gpLC"
72- + "z30mAwz3wwv2351LftUA2lrcohDDkyZ2jbanHJ4PTwEKWyLjGTXih739mRH96zOoyLHvNMQLlIO42DQStKERnaK0NX4snvcowg59oN1trY"
73- + "tsNIbtZFupn041xNGkW760HkhmrKidoYAq8oc3nUTi5vk9m6T3vsfolFVhpw0LgfB4zTgcRAnByEw2SDnIef8DdhxnePZcCmPs3m4Lv13Z"
74- + "0mkhqknFL96ZtF15kp48hlV%2BS%2FCJAbL0sBP5StgiSwuzx8HKL4B"
75- + "&RelayState=hello"
76- + "&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256"
77- + "&Signature="
82+ Map . of (
83+ "SAMLRequest" ,
84+ equalTo ( buildExpectedLogoutRequestString ( LOGOUT_URL )),
85+ "RelayState" ,
86+ equalTo ( "hello" ),
87+ "SigAlg" ,
88+ equalTo ( "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" ),
89+ "Signature" ,
90+ notNullValue ( String . class )
7891 )
7992 );
8093 }
8194
8295 public void testRedirectUrlWithExistingParameters () {
8396 final SamlRedirect redirect = new SamlRedirect (buildLogoutRequest (LOGOUT_URL + "?a=xyz" ), NO_SIGNING );
8497 final String url = redirect .getRedirectUrl ("foo" );
85- assertThat (
98+ assertRedirectUrl (
8699 url ,
87- equalTo (
88- LOGOUT_URL
89- + "?a=xyz"
90- + "&SAMLRequest=nZFBS8QwFIT%2FSnn3tmmsbn00LUIRCqsHXT14kdCGNdAmtS%2BV1V9v2u7CouDBYzLMzDe8vDz0XfChRtLWCE"
91- + "giBoEyjW212Qt42t2GGZRFTrLv%2BIBbu7eTe1DvkyIXeKchXCUB02jQStKERvaK0DX4eHO3RR4xHEbrbGM7CCpv1Ea6pe3NuYE"
92- + "wjnU7RM4L8ZwVd0tJKcXh8wuCuhLwqtuEX6SXV5vs2v8QTao25KRxAjhLspAlYZLuOMd0g4xFjLEXCJ5PozwBHCfgYh7P0f8ml0"
93- + "RqnGmh%2BEWbx%2BeZp4Z7n1FX%2F2qYxXBdGvqp7FSwRhbH548zFN8%3D"
94- + "&RelayState=foo"
100+ Map .of (
101+ "SAMLRequest" ,
102+ equalTo (buildExpectedLogoutRequestString (LOGOUT_URL + "?a=xyz" )),
103+ "RelayState" ,
104+ equalTo ("foo" ),
105+ "a" ,
106+ equalTo ("xyz" )
95107 )
96108 );
97109 }
98110
99111 public void testRedirectUrlWithTrailingQuestionMark () {
100112 final SamlRedirect redirect = new SamlRedirect (buildLogoutRequest (LOGOUT_URL + "?" ), NO_SIGNING );
101113 final String url = redirect .getRedirectUrl ();
102- assertThat (
103- url ,
104- equalTo (
105- LOGOUT_URL
106- + "?SAMLRequest=nZFPS8QwFMS%2FSnj3tmmsbn30D0IRCqsHXffgRUIb1kCb1L5U%2FPim7R"
107- + "YWBQ8ek2HmN8PLyq%2B%2BY59qJG1NDnHIgSnT2FabUw4vh%2FsghbLISPadGHBvT3ZyT%2BpjUuSYdxrCVcphGg1aSZrQyF4Rug"
108- + "af7x72KEKOw2idbWwHrPJGbaRbaO%2FODYRRpNshdF6I5qyoWyAlsLrK4U23sbhKrm926a3%2FIZpUbchJ43IQPE4DHgdxchACkx"
109- + "1yHnLOX4Edtz0eDuf2uJjHy9Z%2Fl5ZEapyLQvGraBZdZm6ER59RV%2F8izGKwLg38VL4B1sji%2FPxxgeIb"
110- )
111- );
114+ assertRedirectUrl (url , Map .of ("SAMLRequest" , equalTo (buildExpectedLogoutRequestString (LOGOUT_URL + "?" ))));
115+ }
116+
117+ private static String buildExpectedLogoutRequestString (String destination ) {
118+ return Strings .format (EXPECTED_LOGOUT_REQUEST_TEMPLATE , destination );
119+ }
120+
121+ private static Map <String , String > parseAndDecodeUrlParameters (String url ) {
122+ return parseAndDecodeUrlParameters (url , (key , value ) -> value );
123+ }
124+
125+ private static Map <String , String > parseAndDecodeUrlParameters (String url , BiFunction <String , String , String > valueDecoder ) {
126+ Map <String , String > params = new HashMap <>();
127+ String [] parts = url .split ("\\ ?" , 2 );
128+ if (parts .length < 2 || parts [1 ].isEmpty ()) {
129+ return params ;
130+ }
131+ for (String param : parts [1 ].split ("&" )) {
132+ String [] keyValue = param .split ("=" , 2 );
133+ if (keyValue .length == 2 ) {
134+ String key = keyValue [0 ];
135+ String value = URLDecoder .decode (keyValue [1 ], StandardCharsets .UTF_8 );
136+ params .put (key , valueDecoder .apply (key , value ));
137+ }
138+ }
139+ return params ;
140+ }
141+
142+ private void assertRedirectUrl (String actualRequestUrl , Map <String , Matcher <String >> expectedParams ) {
143+ String [] parts = actualRequestUrl .split ("\\ ?" , 2 );
144+ assertThat (parts [0 ], equalTo (LOGOUT_URL ));
145+
146+ Map <String , String > actualParams = parseAndDecodeUrlParameters (actualRequestUrl , (key , value ) -> {
147+ if (key .equals ("SAMLRequest" )) {
148+ return decompressAndBase64Decode (value );
149+ }
150+ return value ;
151+ });
152+ assertThat ("URL parameter keys" , actualParams .keySet (), equalTo (expectedParams .keySet ()));
153+
154+ for (Map .Entry <String , Matcher <String >> expected : expectedParams .entrySet ()) {
155+ assertThat ("Parameter " + expected .getKey (), actualParams .get (expected .getKey ()), expected .getValue ());
156+ }
157+ }
158+
159+ private static String decompressAndBase64Decode (String compressedBase64 ) {
160+ byte [] compressed = Base64 .getDecoder ().decode (compressedBase64 );
161+ Inflater inflater = new Inflater (true );
162+ try (InflaterInputStream in = new InflaterInputStream (new ByteArrayInputStream (compressed ), inflater )) {
163+ return new String (in .readAllBytes (), StandardCharsets .UTF_8 );
164+ } catch (IOException e ) {
165+ throw new UncheckedIOException ("Failed to decompress input: " + compressedBase64 , e );
166+ }
112167 }
113168
114169 public void testLogoutRequestSigning () throws Exception {
@@ -156,18 +211,11 @@ public void testAuthnRequestSigning() throws Exception {
156211 assertThat (validateSignature (queryParam .substring (0 , queryParam .length () - 5 ), signature , credential ), equalTo (false ));
157212 }
158213
159- private String parseAndUrlDecodeParameter (String parameter ) {
160- final String value = parameter .split ("=" , 2 )[1 ];
161- return URLDecoder .decode (value , StandardCharsets .UTF_8 );
162- }
163-
164214 private String validateUrlAndGetSignature (String url ) {
165- final String params [] = url .split ("\\ ?" )[1 ].split ("&" );
166- assert (params .length == 3 );
167- String sigAlg = parseAndUrlDecodeParameter (params [1 ]);
215+ Map <String , String > params = parseAndDecodeUrlParameters (url );
168216 // We currently only support signing with SHA256withRSA, this test should be updated if we add support for more
169- assertThat (sigAlg , equalTo ("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" ));
170- return parseAndUrlDecodeParameter ( params [ 2 ] );
217+ assertThat (params . get ( "SigAlg" ) , equalTo ("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" ));
218+ return params . get ( "Signature" );
171219 }
172220
173221 private boolean validateSignature (String queryParam , String signature , X509Credential credential ) {
0 commit comments