From 097553bca8f4bcada86180d4085ba1efcae61862 Mon Sep 17 00:00:00 2001 From: Mauro Molinari Date: Fri, 2 Apr 2021 19:22:12 +0200 Subject: [PATCH 1/7] Add factories as an extension mechanism for Auth This allows library consumers to extend the standard java-saml message classes with custom implementations while still using the built-in Auth class to orchestrate the message flow, by providing a mechanism to plug-in custom object creation logic. --- .../onelogin/saml2/logout/LogoutResponse.java | 6 +- .../main/java/com/onelogin/saml2/Auth.java | 133 ++++++++++- .../saml2/SamlOutgoingMessageFactory.java | 27 +++ .../saml2/SamlReceivedMessageFactory.java | 28 +++ .../com/onelogin/saml2/test/AuthTest.java | 207 ++++++++++++++++++ 5 files changed, 392 insertions(+), 9 deletions(-) create mode 100644 toolkit/src/main/java/com/onelogin/saml2/SamlOutgoingMessageFactory.java create mode 100644 toolkit/src/main/java/com/onelogin/saml2/SamlReceivedMessageFactory.java 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..fcc0354d 100644 --- a/core/src/main/java/com/onelogin/saml2/logout/LogoutResponse.java +++ b/core/src/main/java/com/onelogin/saml2/logout/LogoutResponse.java @@ -95,8 +95,10 @@ public class LogoutResponse { * @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; diff --git a/toolkit/src/main/java/com/onelogin/saml2/Auth.java b/toolkit/src/main/java/com/onelogin/saml2/Auth.java index dac4e4e3..e5bd0bc3 100644 --- a/toolkit/src/main/java/com/onelogin/saml2/Auth.java +++ b/toolkit/src/main/java/com/onelogin/saml2/Auth.java @@ -167,6 +167,20 @@ public class Auth { * encrypted, by default tries to return the decrypted XML */ private String lastResponse; + + private static final SamlOutgoingMessageFactory DEFAULT_AUTHN_REQUEST_FACTORY = AuthnRequest::new; + private static final SamlReceivedMessageFactory DEFAULT_SAML_RESPONSE_FACTORY = SamlResponse::new; + private static final SamlOutgoingMessageFactory DEFAULT_OUTGOING_LOGOUT_REQUEST_FACTORY = LogoutRequest::new; + private static final SamlReceivedMessageFactory DEFAULT_RECEIVED_LOGOUT_REQUEST_FACTORY = LogoutRequest::new; + private static final SamlOutgoingMessageFactory DEFAULT_OUTGOING_LOGOUT_RESPONSE_FACTORY = (settings, nothing) -> new LogoutResponse(settings, null); + private static final SamlReceivedMessageFactory DEFAULT_RECEIVED_LOGOUT_RESPONSE_FACTORY = LogoutResponse::new; + + private SamlOutgoingMessageFactory authnRequestFactory = DEFAULT_AUTHN_REQUEST_FACTORY; + private SamlReceivedMessageFactory samlResponseFactory = DEFAULT_SAML_RESPONSE_FACTORY; + private SamlOutgoingMessageFactory outgoingLogoutRequestFactory = DEFAULT_OUTGOING_LOGOUT_REQUEST_FACTORY; + private SamlReceivedMessageFactory receivedLogoutRequestFactory = DEFAULT_RECEIVED_LOGOUT_REQUEST_FACTORY; + private SamlOutgoingMessageFactory outgoingLogoutResponseFactory = DEFAULT_OUTGOING_LOGOUT_RESPONSE_FACTORY; + private SamlReceivedMessageFactory receivedLogoutResponseFactory = DEFAULT_RECEIVED_LOGOUT_RESPONSE_FACTORY; /** * Initializes the SP SAML instance. @@ -609,7 +623,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 = authnRequestFactory.create(settings, authnRequestParams); if (parameters == null) { parameters = new HashMap(); @@ -785,7 +799,7 @@ public String logout(String relayState, LogoutRequestParams logoutRequestParams, parameters = new HashMap(); } - LogoutRequest logoutRequest = new LogoutRequest(settings, logoutRequestParams); + LogoutRequest logoutRequest = outgoingLogoutRequestFactory.create(settings, logoutRequestParams); String samlLogoutRequest = logoutRequest.getEncodedLogoutRequest(); parameters.put("SAMLRequest", samlLogoutRequest); @@ -1196,7 +1210,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 = samlResponseFactory.create(settings, httpRequest); lastResponse = samlResponse.getSAMLResponseXml(); if (samlResponse.isValid(requestId)) { @@ -1229,7 +1243,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 +1283,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 = receivedLogoutResponseFactory.create(settings, httpRequest); lastResponse = logoutResponse.getLogoutResponseXml(); if (!logoutResponse.isValid(requestId)) { errors.add("invalid_logout_response"); @@ -1299,7 +1313,7 @@ public String processSLO(Boolean keepLocalSession, String requestId, Boolean sta } return null; } else if (samlRequestParameter != null) { - LogoutRequest logoutRequest = new LogoutRequest(settings, httpRequest); + LogoutRequest logoutRequest = receivedLogoutRequestFactory.create(settings, httpRequest); lastRequest = logoutRequest.getLogoutRequestXml(); if (!logoutRequest.isValid()) { errors.add("invalid_logout_request"); @@ -1317,7 +1331,7 @@ public String processSLO(Boolean keepLocalSession, String requestId, Boolean sta } String inResponseTo = logoutRequest.id; - LogoutResponse logoutResponseBuilder = new LogoutResponse(settings, httpRequest); + LogoutResponse logoutResponseBuilder = outgoingLogoutResponseFactory.create(settings, null); logoutResponseBuilder.build(inResponseTo, Constants.STATUS_SUCCESS); lastResponse = logoutResponseBuilder.getLogoutResponseXml(); @@ -1644,4 +1658,109 @@ public String getLastRequestXML() { public String getLastResponseXML() { return lastResponse; } + + /** + * Sets the factory this {@link Auth} will use to create {@link AuthnRequest} + * objects. + *

+ * This allows consumers to provide their own extension of {@link AuthnRequest} + * possibly implementing custom features and/or XML post-processing. + * + * @param authnRequestFactory + * the factory to use to create {@link AuthnRequest} objects; if + * null, a default provider will be used which creates + * plain {@link AuthnRequest} instances + */ + public void setAuthnRequestFactory( + final SamlOutgoingMessageFactory authnRequestFactory) { + this.authnRequestFactory = authnRequestFactory != null ? authnRequestFactory + : DEFAULT_AUTHN_REQUEST_FACTORY; + } + + /** + * Sets the factory this {@link Auth} will use to create {@link SamlResponse} + * objects. + *

+ * This allows consumers to provide their own extension of {@link SamlResponse} + * possibly implementing custom features and/or XML validation. + * + * @param samlResponseFactory + * the factory to use to create {@link SamlResponse} objects; if + * null, a default factory will be used which creates + * plain {@link SamlResponse} instances + */ + public void setSamlResponseFactory(final SamlReceivedMessageFactory samlResponseFactory) { + this.samlResponseFactory = samlResponseFactory != null? samlResponseFactory: DEFAULT_SAML_RESPONSE_FACTORY; + } + + /** + * Sets the factory this {@link Auth} will use to create outgoing + * {@link LogoutRequest} objects. + *

+ * This allows consumers to provide their own extension of {@link LogoutRequest} + * possibly implementing custom features and/or XML post-processing. + * + * @param outgoingLogoutRequestFactory + * the factory to use to create outgoing {@link LogoutRequest} + * objects; if null, a default provider will be used + * which creates plain {@link LogoutRequest} instances + */ + public void setOutgoingLogoutRequestFactory(final + SamlOutgoingMessageFactory outgoingLogoutRequestFactory) { + this.outgoingLogoutRequestFactory = outgoingLogoutRequestFactory != null? outgoingLogoutRequestFactory: DEFAULT_OUTGOING_LOGOUT_REQUEST_FACTORY; + } + + /** + * Sets the factory this {@link Auth} will use to create received + * {@link LogoutRequest} objects. + *

+ * This allows consumers to provide their own extension of {@link LogoutRequest} + * possibly implementing custom features and/or XML validation. + * + * @param receivedLogoutRequestFactory + * the factory to use to create received {@link LogoutRequest} + * objects; if null, a default provider will be used + * which creates plain {@link LogoutRequest} instances + */ + public void setReceivedLogoutRequestFactory( + final SamlReceivedMessageFactory receivedLogoutRequestFactory) { + this.receivedLogoutRequestFactory = receivedLogoutRequestFactory != null ? receivedLogoutRequestFactory + : DEFAULT_RECEIVED_LOGOUT_REQUEST_FACTORY; + } + + /** + * Sets the factory this {@link Auth} will use to create outgoing + * {@link LogoutResponse} objects. + *

+ * This allows consumers to provide their own extension of + * {@link LogoutResponse} possibly implementing custom features and/or XML + * post-processing. + * + * @param outgoingLogoutResponseFactory + * the factory to use to create outgoing {@link LogoutResponse} + * objects; if null, a default provider will be used + * which creates plain {@link LogoutResponse} instances + */ + public void setOutgoingLogoutResponseFactory(final + SamlOutgoingMessageFactory outgoingLogoutResponseFactory) { + this.outgoingLogoutResponseFactory = outgoingLogoutResponseFactory != null? outgoingLogoutResponseFactory: DEFAULT_OUTGOING_LOGOUT_RESPONSE_FACTORY; + } + + /** + * Sets the factory this {@link Auth} will use to create received + * {@link LogoutResponse} objects. + *

+ * This allows consumers to provide their own extension of + * {@link LogoutResponse} possibly implementing custom features and/or XML + * validation. + * + * @param receivedLogoutResponseFactory + * the factory to use to create received {@link LogoutResponse} + * objects; if null, a default provider will be used + * which creates plain {@link LogoutResponse} instances + */ + public void setReceivedLogoutResponseFactory(final + SamlReceivedMessageFactory receivedLogoutResponseFactory) { + this.receivedLogoutResponseFactory = receivedLogoutResponseFactory != null? receivedLogoutResponseFactory: DEFAULT_RECEIVED_LOGOUT_RESPONSE_FACTORY; + } } diff --git a/toolkit/src/main/java/com/onelogin/saml2/SamlOutgoingMessageFactory.java b/toolkit/src/main/java/com/onelogin/saml2/SamlOutgoingMessageFactory.java new file mode 100644 index 00000000..8c83b10f --- /dev/null +++ b/toolkit/src/main/java/com/onelogin/saml2/SamlOutgoingMessageFactory.java @@ -0,0 +1,27 @@ +package com.onelogin.saml2; + +import com.onelogin.saml2.settings.Saml2Settings; + +/** + * Factory which can create an outgoing SAML message object from a + * {@link Saml2Settings} instance and other input parameters. + * + * @param + * the type of input parameters required + * @param + * the type of SAML outgoing message object created + */ +@FunctionalInterface +public interface SamlOutgoingMessageFactory { + + /** + * Creates an outgoing SAML message object. + * + * @param settings + * the settings + * @param params + * the input parameters + * @return the created received SAML message object + */ + R create(Saml2Settings settings, U params); +} \ No newline at end of file diff --git a/toolkit/src/main/java/com/onelogin/saml2/SamlReceivedMessageFactory.java b/toolkit/src/main/java/com/onelogin/saml2/SamlReceivedMessageFactory.java new file mode 100644 index 00000000..c086c558 --- /dev/null +++ b/toolkit/src/main/java/com/onelogin/saml2/SamlReceivedMessageFactory.java @@ -0,0 +1,28 @@ +package com.onelogin.saml2; + +import com.onelogin.saml2.http.HttpRequest; +import com.onelogin.saml2.settings.Saml2Settings; + +/** + * Factory which can create a received SAML message object from a + * {@link Saml2Settings} instance and other input parameters. + * + * @param + * the type of received SAML message object created + */ +@FunctionalInterface +public interface SamlReceivedMessageFactory { + + /** + * Creates a received SAML message object. + * + * @param settings + * the settings + * @param httpRequest + * the HTTP request + * @return the created received SAML message object + * @throws Exception + * if the message creation fails + */ + R create(Saml2Settings settings, HttpRequest httpRequest) throws Exception; +} \ 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..8679033e 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,19 @@ 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.http.HttpRequest; +import com.onelogin.saml2.logout.LogoutRequest; import com.onelogin.saml2.logout.LogoutRequestParams; +import com.onelogin.saml2.logout.LogoutResponse; import com.onelogin.saml2.model.KeyStoreSettings; +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 +2228,204 @@ public void testGetLastLogoutResponseReceived() throws Exception { String logoutResponseXML = auth.getLastResponseXML(); assertThat(logoutResponseXML, containsString(" new LogoutResponseEx(settings, null)); + auth.processSLO(false, null); + } + + /** + * Tests that the received LogoutResponse factory gets invoked by Auth and the right parameters are passed to it. + * + * @throws Exception + * + * @see com.onelogin.saml2.Auth#setReceivedLogoutResponseFactory(com.onelogin.saml2.SamlReceivedMessageFactory) + */ + @Test(expected = FactoryInvokedException.class) + public void testReceivedLogoutResponseFactory() throws Exception { + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + HttpSession session = mock(HttpSession.class); + when(request.getRequestURL()).thenReturn(new StringBuffer("http://stuff.com/endpoints/endpoints/sls.php")); + when(request.getSession()).thenReturn(session); + + String samlResponseEncoded = Util.getFileAsString("data/logout_responses/logout_response_deflated.xml.base64"); + when(request.getParameterMap()).thenReturn(singletonMap("SAMLResponse", new String[]{samlResponseEncoded})); + Saml2Settings settings = new SettingsBuilder().fromFile("config/config.min.properties").build(); + + class LogoutResponseEx extends LogoutResponse { + + public LogoutResponseEx(Saml2Settings sett, HttpRequest req) { + super(sett, req); + assertSame(settings, sett); + assertEquals(ServletUtils.makeHttpRequest(request), req); + throw new FactoryInvokedException(); + } + + } + + Auth auth = new Auth(settings, request, response); + auth.setReceivedLogoutResponseFactory(LogoutResponseEx::new); + auth.processSLO(); + } } From 1246cd5e359fb193c9edaa4e05a33103eaf61ca5 Mon Sep 17 00:00:00 2001 From: Mauro Molinari Date: Tue, 27 Jul 2021 14:35:21 +0200 Subject: [PATCH 2/7] Change syntax to avoid javac 8 compilation errors It seems like neither Eclipse ecj nor javac in Java 11 complains when using constructor references in these very special cases used for unit testing. But javac in Java 8 does. So, we're now using lambda expressions in place of constructor references: this seems to make all compilers happy. --- .../test/java/com/onelogin/saml2/test/AuthTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 8679033e..a77e5583 100644 --- a/toolkit/src/test/java/com/onelogin/saml2/test/AuthTest.java +++ b/toolkit/src/test/java/com/onelogin/saml2/test/AuthTest.java @@ -2257,7 +2257,7 @@ public AuthnRequestEx(Saml2Settings sett, AuthnRequestParams par) { } Auth auth = new Auth(settings, request, response); - auth.setAuthnRequestFactory(AuthnRequestEx::new); + auth.setAuthnRequestFactory((sett, param) -> new AuthnRequestEx(sett, param)); auth.login(params); } @@ -2289,7 +2289,7 @@ public SamlResponseEx(Saml2Settings sett, HttpRequest req) throws Exception { } Auth auth = new Auth(settings, request, response); - auth.setSamlResponseFactory(SamlResponseEx::new); + auth.setSamlResponseFactory((sett, req) -> new SamlResponseEx(sett, req)); auth.processResponse(); } @@ -2320,7 +2320,7 @@ public LogoutRequestEx(Saml2Settings sett, LogoutRequestParams par) { } Auth auth = new Auth(settings, request, response); - auth.setOutgoingLogoutRequestFactory(LogoutRequestEx::new); + auth.setOutgoingLogoutRequestFactory((sett, param) -> new LogoutRequestEx(sett, param)); auth.logout(null, params); } @@ -2355,7 +2355,7 @@ public LogoutRequestEx(Saml2Settings sett, HttpRequest req) { } Auth auth = new Auth(settings, request, response); - auth.setReceivedLogoutRequestFactory(LogoutRequestEx::new); + auth.setReceivedLogoutRequestFactory((sett, req) -> new LogoutRequestEx(sett, req)); auth.processSLO(); } @@ -2425,7 +2425,7 @@ public LogoutResponseEx(Saml2Settings sett, HttpRequest req) { } Auth auth = new Auth(settings, request, response); - auth.setReceivedLogoutResponseFactory(LogoutResponseEx::new); + auth.setReceivedLogoutResponseFactory((sett, req) -> new LogoutResponseEx(sett, req)); auth.processSLO(); } } From abd632656b151bd8b984e2a5f055fccc314d63f5 Mon Sep 17 00:00:00 2001 From: Mauro Molinari Date: Fri, 13 Aug 2021 19:13:04 +0200 Subject: [PATCH 3/7] Move message factories to their own package --- toolkit/src/main/java/com/onelogin/saml2/Auth.java | 2 ++ .../{ => factory}/SamlOutgoingMessageFactory.java | 2 +- .../{ => factory}/SamlReceivedMessageFactory.java | 2 +- .../test/java/com/onelogin/saml2/test/AuthTest.java | 12 ++++++------ 4 files changed, 10 insertions(+), 8 deletions(-) rename toolkit/src/main/java/com/onelogin/saml2/{ => factory}/SamlOutgoingMessageFactory.java (94%) rename toolkit/src/main/java/com/onelogin/saml2/{ => factory}/SamlReceivedMessageFactory.java (95%) diff --git a/toolkit/src/main/java/com/onelogin/saml2/Auth.java b/toolkit/src/main/java/com/onelogin/saml2/Auth.java index e5bd0bc3..d3c579bb 100644 --- a/toolkit/src/main/java/com/onelogin/saml2/Auth.java +++ b/toolkit/src/main/java/com/onelogin/saml2/Auth.java @@ -26,6 +26,8 @@ import com.onelogin.saml2.authn.AuthnRequestParams; import com.onelogin.saml2.authn.SamlResponse; import com.onelogin.saml2.exception.SettingsException; +import com.onelogin.saml2.factory.SamlOutgoingMessageFactory; +import com.onelogin.saml2.factory.SamlReceivedMessageFactory; import com.onelogin.saml2.exception.Error; import com.onelogin.saml2.http.HttpRequest; import com.onelogin.saml2.logout.LogoutRequest; diff --git a/toolkit/src/main/java/com/onelogin/saml2/SamlOutgoingMessageFactory.java b/toolkit/src/main/java/com/onelogin/saml2/factory/SamlOutgoingMessageFactory.java similarity index 94% rename from toolkit/src/main/java/com/onelogin/saml2/SamlOutgoingMessageFactory.java rename to toolkit/src/main/java/com/onelogin/saml2/factory/SamlOutgoingMessageFactory.java index 8c83b10f..7cbdd32c 100644 --- a/toolkit/src/main/java/com/onelogin/saml2/SamlOutgoingMessageFactory.java +++ b/toolkit/src/main/java/com/onelogin/saml2/factory/SamlOutgoingMessageFactory.java @@ -1,4 +1,4 @@ -package com.onelogin.saml2; +package com.onelogin.saml2.factory; import com.onelogin.saml2.settings.Saml2Settings; diff --git a/toolkit/src/main/java/com/onelogin/saml2/SamlReceivedMessageFactory.java b/toolkit/src/main/java/com/onelogin/saml2/factory/SamlReceivedMessageFactory.java similarity index 95% rename from toolkit/src/main/java/com/onelogin/saml2/SamlReceivedMessageFactory.java rename to toolkit/src/main/java/com/onelogin/saml2/factory/SamlReceivedMessageFactory.java index c086c558..0a78a800 100644 --- a/toolkit/src/main/java/com/onelogin/saml2/SamlReceivedMessageFactory.java +++ b/toolkit/src/main/java/com/onelogin/saml2/factory/SamlReceivedMessageFactory.java @@ -1,4 +1,4 @@ -package com.onelogin.saml2; +package com.onelogin.saml2.factory; import com.onelogin.saml2.http.HttpRequest; import com.onelogin.saml2.settings.Saml2Settings; 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 a77e5583..6bf588f1 100644 --- a/toolkit/src/test/java/com/onelogin/saml2/test/AuthTest.java +++ b/toolkit/src/test/java/com/onelogin/saml2/test/AuthTest.java @@ -2237,7 +2237,7 @@ private static class FactoryInvokedException extends RuntimeException { * * @throws Exception * - * @see com.onelogin.saml2.Auth#setAuthnRequestFactory(com.onelogin.saml2.SamlOutgoingMessageFactory) + * @see com.onelogin.saml2.Auth#setAuthnRequestFactory(com.onelogin.saml2.factory.SamlOutgoingMessageFactory) */ @Test(expected = FactoryInvokedException.class) public void testAuthnRequestFactory() throws Exception { @@ -2266,7 +2266,7 @@ public AuthnRequestEx(Saml2Settings sett, AuthnRequestParams par) { * * @throws Exception * - * @see com.onelogin.saml2.Auth#setSamlResponseFactory(com.onelogin.saml2.SamlReceivedMessageFactory) + * @see com.onelogin.saml2.Auth#setSamlResponseFactory(com.onelogin.saml2.factory.SamlReceivedMessageFactory) */ @Test(expected = FactoryInvokedException.class) public void testSamlResponseFactory() throws Exception { @@ -2298,7 +2298,7 @@ public SamlResponseEx(Saml2Settings sett, HttpRequest req) throws Exception { * * @throws Exception * - * @see com.onelogin.saml2.Auth#setOutgoingLogoutRequestFactory(com.onelogin.saml2.SamlOutgoingMessageFactory) + * @see com.onelogin.saml2.Auth#setOutgoingLogoutRequestFactory(com.onelogin.saml2.factory.SamlOutgoingMessageFactory) */ @Test(expected = FactoryInvokedException.class) public void testOutgoingLogoutRequestFactory() throws Exception { @@ -2329,7 +2329,7 @@ public LogoutRequestEx(Saml2Settings sett, LogoutRequestParams par) { * * @throws Exception * - * @see com.onelogin.saml2.Auth#setReceivedLogoutRequestFactory(com.onelogin.saml2.SamlReceivedMessageFactory) + * @see com.onelogin.saml2.Auth#setReceivedLogoutRequestFactory(com.onelogin.saml2.factory.SamlReceivedMessageFactory) */ @Test(expected = FactoryInvokedException.class) public void testReceivedLogoutRequestFactory() throws Exception { @@ -2364,7 +2364,7 @@ public LogoutRequestEx(Saml2Settings sett, HttpRequest req) { * * @throws Exception * - * @see com.onelogin.saml2.Auth#setOutgoingLogoutResponseFactory(com.onelogin.saml2.SamlOutgoingMessageFactory) + * @see com.onelogin.saml2.Auth#setOutgoingLogoutResponseFactory(com.onelogin.saml2.factory.SamlOutgoingMessageFactory) */ @Test(expected = FactoryInvokedException.class) public void testOutgoingLogoutResponseFactory() throws Exception { @@ -2399,7 +2399,7 @@ public LogoutResponseEx(Saml2Settings sett, HttpRequest req) { * * @throws Exception * - * @see com.onelogin.saml2.Auth#setReceivedLogoutResponseFactory(com.onelogin.saml2.SamlReceivedMessageFactory) + * @see com.onelogin.saml2.Auth#setReceivedLogoutResponseFactory(com.onelogin.saml2.factory.SamlReceivedMessageFactory) */ @Test(expected = FactoryInvokedException.class) public void testReceivedLogoutResponseFactory() throws Exception { From 11bb45d30145430c653b7d0a46ab8e586852a810 Mon Sep 17 00:00:00 2001 From: Mauro Molinari Date: Fri, 13 Aug 2021 21:54:51 +0200 Subject: [PATCH 4/7] Improve LogoutResponse API to match LogoutRequest/AuthnRequest ones This introduces LogoutResponseParams and allows to make the whole API coherent when building outgoing SAML messages. The Auth factories benefit from this as well, because they now share a common construction and usage pattern. --- .../onelogin/saml2/logout/LogoutResponse.java | 136 +++++++--- .../saml2/logout/LogoutResponseParams.java | 105 ++++++++ .../saml2/test/logout/LogoutResponseTest.java | 243 +++++++++++++++--- .../main/java/com/onelogin/saml2/Auth.java | 11 +- .../com/onelogin/saml2/test/AuthTest.java | 14 +- 5 files changed, 425 insertions(+), 84 deletions(-) create mode 100644 core/src/main/java/com/onelogin/saml2/logout/LogoutResponseParams.java 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 fcc0354d..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,12 +80,8 @@ 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 @@ -115,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) @@ -355,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); } /** @@ -379,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); } @@ -412,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)); @@ -447,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/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 d3c579bb..e0bb66c3 100644 --- a/toolkit/src/main/java/com/onelogin/saml2/Auth.java +++ b/toolkit/src/main/java/com/onelogin/saml2/Auth.java @@ -33,6 +33,7 @@ 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; @@ -174,14 +175,14 @@ public class Auth { private static final SamlReceivedMessageFactory DEFAULT_SAML_RESPONSE_FACTORY = SamlResponse::new; private static final SamlOutgoingMessageFactory DEFAULT_OUTGOING_LOGOUT_REQUEST_FACTORY = LogoutRequest::new; private static final SamlReceivedMessageFactory DEFAULT_RECEIVED_LOGOUT_REQUEST_FACTORY = LogoutRequest::new; - private static final SamlOutgoingMessageFactory DEFAULT_OUTGOING_LOGOUT_RESPONSE_FACTORY = (settings, nothing) -> new LogoutResponse(settings, null); + private static final SamlOutgoingMessageFactory DEFAULT_OUTGOING_LOGOUT_RESPONSE_FACTORY = LogoutResponse::new; private static final SamlReceivedMessageFactory DEFAULT_RECEIVED_LOGOUT_RESPONSE_FACTORY = LogoutResponse::new; private SamlOutgoingMessageFactory authnRequestFactory = DEFAULT_AUTHN_REQUEST_FACTORY; private SamlReceivedMessageFactory samlResponseFactory = DEFAULT_SAML_RESPONSE_FACTORY; private SamlOutgoingMessageFactory outgoingLogoutRequestFactory = DEFAULT_OUTGOING_LOGOUT_REQUEST_FACTORY; private SamlReceivedMessageFactory receivedLogoutRequestFactory = DEFAULT_RECEIVED_LOGOUT_REQUEST_FACTORY; - private SamlOutgoingMessageFactory outgoingLogoutResponseFactory = DEFAULT_OUTGOING_LOGOUT_RESPONSE_FACTORY; + private SamlOutgoingMessageFactory outgoingLogoutResponseFactory = DEFAULT_OUTGOING_LOGOUT_RESPONSE_FACTORY; private SamlReceivedMessageFactory receivedLogoutResponseFactory = DEFAULT_RECEIVED_LOGOUT_RESPONSE_FACTORY; /** @@ -1333,8 +1334,8 @@ public String processSLO(Boolean keepLocalSession, String requestId, Boolean sta } String inResponseTo = logoutRequest.id; - LogoutResponse logoutResponseBuilder = outgoingLogoutResponseFactory.create(settings, null); - logoutResponseBuilder.build(inResponseTo, Constants.STATUS_SUCCESS); + LogoutResponse logoutResponseBuilder = outgoingLogoutResponseFactory.create(settings, + new LogoutResponseParams(inResponseTo, Constants.STATUS_SUCCESS)); lastResponse = logoutResponseBuilder.getLogoutResponseXml(); String samlLogoutResponse = logoutResponseBuilder.getEncodedLogoutResponse(); @@ -1744,7 +1745,7 @@ public void setReceivedLogoutRequestFactory( * which creates plain {@link LogoutResponse} instances */ public void setOutgoingLogoutResponseFactory(final - SamlOutgoingMessageFactory outgoingLogoutResponseFactory) { + SamlOutgoingMessageFactory outgoingLogoutResponseFactory) { this.outgoingLogoutResponseFactory = outgoingLogoutResponseFactory != null? outgoingLogoutResponseFactory: DEFAULT_OUTGOING_LOGOUT_RESPONSE_FACTORY; } 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 6bf588f1..6846bfa7 100644 --- a/toolkit/src/test/java/com/onelogin/saml2/test/AuthTest.java +++ b/toolkit/src/test/java/com/onelogin/saml2/test/AuthTest.java @@ -59,7 +59,9 @@ 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; @@ -2380,17 +2382,21 @@ public void testOutgoingLogoutResponseFactory() throws Exception { class LogoutResponseEx extends LogoutResponse { - public LogoutResponseEx(Saml2Settings sett, HttpRequest req) { - super(sett, req); + public LogoutResponseEx(Saml2Settings sett, LogoutResponseParams par) { + super(sett, par); assertSame(settings, sett); - assertNull(req); + assertEquals("ONELOGIN_21584ccdfaca36a145ae990442dcd96bfe60151e", par.getInResponseTo()); + SamlResponseStatus responseStatus = par.getResponseStatus(); + assertEquals(Constants.STATUS_SUCCESS, responseStatus.getStatusCode()); + assertNull(responseStatus.getSubStatusCode()); + assertNull(responseStatus.getStatusMessage()); throw new FactoryInvokedException(); } } Auth auth = new Auth(settings, request, response); - auth.setOutgoingLogoutResponseFactory((sett, nothing) -> new LogoutResponseEx(settings, null)); + auth.setOutgoingLogoutResponseFactory((sett, param) -> new LogoutResponseEx(settings, param)); auth.processSLO(false, null); } From 41d95ac8c310a1a05c1f666446f55f20ff763b82 Mon Sep 17 00:00:00 2001 From: Mauro Molinari Date: Fri, 13 Aug 2021 21:58:29 +0200 Subject: [PATCH 5/7] Apply minor adjustments to AuthnRequest/LogoutRequest APIs and tests These details were overlooked in the first place: getters of the input params should better be public and fields can be declared as final. The useless NameId setter in LogoutRequestParams was temporarily introduced during development but should have been reverted from the beginning, so it's gone now. Some tests were improved to provide more accurate assertions. --- .../saml2/authn/AuthnRequestParams.java | 2 +- .../onelogin/saml2/logout/LogoutRequest.java | 2 +- .../saml2/logout/LogoutRequestParams.java | 31 +++++++------------ .../saml2/test/logout/LogoutRequestTest.java | 8 +++-- 4 files changed, 18 insertions(+), 25 deletions(-) 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/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"; } }; From b54bff013678e03b6b4a7a4f0adf28e63c788be6 Mon Sep 17 00:00:00 2001 From: Mauro Molinari Date: Fri, 13 Aug 2021 22:41:57 +0200 Subject: [PATCH 6/7] Document the new API and extensibility features in README --- README.md | 76 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index d1bf4d3b..321cfb64 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,56 @@ 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, appropriate factories can then be +specified: + +```java +// let AuthnRequestEx, SamlResponseEx, LogoutRequestEx, LogoutResponseEx be your extension classes +Auth auth = new Auth(request, response); +auth.setAuthnRequestFactory(AuthnRequestEx::new); // to make it build custom AuthnRequests +auth.setSamlResponseFactory(SamlResponseEx::new); // to make it build custom SamlResponses +auth.setOutgoingLogoutRequestFactory(LogoutRequestEx::new); // to make it build custom outgoing LogoutRequests +auth.setReceivedLogoutRequestFactory(LogoutRequestEx::new); // to make it build custom incoming LogoutRequests +auth.setOutgoingLogoutResponseFactory(LogoutResponseEx::new); // to make it build custom outgoing LogoutResponses +auth.setReceivedLogoutResponseFactory(LogoutResponseEx::new); // to make it build custom incoming LogoutResponses +// then proceed with login/processResponse/logout/processSLO... +``` ### Working behind load balancer From 2917a41f6242554e412d446a092bdc0a59ba1f42 Mon Sep 17 00:00:00 2001 From: Mauro Molinari Date: Wed, 18 Aug 2021 18:02:37 +0200 Subject: [PATCH 7/7] Change factory approach to use just a single multi-message factory In this way the API of Auth gets simplified. --- README.md | 26 ++-- .../main/java/com/onelogin/saml2/Auth.java | 137 +++--------------- .../saml2/factory/SamlMessageFactory.java | 119 +++++++++++++++ .../factory/SamlOutgoingMessageFactory.java | 27 ---- .../factory/SamlReceivedMessageFactory.java | 28 ---- .../com/onelogin/saml2/test/AuthTest.java | 61 ++++++-- 6 files changed, 201 insertions(+), 197 deletions(-) create mode 100644 toolkit/src/main/java/com/onelogin/saml2/factory/SamlMessageFactory.java delete mode 100644 toolkit/src/main/java/com/onelogin/saml2/factory/SamlOutgoingMessageFactory.java delete mode 100644 toolkit/src/main/java/com/onelogin/saml2/factory/SamlReceivedMessageFactory.java diff --git a/README.md b/README.md index 321cfb64..ac1fd237 100644 --- a/README.md +++ b/README.md @@ -653,19 +653,25 @@ All the provided SAML message classes (`AuthnRequest`, `SamlResponse`, `LogoutRe 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, appropriate factories can then be -specified: +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 -// let AuthnRequestEx, SamlResponseEx, LogoutRequestEx, LogoutResponseEx be your extension classes Auth auth = new Auth(request, response); -auth.setAuthnRequestFactory(AuthnRequestEx::new); // to make it build custom AuthnRequests -auth.setSamlResponseFactory(SamlResponseEx::new); // to make it build custom SamlResponses -auth.setOutgoingLogoutRequestFactory(LogoutRequestEx::new); // to make it build custom outgoing LogoutRequests -auth.setReceivedLogoutRequestFactory(LogoutRequestEx::new); // to make it build custom incoming LogoutRequests -auth.setOutgoingLogoutResponseFactory(LogoutResponseEx::new); // to make it build custom outgoing LogoutResponses -auth.setReceivedLogoutResponseFactory(LogoutResponseEx::new); // to make it build custom incoming LogoutResponses -// then proceed with login/processResponse/logout/processSLO... +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/toolkit/src/main/java/com/onelogin/saml2/Auth.java b/toolkit/src/main/java/com/onelogin/saml2/Auth.java index e0bb66c3..511914b8 100644 --- a/toolkit/src/main/java/com/onelogin/saml2/Auth.java +++ b/toolkit/src/main/java/com/onelogin/saml2/Auth.java @@ -26,8 +26,7 @@ import com.onelogin.saml2.authn.AuthnRequestParams; import com.onelogin.saml2.authn.SamlResponse; import com.onelogin.saml2.exception.SettingsException; -import com.onelogin.saml2.factory.SamlOutgoingMessageFactory; -import com.onelogin.saml2.factory.SamlReceivedMessageFactory; +import com.onelogin.saml2.factory.SamlMessageFactory; import com.onelogin.saml2.exception.Error; import com.onelogin.saml2.http.HttpRequest; import com.onelogin.saml2.logout.LogoutRequest; @@ -171,19 +170,9 @@ public class Auth { */ private String lastResponse; - private static final SamlOutgoingMessageFactory DEFAULT_AUTHN_REQUEST_FACTORY = AuthnRequest::new; - private static final SamlReceivedMessageFactory DEFAULT_SAML_RESPONSE_FACTORY = SamlResponse::new; - private static final SamlOutgoingMessageFactory DEFAULT_OUTGOING_LOGOUT_REQUEST_FACTORY = LogoutRequest::new; - private static final SamlReceivedMessageFactory DEFAULT_RECEIVED_LOGOUT_REQUEST_FACTORY = LogoutRequest::new; - private static final SamlOutgoingMessageFactory DEFAULT_OUTGOING_LOGOUT_RESPONSE_FACTORY = LogoutResponse::new; - private static final SamlReceivedMessageFactory DEFAULT_RECEIVED_LOGOUT_RESPONSE_FACTORY = LogoutResponse::new; + private static final SamlMessageFactory DEFAULT_SAML_MESSAGE_FACTORY = new SamlMessageFactory() {}; - private SamlOutgoingMessageFactory authnRequestFactory = DEFAULT_AUTHN_REQUEST_FACTORY; - private SamlReceivedMessageFactory samlResponseFactory = DEFAULT_SAML_RESPONSE_FACTORY; - private SamlOutgoingMessageFactory outgoingLogoutRequestFactory = DEFAULT_OUTGOING_LOGOUT_REQUEST_FACTORY; - private SamlReceivedMessageFactory receivedLogoutRequestFactory = DEFAULT_RECEIVED_LOGOUT_REQUEST_FACTORY; - private SamlOutgoingMessageFactory outgoingLogoutResponseFactory = DEFAULT_OUTGOING_LOGOUT_RESPONSE_FACTORY; - private SamlReceivedMessageFactory receivedLogoutResponseFactory = DEFAULT_RECEIVED_LOGOUT_RESPONSE_FACTORY; + private SamlMessageFactory samlMessageFactory = DEFAULT_SAML_MESSAGE_FACTORY; /** * Initializes the SP SAML instance. @@ -626,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 = authnRequestFactory.create(settings, authnRequestParams); + AuthnRequest authnRequest = samlMessageFactory.createAuthnRequest(settings, authnRequestParams); if (parameters == null) { parameters = new HashMap(); @@ -802,7 +791,7 @@ public String logout(String relayState, LogoutRequestParams logoutRequestParams, parameters = new HashMap(); } - LogoutRequest logoutRequest = outgoingLogoutRequestFactory.create(settings, logoutRequestParams); + LogoutRequest logoutRequest = samlMessageFactory.createOutgoingLogoutRequest(settings, logoutRequestParams); String samlLogoutRequest = logoutRequest.getEncodedLogoutRequest(); parameters.put("SAMLRequest", samlLogoutRequest); @@ -1213,7 +1202,7 @@ public void processResponse(String requestId) throws Exception { final String samlResponseParameter = httpRequest.getParameter("SAMLResponse"); if (samlResponseParameter != null) { - SamlResponse samlResponse = samlResponseFactory.create(settings, httpRequest); + SamlResponse samlResponse = samlMessageFactory.createSamlResponse(settings, httpRequest); lastResponse = samlResponse.getSAMLResponseXml(); if (samlResponse.isValid(requestId)) { @@ -1286,7 +1275,7 @@ public String processSLO(Boolean keepLocalSession, String requestId, Boolean sta final String samlResponseParameter = httpRequest.getParameter("SAMLResponse"); if (samlResponseParameter != null) { - LogoutResponse logoutResponse = receivedLogoutResponseFactory.create(settings, httpRequest); + LogoutResponse logoutResponse = samlMessageFactory.createIncomingLogoutResponse(settings, httpRequest); lastResponse = logoutResponse.getLogoutResponseXml(); if (!logoutResponse.isValid(requestId)) { errors.add("invalid_logout_response"); @@ -1316,7 +1305,7 @@ public String processSLO(Boolean keepLocalSession, String requestId, Boolean sta } return null; } else if (samlRequestParameter != null) { - LogoutRequest logoutRequest = receivedLogoutRequestFactory.create(settings, httpRequest); + LogoutRequest logoutRequest = samlMessageFactory.createIncomingLogoutRequest(settings, httpRequest); lastRequest = logoutRequest.getLogoutRequestXml(); if (!logoutRequest.isValid()) { errors.add("invalid_logout_request"); @@ -1334,7 +1323,7 @@ public String processSLO(Boolean keepLocalSession, String requestId, Boolean sta } String inResponseTo = logoutRequest.id; - LogoutResponse logoutResponseBuilder = outgoingLogoutResponseFactory.create(settings, + LogoutResponse logoutResponseBuilder = samlMessageFactory.createOutgoingLogoutResponse(settings, new LogoutResponseParams(inResponseTo, Constants.STATUS_SUCCESS)); lastResponse = logoutResponseBuilder.getLogoutResponseXml(); @@ -1663,107 +1652,19 @@ public String getLastResponseXML() { } /** - * Sets the factory this {@link Auth} will use to create {@link AuthnRequest} - * objects. + * Sets the factory this {@link Auth} will use to create SAML messages. *

- * This allows consumers to provide their own extension of {@link AuthnRequest} - * possibly implementing custom features and/or XML post-processing. + * This allows consumers to provide their own extension classes for SAML message + * XML generation and/or processing. * - * @param authnRequestFactory - * the factory to use to create {@link AuthnRequest} objects; if + * @param samlMessageFactory + * the factory to use to create SAML message objects; if * null, a default provider will be used which creates - * plain {@link AuthnRequest} instances + * the standard message implementation provided by this library + * (i.e.: {@link AuthnRequest}, {@link SamlResponse}, + * {@link LogoutRequest} and {@link LogoutResponse}) */ - public void setAuthnRequestFactory( - final SamlOutgoingMessageFactory authnRequestFactory) { - this.authnRequestFactory = authnRequestFactory != null ? authnRequestFactory - : DEFAULT_AUTHN_REQUEST_FACTORY; - } - - /** - * Sets the factory this {@link Auth} will use to create {@link SamlResponse} - * objects. - *

- * This allows consumers to provide their own extension of {@link SamlResponse} - * possibly implementing custom features and/or XML validation. - * - * @param samlResponseFactory - * the factory to use to create {@link SamlResponse} objects; if - * null, a default factory will be used which creates - * plain {@link SamlResponse} instances - */ - public void setSamlResponseFactory(final SamlReceivedMessageFactory samlResponseFactory) { - this.samlResponseFactory = samlResponseFactory != null? samlResponseFactory: DEFAULT_SAML_RESPONSE_FACTORY; - } - - /** - * Sets the factory this {@link Auth} will use to create outgoing - * {@link LogoutRequest} objects. - *

- * This allows consumers to provide their own extension of {@link LogoutRequest} - * possibly implementing custom features and/or XML post-processing. - * - * @param outgoingLogoutRequestFactory - * the factory to use to create outgoing {@link LogoutRequest} - * objects; if null, a default provider will be used - * which creates plain {@link LogoutRequest} instances - */ - public void setOutgoingLogoutRequestFactory(final - SamlOutgoingMessageFactory outgoingLogoutRequestFactory) { - this.outgoingLogoutRequestFactory = outgoingLogoutRequestFactory != null? outgoingLogoutRequestFactory: DEFAULT_OUTGOING_LOGOUT_REQUEST_FACTORY; - } - - /** - * Sets the factory this {@link Auth} will use to create received - * {@link LogoutRequest} objects. - *

- * This allows consumers to provide their own extension of {@link LogoutRequest} - * possibly implementing custom features and/or XML validation. - * - * @param receivedLogoutRequestFactory - * the factory to use to create received {@link LogoutRequest} - * objects; if null, a default provider will be used - * which creates plain {@link LogoutRequest} instances - */ - public void setReceivedLogoutRequestFactory( - final SamlReceivedMessageFactory receivedLogoutRequestFactory) { - this.receivedLogoutRequestFactory = receivedLogoutRequestFactory != null ? receivedLogoutRequestFactory - : DEFAULT_RECEIVED_LOGOUT_REQUEST_FACTORY; - } - - /** - * Sets the factory this {@link Auth} will use to create outgoing - * {@link LogoutResponse} objects. - *

- * This allows consumers to provide their own extension of - * {@link LogoutResponse} possibly implementing custom features and/or XML - * post-processing. - * - * @param outgoingLogoutResponseFactory - * the factory to use to create outgoing {@link LogoutResponse} - * objects; if null, a default provider will be used - * which creates plain {@link LogoutResponse} instances - */ - public void setOutgoingLogoutResponseFactory(final - SamlOutgoingMessageFactory outgoingLogoutResponseFactory) { - this.outgoingLogoutResponseFactory = outgoingLogoutResponseFactory != null? outgoingLogoutResponseFactory: DEFAULT_OUTGOING_LOGOUT_RESPONSE_FACTORY; - } - - /** - * Sets the factory this {@link Auth} will use to create received - * {@link LogoutResponse} objects. - *

- * This allows consumers to provide their own extension of - * {@link LogoutResponse} possibly implementing custom features and/or XML - * validation. - * - * @param receivedLogoutResponseFactory - * the factory to use to create received {@link LogoutResponse} - * objects; if null, a default provider will be used - * which creates plain {@link LogoutResponse} instances - */ - public void setReceivedLogoutResponseFactory(final - SamlReceivedMessageFactory receivedLogoutResponseFactory) { - this.receivedLogoutResponseFactory = receivedLogoutResponseFactory != null? receivedLogoutResponseFactory: DEFAULT_RECEIVED_LOGOUT_RESPONSE_FACTORY; + 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/main/java/com/onelogin/saml2/factory/SamlOutgoingMessageFactory.java b/toolkit/src/main/java/com/onelogin/saml2/factory/SamlOutgoingMessageFactory.java deleted file mode 100644 index 7cbdd32c..00000000 --- a/toolkit/src/main/java/com/onelogin/saml2/factory/SamlOutgoingMessageFactory.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.onelogin.saml2.factory; - -import com.onelogin.saml2.settings.Saml2Settings; - -/** - * Factory which can create an outgoing SAML message object from a - * {@link Saml2Settings} instance and other input parameters. - * - * @param - * the type of input parameters required - * @param - * the type of SAML outgoing message object created - */ -@FunctionalInterface -public interface SamlOutgoingMessageFactory { - - /** - * Creates an outgoing SAML message object. - * - * @param settings - * the settings - * @param params - * the input parameters - * @return the created received SAML message object - */ - R create(Saml2Settings settings, U params); -} \ No newline at end of file diff --git a/toolkit/src/main/java/com/onelogin/saml2/factory/SamlReceivedMessageFactory.java b/toolkit/src/main/java/com/onelogin/saml2/factory/SamlReceivedMessageFactory.java deleted file mode 100644 index 0a78a800..00000000 --- a/toolkit/src/main/java/com/onelogin/saml2/factory/SamlReceivedMessageFactory.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.onelogin.saml2.factory; - -import com.onelogin.saml2.http.HttpRequest; -import com.onelogin.saml2.settings.Saml2Settings; - -/** - * Factory which can create a received SAML message object from a - * {@link Saml2Settings} instance and other input parameters. - * - * @param - * the type of received SAML message object created - */ -@FunctionalInterface -public interface SamlReceivedMessageFactory { - - /** - * Creates a received SAML message object. - * - * @param settings - * the settings - * @param httpRequest - * the HTTP request - * @return the created received SAML message object - * @throws Exception - * if the message creation fails - */ - R create(Saml2Settings settings, HttpRequest httpRequest) throws Exception; -} \ 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 6846bfa7..6c376839 100644 --- a/toolkit/src/test/java/com/onelogin/saml2/test/AuthTest.java +++ b/toolkit/src/test/java/com/onelogin/saml2/test/AuthTest.java @@ -55,6 +55,7 @@ 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; @@ -2235,7 +2236,7 @@ private static class FactoryInvokedException extends RuntimeException { } /** - * Tests that the AuthnRequest factory gets invoked by Auth and the right parameters are passed to it. + * Tests that the SAML message factory gets invoked by Auth for AuthnRequests and the right parameters are passed to it. * * @throws Exception * @@ -2259,12 +2260,17 @@ public AuthnRequestEx(Saml2Settings sett, AuthnRequestParams par) { } Auth auth = new Auth(settings, request, response); - auth.setAuthnRequestFactory((sett, param) -> new AuthnRequestEx(sett, param)); + auth.setSamlMessageFactory(new SamlMessageFactory() { + @Override + public AuthnRequest createAuthnRequest(Saml2Settings settings, AuthnRequestParams params) { + return new AuthnRequestEx(settings, params); + } + }); auth.login(params); } /** - * Tests that the SamlResponse factory gets invoked by Auth and the right parameters are passed to it. + * Tests that the SAML message factory gets invoked by Auth for SamlResponses and the right parameters are passed to it. * * @throws Exception * @@ -2291,12 +2297,17 @@ public SamlResponseEx(Saml2Settings sett, HttpRequest req) throws Exception { } Auth auth = new Auth(settings, request, response); - auth.setSamlResponseFactory((sett, req) -> new SamlResponseEx(sett, req)); + auth.setSamlMessageFactory(new SamlMessageFactory() { + @Override + public SamlResponse createSamlResponse(Saml2Settings settings, HttpRequest request) throws Exception { + return new SamlResponseEx(settings, request); + } + }); auth.processResponse(); } /** - * Tests that the outgoing LogoutRequest factory gets invoked by Auth and the right parameters are passed to it. + * Tests that the SAML message factory gets invoked by Auth for outgoing LogoutRequests and the right parameters are passed to it. * * @throws Exception * @@ -2322,19 +2333,24 @@ public LogoutRequestEx(Saml2Settings sett, LogoutRequestParams par) { } Auth auth = new Auth(settings, request, response); - auth.setOutgoingLogoutRequestFactory((sett, param) -> new LogoutRequestEx(sett, param)); + auth.setSamlMessageFactory(new SamlMessageFactory() { + @Override + public LogoutRequest createOutgoingLogoutRequest(Saml2Settings settings, LogoutRequestParams params) { + return new LogoutRequestEx(settings, params); + } + }); auth.logout(null, params); } /** - * Tests that the received LogoutRequest factory gets invoked by Auth and the right parameters are passed to it. + * Tests that the SAML message factory gets invoked by Auth for incoming LogoutRequests and the right parameters are passed to it. * * @throws Exception * * @see com.onelogin.saml2.Auth#setReceivedLogoutRequestFactory(com.onelogin.saml2.factory.SamlReceivedMessageFactory) */ @Test(expected = FactoryInvokedException.class) - public void testReceivedLogoutRequestFactory() throws Exception { + public void testIncomingLogoutRequestFactory() throws Exception { HttpServletRequest request = mock(HttpServletRequest.class); HttpServletResponse response = mock(HttpServletResponse.class); HttpSession session = mock(HttpSession.class); @@ -2357,12 +2373,18 @@ public LogoutRequestEx(Saml2Settings sett, HttpRequest req) { } Auth auth = new Auth(settings, request, response); - auth.setReceivedLogoutRequestFactory((sett, req) -> new LogoutRequestEx(sett, req)); + auth.setSamlMessageFactory(new SamlMessageFactory() { + @Override + public LogoutRequest createIncomingLogoutRequest(Saml2Settings settings, HttpRequest request) + throws Exception { + return new LogoutRequestEx(settings, request); + } + }); auth.processSLO(); } /** - * Tests that the outgoing LogoutResponse factory gets invoked by Auth and the right parameters are passed to it. + * Tests that the SAML message factory gets invoked by Auth for outgoing LogoutResponses and the right parameters are passed to it. * * @throws Exception * @@ -2396,19 +2418,25 @@ public LogoutResponseEx(Saml2Settings sett, LogoutResponseParams par) { } Auth auth = new Auth(settings, request, response); - auth.setOutgoingLogoutResponseFactory((sett, param) -> new LogoutResponseEx(settings, param)); + auth.setSamlMessageFactory(new SamlMessageFactory() { + @Override + public LogoutResponse createOutgoingLogoutResponse(Saml2Settings settings, + LogoutResponseParams params) { + return new LogoutResponseEx(settings, params); + } + }); auth.processSLO(false, null); } /** - * Tests that the received LogoutResponse factory gets invoked by Auth and the right parameters are passed to it. + * Tests that the SAML message factory gets invoked by Auth for incoming LogoutResponses and the right parameters are passed to it. * * @throws Exception * * @see com.onelogin.saml2.Auth#setReceivedLogoutResponseFactory(com.onelogin.saml2.factory.SamlReceivedMessageFactory) */ @Test(expected = FactoryInvokedException.class) - public void testReceivedLogoutResponseFactory() throws Exception { + public void testIncomingLogoutResponseFactory() throws Exception { HttpServletRequest request = mock(HttpServletRequest.class); HttpServletResponse response = mock(HttpServletResponse.class); HttpSession session = mock(HttpSession.class); @@ -2431,7 +2459,12 @@ public LogoutResponseEx(Saml2Settings sett, HttpRequest req) { } Auth auth = new Auth(settings, request, response); - auth.setReceivedLogoutResponseFactory((sett, req) -> new LogoutResponseEx(sett, req)); + auth.setSamlMessageFactory(new SamlMessageFactory() { + @Override + public LogoutResponse createIncomingLogoutResponse(Saml2Settings settings, HttpRequest request) { + return new LogoutResponseEx(settings, request); + } + }); auth.processSLO(); } }