Skip to content

Commit e1a9d09

Browse files
author
merit\rembjo0
committed
Use raw url parameters in redirect signature validation
This patch implements the 'use original URL-encoded values it receives on the query string' to verify the signature when http redirect is used. We encountered this problem when integrating with Microsoft ADFS 2.0. The server uses UPPERCASE in url encodings, this conflicts with the lowercase url encoding used here (ex %2B vs %2b). Solution Modified the HttpRequest class to contain the original query string. Extracts the raw url paramters using the getEncodedParamter(). This solution is inspired by the python implementation. Added test cases to demonstate handling of different signatures based on the url encoding used. From saml-bindings-2.0 (http://docs.oasis-open.org/security/saml/v2.0/) 3.4.4.1 DEFLATE Encoding Identification: urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE To construct the signature, a string consisting of the concatenation of the RelayState (if present), SigAlg, and SAMLRequest (or SAMLResponse) query string parameters (each one URLencoded) is constructed in one of the following ways (ordered as below): SAMLRequest=value&RelayState=value&SigAlg=value SAMLResponse=value&RelayState=value&SigAlg=value Further, note that URL-encoding is not canonical; that is, there are multiple legal encodings for a given value. The relying party MUST therefore perform the verification step using the original URL-encoded values it received on the query string. It is not sufficient to re-encode the parameters after they have been processed by software because the resulting encoding may not match the signer's encoding Finally, note that if there is no RelayState value, the entire parameter should be omitted from the signature computation (and not included as an empty parameter name).
1 parent 5ad511c commit e1a9d09

11 files changed

Lines changed: 509 additions & 17 deletions

File tree

core/src/main/java/com/onelogin/saml2/http/HttpRequest.java

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,25 @@
1010
import java.util.List;
1111
import java.util.Map;
1212
import java.util.Objects;
13+
import java.util.regex.Matcher;
14+
import java.util.regex.Pattern;
15+
16+
import org.apache.commons.lang3.StringUtils;
17+
18+
import com.onelogin.saml2.util.Util;
1319

1420
/**
1521
* Framework-agnostic representation of an HTTP request.
1622
*
1723
* @since 2.0.0
1824
*/
1925
public final class HttpRequest {
26+
27+
public static final Map<String, List<String>> EMPTY_PARAMETERS = Collections.<String, List<String>>emptyMap();
28+
2029
private final String requestURL;
2130
private final Map<String, List<String>> parameters;
31+
private final String queryString;
2232

2333
/**
2434
* Creates a new HttpRequest.
@@ -27,9 +37,19 @@ public final class HttpRequest {
2737
* @throws NullPointerException if requestURL is null
2838
*/
2939
public HttpRequest(String requestURL) {
30-
this(requestURL, Collections.<String, List<String>>emptyMap());
40+
this(requestURL, EMPTY_PARAMETERS);
3141
}
3242

43+
/**
44+
* Creates a new HttpRequest.
45+
*
46+
* @param requestURL the request URL (up to but not including query parameters)
47+
* @param query string that is contained in the request URL after the path
48+
*/
49+
public HttpRequest(String requestURL, String queryString) {
50+
this(requestURL, EMPTY_PARAMETERS, queryString);
51+
}
52+
3353
/**
3454
* Creates a new HttpRequest.
3555
*
@@ -38,10 +58,24 @@ public HttpRequest(String requestURL) {
3858
* @throws NullPointerException if any of the parameters is null
3959
*/
4060
public HttpRequest(String requestURL, Map<String, List<String>> parameters) {
61+
this(requestURL, parameters, null);
62+
}
63+
64+
/**
65+
* Creates a new HttpRequest.
66+
*
67+
* @param requestURL the request URL (up to but not including query parameters)
68+
* @param parameters the request query parameters
69+
* @param query string that is contained in the request URL after the path
70+
* @throws NullPointerException if any of the parameters is null
71+
*/
72+
public HttpRequest(String requestURL, Map<String, List<String>> parameters, String queryString) {
4173
this.requestURL = checkNotNull(requestURL, "requestURL");
4274
this.parameters = unmodifiableCopyOf(checkNotNull(parameters, "queryParams"));
75+
this.queryString = StringUtils.trimToEmpty(queryString);
4376
}
4477

78+
4579
/**
4680
* @param name the query parameter name
4781
* @param value the query parameter value
@@ -58,7 +92,7 @@ public HttpRequest addParameter(String name, String value) {
5892
final Map<String, List<String>> params = new HashMap<>(parameters);
5993
params.put(name, newValues);
6094

61-
return new HttpRequest(requestURL, params);
95+
return new HttpRequest(requestURL, params, queryString);
6296
}
6397

6498
/**
@@ -72,7 +106,7 @@ public HttpRequest removeParameter(String name) {
72106
final Map<String, List<String>> params = new HashMap<>(parameters);
73107
params.remove(name);
74108

75-
return new HttpRequest(requestURL, params);
109+
return new HttpRequest(requestURL, params, queryString);
76110
}
77111

78112
/**
@@ -110,6 +144,38 @@ public Map<String, List<String>> getParameters() {
110144
return parameters;
111145
}
112146

147+
/**
148+
* Return an url encoded get parameter value
149+
* Prefer to extract the original encoded value directly from queryString since url
150+
* encoding is not canonical.
151+
*
152+
* @param name
153+
* @return the first value for the parameter, or null
154+
*/
155+
public String getEncodedParameter(String name) {
156+
Matcher matcher = Pattern.compile(Pattern.quote(name) + "=([^&]+)").matcher(queryString);
157+
if (matcher.find()) {
158+
return matcher.group(1);
159+
} else {
160+
return Util.urlEncoder(getParameter(name));
161+
}
162+
}
163+
164+
/**
165+
* Return an url encoded get parameter value
166+
* Prefer to extract the original encoded value directly from queryString since url
167+
* encoding is not canonical.
168+
*
169+
* @param name
170+
* @param defaultValue
171+
* @return the first value for the parameter, or url encoded default value
172+
*/
173+
public String getEncodedParameter(String name, String defaultValue) {
174+
String value = getEncodedParameter(name);
175+
return (value != null ? value : Util.urlEncoder(defaultValue));
176+
}
177+
178+
113179
@Override
114180
public boolean equals(Object o) {
115181
if (this == o) {
@@ -122,19 +188,21 @@ public boolean equals(Object o) {
122188

123189
HttpRequest that = (HttpRequest) o;
124190
return Objects.equals(requestURL, that.requestURL) &&
125-
Objects.equals(parameters, that.parameters);
191+
Objects.equals(parameters, that.parameters) &&
192+
Objects.equals(queryString, this.queryString);
126193
}
127194

128195
@Override
129196
public int hashCode() {
130-
return Objects.hash(requestURL, parameters);
197+
return Objects.hash(requestURL, parameters, queryString);
131198
}
132199

133200
@Override
134201
public String toString() {
135202
return "HttpRequest{" +
136203
"requestURL='" + requestURL + '\'' +
137204
", parameters=" + parameters +
205+
", queryString=" + queryString +
138206
'}';
139207
}
140208

@@ -146,4 +214,6 @@ private static Map<String, List<String>> unmodifiableCopyOf(Map<String, List<Str
146214

147215
return unmodifiableMap(copy);
148216
}
217+
218+
149219
}

core/src/main/java/com/onelogin/saml2/logout/LogoutRequest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -334,16 +334,16 @@ public Boolean isValid() throws Exception {
334334
if (signAlg == null || signAlg.isEmpty()) {
335335
signAlg = Constants.RSA_SHA1;
336336
}
337-
String relayState = request.getParameter("RelayState");
337+
String relayState = request.getEncodedParameter("RelayState");
338338

339-
String signedQuery = "SAMLRequest=" + Util.urlEncoder(request.getParameter("SAMLRequest"));
339+
String signedQuery = "SAMLRequest=" + request.getEncodedParameter("SAMLRequest");
340340

341341
if (relayState != null && !relayState.isEmpty()) {
342-
signedQuery += "&RelayState=" + Util.urlEncoder(relayState);
342+
signedQuery += "&RelayState=" + relayState;
343343
}
344344

345-
signedQuery += "&SigAlg=" + Util.urlEncoder(signAlg);
346-
345+
signedQuery += "&SigAlg=" + request.getEncodedParameter("SigAlg", signAlg);
346+
347347
if (!Util.validateBinarySignature(signedQuery, Util.base64decoder(signature), cert, signAlg)) {
348348
throw new ValidationError("Signature validation failed. Logout Request rejected", ValidationError.INVALID_SIGNATURE);
349349
}

core/src/main/java/com/onelogin/saml2/logout/LogoutResponse.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -220,15 +220,15 @@ public Boolean isValid(String requestId) {
220220
signAlg = Constants.RSA_SHA1;
221221
}
222222

223-
String signedQuery = "SAMLResponse=" + Util.urlEncoder(request.getParameter("SAMLResponse"));
223+
String signedQuery = "SAMLResponse=" + request.getEncodedParameter("SAMLResponse");
224224

225-
String relayState = request.getParameter("RelayState");
225+
String relayState = request.getEncodedParameter("RelayState");
226226
if (relayState != null && !relayState.isEmpty()) {
227-
signedQuery += "&RelayState=" + Util.urlEncoder(relayState);
227+
signedQuery += "&RelayState=" + relayState;
228228
}
229229

230-
signedQuery += "&SigAlg=" + Util.urlEncoder(signAlg);
231-
230+
signedQuery += "&SigAlg=" + request.getEncodedParameter("SigAlg", signAlg);
231+
232232
if (!Util.validateBinarySignature(signedQuery, Util.base64decoder(signature), cert, signAlg)) {
233233
throw new ValidationError("Signature validation failed. Logout Response rejected", ValidationError.INVALID_SIGNATURE);
234234
}

core/src/test/java/com/onelogin/saml2/http/HttpRequestTest.java

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@
1010

1111
import java.util.Arrays;
1212
import java.util.Collections;
13+
import java.util.HashMap;
1314
import java.util.List;
1415
import java.util.Map;
1516

1617
import org.junit.Test;
1718

19+
import com.onelogin.saml2.test.NaiveUrlEncoder;
20+
import com.onelogin.saml2.util.Util;
21+
1822
public class HttpRequestTest {
1923
@Test
2024
public void testConstructorWithNoQueryParams() throws Exception {
@@ -43,7 +47,7 @@ public void testConstructorWithQueryParams() throws Exception {
4347
assertThat(request.getParameters(name), equalTo(values));
4448
assertThat(request.getParameter(name), equalTo(value1));
4549
}
46-
50+
4751
@Test
4852
public void testAddParameter() throws Exception {
4953
final String url = "some_url";
@@ -55,6 +59,9 @@ public void testAddParameter() throws Exception {
5559
assertThat(request.getParameters(), equalTo(singletonMap(name, singletonList(value))));
5660
assertThat(request.getParameters(name), equalTo(singletonList(value)));
5761
assertThat(request.getParameter(name), equalTo(value));
62+
63+
final HttpRequest request2 = request.addParameter(name, value);
64+
assertThat(request2.getParameters(name), equalTo(Arrays.asList(value, value)));
5865
}
5966

6067
@Test
@@ -75,4 +82,107 @@ public void testRemoveParameter() throws Exception {
7582
assertTrue(request.getParameters(name).isEmpty());
7683
assertNull(request.getParameter(name));
7784
}
85+
86+
@Test
87+
public void testGetEncodedParameter_encodesParametersNotOnQueryString() throws Exception {
88+
final String url = "url";
89+
final String name = "name";
90+
final String value1 = "val/1!";
91+
final String addedName = "added";
92+
final String addedValue = "added#value!";
93+
94+
final List<String> values = Arrays.asList(value1);
95+
final Map<String, List<String>> parametersMap = singletonMap(name, values);
96+
97+
final HttpRequest request = new HttpRequest(url, parametersMap).addParameter(addedName, addedValue);
98+
99+
assertThat(request.getEncodedParameter(name), equalTo(Util.urlEncoder(value1)));
100+
assertThat(request.getEncodedParameter(addedName), equalTo(Util.urlEncoder(addedValue)));
101+
}
102+
103+
@Test
104+
public void testGetEncodedParameter_prefersValueFromQueryString() throws Exception {
105+
final String url = "url";
106+
final String name = "name";
107+
final String value1 = "value1";
108+
final String urlValue1 = "onUrl1";
109+
final String queryString = name + "=" + urlValue1;
110+
111+
final List<String> values = Arrays.asList(value1);
112+
final Map<String, List<String>> parametersMap = singletonMap(name, values);
113+
114+
final HttpRequest request = new HttpRequest(url, parametersMap, queryString);
115+
116+
assertThat(request.getEncodedParameter(name), equalTo(urlValue1));
117+
assertThat(request.getParameter(name), equalTo(value1));
118+
}
119+
120+
@Test
121+
public void testGetEncodedParameter_returnsExactAsGivenInQueryString() throws Exception {
122+
final String url = "url";
123+
final String name = "name";
124+
String encodedValue1 = NaiveUrlEncoder.encode("do not alter!");
125+
final String queryString = name + "=" + encodedValue1;
126+
127+
final HttpRequest request = new HttpRequest(url, queryString);
128+
129+
assertThat(request.getEncodedParameter(name), equalTo(encodedValue1));
130+
}
131+
132+
@Test
133+
public void testGetEncodedParameter_handlesMultipleValuesOnQueryString() throws Exception {
134+
final String url = "url";
135+
final String queryString = "k1=v1&k2=v2&k3=v3";
136+
137+
final Map<String, List<String>> parametersMap = new HashMap<>();
138+
final HttpRequest request = new HttpRequest(url, parametersMap, queryString);
139+
140+
assertThat(request.getEncodedParameter("k1"), equalTo("v1"));
141+
assertThat(request.getEncodedParameter("k2"), equalTo("v2"));
142+
assertThat(request.getEncodedParameter("k3"), equalTo("v3"));
143+
}
144+
145+
146+
@Test
147+
public void testGetEncodedParameter_withDefault_usesDefaultWhenParameterMissing() throws Exception {
148+
final String url = "url";
149+
final String foobar = "foo/bar!";
150+
151+
final HttpRequest request = new HttpRequest(url);
152+
assertThat(request.getEncodedParameter("missing", foobar), equalTo(Util.urlEncoder(foobar)));
153+
}
154+
155+
156+
@Test
157+
public void testAddParameter_preservesQueryString() throws Exception {
158+
final String url = "url";
159+
final String name = "name";
160+
final String value1 = "val/1!";
161+
String encodedValue1 = NaiveUrlEncoder.encode(value1);
162+
final String queryString = name + "=" + encodedValue1;
163+
164+
final Map<String, List<String>> parametersMap = new HashMap<>();
165+
final HttpRequest request = new HttpRequest(url, parametersMap, queryString).addParameter(name, value1);
166+
167+
assertThat(request.getEncodedParameter(name), equalTo(encodedValue1));
168+
}
169+
170+
@Test
171+
public void testRemoveParameter_preservesQueryString() throws Exception {
172+
final String url = "url";
173+
final String name = "name";
174+
final String value1 = "val/1!";
175+
String encodedValue1 = NaiveUrlEncoder.encode(value1);
176+
final String queryString = name + "=" + encodedValue1;
177+
178+
final List<String> values = Arrays.asList(value1);
179+
final Map<String, List<String>> parametersMap = singletonMap(name, values);
180+
181+
final HttpRequest request = new HttpRequest(url, parametersMap, queryString).removeParameter(name);
182+
183+
assertThat(request.getEncodedParameter(name), equalTo(encodedValue1));
184+
}
185+
186+
187+
78188
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.onelogin.saml2.test;
2+
3+
import java.io.UnsupportedEncodingException;
4+
import java.net.URLDecoder;
5+
6+
import org.junit.Assert;
7+
import org.junit.Test;
8+
9+
import com.onelogin.saml2.util.Util;
10+
11+
public class NaiveUrlEncodeTest {
12+
13+
@Test
14+
public void testDemonstratingUrlEncodingNotCanonical () throws UnsupportedEncodingException {
15+
String theString = "Hello World!";
16+
17+
String naiveEncoded = NaiveUrlEncoder.encode(theString);
18+
String propperEncoded = Util.urlEncoder(theString);
19+
20+
Assert.assertNotEquals("Encoded versions should differ", naiveEncoded, propperEncoded);
21+
Assert.assertEquals("Decoded versions equal", URLDecoder.decode(naiveEncoded, "UTF-8"), URLDecoder.decode(propperEncoded, "UTF-8"));
22+
}
23+
24+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.onelogin.saml2.test;
2+
3+
import java.io.UnsupportedEncodingException;
4+
5+
public final class NaiveUrlEncoder {
6+
7+
private NaiveUrlEncoder () {
8+
//static class only
9+
}
10+
11+
/**
12+
* Encodes ALL characters in URL using %xx encoding.<br>
13+
* This differs from 'normal' url encoding.
14+
*/
15+
public static String encode (String s) throws UnsupportedEncodingException {
16+
StringBuilder buf = new StringBuilder();
17+
for (byte b : s.getBytes("UTF-8")) {
18+
buf.append("%");
19+
buf.append(String.format("%02x", b));
20+
}
21+
return buf.toString();
22+
}
23+
24+
}

0 commit comments

Comments
 (0)