Skip to content

Commit 54afc56

Browse files
committed
Add support for Subjects on AuthNRequests by the new parameter nameIdValueReq
1 parent 952deb6 commit 54afc56

File tree

5 files changed

+168
-6
lines changed

5 files changed

+168
-6
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,11 +393,12 @@ We can set a 'returnTo' url parameter to the login function and that will be con
393393
String targetUrl = 'https://example.com';
394394
auth.login(returnTo=targetUrl)
395395
```
396-
The login method can receive 4 more optional parameters:
396+
The login method can receive 5 more optional parameters:
397397
- *forceAuthn* When true the AuthNRequest will have the 'ForceAuthn' attribute set to 'true'
398398
- *isPassive* When true the AuthNRequest will have the 'Ispassive' attribute set to 'true'
399399
- *setNameIdPolicy* When true the AuthNRequest will set a nameIdPolicy element.
400400
- *stay* Set to true to stay (returns the url string), otherwise set to false to execute a redirection to that url (IdP SSO URL)
401+
- *nameIdValueReq* Indicates to the IdP the subject that should be authenticated
401402

402403
By default, the login method initiates a redirect to the SAML Identity Provider. You can use the *stay* parameter, to prevent that, and execute the redirection manually. We need to use that if a match on the future SAMLResponse ID and the AuthNRequest ID to be sent is required. That AuthNRequest ID must be extracted and stored for future validation, so we can't execute the redirection on the login. Instead, set *stay* to true, then get that ID by
403404
```

core/src/main/java/com/onelogin/saml2/authn/AuthnRequest.java

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ public class AuthnRequest {
5757
*/
5858
private final boolean setNameIdPolicy;
5959

60+
/**
61+
* Indicates to the IdP the subject that should be authenticated
62+
*/
63+
private final String nameIdValueReq;
6064

6165
/**
6266
* Time stamp that indicates when the AuthNRequest was created
@@ -84,20 +88,39 @@ public AuthnRequest(Saml2Settings settings) {
8488
* When true the AuthNReuqest will set the IsPassive='true'
8589
* @param setNameIdPolicy
8690
* When true the AuthNReuqest will set a nameIdPolicy
91+
* @param nameIdValueReq
92+
* Indicates to the IdP the subject that should be authenticated
8793
*/
88-
public AuthnRequest(Saml2Settings settings, boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy) {
94+
public AuthnRequest(Saml2Settings settings, boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy, String nameIdValueReq) {
8995
this.id = Util.generateUniqueID(settings.getUniqueIDPrefix());
9096
issueInstant = Calendar.getInstance();
9197
this.isPassive = isPassive;
9298
this.settings = settings;
9399
this.forceAuthn = forceAuthn;
94100
this.setNameIdPolicy = setNameIdPolicy;
101+
this.nameIdValueReq = nameIdValueReq;
95102

96103
StrSubstitutor substitutor = generateSubstitutor(settings);
97104
authnRequestString = substitutor.replace(getAuthnRequestTemplate());
98105
LOGGER.debug("AuthNRequest --> " + authnRequestString);
99106
}
100107

108+
/**
109+
* Constructs the AuthnRequest object.
110+
*
111+
* @param settings
112+
* OneLogin_Saml2_Settings
113+
* @param forceAuthn
114+
* When true the AuthNReuqest will set the ForceAuthn='true'
115+
* @param isPassive
116+
* When true the AuthNReuqest will set the IsPassive='true'
117+
* @param setNameIdPolicy
118+
* When true the AuthNReuqest will set a nameIdPolicy
119+
*/
120+
public AuthnRequest(Saml2Settings settings, boolean forceAuthn, boolean isPassive, boolean setNameIdPolicy) {
121+
this(settings, forceAuthn, isPassive, setNameIdPolicy, null);
122+
}
123+
101124
/**
102125
* @return the base64 encoded unsigned AuthnRequest (deflated or not)
103126
*
@@ -167,6 +190,16 @@ private StrSubstitutor generateSubstitutor(Saml2Settings settings) {
167190
}
168191
valueMap.put("destinationStr", destinationStr);
169192

193+
String subjectStr = "";
194+
if (nameIdValueReq != null && !nameIdValueReq.isEmpty()) {
195+
String nameIDFormat = settings.getSpNameIDFormat();
196+
subjectStr = "<saml:Subject>";
197+
subjectStr += "<saml:NameID Format=\"" + nameIDFormat + "\">" + nameIdValueReq + "</saml:NameID>";
198+
subjectStr += "<saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"></saml:SubjectConfirmation>";
199+
subjectStr += "</saml:Subject>";
200+
}
201+
valueMap.put("subjectStr", subjectStr);
202+
170203
String nameIDPolicyStr = "";
171204
if (setNameIdPolicy) {
172205
String nameIDPolicyFormat = settings.getSpNameIDFormat();
@@ -216,7 +249,7 @@ private static StringBuilder getAuthnRequestTemplate() {
216249
StringBuilder template = new StringBuilder();
217250
template.append("<samlp:AuthnRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"${id}\" Version=\"2.0\" IssueInstant=\"${issueInstant}\"${providerStr}${forceAuthnStr}${isPassiveStr}${destinationStr} ProtocolBinding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" AssertionConsumerServiceURL=\"${assertionConsumerServiceURL}\">");
218251
template.append("<saml:Issuer>${spEntityid}</saml:Issuer>");
219-
template.append("${nameIDPolicyStr}${requestedAuthnContextStr}</samlp:AuthnRequest>");
252+
template.append("${subjectStr}${nameIDPolicyStr}${requestedAuthnContextStr}</samlp:AuthnRequest>");
220253
return template;
221254
}
222255

core/src/test/java/com/onelogin/saml2/test/authn/AuthnRequestTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,42 @@ public void testAuthNContext() throws Exception {
272272
assertThat(authnRequestStr, containsString("<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:X509</saml:AuthnContextClassRef>"));
273273
}
274274

275+
/**
276+
* Tests the AuthnRequest Constructor
277+
* The creation of a deflated SAML Request with and without Subject
278+
*
279+
* @throws Exception
280+
*
281+
* @see com.onelogin.saml2.authn.AuthnRequest
282+
*/
283+
@Test
284+
public void testSubject() throws Exception {
285+
Saml2Settings settings = new SettingsBuilder().fromFile("config/config.min.properties").build();
286+
287+
AuthnRequest authnRequest = new AuthnRequest(settings);
288+
String authnRequestStringBase64 = authnRequest.getEncodedAuthnRequest();
289+
String authnRequestStr = Util.base64decodedInflated(authnRequestStringBase64);
290+
assertThat(authnRequestStr, containsString("<samlp:AuthnRequest"));
291+
assertThat(authnRequestStr, not(containsString("<saml:Subject")));
292+
293+
authnRequest = new AuthnRequest(settings, false, false, false, "testuser@example.com");
294+
authnRequestStringBase64 = authnRequest.getEncodedAuthnRequest();
295+
authnRequestStr = Util.base64decodedInflated(authnRequestStringBase64);
296+
assertThat(authnRequestStr, containsString("<samlp:AuthnRequest"));
297+
assertThat(authnRequestStr, containsString("<saml:Subject"));
298+
assertThat(authnRequestStr, containsString("Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\">testuser@example.com</saml:NameID>"));
299+
assertThat(authnRequestStr, containsString("<saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\">"));
300+
301+
settings = new SettingsBuilder().fromFile("config/config.emailaddressformat.properties").build();
302+
authnRequest = new AuthnRequest(settings, false, false, false, "testuser@example.com");
303+
authnRequestStringBase64 = authnRequest.getEncodedAuthnRequest();
304+
authnRequestStr = Util.base64decodedInflated(authnRequestStringBase64);
305+
assertThat(authnRequestStr, containsString("<samlp:AuthnRequest"));
306+
assertThat(authnRequestStr, containsString("<saml:Subject"));
307+
assertThat(authnRequestStr, containsString("Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\">testuser@example.com</saml:NameID>"));
308+
assertThat(authnRequestStr, containsString("<saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\">"));
309+
}
310+
275311
/**
276312
* Tests the getId method of AuthnRequest
277313
*

toolkit/src/main/java/com/onelogin/saml2/Auth.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,16 +261,18 @@ public void setStrict(Boolean value)
261261
* When true the AuthNRequest will set a nameIdPolicy
262262
* @param stay
263263
* True if we want to stay (returns the url string) False to execute redirection
264+
* @param nameIdValueReq
265+
* Indicates to the IdP the subject that should be authenticated
264266
*
265267
* @return the SSO URL with the AuthNRequest if stay = True
266268
*
267269
* @throws IOException
268270
* @throws SettingsException
269271
*/
270-
public String login(String returnTo, Boolean forceAuthn, Boolean isPassive, Boolean setNameIdPolicy, Boolean stay) throws IOException, SettingsException {
272+
public String login(String returnTo, Boolean forceAuthn, Boolean isPassive, Boolean setNameIdPolicy, Boolean stay, String nameIdValueReq) throws IOException, SettingsException {
271273
Map<String, String> parameters = new HashMap<String, String>();
272274

273-
AuthnRequest authnRequest = new AuthnRequest(settings, forceAuthn, isPassive, setNameIdPolicy);
275+
AuthnRequest authnRequest = new AuthnRequest(settings, forceAuthn, isPassive, setNameIdPolicy, nameIdValueReq);
274276

275277
String samlRequest = authnRequest.getEncodedAuthnRequest();
276278

@@ -305,6 +307,30 @@ public String login(String returnTo, Boolean forceAuthn, Boolean isPassive, Bool
305307
return ServletUtils.sendRedirect(response, ssoUrl, parameters, stay);
306308
}
307309

310+
/**
311+
* Initiates the SSO process.
312+
*
313+
* @param returnTo
314+
* The target URL the user should be returned to after login (relayState).
315+
* Will be a self-routed URL when null, or not be appended at all when an empty string is provided
316+
* @param forceAuthn
317+
* When true the AuthNRequest will set the ForceAuthn='true'
318+
* @param isPassive
319+
* When true the AuthNRequest will set the IsPassive='true'
320+
* @param setNameIdPolicy
321+
* When true the AuthNRequest will set a nameIdPolicy
322+
* @param stay
323+
* True if we want to stay (returns the url string) False to execute redirection
324+
*
325+
* @return the SSO URL with the AuthNRequest if stay = True
326+
*
327+
* @throws IOException
328+
* @throws SettingsException
329+
*/
330+
public String login(String returnTo, Boolean forceAuthn, Boolean isPassive, Boolean setNameIdPolicy, Boolean stay) throws IOException, SettingsException {
331+
return login(returnTo ,forceAuthn, isPassive, setNameIdPolicy, stay, null);
332+
}
333+
308334
/**
309335
* Initiates the SSO process.
310336
*

toolkit/src/test/java/com/onelogin/saml2/test/AuthTest.java

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
import static org.mockito.Mockito.when;
2020

2121
import java.io.IOException;
22+
import java.io.UnsupportedEncodingException;
23+
import java.net.URI;
2224
import java.net.URISyntaxException;
25+
import java.net.URLDecoder;
2326
import java.util.ArrayList;
2427
import java.util.HashMap;
2528
import java.util.List;
@@ -51,6 +54,20 @@ public class AuthTest {
5154
@Rule
5255
public ExpectedException expectedEx = ExpectedException.none();
5356

57+
private String getSAMLRequestFromURL(String url) throws URISyntaxException, UnsupportedEncodingException {
58+
String xml = "";
59+
URI uri = new URI(url);
60+
String query = uri.getQuery();
61+
String[] pairs = query.split("&");
62+
for (String pair : pairs) {
63+
int idx = pair.indexOf("=");
64+
if (pair.substring(0, idx).equals("SAMLRequest")) {
65+
xml = Util.base64decodedInflated(pair.substring(idx + 1));
66+
}
67+
}
68+
return xml;
69+
}
70+
5471
/**
5572
* Tests the constructor of Auth
5673
* Case: No parameters
@@ -1161,7 +1178,56 @@ public void testLoginStay() throws IOException, SettingsException, URISyntaxExce
11611178
assertThat(target, startsWith("https://pitbulk.no-ip.org/simplesaml/saml2/idp/SSOService.php?SAMLRequest="));
11621179
assertThat(target, containsString("&RelayState=http%3A%2F%2Flocalhost%3A8080%2Fexpected.jsp"));
11631180
}
1164-
1181+
1182+
/**
1183+
* Tests the login method of Auth
1184+
* Case: Login with Subject enabled
1185+
*
1186+
* @throws SettingsException
1187+
* @throws IOException
1188+
* @throws URISyntaxException
1189+
* @throws Error
1190+
*
1191+
* @see com.onelogin.saml2.Auth#login
1192+
*/
1193+
@Test
1194+
public void testLoginSubject() throws IOException, SettingsException, URISyntaxException, Error {
1195+
HttpServletRequest request = mock(HttpServletRequest.class);
1196+
HttpServletResponse response = mock(HttpServletResponse.class);
1197+
when(request.getScheme()).thenReturn("http");
1198+
when(request.getServerPort()).thenReturn(8080);
1199+
when(request.getServerName()).thenReturn("localhost");
1200+
when(request.getRequestURI()).thenReturn("/initial.jsp");
1201+
1202+
Saml2Settings settings = new SettingsBuilder().fromFile("config/config.min.properties").build();
1203+
1204+
Auth auth = new Auth(settings, request, response);
1205+
String target = auth.login("", false, false, false, true);
1206+
assertThat(target, startsWith("http://idp.example.com/simplesaml/saml2/idp/SSOService.php?SAMLRequest="));
1207+
String authNRequestStr = getSAMLRequestFromURL(target);
1208+
assertThat(authNRequestStr, containsString("<samlp:AuthnRequest"));
1209+
assertThat(authNRequestStr, not(containsString("<saml:Subject")));
1210+
1211+
target = auth.login("", false, false, false, true, "testuser@example.com");
1212+
assertThat(target, startsWith("http://idp.example.com/simplesaml/saml2/idp/SSOService.php?SAMLRequest="));
1213+
authNRequestStr = getSAMLRequestFromURL(target);
1214+
assertThat(authNRequestStr, containsString("<samlp:AuthnRequest"));
1215+
assertThat(authNRequestStr, containsString("<saml:Subject"));
1216+
assertThat(authNRequestStr, containsString("Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\">testuser@example.com</saml:NameID>"));
1217+
assertThat(authNRequestStr, containsString("<saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\">"));
1218+
1219+
settings = new SettingsBuilder().fromFile("config/config.emailaddressformat.properties").build();
1220+
auth = new Auth(settings, request, response);
1221+
target = auth.login("", false, false, false, true, "testuser@example.com");
1222+
assertThat(target, startsWith("http://idp.example.com/simplesaml/saml2/idp/SSOService.php?SAMLRequest="));
1223+
authNRequestStr = getSAMLRequestFromURL(target);
1224+
assertThat(authNRequestStr, containsString("<samlp:AuthnRequest"));
1225+
assertThat(authNRequestStr, containsString("<saml:Subject"));
1226+
assertThat(authNRequestStr, containsString("Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\">testuser@example.com</saml:NameID>"));
1227+
assertThat(authNRequestStr, containsString("<saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\">"));
1228+
1229+
}
1230+
11651231
/**
11661232
* Tests the login method of Auth
11671233
* Case: Signed Login but no sp key

0 commit comments

Comments
 (0)