diff --git a/README.md b/README.md index 488b62bf91..87fbb71257 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ It has a **very simple and unified API** to support these 4 protocols on client 1. OAuth (1.0 & 2.0) 2. CAS (1.0, 2.0, SAML, logout & proxy) 3. HTTP (form & basic auth authentications) -4. OpenID. +4. OpenID +5. SAML (2.0) There are 5 libraries implementing **pac4j** for the following environments: @@ -40,7 +41,7 @@ This Maven project is composed of 6 modules: #### pac4j-core: this is the core module of the project with the core classes/interfaces -* the *Client* interface is the **main API of the project** as it defines the mechanism that all clients must follow: getRedirectionUrl(WebContext,boolean,boolean), getCredentials(WebContext) and getUserProfile(Credentials,WebContext) +* the *Client* interface is the **main API of the project** as it defines the mechanism that all clients must follow: redirect(WebContext,boolean,boolean), getCredentials(WebContext) and getUserProfile(Credentials,WebContext) * the *Credentials* class is the base class for all credentials * the *UserProfile* class is the base class for all user profiles (it is associated with attributes definition and converters) * the *CommonProfile* class inherits from the *UserProfile* class and implements all the common getters that profiles must have (getFirstName(), getEmail()...) @@ -79,6 +80,14 @@ This module is based on the **pac4j-core** module and the [commons-codec](http:/ This module is based on the **pac4j-core** module and the [openid4java](http://code.google.com/p/openid4java/) library. +#### pac4j-saml: this module is dedicated to SAML support: + +* the *Saml2Client* class is the client for integrating with a SAML2 compliant Identity Provider +* the *Saml2Credentials* class is the credentials for SAML2 support +* the *Saml2Profile* class is the user profile returned by the *Saml2Client*. + +This module is based on the **pac4j-core** module and the [OpenSAML library](https://wiki.shibboleth.net/confluence/display/OpenSAML/Home). + #### pac4j-test-cas: this module is made to test CAS support in pac4j. Learn more by browsing the [Javadoc](http://www.pac4j.org/apidocs/pac4j/index.html). @@ -112,7 +121,7 @@ Learn more by browsing the [Javadoc](http://www.pac4j.org/apidocs/pac4j/index.ht ### Maven dependencies -First, you have define the right dependency: pac4j-oauth for OAuth support or/and pac4j-cas for CAS support or/and pac4j-http for HTTP support or/and pac4j-openid for OpenID support. +First, you have define the right dependency: pac4j-oauth for OAuth support or/and pac4j-cas for CAS support or/and pac4j-http for HTTP support or/and pac4j-openid for OpenID support or/and pac4j-saml for SAML support. For example: @@ -145,7 +154,7 @@ If you want to authenticate and get the user profile from Facebook, you have to client.setCallbackUrl("http://myserver/myapp/callbackUrl"); // send the user to Facebook for authentication and permissions WebContext context = new J2EContext(request, response); - response.sendRedirect(client.getRedirectionUrl(context, false, false)); + client.redirect(context, false, false); ...after successfull authentication, in the client application, on the callback url (for Facebook)... @@ -165,7 +174,7 @@ For integrating an application with a CAS server, you should use the *org.pac4j. client.setCallbackUrl("http://myserver/myapp/callbackUrl"); // send the user to the CAS server for authentication WebContext context = new J2EContext(request, response); - response.sendRedirect(client.getRedirectionUrl(context, false, false)); + client.redirect(context, false, false); ...after successfull authentication, in the client application, on the callback url... @@ -195,7 +204,7 @@ To use form authentication in a web application, you should use the *org.pac4j.h client.setCallbackUrl("http://myserver/myapp/callbackUrl"); // send the user to the form for authentication WebContext context = new J2EContext(request, response); - response.sendRedirect(client.getRedirectionUrl(context, false, false)); + client.redirect(context, false, false); ...after successfull authentication... @@ -220,7 +229,7 @@ To use Google and OpenID for authentication, you should use the *org.pac4j.openi // send the user to Google for authentication WebContext context = new J2EContext(request, response); // we assume the user identifier is in the "openIdUser" request parameter - response.sendRedirect(client.getRedirectionUrl(context, false, false)); + client.redirect(context, false, false); ...after successfull authentication, in the client application, on the callback url... @@ -230,6 +239,36 @@ To use Google and OpenID for authentication, you should use the *org.pac4j.openi GoogleOpenIdProfile profile = client.getUserProfile(credentials, context); System.out.println("Hello: " + profile.getDisplayName()); +### SAML support + +For integrating an application with a SAML2 Identity Provider server, you should use the *org.pac4j.saml.client.Saml2Client*: + + //Generate a keystore for all signature and encryption stuff: + keytool -genkeypair -alias pac4j-demo -keypass pac4j-demo-passwd -keystore samlKeystore.jks -storepass pac4j-demo-passwd -keyalg RSA -keysize 2048 -validity 3650 + + // declare the client + Saml2Client client = new Saml2Client(); + // configure keystore + client.setKeystorePath("samlKeystore.jks"); + client.setKeystorePassword("pac4j-demo-passwd"); + client.setPrivateKeyPassword("pac4j-demo-passwd"); + // configure a file containing the Identity Provider metadata + client.setIdpMetadataPath("testshib-providers.xml"); + + // generate pac4j SAML2 Service Provider metadata to import on Idenity Provider side + String spMetadata = client.printClientMetadata(); + + // send the user to the Identity Provider server for authentication + WebContext context = new J2EContext(request, response); + client.redirect(context, false, false); + +...after successfull authentication, in the Service Provider application, on the assertion consumer service url... + + // get SAML2 credentials + Saml2Credentials credentials = client.getCredentials(context)); + // get the SAML2 profile + Saml2Profile saml2Profile = client.getUserProfile(credentials, context); + ### Multiple clients If you use multiple clients, you can use more generic objects. All profiles inherit from the *org.pac4j.core.profile.CommonProfile* class: diff --git a/pac4j-cas/src/main/java/org/pac4j/cas/client/CasClient.java b/pac4j-cas/src/main/java/org/pac4j/cas/client/CasClient.java index 0a897efa7f..2d0906a9e5 100644 --- a/pac4j-cas/src/main/java/org/pac4j/cas/client/CasClient.java +++ b/pac4j-cas/src/main/java/org/pac4j/cas/client/CasClient.java @@ -34,6 +34,7 @@ import org.pac4j.cas.profile.CasProxyProfile; import org.pac4j.core.client.BaseClient; import org.pac4j.core.client.Protocol; +import org.pac4j.core.client.RedirectAction; import org.pac4j.core.context.WebContext; import org.pac4j.core.exception.CredentialsException; import org.pac4j.core.exception.RequiresHttpAction; @@ -124,14 +125,14 @@ public enum CasProtocol { * @return the redirection url */ @Override - protected String retrieveRedirectionUrl(final WebContext context) { + protected RedirectAction retrieveRedirectAction(final WebContext context) { final String contextualCasLoginUrl = prependHostToUrlIfNotPresent(this.casLoginUrl, context); final String contextualCallbackUrl = getContextualCallbackUrl(context); final String redirectionUrl = CommonUtils.constructRedirectUrl(contextualCasLoginUrl, SERVICE_PARAMETER, contextualCallbackUrl, this.renew, this.gateway); logger.debug("redirectionUrl : {}", redirectionUrl); - return redirectionUrl; + return RedirectAction.redirect(redirectionUrl); } @Override diff --git a/pac4j-cas/src/main/java/org/pac4j/cas/client/CasProxyReceptor.java b/pac4j-cas/src/main/java/org/pac4j/cas/client/CasProxyReceptor.java index 98fe63e092..df6a2eaab3 100644 --- a/pac4j-cas/src/main/java/org/pac4j/cas/client/CasProxyReceptor.java +++ b/pac4j-cas/src/main/java/org/pac4j/cas/client/CasProxyReceptor.java @@ -26,6 +26,7 @@ import org.pac4j.cas.profile.CasProfile; import org.pac4j.core.client.BaseClient; import org.pac4j.core.client.Protocol; +import org.pac4j.core.client.RedirectAction; import org.pac4j.core.context.WebContext; import org.pac4j.core.exception.RequiresHttpAction; import org.pac4j.core.exception.TechnicalException; @@ -146,7 +147,7 @@ public String toString() { /** * {@inheritDoc} */ - protected String retrieveRedirectionUrl(final WebContext context) { + protected RedirectAction retrieveRedirectAction(final WebContext context) { throw new TechnicalException("Not supported by the CAS proxy receptor"); } diff --git a/pac4j-cas/src/test/java/org/pac4j/cas/client/TestCasClient.java b/pac4j-cas/src/test/java/org/pac4j/cas/client/TestCasClient.java index fe338c0631..cf595ae2a1 100644 --- a/pac4j-cas/src/test/java/org/pac4j/cas/client/TestCasClient.java +++ b/pac4j-cas/src/test/java/org/pac4j/cas/client/TestCasClient.java @@ -72,10 +72,14 @@ public void testRenew() throws RequiresHttpAction { final CasClient casClient = new CasClient(); casClient.setCallbackUrl(CALLBACK_URL); casClient.setCasLoginUrl(LOGIN_URL); - assertFalse(casClient.getRedirectionUrl(MockWebContext.create(), false, false).indexOf("renew=true") >= 0); + MockWebContext context = MockWebContext.create(); + casClient.redirect(context, false, false); + assertFalse(context.getResponseLocation().indexOf("renew=true") >= 0); casClient.setRenew(true); casClient.reinit(); - assertTrue(casClient.getRedirectionUrl(MockWebContext.create(), false, false).indexOf("renew=true") >= 0); + context = MockWebContext.create(); + casClient.redirect(context, false, false); + assertTrue(context.getResponseLocation().indexOf("renew=true") >= 0); } public void testGateway() throws RequiresHttpAction { @@ -83,10 +87,12 @@ public void testGateway() throws RequiresHttpAction { casClient.setCallbackUrl(CALLBACK_URL); casClient.setCasLoginUrl(LOGIN_URL); final MockWebContext context = MockWebContext.create(); - assertFalse(casClient.getRedirectionUrl(context, false, false).indexOf("gateway=true") >= 0); + casClient.redirect(context, false, false); + assertFalse(context.getResponseLocation().indexOf("gateway=true") >= 0); casClient.setGateway(true); casClient.reinit(); - assertTrue(casClient.getRedirectionUrl(context, false, false).indexOf("gateway=true") >= 0); + casClient.redirect(context, false, false); + assertTrue(context.getResponseLocation().indexOf("gateway=true") >= 0); final CasCredentials credentials = casClient.getCredentials(context); assertNull(credentials); } diff --git a/pac4j-core/pom.xml b/pac4j-core/pom.xml index d034c799a4..974987e45d 100644 --- a/pac4j-core/pom.xml +++ b/pac4j-core/pom.xml @@ -41,6 +41,12 @@ kryo provided + + commons-io + commons-io + 2.4 + + junit diff --git a/pac4j-core/src/main/java/org/pac4j/core/client/BaseClient.java b/pac4j-core/src/main/java/org/pac4j/core/client/BaseClient.java index a01cab36a1..fb340f7b34 100644 --- a/pac4j-core/src/main/java/org/pac4j/core/client/BaseClient.java +++ b/pac4j-core/src/main/java/org/pac4j/core/client/BaseClient.java @@ -15,6 +15,7 @@ */ package org.pac4j.core.client; +import org.pac4j.core.client.RedirectAction.RedirectType; import org.pac4j.core.context.HttpConstants; import org.pac4j.core.context.WebContext; import org.pac4j.core.credentials.Credentials; @@ -35,10 +36,10 @@ *
  • the callback url is handled through the {@link #setCallbackUrl(String)} and {@link #getCallbackUrl()} methods
  • *
  • the name of the client is handled through the {@link #setName(String)} and {@link #getName()} methods
  • *
  • the concept of "direct" redirection is defined through the {@link #isDirectRedirection()} method : if true, the - * {@link #getRedirectionUrl(WebContext,boolean,boolean)} method will always return the redirection to the provider where as if it's false, + * {@link #getRedirection(WebContext,boolean,boolean)} method will always return the redirection to the provider where as if it's false, * the redirection url will be the callback url with an additionnal parameter : {@link #NEEDS_CLIENT_REDIRECTION_PARAMETER} to require the * redirection, which will be handled later in the {@link #getCredentials(WebContext)} method.
    - * To force a direct redirection, the {@link #getRedirectionUrl(WebContext, boolean, boolean)} must be used with true for the + * To force a direct redirection, the {@link #getRedirection(WebContext, boolean, boolean)} must be used with true for the * forceDirectRedirection parameter
  • *
  • if you enable "contextual redirects" by using the {@link #setEnableContextualRedirects(boolean)}, you can use relative callback urls * which will be completed according to the current host, port and scheme. Disabled by default.
  • @@ -117,7 +118,7 @@ public String getName() { */ protected abstract boolean isDirectRedirection(); - public final String getRedirectionUrl(final WebContext context, final boolean protectedTarget, + public final void redirect(final WebContext context, final boolean protectedTarget, final boolean ajaxRequest) throws RequiresHttpAction { init(); // it's an AJAX request -> unauthorized (instead of a redirection) @@ -136,22 +137,40 @@ public final String getRedirectionUrl(final WebContext context, final boolean pr } // it's a direct redirection or force the redirection -> return the redirection url if (isDirectRedirection() || protectedTarget) { - return retrieveRedirectionUrl(context); + RedirectAction action = retrieveRedirectAction(context); + if (action.getType() == RedirectType.REDIRECT) { + context.sendRedirect(action.getLocation()); + } else if (action.getType() == RedirectType.SUCCESS) { + context.writeResponseContent(action.getContent()); + context.setResponseStatus(HttpConstants.OK); + } } else { // return an intermediate url which is the callback url with a specific parameter requiring redirection - return CommonHelper.addParameter(getContextualCallbackUrl(context), NEEDS_CLIENT_REDIRECTION_PARAMETER, - "true"); + context.sendRedirect(CommonHelper.addParameter(getContextualCallbackUrl(context), NEEDS_CLIENT_REDIRECTION_PARAMETER, + "true")); } } - protected abstract String retrieveRedirectionUrl(final WebContext context); + /** + * Get the redirectAction computed when we call redirect. This is rather a utility method than the + * correct way to get redirected to the authentication provider. + * + * @param context + * @return + */ + final public RedirectAction getRedirectAction(final WebContext context) { + init(); + return retrieveRedirectAction(context); + } + + protected abstract RedirectAction retrieveRedirectAction(final WebContext context); public final C getCredentials(final WebContext context) throws RequiresHttpAction { init(); final String value = context.getRequestParameter(NEEDS_CLIENT_REDIRECTION_PARAMETER); // needs redirection -> return the redirection url if (CommonHelper.isNotBlank(value)) { - throw RequiresHttpAction.redirect("Needs client redirection", context, retrieveRedirectionUrl(context)); + throw RequiresHttpAction.redirect("Needs client redirection", context, retrieveRedirectAction(context).getLocation()); } else { // else get the credentials C credentials = retrieveCredentials(context); diff --git a/pac4j-core/src/main/java/org/pac4j/core/client/Client.java b/pac4j-core/src/main/java/org/pac4j/core/client/Client.java index 73032857d0..05eef463ad 100644 --- a/pac4j-core/src/main/java/org/pac4j/core/client/Client.java +++ b/pac4j-core/src/main/java/org/pac4j/core/client/Client.java @@ -26,7 +26,7 @@ * A client has a type accessible by the {@link #getName()} method.
    * A client supports the authentication process and user profile retrieval through :
    *