diff --git a/README.md b/README.md index d1bf4d3b..ac1fd237 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ The toolkit is hosted on github. You can download it from: The toolkit is hosted at [Sonatype OSSRH (OSS Repository Hosting)](http://central.sonatype.org/pages/ossrh-guide.html) that is synced to the Central Repository. Install it as a maven dependency: -``` +```xml com.onelogin java-saml @@ -441,7 +441,7 @@ If you want to use anything different than javax.servlet.http, you will need to #### Initiate SSO In order to send an AuthNRequest to the IdP: -``` +```java Auth auth = new Auth(request, response); auth.login(); ``` @@ -450,16 +450,18 @@ The AuthNRequest will be sent signed or unsigned based on the security settings The IdP will then return the SAML Response to the user's client. The client is then forwarded to the Attribute Consumer Service of the SP with this information. We can set a 'RelayState' parameter containing a return url to the login function: -``` +```java String returnUrl = 'https://example.com'; auth.login(relayState=returnUrl) ``` -The login method can receive 6 more optional parameters: -- *forceAuthn* When true the AuthNRequest will have the 'ForceAuthn' attribute set to 'true' -- *isPassive* When true the AuthNRequest will have the 'Ispassive' attribute set to 'true' -- *setNameIdPolicy* When true the AuthNRequest will set a nameIdPolicy element. +The login method can receive 3 more optional parameters: +- *authnRequestParams* which in turn allows to shape the AuthNRequest with the following properties: + - *forceAuthn* When true the AuthNRequest will have the `ForceAuthn` attribute set to `true` + - *isPassive* When true the AuthNRequest will have the `IsPassive` attribute set to `true` + - *setNameIdPolicy* When true the AuthNRequest will set a `NameIdPolicy` element + - *allowCreate* When true, and *setNameIdPolicy* is also true, the AuthNRequest will have the `AllowCreate` attribute set to `true` on the `NameIdPolicy` element + - *nameIdValueReq* Indicates to the IdP the subject that should be authenticated - *stay* Set to true to stay (returns the url string), otherwise set to false to execute a redirection to that url (IdP SSO URL) -- *nameIdValueReq* Indicates to the IdP the subject that should be authenticated - *parameters* Use it to send extra parameters in addition to the AuthNRequest 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 @@ -474,7 +476,7 @@ Related to the SP there are 3 important endpoints: The metadata view, the ACS vi ##### SP Metadata This code will provide the XML metadata file of our SP, based on the info that we provided in the settings files. -``` +```java Auth auth = new Auth(); Saml2Settings settings = auth.getSettings(); String metadata = settings.getSPMetadata(); @@ -494,7 +496,7 @@ Before the XML metadata is exposed, a check takes place to ensure that the info ##### Attribute Consumer Service(ACS) This code handles the SAML response that the IdP forwards to the SP through the user's client. -``` +```java Auth auth = new Auth(request, response); auth.processResponse(); if (!auth.isAuthenticated()) { @@ -572,7 +574,7 @@ Before trying to get an attribute, check that the user is authenticated. If the ##### Single Logout Service (SLS) This code handles the Logout Request and the Logout Responses. -``` +```java Auth auth = new Auth(request, response); auth.processSLO(); List errors = auth.getErrors(); @@ -592,7 +594,7 @@ If we don't want that processSLO to destroy the session, pass the keepLocalSessi #### Initiate SLO In order to send a Logout Request to the IdP: -``` +```java Auth auth = new Auth(request, response); String nameId = null; @@ -615,36 +617,62 @@ String sessionIndex = null; if (session.getAttribute("sessionIndex") != null) { sessionIndex = session.getAttribute("sessionIndex").toString(); } -auth.logout(null, nameId, sessionIndex, nameIdFormat); -``` +auth.logout(null, new LogoutRequestParams(sessionIndex, nameId, nameIdFormat)); +```java The Logout Request will be sent signed or unsigned based on the security settings 'onelogin.saml2.security.logoutrequest_signed' The IdP will return the Logout Response through the user's client to the Single Logout Service of the SP. We can set a 'RelayState' parameter containing a return url to the login function: -``` +```java String returnUrl = 'https://example.com'; auth.logout(relayState=returnUrl) ``` -Also there are 7 optional parameters that can be set: -- nameId. That will be used to build the LogoutRequest. If not name_id parameter is set and the auth object processed a SAML Response with a NameId, then this NameId will be used. -- sessionIndex. Identifies the session of the user. -If a match on the LogoutResponse ID and the LogoutRequest ID to be sent is required, that LogoutRequest ID must to be extracted and stored for future validation, we can get that ID by -- stay. True if we want to stay (returns the url string) False to execute a redirection to that url (IdP SLS URL) -- nameidFormat. The NameID Format that will be set in the LogoutRequest -- nameIdNameQualifier. The NameID NameQualifier that will be set in the LogoutRequest -- nameIdSPNameQualifier. The NameID SP Name Qualifier that will be set in the LogoutRequest -- parameters. Use it to send extra parameters in addition to the LogoutRequest - -By default the logout 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 +Also there are other 3 optional parameters that can be set: +- *logoutRequestParams* which in turn allows to shape the LogoutRequest with the following properties: + - *sessionIndex* Identifies the session of the user + - *nameId* That will be used to build the LogoutRequest. If no *nameId* parameter is set and the auth object processed a SAML Response with a `NameID`, then this `NameID` will be used + - *nameidFormat* The `NameID` `Format` that will be set on the LogoutRequest + - *nameIdNameQualifier* The `NameID` `NameQualifier` that will be set on the LogoutRequest + - *nameIdSPNameQualifier* The `NameID` `SPNameQualifier` that will be set on the LogoutRequest +- *stay* True if we want to stay (returns the url string) False to execute a redirection to that url (IdP SLS URL) +- *parameters* Use it to send extra parameters in addition to the LogoutRequest + +By default the logout 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 LogoutResponse ID and the LogoutRequest ID to be sent is required, that LogoutRequest ID must be extracted and stored for future validation so we can't execute the redirection on the logout, instead set stay to true, then get that ID by -``` +```java auth.getLastRequestId() ``` and later executing the redirection manually. +### Extending the provided implementation + +All the provided SAML message classes (`AuthnRequest`, `SamlResponse`, `LogoutRequest`, `LogoutResponse`) can be extended to add or change the processing behavior. + +In particular, the classes used to produce outgoing messages (`AuthnRequest`, `LogoutRequest`, and `LogoutResponse`) also provide a `postProcessXml` method that can be overridden to customise the generation of the corresponding SAML message XML, along with the ability to pass in proper extensions of the input parameter classes (`AuthnRequestParams`, `LogoutRequestParams`, and `LogoutResponseParams` respectively). + +Once you have prepared your extension classes, in order to make the `Auth` class use them, an appropriate `SamlMessageFactory` implementation can be specified. As an example, assuming you've created two extension classes `AuthnRequestEx` and `SamlResponseEx` to customise the creation of AuthnRequest SAML messages and the validation of SAML responses respectively, as well as an extended `AuthnRequestParamsEx` input parameter class to drive the AuthnRequest generation post-processing, you can do the following: + +```java +Auth auth = new Auth(request, response); +auth.setSamlMessageFactory(new SamlMessageFactory() { + @Override + public AuthnRequest createAuthnRequest(Saml2Settings settings, AuthnRequestParams params) { + return new AuthnRequestEx(settings, (AuthnRequestParamsEx) params); + } + + @Override + public SamlResponse createSamlResponse(Saml2Settings settings, HttpRequest request) throws Exception { + return new SamlResponseEx(settings, request); + } +}); +// then proceed with login... +auth.login(relayState, new AuthnRequestParamsEx()); // the custom generation of AuthnReqeustEx will be executed +// ... or process the response as usual +auth.processResponse(); // the custom validation of SamlResponseEx will be executed +``` ### Working behind load balancer diff --git a/core/src/main/java/com/onelogin/saml2/authn/AuthnRequestParams.java b/core/src/main/java/com/onelogin/saml2/authn/AuthnRequestParams.java index 0a7efa48..69954254 100644 --- a/core/src/main/java/com/onelogin/saml2/authn/AuthnRequestParams.java +++ b/core/src/main/java/com/onelogin/saml2/authn/AuthnRequestParams.java @@ -160,7 +160,7 @@ public boolean isAllowCreate() { /** * @return the subject that should be authenticated */ - protected String getNameIdValueReq() { + public String getNameIdValueReq() { return nameIdValueReq; } } \ No newline at end of file diff --git a/core/src/main/java/com/onelogin/saml2/logout/LogoutRequest.java b/core/src/main/java/com/onelogin/saml2/logout/LogoutRequest.java index 234b89ac..cacd7bf8 100644 --- a/core/src/main/java/com/onelogin/saml2/logout/LogoutRequest.java +++ b/core/src/main/java/com/onelogin/saml2/logout/LogoutRequest.java @@ -242,7 +242,7 @@ public LogoutRequest(Saml2Settings settings, HttpRequest request) { * @param settings * OneLogin_Saml2_Settings * @param params - * a set of authentication request input parameters that shape the + * a set of logout request input parameters that shape the * request to create */ public LogoutRequest(Saml2Settings settings, LogoutRequestParams params) { diff --git a/core/src/main/java/com/onelogin/saml2/logout/LogoutRequestParams.java b/core/src/main/java/com/onelogin/saml2/logout/LogoutRequestParams.java index ce95626f..79cd2ee8 100644 --- a/core/src/main/java/com/onelogin/saml2/logout/LogoutRequestParams.java +++ b/core/src/main/java/com/onelogin/saml2/logout/LogoutRequestParams.java @@ -9,30 +9,31 @@ public class LogoutRequestParams { * SessionIndex. When the user is logged, this stored it from the AuthnStatement * of the SAML Response */ - private String sessionIndex; + private final String sessionIndex; /** * NameID. */ - private String nameId; + private final String nameId; /** * NameID Format. */ - private String nameIdFormat; + private final String nameIdFormat; /** * nameId NameQualifier */ - private String nameIdNameQualifier; + private final String nameIdNameQualifier; /** * nameId SP NameQualifier */ - private String nameIdSPNameQualifier; + private final String nameIdSPNameQualifier; /** Create an empty set of logout request input parameters. */ public LogoutRequestParams() { + this(null, null); } /** @@ -118,45 +119,35 @@ protected LogoutRequestParams(LogoutRequestParams source) { /** * @return the name ID */ - protected String getNameId() { + public String getNameId() { return nameId; } - /** - * Sets the name ID - * - * @param nameId - * the name ID to set - */ - protected void setNameId(String nameId) { - this.nameId = nameId; - } - /** * @return the name ID format */ - protected String getNameIdFormat() { + public String getNameIdFormat() { return nameIdFormat; } /** * @return the name ID name qualifier */ - protected String getNameIdNameQualifier() { + public String getNameIdNameQualifier() { return nameIdNameQualifier; } /** * @return the name ID SP name qualifier */ - protected String getNameIdSPNameQualifier() { + public String getNameIdSPNameQualifier() { return nameIdSPNameQualifier; } /** * @return the session index */ - protected String getSessionIndex() { + public String getSessionIndex() { return sessionIndex; } } \ No newline at end of file diff --git a/core/src/main/java/com/onelogin/saml2/logout/LogoutResponse.java b/core/src/main/java/com/onelogin/saml2/logout/LogoutResponse.java index 6deb9ee2..7199fae9 100644 --- a/core/src/main/java/com/onelogin/saml2/logout/LogoutResponse.java +++ b/core/src/main/java/com/onelogin/saml2/logout/LogoutResponse.java @@ -69,11 +69,6 @@ public class LogoutResponse { */ private String currentUrl; - /** - * The inResponseTo attribute of the Logout Request - */ - private String inResponseTo; - /** * Time when the Logout Request was created */ @@ -85,18 +80,16 @@ public class LogoutResponse { private Exception validationException; /** - * The respone status code and messages - */ - private SamlResponseStatus responseStatus; - - /** - * Constructs the LogoutResponse object. + * Constructs the LogoutResponse object when a received response should be + * extracted from the HTTP request and parsed. * * @param settings * OneLogin_Saml2_Settings * @param request - * the HttpRequest object to be processed (Contains GET and POST parameters, request URL, ...). - * + * the HttpRequest object to be processed (Contains GET and POST + * parameters, request URL, ...); may be null when + * building an outgoing logout response + * */ public LogoutResponse(Saml2Settings settings, HttpRequest request) { this.settings = settings; @@ -113,6 +106,25 @@ public LogoutResponse(Saml2Settings settings, HttpRequest request) { logoutResponseDocument = Util.loadXML(logoutResponseString); } } + + /** + * Constructs the LogoutResponse object when a new response should be generated + * and sent. + * + * @param settings + * OneLogin_Saml2_Settings + * @param params + * a set of logout response input parameters that shape the + * request to create + */ + public LogoutResponse(Saml2Settings settings, LogoutResponseParams params) { + this.settings = settings; + this.request = null; + id = Util.generateUniqueID(settings.getUniqueIDPrefix()); + issueInstant = Calendar.getInstance(); + StrSubstitutor substitutor = generateSubstitutor(params, settings); + this.logoutResponseString = postProcessXml(substitutor.replace(getLogoutResponseTemplate()), params, settings); + } /** * @return the base64 encoded unsigned Logout Response (deflated or not) @@ -353,21 +365,33 @@ protected NodeList query (String query) throws XPathExpressionException { return Util.query(this.logoutResponseDocument, query, null); } - /** - * Generates a Logout Response XML string. - * - * @param inResponseTo - * InResponseTo attribute value to bet set at the Logout Response. + /** + * Generates a Logout Response XML string. + * + * @param inResponseTo + * InResponseTo attribute value to bet set at the Logout Response. * @param responseStatus - * SamlResponseStatus response status to be set on the LogoutResponse - */ + * SamlResponseStatus response status to be set on the + * LogoutResponse + * @deprecated use {@link #LogoutResponse(Saml2Settings, LogoutResponseParams)} + * instead, in which case this method becomes completely useless; + * indeed, invoking this method in an outgoing logout response + * scenario will cause the response parameters specified at + * construction time (inResponseTo and + * responseStatus) to be overwritten, as well as its + * generated id and issue instant; on the other hand invoking this + * method in a received logout response scenario does not make sense + * at all (and in that case + * {@link #LogoutResponse(Saml2Settings, HttpRequest)} should be + * used instead) + */ + @Deprecated public void build(String inResponseTo, SamlResponseStatus responseStatus) { id = Util.generateUniqueID(settings.getUniqueIDPrefix()); issueInstant = Calendar.getInstance(); - this.inResponseTo = inResponseTo; - - StrSubstitutor substitutor = generateSubstitutor(settings, responseStatus); - this.logoutResponseString = postProcessXml(substitutor.replace(getLogoutResponseTemplate()), settings); + final LogoutResponseParams params = new LogoutResponseParams(inResponseTo, responseStatus); + StrSubstitutor substitutor = generateSubstitutor(params, settings); + this.logoutResponseString = postProcessXml(substitutor.replace(getLogoutResponseTemplate()), params, settings); } /** @@ -377,24 +401,61 @@ public void build(String inResponseTo, SamlResponseStatus responseStatus) { * InResponseTo attribute value to bet set at the Logout Response. * @param statusCode * String StatusCode to be set on the LogoutResponse + * @deprecated use {@link #LogoutResponse(Saml2Settings, LogoutResponseParams)} + * instead, in which case this method becomes completely useless; + * indeed, invoking this method in an outgoing logout response + * scenario will cause the response parameters specified at + * construction time (inResponseTo and + * responseStatus) to be overwritten, as well as its + * generated id and issue instant; on the other hand invoking this + * method in a received logout response scenario does not make sense + * at all (and in that case + * {@link #LogoutResponse(Saml2Settings, HttpRequest)} should be + * used instead) */ + @Deprecated public void build(String inResponseTo, String statusCode) { build(inResponseTo, new SamlResponseStatus(statusCode)); } - /** - * Generates a Logout Response XML string. - * - * @param inResponseTo - * InResponseTo attribute value to bet set at the Logout Response. - */ + /** + * Generates a Logout Response XML string. + * + * @param inResponseTo + * InResponseTo attribute value to bet set at the Logout Response. + * @deprecated use {@link #LogoutResponse(Saml2Settings, LogoutResponseParams)} + * instead, in which case this method becomes completely useless; + * indeed, invoking this method in an outgoing logout response + * scenario will cause the response parameters specified at + * construction time (inResponseTo and + * responseStatus) to be overwritten, as well as its + * generated id and issue instant; on the other hand invoking this + * method in a received logout response scenario does not make sense + * at all (and in that case + * {@link #LogoutResponse(Saml2Settings, HttpRequest)} should be + * used instead) + */ + @Deprecated public void build(String inResponseTo) { build(inResponseTo, Constants.STATUS_SUCCESS); } - /** - * Generates a Logout Response XML string. - */ + /** + * Generates a Logout Response XML string. + * + * @deprecated use {@link #LogoutResponse(Saml2Settings, LogoutResponseParams)} + * instead, in which case this method becomes completely useless; + * indeed, invoking this method in an outgoing logout response + * scenario will cause the response parameters specified at + * construction time (inResponseTo and + * responseStatus) to be overwritten, as well as its + * generated id and issue instant; on the other hand invoking this + * method in a received logout response scenario does not make sense + * at all (and in that case + * {@link #LogoutResponse(Saml2Settings, HttpRequest)} should be + * used instead) + */ + @Deprecated public void build() { build(null); } @@ -410,26 +471,29 @@ public void build() { * @param logoutResponseXml * the XML produced for this LogoutResponse by the standard * implementation provided by {@link LogoutResponse} + * @param params + * the logout request input parameters * @param settings * the settings * @return the post-processed XML for this LogoutResponse, which will then be * returned by any call to {@link #getLogoutResponseXml()} */ - protected String postProcessXml(final String logoutResponseXml, final Saml2Settings settings) { + protected String postProcessXml(final String logoutResponseXml, final LogoutResponseParams params, + final Saml2Settings settings) { return logoutResponseXml; } /** * Substitutes LogoutResponse variables within a string by values. * + * @param params + * the logout response input parameters * @param settings - * Saml2Settings object. Setting data - * @param responseStatus - * SamlResponseStatus response status to be set on the LogoutResponse + * Saml2Settings object. Setting data * * @return the StrSubstitutor object of the LogoutResponse */ - private StrSubstitutor generateSubstitutor(Saml2Settings settings, SamlResponseStatus responseStatus) { + private StrSubstitutor generateSubstitutor(LogoutResponseParams params, Saml2Settings settings) { Map valueMap = new HashMap(); valueMap.put("id", Util.toXml(id)); @@ -445,12 +509,14 @@ private StrSubstitutor generateSubstitutor(Saml2Settings settings, SamlResponseS valueMap.put("destinationStr", destinationStr); String inResponseStr = ""; + final String inResponseTo = params.getInResponseTo(); if (inResponseTo != null) { inResponseStr = " InResponseTo=\"" + Util.toXml(inResponseTo) + "\""; } valueMap.put("inResponseStr", inResponseStr); StringBuilder statusStr = new StringBuilder("inResponseTo attribute and a + * response status with a top-level {@link Constants#STATUS_SUCCESS} status + * code. + */ + public LogoutResponseParams() { + this((String) null); + } + + /** + * Creates a logout response with a response status with a top-level + * {@link Constants#STATUS_SUCCESS} status code. + * + * @param inResponseTo + * the id of the logout request the response refers to; may be + * null if such id cannot be determined (possibly + * because the request is malformed) + */ + public LogoutResponseParams(String inResponseTo) { + this(inResponseTo, Constants.STATUS_SUCCESS); + } + + /** + * Creates a logout response. + * + * @param inResponseTo + * the id of the logout request the response refers to; may be + * null if such id cannot be determined (possibly + * because the request is malformed) + * @param statusCode + * the top-level status code code to set on the response + */ + public LogoutResponseParams(String inResponseTo, String statusCode) { + this(inResponseTo, new SamlResponseStatus(statusCode)); + } + + /** + * Creates a logout response. + * + * @param inResponseTo + * the id of the logout request the response refers to; may be + * null if such id cannot be determined (possibly + * because the request is malformed) + * @param responseStatus + * the response status; should not be null + * @throws NullPointerException + * if the specified response status is null + */ + public LogoutResponseParams(String inResponseTo, SamlResponseStatus responseStatus) throws NullPointerException { + this.inResponseTo = inResponseTo; + this.responseStatus = responseStatus; + if (responseStatus == null) + throw new NullPointerException("response status must not be null"); + } + + /** + * Create a set of logout request input parameters, by copying them from another + * set. + * + * @param source + * the source set of logout request input parameters + */ + protected LogoutResponseParams(LogoutResponseParams source) { + this.inResponseTo = source.getInResponseTo(); + this.responseStatus = source.getResponseStatus(); + } + + /** + * Returns the response status. + * + * @return the response status + */ + public SamlResponseStatus getResponseStatus() { + return responseStatus; + } + + /** + * Returns the id of the logout request this response refers to. + * + * @return the inResponseTo + */ + public String getInResponseTo() { + return inResponseTo; + } +} \ No newline at end of file diff --git a/core/src/test/java/com/onelogin/saml2/test/logout/LogoutRequestTest.java b/core/src/test/java/com/onelogin/saml2/test/logout/LogoutRequestTest.java index b3ae9c59..c81f172e 100644 --- a/core/src/test/java/com/onelogin/saml2/test/logout/LogoutRequestTest.java +++ b/core/src/test/java/com/onelogin/saml2/test/logout/LogoutRequestTest.java @@ -1095,11 +1095,13 @@ private static HttpRequest newHttpRequest(String requestURL, String samlRequestE @Test public void testPostProcessXml() throws Exception { Saml2Settings settings = new SettingsBuilder().fromFile("config/config.min.properties").build(); - LogoutRequest logoutRequest = new LogoutRequest(settings) { + final LogoutRequestParams params = new LogoutRequestParams(); + LogoutRequest logoutRequest = new LogoutRequest(settings, params) { @Override - protected String postProcessXml(String logoutRequestXml, LogoutRequestParams params, Saml2Settings sett) { - assertEquals(logoutRequestXml, super.postProcessXml(logoutRequestXml, params, sett)); + protected String postProcessXml(String logoutRequestXml, LogoutRequestParams par, Saml2Settings sett) { + assertEquals(logoutRequestXml, super.postProcessXml(logoutRequestXml, par, sett)); assertSame(settings, sett); + assertSame(params, par); return "changed"; } }; diff --git a/core/src/test/java/com/onelogin/saml2/test/logout/LogoutResponseTest.java b/core/src/test/java/com/onelogin/saml2/test/logout/LogoutResponseTest.java index c561aec3..dbc68c51 100644 --- a/core/src/test/java/com/onelogin/saml2/test/logout/LogoutResponseTest.java +++ b/core/src/test/java/com/onelogin/saml2/test/logout/LogoutResponseTest.java @@ -24,6 +24,7 @@ import com.onelogin.saml2.exception.XMLEntityException; import com.onelogin.saml2.http.HttpRequest; import com.onelogin.saml2.logout.LogoutResponse; +import com.onelogin.saml2.logout.LogoutResponseParams; import com.onelogin.saml2.model.SamlResponseStatus; import com.onelogin.saml2.settings.Saml2Settings; import com.onelogin.saml2.settings.SettingsBuilder; @@ -114,7 +115,7 @@ public String getLogoutResponseXml() { * @see com.onelogin.saml2.logout.LogoutResponse */ @Test - public void testConstructor() throws IOException, XMLEntityException, URISyntaxException, Error { + public void testReceivedMessageConstructor() throws IOException, XMLEntityException, URISyntaxException, Error { Saml2Settings settings = new SettingsBuilder().fromFile("config/config.min.properties").build(); String samlResponseEncoded = Util.getFileAsString("data/logout_responses/logout_response_deflated.xml.base64"); final String requestURL = "/"; @@ -125,7 +126,7 @@ public void testConstructor() throws IOException, XMLEntityException, URISyntaxE String logoutResponseStringBase64 = logoutResponse.getEncodedLogoutResponse(); assertEquals(logoutResponseStringBase64, expectedLogoutResponseStringBase64); } - + /** * Tests the build method of LogoutResponse * @@ -137,6 +138,66 @@ public void testConstructor() throws IOException, XMLEntityException, URISyntaxE * @see com.onelogin.saml2.logout.LogoutResponse#build */ @Test + public void testOutgoingMessageConstructor() throws IOException, XMLEntityException, URISyntaxException, Error { + Saml2Settings settings = new SettingsBuilder().fromFile("config/config.min.properties").build(); + + LogoutResponse logoutResponse = new LogoutResponse(settings, new LogoutResponseParams()); + assertFalse(logoutResponse.isValid()); + assertEquals("SAML Logout Response is not loaded", logoutResponse.getError()); + String logoutResponseStringBase64 = logoutResponse.getEncodedLogoutResponse(); + assertFalse(logoutResponseStringBase64.isEmpty()); + + String logoutResponseStr = Util.base64decodedInflated(logoutResponseStringBase64); + assertThat(logoutResponseStr, containsString("")); + assertThat(logoutResponseStr, not(containsString(""))); + assertThat(logoutResponseStr, not(containsString(""))); + + SamlResponseStatus responseStatus = new SamlResponseStatus(Constants.STATUS_RESPONDER); + responseStatus.setSubStatusCode(Constants.STATUS_PARTIAL_LOGOUT); + LogoutResponse logoutResponse4 = new LogoutResponse(settings, new LogoutResponseParams("inResponseValue", responseStatus)); + logoutResponseStringBase64 = logoutResponse4.getEncodedLogoutResponse(); + logoutResponseStr = Util.base64decodedInflated(logoutResponseStringBase64); + assertThat(logoutResponseStr, containsString("")); + assertThat(logoutResponseStr, not(containsString(""))); + + responseStatus.setStatusMessage("status message"); + LogoutResponse logoutResponse5 = new LogoutResponse(settings, new LogoutResponseParams("inResponseValue", responseStatus)); + logoutResponseStringBase64 = logoutResponse5.getEncodedLogoutResponse(); + logoutResponseStr = Util.base64decodedInflated(logoutResponseStringBase64); + assertThat(logoutResponseStr, containsString("")); + assertThat(logoutResponseStr, containsString("status message")); + } + + /** + * Tests the legacy build method of LogoutResponse + * + * @throws IOException + * @throws XMLEntityException + * @throws URISyntaxException + * @throws Error + * + * @see com.onelogin.saml2.logout.LogoutResponse#build + */ + @Test public void testBuild() throws IOException, XMLEntityException, URISyntaxException, Error { Saml2Settings settings = new SettingsBuilder().fromFile("config/config.min.properties").build(); @@ -147,50 +208,50 @@ public void testBuild() throws IOException, XMLEntityException, URISyntaxExcepti assertFalse(logoutResponse.isValid()); assertEquals("SAML Logout Response is not loaded", logoutResponse.getError()); logoutResponse.build(); - String logoutRequestStringBase64 = logoutResponse.getEncodedLogoutResponse(); - assertFalse(logoutRequestStringBase64.isEmpty()); + String logoutResponseStringBase64 = logoutResponse.getEncodedLogoutResponse(); + assertFalse(logoutResponseStringBase64.isEmpty()); - String logoutRequestStr = Util.base64decodedInflated(logoutRequestStringBase64); - assertThat(logoutRequestStr, containsString("")); - assertThat(logoutRequestStr, not(containsString(""))); - assertThat(logoutRequestStr, not(containsString(""))); + logoutResponseStringBase64 = logoutResponse3.getEncodedLogoutResponse(); + logoutResponseStr = Util.base64decodedInflated(logoutResponseStringBase64); + assertThat(logoutResponseStr, containsString("")); + assertThat(logoutResponseStr, not(containsString(""))); + assertThat(logoutResponseStr, not(containsString(""))); LogoutResponse logoutResponse4 = new LogoutResponse(settings, httpRequest); SamlResponseStatus responseStatus = new SamlResponseStatus(Constants.STATUS_RESPONDER); responseStatus.setSubStatusCode(Constants.STATUS_PARTIAL_LOGOUT); logoutResponse4.build("inResponseValue", responseStatus); - logoutRequestStringBase64 = logoutResponse4.getEncodedLogoutResponse(); - logoutRequestStr = Util.base64decodedInflated(logoutRequestStringBase64); - assertThat(logoutRequestStr, containsString("")); - assertThat(logoutRequestStr, not(containsString(""))); + logoutResponseStringBase64 = logoutResponse4.getEncodedLogoutResponse(); + logoutResponseStr = Util.base64decodedInflated(logoutResponseStringBase64); + assertThat(logoutResponseStr, containsString("")); + assertThat(logoutResponseStr, not(containsString(""))); responseStatus.setStatusMessage("status message"); logoutResponse4.build("inResponseValue", responseStatus); - logoutRequestStringBase64 = logoutResponse4.getEncodedLogoutResponse(); - logoutRequestStr = Util.base64decodedInflated(logoutRequestStringBase64); - assertThat(logoutRequestStr, containsString("")); - assertThat(logoutRequestStr, containsString("status message")); + logoutResponseStringBase64 = logoutResponse4.getEncodedLogoutResponse(); + logoutResponseStr = Util.base64decodedInflated(logoutResponseStringBase64); + assertThat(logoutResponseStr, containsString("")); + assertThat(logoutResponseStr, containsString("status message")); } /** @@ -203,7 +264,32 @@ public void testBuild() throws IOException, XMLEntityException, URISyntaxExcepti @Test public void testGetLogoutResponseXml() throws Exception { Saml2Settings settings = new SettingsBuilder().fromFile("config/config.min.properties").build(); - LogoutResponse logoutResponse = new LogoutResponse(settings, null); + LogoutResponse logoutResponse = new LogoutResponse(settings, new LogoutResponseParams()); + String logoutResponseXML = logoutResponse.getLogoutResponseXml(); + assertThat(logoutResponseXML, containsString(" + * Case: logout destination contains special chars and the legacy build method is used to build the outgoing response. + * + * @throws Exception + * + * @see com.onelogin.saml2.logout.LogoutResponse#getLogoutResponseXml + */ + @Test + public void testGetLogoutResponseXmlSpecialCharsLegacy() throws Exception { + Saml2Settings settings = new SettingsBuilder().fromFile("config/config.min_specialchars.properties").build(); + LogoutResponse logoutResponse = new LogoutResponse(settings, (HttpRequest) null); logoutResponse.build(); String logoutResponseXML = logoutResponse.getLogoutResponseXml(); assertThat(logoutResponseXML, containsString(" - * Case: LogoutResponse message built by the caller. + * Case: outgoing LogoutResponse message created by the caller. + * + * @throws IOException + * @throws Error + * @throws ValidationError + * + * @see com.onelogin.saml2.logout.LogoutResponse#getIssueInstant() + */ + @Test + public void testGetIssueInstantOutgoingMessage() throws IOException, Error, ValidationError { + Saml2Settings settings = new SettingsBuilder().fromFile("config/config.min.properties").build(); + long start = System.currentTimeMillis(); + LogoutResponse logoutResponse = new LogoutResponse(settings, new LogoutResponseParams()); + long end = System.currentTimeMillis(); + Calendar issueInstant = logoutResponse.getIssueInstant(); + assertNotNull(issueInstant); + long millis = issueInstant.getTimeInMillis(); + assertTrue(millis >= start && millis <= end); + } + + /** + * Tests the getIssueInstant method of LogoutResponse + *

+ * Case: outgoing LogoutResponse message created by the caller and legacy build() method invoked * * @throws IOException * @throws Error @@ -330,10 +465,10 @@ public void testGetIssueInstant() throws IOException, Error, ValidationError { * @see com.onelogin.saml2.logout.LogoutResponse#getIssueInstant() */ @Test - public void testGetIssueInstantBuiltMessage() throws IOException, Error, ValidationError { + public void testGetIssueInstantOutgoingMessageLegacy() throws IOException, Error, ValidationError { Saml2Settings settings = new SettingsBuilder().fromFile("config/config.min.properties").build(); + LogoutResponse logoutResponse = new LogoutResponse(settings, new LogoutResponseParams()); long start = System.currentTimeMillis(); - LogoutResponse logoutResponse = new LogoutResponse(settings, null); logoutResponse.build(); long end = System.currentTimeMillis(); Calendar issueInstant = logoutResponse.getIssueInstant(); @@ -779,11 +914,41 @@ private static HttpRequest newHttpRequest(String requestURL, String samlResponse @Test public void testPostProcessXml() throws Exception { Saml2Settings settings = new SettingsBuilder().fromFile("config/config.min.properties").build(); - LogoutResponse logoutResponse = new LogoutResponse(settings, null) { + final LogoutResponseParams params = new LogoutResponseParams(); + LogoutResponse logoutResponse = new LogoutResponse(settings, params) { + @Override + protected String postProcessXml(String logoutResponseXml, LogoutResponseParams par, Saml2Settings sett) { + assertEquals(logoutResponseXml, super.postProcessXml(logoutResponseXml, par, sett)); + assertSame(settings, sett); + assertSame(params, par); + return "changed"; + } + }; + assertEquals("changed", logoutResponse.getLogoutResponseXml()); + } + + /** + * Tests the postProcessXml method of LogoutResponse + * + * Case: the legacy build method is used to build the outgoing response. + * + * @throws Exception + * + * @see com.onelogin.saml2.logout.LogoutResponse#postProcessXml + */ + @Test + public void testPostProcessXmlLegacy() throws Exception { + Saml2Settings settings = new SettingsBuilder().fromFile("config/config.min.properties").build(); + LogoutResponse logoutResponse = new LogoutResponse(settings, (HttpRequest) null) { @Override - protected String postProcessXml(String logoutResponseXml, Saml2Settings sett) { - assertEquals(logoutResponseXml, super.postProcessXml(logoutResponseXml, sett)); + protected String postProcessXml(String logoutResponseXml, LogoutResponseParams params, Saml2Settings sett) { + assertEquals(logoutResponseXml, super.postProcessXml(logoutResponseXml, params, sett)); assertSame(settings, sett); + assertNull(params.getInResponseTo()); + SamlResponseStatus responseStatus = params.getResponseStatus(); + assertEquals(Constants.STATUS_SUCCESS, responseStatus.getStatusCode()); + assertNull(responseStatus.getSubStatusCode()); + assertNull(responseStatus.getStatusMessage()); return "changed"; } }; diff --git a/toolkit/src/main/java/com/onelogin/saml2/Auth.java b/toolkit/src/main/java/com/onelogin/saml2/Auth.java index dac4e4e3..511914b8 100644 --- a/toolkit/src/main/java/com/onelogin/saml2/Auth.java +++ b/toolkit/src/main/java/com/onelogin/saml2/Auth.java @@ -26,11 +26,13 @@ import com.onelogin.saml2.authn.AuthnRequestParams; import com.onelogin.saml2.authn.SamlResponse; import com.onelogin.saml2.exception.SettingsException; +import com.onelogin.saml2.factory.SamlMessageFactory; import com.onelogin.saml2.exception.Error; import com.onelogin.saml2.http.HttpRequest; import com.onelogin.saml2.logout.LogoutRequest; import com.onelogin.saml2.logout.LogoutRequestParams; import com.onelogin.saml2.logout.LogoutResponse; +import com.onelogin.saml2.logout.LogoutResponseParams; import com.onelogin.saml2.model.SamlResponseStatus; import com.onelogin.saml2.model.KeyStoreSettings; import com.onelogin.saml2.servlet.ServletUtils; @@ -167,6 +169,10 @@ public class Auth { * encrypted, by default tries to return the decrypted XML */ private String lastResponse; + + private static final SamlMessageFactory DEFAULT_SAML_MESSAGE_FACTORY = new SamlMessageFactory() {}; + + private SamlMessageFactory samlMessageFactory = DEFAULT_SAML_MESSAGE_FACTORY; /** * Initializes the SP SAML instance. @@ -609,7 +615,7 @@ public String login(String relayState, AuthnRequestParams authnRequestParams, Bo * @throws SettingsException */ public String login(String relayState, AuthnRequestParams authnRequestParams, Boolean stay, Map parameters) throws IOException, SettingsException { - AuthnRequest authnRequest = new AuthnRequest(settings, authnRequestParams); + AuthnRequest authnRequest = samlMessageFactory.createAuthnRequest(settings, authnRequestParams); if (parameters == null) { parameters = new HashMap(); @@ -785,7 +791,7 @@ public String logout(String relayState, LogoutRequestParams logoutRequestParams, parameters = new HashMap(); } - LogoutRequest logoutRequest = new LogoutRequest(settings, logoutRequestParams); + LogoutRequest logoutRequest = samlMessageFactory.createOutgoingLogoutRequest(settings, logoutRequestParams); String samlLogoutRequest = logoutRequest.getEncodedLogoutRequest(); parameters.put("SAMLRequest", samlLogoutRequest); @@ -1196,7 +1202,7 @@ public void processResponse(String requestId) throws Exception { final String samlResponseParameter = httpRequest.getParameter("SAMLResponse"); if (samlResponseParameter != null) { - SamlResponse samlResponse = new SamlResponse(settings, httpRequest); + SamlResponse samlResponse = samlMessageFactory.createSamlResponse(settings, httpRequest); lastResponse = samlResponse.getSAMLResponseXml(); if (samlResponse.isValid(requestId)) { @@ -1229,7 +1235,7 @@ public void processResponse(String requestId) throws Exception { errors.add("invalid_response"); LOGGER.error("processResponse error. invalid_response"); LOGGER.debug(" --> " + samlResponseParameter); - } + } } } else { errors.add("invalid_binding"); @@ -1269,7 +1275,7 @@ public String processSLO(Boolean keepLocalSession, String requestId, Boolean sta final String samlResponseParameter = httpRequest.getParameter("SAMLResponse"); if (samlResponseParameter != null) { - LogoutResponse logoutResponse = new LogoutResponse(settings, httpRequest); + LogoutResponse logoutResponse = samlMessageFactory.createIncomingLogoutResponse(settings, httpRequest); lastResponse = logoutResponse.getLogoutResponseXml(); if (!logoutResponse.isValid(requestId)) { errors.add("invalid_logout_response"); @@ -1299,7 +1305,7 @@ public String processSLO(Boolean keepLocalSession, String requestId, Boolean sta } return null; } else if (samlRequestParameter != null) { - LogoutRequest logoutRequest = new LogoutRequest(settings, httpRequest); + LogoutRequest logoutRequest = samlMessageFactory.createIncomingLogoutRequest(settings, httpRequest); lastRequest = logoutRequest.getLogoutRequestXml(); if (!logoutRequest.isValid()) { errors.add("invalid_logout_request"); @@ -1317,8 +1323,8 @@ public String processSLO(Boolean keepLocalSession, String requestId, Boolean sta } String inResponseTo = logoutRequest.id; - LogoutResponse logoutResponseBuilder = new LogoutResponse(settings, httpRequest); - logoutResponseBuilder.build(inResponseTo, Constants.STATUS_SUCCESS); + LogoutResponse logoutResponseBuilder = samlMessageFactory.createOutgoingLogoutResponse(settings, + new LogoutResponseParams(inResponseTo, Constants.STATUS_SUCCESS)); lastResponse = logoutResponseBuilder.getLogoutResponseXml(); String samlLogoutResponse = logoutResponseBuilder.getEncodedLogoutResponse(); @@ -1644,4 +1650,21 @@ public String getLastRequestXML() { public String getLastResponseXML() { return lastResponse; } + + /** + * Sets the factory this {@link Auth} will use to create SAML messages. + *

+ * This allows consumers to provide their own extension classes for SAML message + * XML generation and/or processing. + * + * @param samlMessageFactory + * the factory to use to create SAML message objects; if + * null, a default provider will be used which creates + * the standard message implementation provided by this library + * (i.e.: {@link AuthnRequest}, {@link SamlResponse}, + * {@link LogoutRequest} and {@link LogoutResponse}) + */ + public void setSamlMessageFactory(final SamlMessageFactory samlMessageFactory) { + this.samlMessageFactory = samlMessageFactory != null ? samlMessageFactory : DEFAULT_SAML_MESSAGE_FACTORY; + } } diff --git a/toolkit/src/main/java/com/onelogin/saml2/factory/SamlMessageFactory.java b/toolkit/src/main/java/com/onelogin/saml2/factory/SamlMessageFactory.java new file mode 100644 index 00000000..08c71e91 --- /dev/null +++ b/toolkit/src/main/java/com/onelogin/saml2/factory/SamlMessageFactory.java @@ -0,0 +1,119 @@ +package com.onelogin.saml2.factory; + +import com.onelogin.saml2.Auth; +import com.onelogin.saml2.authn.AuthnRequest; +import com.onelogin.saml2.authn.AuthnRequestParams; +import com.onelogin.saml2.authn.SamlResponse; +import com.onelogin.saml2.http.HttpRequest; +import com.onelogin.saml2.logout.LogoutRequest; +import com.onelogin.saml2.logout.LogoutRequestParams; +import com.onelogin.saml2.logout.LogoutResponse; +import com.onelogin.saml2.logout.LogoutResponseParams; +import com.onelogin.saml2.settings.Saml2Settings; + +/** + * Factory which can create all kind of SAML message objects. + *

+ * One such factory is used by the {@link Auth} class to orchestrate login and + * logout operations. + *

+ * Default implementations for all creation methods are provided: they create + * instances of the standard classes provided by the library. Any extension + * class may simply override the desired creation methods in order to return + * instances of custom extensions of those standard classes. + */ +public interface SamlMessageFactory { + + /** + * Creates an {@link AuthnRequest} instance. + * + * @param settings + * the settings + * @param params + * the authentication request input parameters + * @return the created {@link AuthnRequest} instance + */ + default AuthnRequest createAuthnRequest(final Saml2Settings settings, final AuthnRequestParams params) { + return new AuthnRequest(settings, params); + } + + /** + * Creates a {@link SamlResponse} instance. + * + * @param settings + * the settings + * @param request + * the HTTP request from which the response is to be extracted and + * parsed + * @return the created {@link SamlResponse} instance + * @throws Exception + * in case some error occurred while trying to create the + * {@link SamlResponse} instance + */ + default SamlResponse createSamlResponse(final Saml2Settings settings, final HttpRequest request) + throws Exception { + return new SamlResponse(settings, request); + } + + /** + * Creates a {@link LogoutRequest} instance for an outgoing request. + * + * @param settings + * the settings + * @param params + * the logout request input parameters + * @return the created {@link LogoutRequest} instance + */ + default LogoutRequest createOutgoingLogoutRequest(final Saml2Settings settings, final LogoutRequestParams params) { + return new LogoutRequest(settings, params); + } + + /** + * Creates a {@link LogoutRequest} instance for an incoming request. + * + * @param settings + * the settings + * @param request + * the HTTP request from which the logout request is to be + * extracted and parsed + * @return the created {@link LogoutRequest} instance + * @throws Exception + * in case some error occurred while trying to create the + * {@link LogoutRequest} instance + */ + default LogoutRequest createIncomingLogoutRequest(final Saml2Settings settings, final HttpRequest request) + throws Exception { + return new LogoutRequest(settings, request); + } + + /** + * Creates a {@link LogoutResponse} instance for an outgoing response. + * + * @param settings + * the settings + * @param params + * the logout response input parameters + * @return the created {@link LogoutResponse} instance + */ + default LogoutResponse createOutgoingLogoutResponse(final Saml2Settings settings, final LogoutResponseParams params) { + return new LogoutResponse(settings, params); + } + + /** + * Creates a {@link LogoutRequest} instance for an incoming response. + * + * @param settings + * the settings + * @param request + * the HTTP request from which the logout response is to be + * extracted and parsed + * @return the created {@link LogoutResponse} instance + * @throws Exception + * in case some error occurred while trying to create the + * {@link LogoutResponse} instance + */ + default LogoutResponse createIncomingLogoutResponse(final Saml2Settings settings, final HttpRequest request) + throws Exception { + return new LogoutResponse(settings, request); + } +} \ No newline at end of file diff --git a/toolkit/src/test/java/com/onelogin/saml2/test/AuthTest.java b/toolkit/src/test/java/com/onelogin/saml2/test/AuthTest.java index ff8f1e54..6c376839 100644 --- a/toolkit/src/test/java/com/onelogin/saml2/test/AuthTest.java +++ b/toolkit/src/test/java/com/onelogin/saml2/test/AuthTest.java @@ -10,6 +10,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.matches; @@ -47,13 +48,22 @@ import org.w3c.dom.Document; import com.onelogin.saml2.Auth; +import com.onelogin.saml2.authn.AuthnRequest; import com.onelogin.saml2.authn.AuthnRequestParams; +import com.onelogin.saml2.authn.SamlResponse; import com.onelogin.saml2.exception.Error; import com.onelogin.saml2.exception.SettingsException; import com.onelogin.saml2.exception.ValidationError; import com.onelogin.saml2.exception.XMLEntityException; +import com.onelogin.saml2.factory.SamlMessageFactory; +import com.onelogin.saml2.http.HttpRequest; +import com.onelogin.saml2.logout.LogoutRequest; import com.onelogin.saml2.logout.LogoutRequestParams; +import com.onelogin.saml2.logout.LogoutResponse; +import com.onelogin.saml2.logout.LogoutResponseParams; import com.onelogin.saml2.model.KeyStoreSettings; +import com.onelogin.saml2.model.SamlResponseStatus; +import com.onelogin.saml2.servlet.ServletUtils; import com.onelogin.saml2.settings.Saml2Settings; import com.onelogin.saml2.settings.SettingsBuilder; import com.onelogin.saml2.util.Constants; @@ -2221,4 +2231,240 @@ public void testGetLastLogoutResponseReceived() throws Exception { String logoutResponseXML = auth.getLastResponseXML(); assertThat(logoutResponseXML, containsString("