Skip to content

Commit 14b1d78

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 Added extra parameter to the classes where the raw parameters were needed. Basically, just a map containing the raw URL parameters. Keyed on the name and with key=value as the value. This is to preserve all variations like foo=bar, blank= and empty (ex foo=bar&blank=&empty). Existing HttpRequest abstraction not modified, as the use of raw url parameters is unusual in most situations. Note: The SigAlg parameter is now mandatory, test modified. There was a default SigAlg added to the url in the verification step when missing. This conflicted with the raw url parameters solution. 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 560d41d commit 14b1d78

7 files changed

Lines changed: 352 additions & 99 deletions

File tree

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

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import javax.xml.xpath.XPathExpressionException;
1414

15+
import org.apache.commons.lang3.StringUtils;
1516
import org.apache.commons.lang3.text.StrSubstitutor;
1617
import org.joda.time.DateTime;
1718
import org.slf4j.Logger;
@@ -60,6 +61,11 @@ public class LogoutRequest {
6061
*/
6162
private final HttpRequest request;
6263

64+
/**
65+
* Map with raw request parameters as received in the queryString. Required for accurate signature verification.
66+
*/
67+
private Map<String, String> rawRequestParams;
68+
6369
/**
6470
* NameID.
6571
*/
@@ -92,16 +98,19 @@ public class LogoutRequest {
9298
* OneLogin_Saml2_Settings
9399
* @param request
94100
* the HttpRequest object to be processed (Contains GET and POST parameters, request URL, ...).
101+
* @param rawRequestParams
102+
* map with 'raw' url request parameters for signature validation (keyed on name, value is key & value)
95103
* @param nameId
96104
* The NameID that will be set in the LogoutRequest.
97105
* @param sessionIndex
98106
* The SessionIndex (taken from the SAML Response in the SSO process).
99107
* @throws XMLEntityException
100108
*
101109
*/
102-
public LogoutRequest(Saml2Settings settings, HttpRequest request, String nameId, String sessionIndex) throws XMLEntityException {
110+
public LogoutRequest(Saml2Settings settings, HttpRequest request, Map<String, String> rawRequestParams, String nameId, String sessionIndex) throws XMLEntityException {
103111
this.settings = settings;
104112
this.request = request;
113+
this.rawRequestParams = rawRequestParams;
105114

106115
String samlLogoutRequest = null;
107116

@@ -133,7 +142,7 @@ public LogoutRequest(Saml2Settings settings, HttpRequest request, String nameId,
133142
* @throws XMLEntityException
134143
*/
135144
public LogoutRequest(Saml2Settings settings) throws XMLEntityException {
136-
this(settings, null, null, null);
145+
this(settings, null, null, null, null);
137146
}
138147

139148
/**
@@ -143,11 +152,13 @@ public LogoutRequest(Saml2Settings settings) throws XMLEntityException {
143152
* OneLogin_Saml2_Settings
144153
* @param request
145154
* the HttpRequest object to be processed (Contains GET and POST parameters, request URL, ...).
155+
* @param rawRequestParams
156+
* map with 'raw' url request parameters for signature validation (keyed on name, value is key & value)
146157
*
147158
* @throws XMLEntityException
148159
*/
149-
public LogoutRequest(Saml2Settings settings, HttpRequest request) throws XMLEntityException {
150-
this(settings, request, null, null);
160+
public LogoutRequest(Saml2Settings settings, HttpRequest request, Map<String, String> rawRequestParams) throws XMLEntityException {
161+
this(settings, request, rawRequestParams, null, null);
151162
}
152163

153164
/**
@@ -334,15 +345,19 @@ public Boolean isValid() throws Exception {
334345
if (signAlg == null || signAlg.isEmpty()) {
335346
signAlg = Constants.RSA_SHA1;
336347
}
337-
String relayState = request.getParameter("RelayState");
338-
339-
String signedQuery = "SAMLRequest=" + Util.urlEncoder(request.getParameter("SAMLRequest"));
340-
341-
if (relayState != null && !relayState.isEmpty()) {
342-
signedQuery += "&RelayState=" + Util.urlEncoder(relayState);
348+
349+
String rawSamlRequest = rawRequestParams.get("SAMLRequest");
350+
String rawRelayState = rawRequestParams.get("RelayState");
351+
String rawSigAlg = rawRequestParams.get("SigAlg");
352+
353+
String signedQuery = "";
354+
signedQuery += rawSamlRequest;
355+
356+
if (StringUtils.isNotBlank(rawRelayState)) {
357+
signedQuery += "&" + rawRelayState;
343358
}
344359

345-
signedQuery += "&SigAlg=" + Util.urlEncoder(signAlg);
360+
signedQuery += "&" + rawSigAlg;
346361

347362
if (!Util.validateBinarySignature(signedQuery, Util.base64decoder(signature), cert, signAlg)) {
348363
throw new ValidationError("Signature validation failed. Logout Request rejected", ValidationError.INVALID_SIGNATURE);

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

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
import java.util.HashMap;
88
import java.util.Map;
99
import java.util.Objects;
10+
import java.util.regex.Pattern;
1011

1112
import javax.xml.xpath.XPathExpressionException;
1213

14+
import org.apache.commons.lang3.StringUtils;
1315
import org.apache.commons.lang3.text.StrSubstitutor;
1416
import org.slf4j.Logger;
1517
import org.slf4j.LoggerFactory;
@@ -61,6 +63,11 @@ public class LogoutResponse {
6163
*/
6264
private final HttpRequest request;
6365

66+
/**
67+
* Map with raw request parameters as received in the queryString. Required for accurate signature verification.
68+
*/
69+
private Map<String, String> rawRequestParams;
70+
6471
/**
6572
* URL of the current host + current view
6673
*/
@@ -88,11 +95,14 @@ public class LogoutResponse {
8895
* OneLogin_Saml2_Settings
8996
* @param request
9097
* the HttpRequest object to be processed (Contains GET and POST parameters, request URL, ...).
98+
* @param rawRequestParams
99+
* map with 'raw' url request parameters for signature validation (keyed on name, value is key & value)
91100
*
92101
*/
93-
public LogoutResponse(Saml2Settings settings, HttpRequest request) {
102+
public LogoutResponse(Saml2Settings settings, HttpRequest request, Map<String, String> rawRequestParams) {
94103
this.settings = settings;
95104
this.request = request;
105+
this.rawRequestParams = rawRequestParams;
96106

97107
String samlLogoutResponse = null;
98108
if (request != null) {
@@ -214,20 +224,25 @@ public Boolean isValid(String requestId) {
214224
if (cert == null) {
215225
throw new SettingsException("In order to validate the sign on the Logout Response, the x509cert of the IdP is required", SettingsException.CERT_NOT_FOUND);
216226
}
217-
227+
228+
218229
String signAlg = request.getParameter("SigAlg");
219230
if (signAlg == null || signAlg.isEmpty()) {
220231
signAlg = Constants.RSA_SHA1;
221232
}
222233

223-
String signedQuery = "SAMLResponse=" + Util.urlEncoder(request.getParameter("SAMLResponse"));
224-
225-
String relayState = request.getParameter("RelayState");
226-
if (relayState != null && !relayState.isEmpty()) {
227-
signedQuery += "&RelayState=" + Util.urlEncoder(relayState);
234+
String rawSamlResponse = rawRequestParams.get("SAMLResponse");
235+
String rawRelayState = rawRequestParams.get("RelayState");
236+
String rawSigAlg = rawRequestParams.get("SigAlg");
237+
238+
String signedQuery = "";
239+
signedQuery += rawSamlResponse;
240+
241+
if (StringUtils.isNotBlank(rawRelayState)) {
242+
signedQuery += "&" + rawRelayState;
228243
}
229244

230-
signedQuery += "&SigAlg=" + Util.urlEncoder(signAlg);
245+
signedQuery += "&" + rawSigAlg;
231246

232247
if (!Util.validateBinarySignature(signedQuery, Util.base64decoder(signature), cert, signAlg)) {
233248
throw new ValidationError("Signature validation failed. Logout Response rejected", ValidationError.INVALID_SIGNATURE);
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.onelogin.saml2.util;
2+
3+
import java.util.Collections;
4+
import java.util.LinkedHashMap;
5+
import java.util.Map;
6+
import java.util.regex.Matcher;
7+
import java.util.regex.Pattern;
8+
9+
import org.apache.commons.lang3.StringUtils;
10+
11+
/**
12+
* Utility for splitting raw url a raw url query string
13+
* (foo=bar&hello=world&data=Hi%20there) into
14+
*
15+
*/
16+
public final class QueryStringSplitter {
17+
18+
// Group 1 contains key & value, group 2 contains key only
19+
private static final Pattern RE_QUERY = Pattern.compile("(([^=&]+)(?:=[^&]*)?)(?:&|$)");
20+
21+
private QueryStringSplitter() {
22+
// static methods only, no instance
23+
}
24+
25+
/**
26+
* Splits a raw url query string into a map where:
27+
* <ul>
28+
* <li>the key is the parameter name
29+
* <li>the value is the key & value
30+
* </ul>
31+
* <b>Note:</b> For keys with multiple values, the first value is used.
32+
* <p>
33+
* Given the string "foo=bar&hello=world&empty&blank=&data=Hi%20there" it
34+
* will return the map:
35+
* <ul>
36+
* <li>foo: foo=bar
37+
* <li>hello: hello=world
38+
* <li>empty: empty
39+
* <li>blank: blank=
40+
* <li>data: data=Hi%20there
41+
* </ul>
42+
*
43+
* @param queryString
44+
* raw url query string
45+
*
46+
* @returns Unmodifiable map, empty if input is empty or null
47+
*/
48+
public static Map<String, String> split(final String queryString) {
49+
if (StringUtils.isBlank(queryString)) {
50+
return Collections.emptyMap();
51+
}
52+
53+
Map<String, String> results = new LinkedHashMap<>();
54+
Matcher matcher = RE_QUERY.matcher(queryString);
55+
while (matcher.find()) {
56+
String key = matcher.group(2);
57+
if (!results.containsKey(key)) {
58+
results.put(key, matcher.group(1));
59+
}
60+
}
61+
62+
return Collections.unmodifiableMap(results);
63+
}
64+
65+
}

0 commit comments

Comments
 (0)