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 :
*
- * - the {@link #getRedirectionUrl(WebContext,boolean,boolean)} method to get the url where to redirect the user for authentication (at
+ *
- the {@link #getRedirection(WebContext,boolean,boolean)} method to get the url where to redirect the user for authentication (at
* the provider)
* - the {@link #getCredentials(WebContext)} method to get the credentials (in the application) after the user has been successfully
* authenticated at the provider
@@ -46,7 +46,7 @@ public interface Client {
public String getName();
/**
- * Get the redirection url. Generally, the redirection url is the url of the provider for authentication.
+ * Redirect to the authentication provider by updating the WebContext accordingly.
*
* Though, if this client requires an indirect redirection, the url will be the callback url (with an additionnal parameter requesting a
* redirection). Whatever the kind of client's redirection, the protectedTarget
parameter set to true
enforces
@@ -64,14 +64,14 @@ public interface Client {
* @return the redirection url
* @throws RequiresHttpAction
*/
- public String getRedirectionUrl(WebContext context, boolean protectedTarget, boolean ajaxRequest)
+ public void redirect(WebContext context, boolean protectedTarget, boolean ajaxRequest)
throws RequiresHttpAction;
/**
* Get the credentials from the web context. In some cases, a {@link RequiresHttpAction} may be thrown instead:
*
* - if this client requires an indirect redirection, the redirection will be actually performed by these method and not by the
- * {@link #getRedirectionUrl(WebContext, boolean, boolean)} one (302 HTTP status code)
+ * {@link #getRedirection(WebContext, boolean, boolean)} one (302 HTTP status code)
* - if the
CasClient
receives a logout request, it returns a 200 HTTP status code
* - for the
BasicAuthClient
, if no credentials are sent to the callback url, an unauthorized response (401 HTTP status
* code) is returned to request credentials through a popup.
diff --git a/pac4j-core/src/main/java/org/pac4j/core/client/RedirectAction.java b/pac4j-core/src/main/java/org/pac4j/core/client/RedirectAction.java
new file mode 100644
index 0000000000..916e0b6de0
--- /dev/null
+++ b/pac4j-core/src/main/java/org/pac4j/core/client/RedirectAction.java
@@ -0,0 +1,72 @@
+/*
+ Copyright 2012 - 2014 Michael Remond
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package org.pac4j.core.client;
+
+/**
+ * Indicates the action when the {@link Client} requires a redirection to achieve
+ * user authentication. Valid redirection type are :
+ *
+ * - REDIRECT (HTTP 302)
+ * - SUCCESS (HTTP 200)
+ *
+ *
+ * @author Michael Remond
+ * @since 1.5.0
+ *
+ */
+public class RedirectAction {
+
+ public enum RedirectType {
+ REDIRECT, SUCCESS
+ }
+
+ private RedirectType type;
+
+ private String location;
+
+ private String content;
+
+ private RedirectAction() {
+
+ }
+
+ public static RedirectAction redirect(final String location) {
+ RedirectAction action = new RedirectAction();
+ action.type = RedirectType.REDIRECT;
+ action.location = location;
+ return action;
+ }
+
+ public static RedirectAction success(final String content) {
+ RedirectAction action = new RedirectAction();
+ action.type = RedirectType.SUCCESS;
+ action.content = content;
+ return action;
+ }
+
+ public RedirectType getType() {
+ return type;
+ }
+
+ public String getLocation() {
+ return location;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+}
diff --git a/pac4j-core/src/main/java/org/pac4j/core/context/BaseResponseContext.java b/pac4j-core/src/main/java/org/pac4j/core/context/BaseResponseContext.java
index 6dfc438a53..7a6dad392c 100644
--- a/pac4j-core/src/main/java/org/pac4j/core/context/BaseResponseContext.java
+++ b/pac4j-core/src/main/java/org/pac4j/core/context/BaseResponseContext.java
@@ -25,36 +25,46 @@
* @since 1.4.0
*/
public abstract class BaseResponseContext implements WebContext {
-
+
protected String responseContent = "";
-
+
protected int responseStatus = -1;
-
+
+ protected String responseLocation;
+
protected final Map responseHeaders = new HashMap();
-
+
public void writeResponseContent(final String content) {
if (content != null) {
this.responseContent += content;
}
}
-
+
public void setResponseStatus(final int code) {
this.responseStatus = code;
}
-
+
public void setResponseHeader(final String name, final String value) {
this.responseHeaders.put(name, value);
}
-
+
public String getResponseContent() {
return this.responseContent;
}
-
+
public int getResponseStatus() {
return this.responseStatus;
}
-
+
+ public String getResponseLocation() {
+ return this.responseLocation;
+ }
+
public Map getResponseHeaders() {
return this.responseHeaders;
}
+
+ public void sendRedirect(final String location) {
+ responseLocation = location;
+ }
}
diff --git a/pac4j-core/src/main/java/org/pac4j/core/context/J2EContext.java b/pac4j-core/src/main/java/org/pac4j/core/context/J2EContext.java
index 12fa2be16e..82e8945e27 100644
--- a/pac4j-core/src/main/java/org/pac4j/core/context/J2EContext.java
+++ b/pac4j-core/src/main/java/org/pac4j/core/context/J2EContext.java
@@ -21,110 +21,124 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.io.IOUtils;
import org.pac4j.core.exception.TechnicalException;
/**
* This implementation uses the J2E request and session.
- *
+ *
* @author Jerome Leleu
* @since 1.4.0
*/
public class J2EContext implements WebContext {
-
+
private final HttpServletRequest request;
-
+
private final HttpServletResponse response;
-
+
/**
* Build a J2E context from the current HTTP request.
- *
+ *
* @param request
*/
public J2EContext(final HttpServletRequest request, final HttpServletResponse response) {
this.request = request;
this.response = response;
}
-
+
/**
* Return a request parameter.
- *
+ *
* @param name
* @return the request parameter
*/
public String getRequestParameter(final String name) {
return this.request.getParameter(name);
}
-
+
/**
* Return all request parameters.
- *
+ *
* @return all request parameters
*/
@SuppressWarnings("unchecked")
public Map getRequestParameters() {
return this.request.getParameterMap();
}
-
+
/**
* Return a request header.
- *
+ *
* @param name
* @return the request header
*/
public String getRequestHeader(final String name) {
return this.request.getHeader(name);
}
-
+
/**
* Save an attribute in session.
- *
+ *
* @param name
* @param value
*/
public void setSessionAttribute(final String name, final Object value) {
this.request.getSession().setAttribute(name, value);
}
-
+
/**
* Get an attribute from session.
- *
+ *
* @param name
* @return the session attribute
*/
public Object getSessionAttribute(final String name) {
return this.request.getSession().getAttribute(name);
}
-
+
/**
* Return the request method : GET, POST...
- *
+ *
* @return the request method
*/
public String getRequestMethod() {
return this.request.getMethod();
}
-
+
/**
* Return the HTTP request.
- *
+ *
* @return the HTTP request
*/
public HttpServletRequest getRequest() {
return this.request;
}
-
+
/**
* Return the HTTP response.
- *
+ *
* @return the HTTP response
*/
public HttpServletResponse getResponse() {
return this.response;
}
-
+
+ /**
+ * Read content from the request.
+ *
+ * @return the content of the request
+ */
+ public String readRequestContent() {
+ try {
+ return IOUtils.toString(request.getInputStream(), request.getCharacterEncoding());
+ } catch (final IOException e) {
+ throw new TechnicalException(e);
+ }
+ }
+
/**
* Write some content in the response.
- *
+ *
* @param content
*/
public void writeResponseContent(final String content) {
@@ -136,10 +150,10 @@ public void writeResponseContent(final String content) {
}
}
}
-
+
/**
* Set the response status.
- *
+ *
* @param code
*/
public void setResponseStatus(final int code) {
@@ -153,10 +167,10 @@ public void setResponseStatus(final int code) {
}
}
}
-
+
/**
* Add a header to the response.
- *
+ *
* @param name
* @param value
*/
@@ -190,4 +204,22 @@ public int getServerPort() {
public String getScheme() {
return this.request.getScheme();
}
+
+ public String getFullRequestURL() {
+ StringBuffer requestURL = request.getRequestURL();
+ String queryString = request.getQueryString();
+ if (queryString == null) {
+ return requestURL.toString();
+ } else {
+ return requestURL.append('?').append(queryString).toString();
+ }
+ }
+
+ public void sendRedirect(final String url) {
+ try {
+ response.sendRedirect(url);
+ } catch (IOException e) {
+ throw new TechnicalException(e);
+ }
+ }
}
diff --git a/pac4j-core/src/main/java/org/pac4j/core/context/J2ERequestContext.java b/pac4j-core/src/main/java/org/pac4j/core/context/J2ERequestContext.java
index a8fca1ae13..290930faef 100644
--- a/pac4j-core/src/main/java/org/pac4j/core/context/J2ERequestContext.java
+++ b/pac4j-core/src/main/java/org/pac4j/core/context/J2ERequestContext.java
@@ -15,77 +15,81 @@
*/
package org.pac4j.core.context;
+import java.io.IOException;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
+import org.apache.commons.io.IOUtils;
+import org.pac4j.core.exception.TechnicalException;
+
/**
* This implementation uses the J2E request.
- *
+ *
* @author Jerome Leleu
* @since 1.4.0
*/
public class J2ERequestContext extends BaseResponseContext {
-
+
private final HttpServletRequest request;
-
+
public J2ERequestContext(final HttpServletRequest request) {
this.request = request;
}
-
+
/**
* Return a request parameter.
- *
+ *
* @param name
* @return the request parameter
*/
public String getRequestParameter(final String name) {
return this.request.getParameter(name);
}
-
+
/**
* Return all request parameters.
- *
+ *
* @return all request parameters
*/
@SuppressWarnings("unchecked")
public Map getRequestParameters() {
return this.request.getParameterMap();
}
-
+
/**
* Return a request header.
- *
+ *
* @param name
* @return the request header
*/
public String getRequestHeader(final String name) {
return this.request.getHeader(name);
}
-
+
/**
* Save an attribute in session.
- *
+ *
* @param name
* @param value
*/
public void setSessionAttribute(final String name, final Object value) {
this.request.getSession().setAttribute(name, value);
}
-
+
/**
* Get an attribute from session.
- *
+ *
* @param name
* @return the session attribute
*/
public Object getSessionAttribute(final String name) {
return this.request.getSession().getAttribute(name);
}
-
+
/**
* Return the request method : GET, POST...
- *
+ *
* @return the request method
*/
public String getRequestMethod() {
@@ -94,7 +98,7 @@ public String getRequestMethod() {
/**
* Return the HTTP request.
- *
+ *
* @return the HTTP request
*/
public HttpServletRequest getRequest() {
@@ -127,4 +131,28 @@ public int getServerPort() {
public String getScheme() {
return this.request.getScheme();
}
+
+ /**
+ * Read content from the request.
+ *
+ * @return the content of the request
+ */
+ public String readRequestContent() {
+ try {
+ return IOUtils.toString(request.getInputStream(), request.getCharacterEncoding());
+ } catch (final IOException e) {
+ throw new TechnicalException(e);
+ }
+ }
+
+ public String getFullRequestURL() {
+ StringBuffer requestURL = request.getRequestURL();
+ String queryString = request.getQueryString();
+ if (queryString == null) {
+ return requestURL.toString();
+ } else {
+ return requestURL.append('?').append(queryString).toString();
+ }
+ }
+
}
diff --git a/pac4j-core/src/main/java/org/pac4j/core/context/WebContext.java b/pac4j-core/src/main/java/org/pac4j/core/context/WebContext.java
index baf81a41c8..03a256d950 100644
--- a/pac4j-core/src/main/java/org/pac4j/core/context/WebContext.java
+++ b/pac4j-core/src/main/java/org/pac4j/core/context/WebContext.java
@@ -19,75 +19,83 @@
/**
* This interface represents the web context to use HTTP request and session.
- *
+ *
* @author Jerome Leleu
* @since 1.4.0
*/
public interface WebContext {
-
+
/**
* Return a request parameter.
- *
+ *
* @param name
* @return the request parameter
*/
public String getRequestParameter(String name);
-
+
/**
* Return all request parameters.
- *
+ *
* @return all request parameters
*/
public Map getRequestParameters();
-
+
/**
* Return a request header.
- *
+ *
* @param name
* @return the request header
*/
public String getRequestHeader(String name);
-
+
/**
* Save an attribute in session.
- *
+ *
* @param name
* @param value
*/
public void setSessionAttribute(String name, Object value);
-
+
/**
* Get an attribute from session.
- *
+ *
* @param name
* @return the session attribute
*/
public Object getSessionAttribute(String name);
-
+
/**
* Return the request method.
- *
+ *
* @return the request method
*/
public String getRequestMethod();
-
+
/**
- * Write some content in the response.
+ * Read content from the request.
*
+ * @return the content of the request
+ * @since 1.5.0
+ */
+ public String readRequestContent();
+
+ /**
+ * Write some content in the response.
+ *
* @param content
*/
public void writeResponseContent(String content);
-
+
/**
* Set the response status.
- *
+ *
* @param code
*/
public void setResponseStatus(int code);
-
+
/**
* Add a header to the response.
- *
+ *
* @param name
* @param value
*/
@@ -113,4 +121,21 @@ public interface WebContext {
* @return the scheme
*/
public String getScheme();
+
+ /**
+ * Return the full URL (with query string) the client used to request the server.
+ *
+ * @return the URL
+ * @since 1.5.0
+ */
+ public String getFullRequestURL();
+
+ /**
+ * Redirect to the given location
+ *
+ * @param location
+ * @since 1.5.0
+ */
+ public void sendRedirect(String location);
+
}
diff --git a/pac4j-core/src/test/java/org/pac4j/core/client/MockBaseClient.java b/pac4j-core/src/test/java/org/pac4j/core/client/MockBaseClient.java
index f4c556daa5..a6f4d91821 100644
--- a/pac4j-core/src/test/java/org/pac4j/core/client/MockBaseClient.java
+++ b/pac4j-core/src/test/java/org/pac4j/core/client/MockBaseClient.java
@@ -60,8 +60,8 @@ protected boolean isDirectRedirection() {
}
@Override
- protected String retrieveRedirectionUrl(final WebContext context) {
- return LOGIN_URL;
+ protected RedirectAction retrieveRedirectAction(final WebContext context) {
+ return RedirectAction.redirect(LOGIN_URL);
}
@Override
diff --git a/pac4j-core/src/test/java/org/pac4j/core/client/TestBaseClient.java b/pac4j-core/src/test/java/org/pac4j/core/client/TestBaseClient.java
index 348d0dcd80..cc665fe144 100644
--- a/pac4j-core/src/test/java/org/pac4j/core/client/TestBaseClient.java
+++ b/pac4j-core/src/test/java/org/pac4j/core/client/TestBaseClient.java
@@ -44,7 +44,8 @@ public void testDirectClient() throws RequiresHttpAction {
final MockBaseClient client = new MockBaseClient(TYPE);
client.setCallbackUrl(CALLBACK_URL);
final MockWebContext context = MockWebContext.create();
- final String redirectionUrl = client.getRedirectionUrl(context, false, false);
+ client.redirect(context, false, false);
+ final String redirectionUrl = context.getResponseLocation();
assertEquals(LOGIN_URL, redirectionUrl);
final Credentials credentials = client.getCredentials(context);
assertNull(credentials);
@@ -54,7 +55,8 @@ public void testIndirectClient() throws RequiresHttpAction {
final MockBaseClient client = new MockBaseClient(TYPE, false);
client.setCallbackUrl(CALLBACK_URL);
final MockWebContext context = MockWebContext.create();
- final String redirectionUrl = client.getRedirectionUrl(context, false, false);
+ client.redirect(context, false, false);
+ final String redirectionUrl = context.getResponseLocation();
assertEquals(CommonHelper.addParameter(CALLBACK_URL, BaseClient.NEEDS_CLIENT_REDIRECTION_PARAMETER, "true"),
redirectionUrl);
context.addRequestParameter(BaseClient.NEEDS_CLIENT_REDIRECTION_PARAMETER, "true");
@@ -72,7 +74,8 @@ public void testIndirectClientWithImmediate() throws RequiresHttpAction {
final MockBaseClient client = new MockBaseClient(TYPE, false);
client.setCallbackUrl(CALLBACK_URL);
final MockWebContext context = MockWebContext.create();
- final String redirectionUrl = client.getRedirectionUrl(context, true, false);
+ client.redirect(context, true, false);
+ final String redirectionUrl = context.getResponseLocation();
assertEquals(LOGIN_URL, redirectionUrl);
}
@@ -148,7 +151,7 @@ public void testAjaxRequest() {
client.setCallbackUrl(CALLBACK_URL);
final MockWebContext context = MockWebContext.create();
try {
- client.getRedirectionUrl(context, false, true);
+ client.redirect(context, false, true);
fail("should fail");
} catch (RequiresHttpAction e) {
assertEquals(401, e.getCode());
@@ -162,7 +165,7 @@ public void testAlreadyTried() {
final MockWebContext context = MockWebContext.create();
context.setSessionAttribute(client.getName() + BaseClient.ATTEMPTED_AUTHENTICATION_SUFFIX, "true");
try {
- client.getRedirectionUrl(context, true, false);
+ client.redirect(context, true, false);
fail("should fail");
} catch (RequiresHttpAction e) {
assertEquals(403, e.getCode());
diff --git a/pac4j-core/src/test/java/org/pac4j/core/client/TestClient.java b/pac4j-core/src/test/java/org/pac4j/core/client/TestClient.java
index 074a422338..766c43f246 100644
--- a/pac4j-core/src/test/java/org/pac4j/core/client/TestClient.java
+++ b/pac4j-core/src/test/java/org/pac4j/core/client/TestClient.java
@@ -133,11 +133,12 @@ protected boolean isJavascriptEnabled() {
protected abstract Client getClient();
- protected HtmlPage getRedirectionPage(final WebClient webClient, final Client client, final WebContext context)
+ protected HtmlPage getRedirectionPage(final WebClient webClient, final Client client, final MockWebContext context)
throws Exception {
final BaseClient baseClient = (BaseClient) client;
// force immediate redirection for tests
- final String redirectionUrl = baseClient.getRedirectionUrl(context, true, false);
+ baseClient.redirect(context, true, false);
+ final String redirectionUrl = context.getResponseLocation();
logger.debug("redirectionUrl : {}", redirectionUrl);
final HtmlPage loginPage = webClient.getPage(redirectionUrl);
return loginPage;
diff --git a/pac4j-core/src/test/java/org/pac4j/core/context/MockWebContext.java b/pac4j-core/src/test/java/org/pac4j/core/context/MockWebContext.java
index dab12d7431..1ccaec0cf9 100644
--- a/pac4j-core/src/test/java/org/pac4j/core/context/MockWebContext.java
+++ b/pac4j-core/src/test/java/org/pac4j/core/context/MockWebContext.java
@@ -20,18 +20,18 @@
/**
* This is a mocked web context to interact with request, response and session (for tests purpose).
- *
+ *
* @author Jerome Leleu
* @since 1.4.0
*/
public class MockWebContext extends BaseResponseContext {
-
+
protected final Map parameters = new HashMap();
-
+
protected final Map headers = new HashMap();
-
+
protected final Map session = new HashMap();
-
+
protected String method = "GET";
protected String serverName = "localhost";
@@ -40,21 +40,23 @@ public class MockWebContext extends BaseResponseContext {
protected int serverPort = 80;
+ protected String requestContent = "";
+
protected MockWebContext() {
}
-
+
/**
* Create a new instance.
- *
+ *
* @return a new instance
*/
public static MockWebContext create() {
return new MockWebContext();
}
-
+
/**
* Add request parameters for mock purpose.
- *
+ *
* @param parameters
* @return this mock web context
*/
@@ -62,10 +64,10 @@ public MockWebContext addRequestParameters(final Map parameters)
this.parameters.putAll(parameters);
return this;
}
-
+
/**
* Add a request parameter for mock purpose.
- *
+ *
* @param key
* @param value
* @return this mock web context
@@ -74,10 +76,10 @@ public MockWebContext addRequestParameter(final String key, final String value)
this.parameters.put(key, value);
return this;
}
-
+
/**
* Add a request header for mock purpose.
- *
+ *
* @param key
* @param value
* @return this mock web context
@@ -86,10 +88,10 @@ public MockWebContext addRequestHeader(final String key, final String value) {
this.headers.put(key, value);
return this;
}
-
+
/**
* Add a session attribute for mock purpose.
- *
+ *
* @param name
* @param value
* @return this mock web context
@@ -98,10 +100,10 @@ public MockWebContext addSessionAttribute(final String name, final Object value)
setSessionAttribute(name, value);
return this;
}
-
+
/**
* Set the request method for mock purpose.
- *
+ *
* @param method
* @return this mock web context
*/
@@ -109,27 +111,27 @@ public MockWebContext setRequestMethod(final String method) {
this.method = method;
return this;
}
-
+
public String getRequestParameter(final String name) {
return this.parameters.get(name);
}
-
+
public String getRequestHeader(final String name) {
return this.headers.get(name);
}
-
+
public void setSessionAttribute(final String name, final Object value) {
this.session.put(name, value);
}
-
+
public Object getSessionAttribute(final String name) {
return this.session.get(name);
}
-
+
public String getRequestMethod() {
return this.method;
}
-
+
public Map getRequestParameters() {
final Map map = new HashMap();
for (final String key : this.parameters.keySet()) {
@@ -165,4 +167,16 @@ public String getScheme() {
public void setScheme(String scheme) {
this.scheme = scheme;
}
+
+ public void setRequestContent(String requestContent) {
+ this.requestContent = requestContent;
+ }
+
+ public String readRequestContent() {
+ return this.requestContent;
+ }
+
+ public String getFullRequestURL() {
+ return scheme + "://" + serverName + ":" + serverPort + "/";
+ }
}
diff --git a/pac4j-http/src/main/java/org/pac4j/http/client/BasicAuthClient.java b/pac4j-http/src/main/java/org/pac4j/http/client/BasicAuthClient.java
index cc5f32a017..ace7b84358 100644
--- a/pac4j-http/src/main/java/org/pac4j/http/client/BasicAuthClient.java
+++ b/pac4j-http/src/main/java/org/pac4j/http/client/BasicAuthClient.java
@@ -19,6 +19,7 @@
import org.apache.commons.codec.binary.Base64;
import org.pac4j.core.client.BaseClient;
+import org.pac4j.core.client.RedirectAction;
import org.pac4j.core.context.HttpConstants;
import org.pac4j.core.context.WebContext;
import org.pac4j.core.exception.CredentialsException;
@@ -75,12 +76,12 @@ protected void internalInit() {
}
@Override
- protected String retrieveRedirectionUrl(final WebContext context) {
- return getContextualCallbackUrl(context);
+ protected RedirectAction retrieveRedirectAction(final WebContext context) {
+ return RedirectAction.redirect(getContextualCallbackUrl(context));
}
@Override
- protected UsernamePasswordCredentials retrieveCredentials(final WebContext context) throws RequiresHttpAction {
+ public UsernamePasswordCredentials retrieveCredentials(final WebContext context) throws RequiresHttpAction {
final String header = context.getRequestHeader(HttpConstants.AUTHORIZATION_HEADER);
if (header == null || !header.startsWith("Basic ")) {
logger.warn("No basic auth found");
diff --git a/pac4j-http/src/main/java/org/pac4j/http/client/FormClient.java b/pac4j-http/src/main/java/org/pac4j/http/client/FormClient.java
index e10aba7eee..8df2d7151b 100644
--- a/pac4j-http/src/main/java/org/pac4j/http/client/FormClient.java
+++ b/pac4j-http/src/main/java/org/pac4j/http/client/FormClient.java
@@ -16,6 +16,7 @@
package org.pac4j.http.client;
import org.pac4j.core.client.BaseClient;
+import org.pac4j.core.client.RedirectAction;
import org.pac4j.core.context.WebContext;
import org.pac4j.core.exception.RequiresHttpAction;
import org.pac4j.core.exception.TechnicalException;
@@ -84,8 +85,8 @@ protected void internalInit() {
}
@Override
- protected String retrieveRedirectionUrl(final WebContext context) {
- return this.loginUrl;
+ protected RedirectAction retrieveRedirectAction(final WebContext context) {
+ return RedirectAction.redirect(this.loginUrl);
}
@Override
diff --git a/pac4j-http/src/test/java/org/pac4j/http/client/TestBasicAuthClient.java b/pac4j-http/src/test/java/org/pac4j/http/client/TestBasicAuthClient.java
index 05f8dfaa3b..7178ff9edc 100644
--- a/pac4j-http/src/test/java/org/pac4j/http/client/TestBasicAuthClient.java
+++ b/pac4j-http/src/test/java/org/pac4j/http/client/TestBasicAuthClient.java
@@ -81,7 +81,9 @@ private BasicAuthClient getBasicAuthClient() {
public void testRedirectionUrl() throws RequiresHttpAction {
final BasicAuthClient basicAuthClient = getBasicAuthClient();
- assertEquals(CALLBACK_URL, basicAuthClient.getRedirectionUrl(MockWebContext.create(), false, false));
+ MockWebContext context = MockWebContext.create();
+ basicAuthClient.redirect(context, false, false);
+ assertEquals(CALLBACK_URL, context.getResponseLocation());
}
public void testGetCredentialsMissingHeader() {
diff --git a/pac4j-http/src/test/java/org/pac4j/http/client/TestFormClient.java b/pac4j-http/src/test/java/org/pac4j/http/client/TestFormClient.java
index 172ddd977b..4dfa0c2ba1 100644
--- a/pac4j-http/src/test/java/org/pac4j/http/client/TestFormClient.java
+++ b/pac4j-http/src/test/java/org/pac4j/http/client/TestFormClient.java
@@ -80,7 +80,9 @@ private FormClient getFormClient() {
public void testRedirectionUrl() throws RequiresHttpAction {
final FormClient formClient = getFormClient();
- assertEquals(LOGIN_URL, formClient.getRedirectionUrl(MockWebContext.create(), false, false));
+ MockWebContext context = MockWebContext.create();
+ formClient.redirect(context, false, false);
+ assertEquals(LOGIN_URL, context.getResponseLocation());
}
public void testGetCredentialsMissingUsername() {
diff --git a/pac4j-oauth/src/main/java/org/pac4j/oauth/client/BaseOAuthClient.java b/pac4j-oauth/src/main/java/org/pac4j/oauth/client/BaseOAuthClient.java
index 5e5757916c..9fe1cda0fb 100644
--- a/pac4j-oauth/src/main/java/org/pac4j/oauth/client/BaseOAuthClient.java
+++ b/pac4j-oauth/src/main/java/org/pac4j/oauth/client/BaseOAuthClient.java
@@ -17,6 +17,7 @@
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.HttpCommunicationException;
import org.pac4j.core.exception.TechnicalException;
@@ -42,34 +43,34 @@
* @since 1.0.0
*/
public abstract class BaseOAuthClient extends BaseClient {
-
+
protected static final Logger logger = LoggerFactory.getLogger(BaseOAuthClient.class);
-
+
protected OAuthService service;
-
+
protected String key;
-
+
protected String secret;
protected boolean tokenAsHeader = false;
-
+
// 0,5 second
protected int connectTimeout = 500;
-
+
// 2 seconds
protected int readTimeout = 2000;
-
+
protected String proxyHost = null;
-
+
protected int proxyPort = 8080;
-
+
@Override
protected void internalInit() {
CommonHelper.assertNotBlank("key", this.key);
CommonHelper.assertNotBlank("secret", this.secret);
CommonHelper.assertNotBlank("callbackUrl", this.callbackUrl);
}
-
+
@Override
public BaseOAuthClient clone() {
final BaseOAuthClient newClient = (BaseOAuthClient) super.clone();
@@ -81,7 +82,7 @@ public BaseOAuthClient clone() {
newClient.setProxyPort(this.proxyPort);
return newClient;
}
-
+
/**
* Get the redirection url.
*
@@ -89,16 +90,16 @@ public BaseOAuthClient clone() {
* @return the redirection url
*/
@Override
- protected String retrieveRedirectionUrl(final WebContext context) {
+ protected RedirectAction retrieveRedirectAction(final WebContext context) {
try {
- return retrieveAuthorizationUrl(context);
+ return RedirectAction.redirect(retrieveAuthorizationUrl(context));
} catch (final OAuthException e) {
throw new TechnicalException(e);
}
}
-
+
protected abstract String retrieveAuthorizationUrl(final WebContext context);
-
+
/**
* Get the credentials from the web context.
*
@@ -116,7 +117,7 @@ protected OAuthCredentials retrieveCredentials(final WebContext context) {
try {
boolean errorFound = false;
final OAuthCredentialsException oauthCredentialsException = new OAuthCredentialsException(
- "Failed to retrieve OAuth credentials, error parameters found");
+ "Failed to retrieve OAuth credentials, error parameters found");
String errorMessage = "";
for (final String key : OAuthCredentialsException.ERROR_NAMES) {
final String value = context.getRequestParameter(key);
@@ -136,7 +137,7 @@ protected OAuthCredentials retrieveCredentials(final WebContext context) {
throw new TechnicalException(e);
}
}
-
+
/**
* Return if the authentication has been cancelled.
*
@@ -144,7 +145,7 @@ protected OAuthCredentials retrieveCredentials(final WebContext context) {
* @return if the authentication has been cancelled.
*/
protected abstract boolean hasBeenCancelled(WebContext context);
-
+
/**
* Get the OAuth credentials from the web context.
*
@@ -152,7 +153,7 @@ protected OAuthCredentials retrieveCredentials(final WebContext context) {
* @return the OAuth credentials
*/
protected abstract OAuthCredentials getOAuthCredentials(final WebContext context);
-
+
/**
* Get the user profile from the credentials.
*
@@ -168,7 +169,7 @@ protected U retrieveUserProfile(final OAuthCredentials credentials, final WebCon
throw new TechnicalException(e);
}
}
-
+
/**
* Get the user profile from the access token.
*
@@ -184,7 +185,7 @@ public U getUserProfile(final String accessToken) {
throw new TechnicalException(e);
}
}
-
+
/**
* Get the access token from OAuth credentials.
*
@@ -192,7 +193,7 @@ public U getUserProfile(final String accessToken) {
* @return the access token
*/
protected abstract Token getAccessToken(OAuthCredentials credentials);
-
+
/**
* Retrieve the user profile from the access token.
*
@@ -208,7 +209,7 @@ protected U retrieveUserProfileFromToken(final Token accessToken) {
addAccessTokenToProfile(profile, accessToken);
return profile;
}
-
+
/**
* Retrieve the url of the profile of the authenticated user for the provider.
*
@@ -217,7 +218,7 @@ protected U retrieveUserProfileFromToken(final Token accessToken) {
*/
@SuppressWarnings("unused")
protected abstract String getProfileUrl(final Token accessToken);
-
+
/**
* Make a request to get the data of the authenticated user for the provider.
*
@@ -246,7 +247,7 @@ protected String sendRequestForData(final Token accessToken, final String dataUr
}
return body;
}
-
+
/**
* Make a request to the OAuth provider to access a protected resource. The profile should contain a valid access token (and secret if
* needed).
@@ -260,7 +261,7 @@ public String sendRequestForData(final OAuth10Profile profile, final String data
final Token accessToken = new Token(profile.getAccessToken(), secret == null ? "" : secret);
return sendRequestForData(accessToken, dataUrl);
}
-
+
/**
* Create a proxy request.
*
@@ -269,9 +270,9 @@ public String sendRequestForData(final OAuth10Profile profile, final String data
*/
protected ProxyOAuthRequest createProxyRequest(final String url) {
return new ProxyOAuthRequest(Verb.GET, url, this.connectTimeout, this.readTimeout, this.proxyHost,
- this.proxyPort);
+ this.proxyPort);
}
-
+
/**
* Extract the user profile from the response (JSON, XML...) of the profile url.
*
@@ -279,7 +280,7 @@ protected ProxyOAuthRequest createProxyRequest(final String url) {
* @return the user profile object
*/
protected abstract U extractUserProfile(String body);
-
+
/**
* Add the access token to the profile (as an attribute).
*
@@ -293,51 +294,51 @@ protected void addAccessTokenToProfile(final U profile, final Token accessToken)
profile.setAccessToken(token);
}
}
-
+
public void setKey(final String key) {
this.key = key;
}
-
+
public void setSecret(final String secret) {
this.secret = secret;
}
-
+
public void setConnectTimeout(final int connectTimeout) {
this.connectTimeout = connectTimeout;
}
-
+
public void setReadTimeout(final int readTimeout) {
this.readTimeout = readTimeout;
}
-
+
public String getKey() {
return this.key;
}
-
+
public String getSecret() {
return this.secret;
}
-
+
public int getConnectTimeout() {
return this.connectTimeout;
}
-
+
public int getReadTimeout() {
return this.readTimeout;
}
-
+
public String getProxyHost() {
return this.proxyHost;
}
-
+
public void setProxyHost(final String proxyHost) {
this.proxyHost = proxyHost;
}
-
+
public int getProxyPort() {
return this.proxyPort;
}
-
+
public void setProxyPort(final int proxyPort) {
this.proxyPort = proxyPort;
}
@@ -349,7 +350,7 @@ public boolean isTokenAsHeader() {
public void setTokenAsHeader(boolean tokenAsHeader) {
this.tokenAsHeader = tokenAsHeader;
}
-
+
@Override
public Protocol getProtocol() {
return Protocol.OAUTH;
diff --git a/pac4j-oauth/src/test/java/org/pac4j/oauth/client/TestGoogle2Client.java b/pac4j-oauth/src/test/java/org/pac4j/oauth/client/TestGoogle2Client.java
index a7b65b3bd4..e0d5e82ec0 100644
--- a/pac4j-oauth/src/test/java/org/pac4j/oauth/client/TestGoogle2Client.java
+++ b/pac4j-oauth/src/test/java/org/pac4j/oauth/client/TestGoogle2Client.java
@@ -43,7 +43,7 @@
* @since 1.2.0
*/
public class TestGoogle2Client extends TestOAuthClient {
-
+
@Override
public void testClone() {
final Google2Client oldClient = new Google2Client();
@@ -51,21 +51,21 @@ public void testClone() {
final Google2Client client = (Google2Client) internalTestClone(oldClient);
assertEquals(oldClient.getScope(), client.getScope());
}
-
+
public void testMissingScope() {
final Google2Client client = (Google2Client) getClient();
client.setScope(null);
TestsHelper.initShouldFail(client, "scope cannot be null");
}
-
+
public void testDefaultScope() throws RequiresHttpAction {
final Google2Client google2Client = new Google2Client();
google2Client.setKey(KEY);
google2Client.setSecret(SECRET);
google2Client.setCallbackUrl(CALLBACK_URL);
- google2Client.getRedirectionUrl(MockWebContext.create(), false, false);
+ google2Client.redirect(MockWebContext.create(), false, false);
}
-
+
@SuppressWarnings("rawtypes")
@Override
protected Client getClient() {
@@ -76,7 +76,7 @@ protected Client getClient() {
google2Client.setScope(Google2Scope.EMAIL_AND_PROFILE);
return google2Client;
}
-
+
@Override
protected String getCallbackUrl(final WebClient webClient, final HtmlPage authorizationPage) throws Exception {
final HtmlForm form = authorizationPage.getForms().get(0);
@@ -90,31 +90,25 @@ protected String getCallbackUrl(final WebClient webClient, final HtmlPage author
logger.debug("callbackUrl : {}", callbackUrl);
return callbackUrl;
}
-
+
@Override
protected void registerForKryo(final Kryo kryo) {
kryo.register(Google2Profile.class);
}
-
+
@Override
protected void verifyProfile(final UserProfile userProfile) {
final Google2Profile profile = (Google2Profile) userProfile;
logger.debug("userProfile : {}", profile);
assertEquals("113675986756217860428", profile.getId());
assertEquals(Google2Profile.class.getSimpleName() + UserProfile.SEPARATOR + "113675986756217860428",
- profile.getTypedId());
+ profile.getTypedId());
assertTrue(ProfileHelper.isTypedIdOf(profile.getTypedId(), Google2Profile.class));
assertTrue(StringUtils.isNotBlank(profile.getAccessToken()));
- assertCommonProfile(userProfile,
- "testscribeup@gmail.com",
- "Jérôme",
- "ScribeUP",
- "Jérôme ScribeUP",
- null,
- Gender.MALE,
- Locale.ENGLISH,
- "https://lh4.googleusercontent.com/-fFUNeYqT6bk/AAAAAAAAAAI/AAAAAAAAAAA/5gBL6csVWio/photo.jpg",
- "https://plus.google.com/113675986756217860428", null);
+ assertCommonProfile(userProfile, "testscribeup@gmail.com", "Jérôme", "ScribeUP", "Jérôme ScribeUP", null,
+ Gender.MALE, Locale.ENGLISH,
+ "https://lh4.googleusercontent.com/-fFUNeYqT6bk/AAAAAAAAAAI/AAAAAAAAAAA/5gBL6csVWio/photo.jpg",
+ "https://plus.google.com/113675986756217860428", null);
assertTrue(profile.getVerifiedEmail());
assertNull(profile.getBirthday());
assertEquals(10, profile.getAttributes().size());
diff --git a/pac4j-openid/src/main/java/org/pac4j/openid/client/BaseOpenIdClient.java b/pac4j-openid/src/main/java/org/pac4j/openid/client/BaseOpenIdClient.java
index 4e11a84e23..5b2c1cac4f 100644
--- a/pac4j-openid/src/main/java/org/pac4j/openid/client/BaseOpenIdClient.java
+++ b/pac4j-openid/src/main/java/org/pac4j/openid/client/BaseOpenIdClient.java
@@ -29,6 +29,7 @@
import org.openid4java.message.ax.FetchRequest;
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.TechnicalException;
import org.pac4j.core.profile.CommonProfile;
@@ -85,7 +86,7 @@ protected String getDiscoveryInformationSessionAttributeName() {
@Override
@SuppressWarnings("rawtypes")
- protected String retrieveRedirectionUrl(final WebContext context) {
+ protected RedirectAction retrieveRedirectAction(final WebContext context) {
final String userIdentifier = getUser(context);
CommonHelper.assertNotBlank("openIdUser", userIdentifier);
@@ -112,7 +113,7 @@ protected String retrieveRedirectionUrl(final WebContext context) {
final String redirectionUrl = authRequest.getDestinationUrl(true);
logger.debug("redirectionUrl : {}", redirectionUrl);
- return redirectionUrl;
+ return RedirectAction.redirect(redirectionUrl);
} catch (final OpenIDException e) {
logger.error("OpenID exception", e);
throw new TechnicalException("OpenID exception", e);
diff --git a/pac4j-saml/pom.xml b/pac4j-saml/pom.xml
new file mode 100644
index 0000000000..ce82235a3b
--- /dev/null
+++ b/pac4j-saml/pom.xml
@@ -0,0 +1,155 @@
+
+
+
+ 4.0.0
+
+
+ org.pac4j
+ pac4j
+ 1.5.0-SNAPSHOT
+
+
+ pac4j-saml
+ pac4j-saml
+ pac4j for SAMLv2 protocol
+
+
+
+ org.pac4j
+ pac4j-core
+
+
+ org.opensaml
+ opensaml
+
+
+ commons-codec
+ commons-codec
+
+
+ xml-security
+ xmlsec
+
+
+ org.slf4j
+ log4j-over-slf4j
+
+
+ javax.servlet
+ servlet-api
+
+
+
+ org.pac4j
+ pac4j-core
+ test-jar
+
+
+ junit
+ junit
+
+
+ net.sourceforge.htmlunit
+ htmlunit
+
+
+ org.slf4j
+ jcl-over-slf4j
+
+
+ ch.qos.logback
+ logback-classic
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ test-jar
+
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+
+
+ org.pac4j.saml2
+ org.pac4j.saml.*;version=${project.version}
+ *
+
+
+
+
+
+
+
+
+ js
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ attach-sources
+
+ jar
+ test-jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+
+ attach-javadocs
+
+ jar
+ test-jar
+
+
+
+
+
+
+
+
+ nr
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.12.4
+
+ false
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pac4j-saml/src/main/java/org/pac4j/saml/client/Saml2Client.java b/pac4j-saml/src/main/java/org/pac4j/saml/client/Saml2Client.java
new file mode 100644
index 0000000000..c3d6a7118b
--- /dev/null
+++ b/pac4j-saml/src/main/java/org/pac4j/saml/client/Saml2Client.java
@@ -0,0 +1,368 @@
+/*
+ Copyright 2012 -2014 Michael Remond
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package org.pac4j.saml.client;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.velocity.app.VelocityEngine;
+import org.opensaml.Configuration;
+import org.opensaml.DefaultBootstrap;
+import org.opensaml.saml2.binding.decoding.HTTPPostDecoder;
+import org.opensaml.saml2.binding.encoding.HTTPPostEncoder;
+import org.opensaml.saml2.core.Assertion;
+import org.opensaml.saml2.core.Attribute;
+import org.opensaml.saml2.core.AttributeStatement;
+import org.opensaml.saml2.core.AuthnRequest;
+import org.opensaml.saml2.core.EncryptedAttribute;
+import org.opensaml.saml2.core.NameID;
+import org.opensaml.saml2.encryption.Decrypter;
+import org.opensaml.saml2.metadata.EntitiesDescriptor;
+import org.opensaml.saml2.metadata.EntityDescriptor;
+import org.opensaml.saml2.metadata.provider.AbstractMetadataProvider;
+import org.opensaml.saml2.metadata.provider.ChainingMetadataProvider;
+import org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider;
+import org.opensaml.saml2.metadata.provider.MetadataProviderException;
+import org.opensaml.xml.ConfigurationException;
+import org.opensaml.xml.XMLObject;
+import org.opensaml.xml.encryption.DecryptionException;
+import org.opensaml.xml.io.MarshallingException;
+import org.opensaml.xml.parse.StaticBasicParserPool;
+import org.opensaml.xml.parse.XMLParserException;
+import org.opensaml.xml.security.keyinfo.NamedKeyInfoGeneratorManager;
+import org.opensaml.xml.security.x509.X509KeyInfoGeneratorFactory;
+import org.opensaml.xml.signature.SignatureTrustEngine;
+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;
+import org.pac4j.core.util.CommonHelper;
+import org.pac4j.saml.context.ExtendedSAMLMessageContext;
+import org.pac4j.saml.context.Saml2ContextProvider;
+import org.pac4j.saml.credentials.Saml2Credentials;
+import org.pac4j.saml.crypto.CredentialProvider;
+import org.pac4j.saml.crypto.EncryptionProvider;
+import org.pac4j.saml.crypto.SignatureTrustEngineProvider;
+import org.pac4j.saml.exceptions.SamlException;
+import org.pac4j.saml.metadata.Saml2MetadataGenerator;
+import org.pac4j.saml.profile.Saml2Profile;
+import org.pac4j.saml.sso.Saml2AuthnRequestBuilder;
+import org.pac4j.saml.sso.Saml2ResponseValidator;
+import org.pac4j.saml.sso.Saml2WebSSOProfileHandler;
+import org.pac4j.saml.transport.Pac4jHTTPPostDecoder;
+import org.pac4j.saml.transport.SimpleResponseAdapter;
+import org.pac4j.saml.util.VelocityEngineFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Element;
+
+/**
+ * This class is the client to authenticate users with a SAML2 Identity Provider.
+ * This implementation relies on the Web Browser SSO profile with HTTP-POST binding.
+ * (http://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf).
+ *
+ * @author Michael Remond
+ * @since 1.5.0
+ *
+ */
+public class Saml2Client extends BaseClient {
+
+ protected static final Logger logger = LoggerFactory.getLogger(Saml2Client.class);
+
+ // Identify the KeyInfoGenerator factory created during opensaml boostrap
+ public static final String SAML_METADATA_KEY_INFO_GENERATOR = "MetadataKeyInfoGenerator";
+
+ private String keystorePath;
+
+ private String keystorePassword;
+
+ private String privateKeyPassword;
+
+ private String idpMetadataPath;
+
+ private String idpEntityId;
+
+ private String spEntityId;
+
+ private CredentialProvider credentialProvider;
+
+ private Saml2ContextProvider contextProvider;
+
+ private Saml2AuthnRequestBuilder authnRequestBuilder;
+
+ private Saml2WebSSOProfileHandler handler;
+
+ private Saml2ResponseValidator responseValidator;
+
+ private SignatureTrustEngineProvider signatureTrustEngineProvider;
+
+ private EncryptionProvider encryptionProvider;
+
+ private String spMetadata;
+
+ @Override
+ protected void internalInit() {
+
+ CommonHelper.assertNotBlank("keystorePath", this.keystorePath);
+ CommonHelper.assertNotBlank("keystorePassword", this.keystorePassword);
+ CommonHelper.assertNotBlank("privateKeyPassword", this.privateKeyPassword);
+ CommonHelper.assertNotBlank("idpMetadataPath", this.idpMetadataPath);
+ CommonHelper.assertNotBlank("callbackUrl", this.callbackUrl);
+ if (!this.callbackUrl.startsWith("http")) {
+ throw new TechnicalException("SAML callbackUrl must be absolute");
+ }
+
+ // Bootsrap OpenSAML
+ try {
+ DefaultBootstrap.bootstrap();
+ NamedKeyInfoGeneratorManager manager = Configuration.getGlobalSecurityConfiguration()
+ .getKeyInfoGeneratorManager();
+ X509KeyInfoGeneratorFactory generator = new X509KeyInfoGeneratorFactory();
+ generator.setEmitEntityCertificate(true);
+ generator.setEmitEntityCertificateChain(true);
+ manager.registerFactory(Saml2Client.SAML_METADATA_KEY_INFO_GENERATOR, generator);
+ } catch (ConfigurationException e) {
+ throw new SamlException("Error bootstrapping OpenSAML", e);
+ }
+
+ // load private key from the keystore and provide it as OpenSAML credentials
+ this.credentialProvider = new CredentialProvider(keystorePath, keystorePassword, privateKeyPassword);
+
+ // required parserPool for XML processing
+ StaticBasicParserPool parserPool = new StaticBasicParserPool();
+ try {
+ parserPool.initialize();
+ } catch (XMLParserException e) {
+ throw new SamlException("Error initializing parserPool", e);
+ }
+
+ // load IDP metadata from a file
+ FilesystemMetadataProvider idpMetadataProvider;
+ try {
+ idpMetadataProvider = new FilesystemMetadataProvider(new File(idpMetadataPath));
+ idpMetadataProvider.setParserPool(parserPool);
+ idpMetadataProvider.initialize();
+ } catch (MetadataProviderException e) {
+ throw new SamlException("Error initializing idpMetadataProvider", e);
+ }
+
+ // If no idpEntityId declared, select first EntityDescriptor entityId as our IDP entityId
+ if (idpEntityId == null) {
+ try {
+ XMLObject md = idpMetadataProvider.getMetadata();
+ if (md instanceof EntitiesDescriptor) {
+ for (EntityDescriptor entity : ((EntitiesDescriptor) md).getEntityDescriptors()) {
+ idpEntityId = entity.getEntityID();
+ break;
+ }
+ } else if (md instanceof EntityDescriptor) {
+ idpEntityId = ((EntityDescriptor) md).getEntityID();
+ }
+ } catch (MetadataProviderException e) {
+ throw new SamlException("Error getting idp entityId from IDP metadata", e);
+ }
+ if (idpEntityId == null) {
+ throw new SamlException("No idp entityId found");
+ }
+ }
+
+ // Generate our Service Provider metadata
+ Saml2MetadataGenerator metadataGenerator = new Saml2MetadataGenerator();
+ metadataGenerator.setCredentialProvider(credentialProvider);
+ // If no spEntityId declared, use callback url
+ if (spEntityId == null) {
+ spEntityId = getCallbackUrl();
+ }
+ metadataGenerator.setEntityId(spEntityId);
+ // Assertion consumer service url is the callback url
+ metadataGenerator.setAssertionConsumerServiceUrl(getCallbackUrl());
+ // for now same for logout url
+ metadataGenerator.setSingleLogoutServiceUrl(getCallbackUrl());
+ AbstractMetadataProvider spMetadataProvider = metadataGenerator.buildMetadataProvider();
+
+ // Initialize metadata provider for our SP and get the XML as a String
+ try {
+ spMetadataProvider.initialize();
+ spMetadata = metadataGenerator.printMetadata();
+ } catch (MetadataProviderException e) {
+ throw new TechnicalException("Error initializing spMetadataProvider", e);
+ } catch (MarshallingException e) {
+ logger.warn("Unable to print SP metadata", e);
+ }
+
+ // Put IDP and SP metadata together
+ ChainingMetadataProvider metadataManager = new ChainingMetadataProvider();
+ try {
+ metadataManager.addMetadataProvider(idpMetadataProvider);
+ metadataManager.addMetadataProvider(spMetadataProvider);
+ } catch (MetadataProviderException e) {
+ throw new TechnicalException("Error adding idp or sp metadatas to manager", e);
+ }
+
+ // Build the contextProvider
+ contextProvider = new Saml2ContextProvider(metadataManager, idpEntityId, spEntityId);
+
+ // Get a velocity engine for the HTTP-POST binding (building of an HTML document)
+ VelocityEngine velocityEngine = VelocityEngineFactory.getEngine();
+ // Get an AuthnRequest builder
+ authnRequestBuilder = new Saml2AuthnRequestBuilder();
+
+ // Build the WebSSO handler for sending and receiving SAML2 messages
+ HTTPPostEncoder postEncoder = new HTTPPostEncoder(velocityEngine, "/templates/saml2-post-binding.vm");
+ HTTPPostDecoder postDecoder = new Pac4jHTTPPostDecoder(parserPool);
+ handler = new Saml2WebSSOProfileHandler(credentialProvider, postEncoder, postDecoder, parserPool);
+
+ // Build provider for digital signature validation and encryption
+ signatureTrustEngineProvider = new SignatureTrustEngineProvider(metadataManager);
+ encryptionProvider = new EncryptionProvider(credentialProvider);
+
+ // Build the SAML response validator
+ responseValidator = new Saml2ResponseValidator();
+
+ }
+
+ @Override
+ protected BaseClient newClient() {
+ Saml2Client client = new Saml2Client();
+ client.setKeystorePath(this.keystorePath);
+ client.setKeystorePassword(this.keystorePassword);
+ client.setPrivateKeyPassword(this.privateKeyPassword);
+ client.setIdpMetadataPath(this.idpMetadataPath);
+ client.setIdpEntityId(this.idpEntityId);
+ client.setSpEntityId(this.spEntityId);
+ client.setCallbackUrl(this.callbackUrl);
+ return client;
+ }
+
+ @Override
+ protected boolean isDirectRedirection() {
+ return true;
+ }
+
+ @Override
+ protected RedirectAction retrieveRedirectAction(final WebContext wc) {
+
+ ExtendedSAMLMessageContext context = contextProvider.buildSpAndIdpContext(wc);
+ final String relayState = getContextualCallbackUrl(wc);
+
+ AuthnRequest authnRequest = authnRequestBuilder.build(context);
+
+ handler.sendMessage(context, authnRequest, relayState);
+
+ String content = ((SimpleResponseAdapter) context.getOutboundMessageTransport()).getOutgoingContent();
+
+ return RedirectAction.success(content);
+ }
+
+ @Override
+ protected Saml2Credentials retrieveCredentials(final WebContext wc) throws RequiresHttpAction {
+
+ ExtendedSAMLMessageContext context = contextProvider.buildSpContext(wc);
+ // assertion consumer url is pac4j callback url
+ context.setAssertionConsumerUrl(getCallbackUrl());
+
+ SignatureTrustEngine trustEngine = signatureTrustEngineProvider.build();
+ Decrypter decrypter = encryptionProvider.buildDecrypter();
+
+ handler.receiveMessage(context, trustEngine);
+
+ responseValidator.validateSamlResponse(context, trustEngine, decrypter);
+
+ return buildSaml2Credentials(context, decrypter);
+
+ }
+
+ private Saml2Credentials buildSaml2Credentials(final ExtendedSAMLMessageContext context, final Decrypter decrypter) {
+
+ NameID nameId = (NameID) context.getSubjectNameIdentifier();
+ Assertion subjectAssertion = context.getSubjectAssertion();
+
+ List attributes = new ArrayList();
+ for (AttributeStatement attributeStatement : subjectAssertion.getAttributeStatements()) {
+ for (Attribute attribute : attributeStatement.getAttributes()) {
+ attributes.add(attribute);
+ }
+ for (EncryptedAttribute encryptedAttribute : attributeStatement.getEncryptedAttributes()) {
+ try {
+ attributes.add(decrypter.decrypt(encryptedAttribute));
+ } catch (DecryptionException e) {
+ logger.warn("Decryption of attribute failed, continue with the next one", e);
+ }
+ }
+ }
+
+ return new Saml2Credentials(nameId, attributes, getName());
+ }
+
+ @Override
+ protected Saml2Profile retrieveUserProfile(final Saml2Credentials credentials, final WebContext context) {
+
+ Saml2Profile profile = new Saml2Profile();
+ profile.setId(credentials.getNameId().getValue());
+
+ // TODO write some attribute mapper
+ for (Attribute attribute : credentials.getAttributes()) {
+ List values = new ArrayList();
+ for (XMLObject attributeValue : attribute.getAttributeValues()) {
+ Element attributeValueElement = attributeValue.getDOM();
+ String value = attributeValueElement.getTextContent();
+ values.add(value);
+ }
+ profile.addAttribute(attribute.getName(), values);
+ }
+
+ return profile;
+ }
+
+ @Override
+ public Protocol getProtocol() {
+ return Protocol.SAML;
+ }
+
+ public void setSpEntityId(final String spEntityId) {
+ this.spEntityId = spEntityId;
+ }
+
+ public void setIdpMetadataPath(final String idpMetadataPath) {
+ this.idpMetadataPath = idpMetadataPath;
+ }
+
+ public void setIdpEntityId(final String idpEntityId) {
+ this.idpEntityId = idpEntityId;
+ }
+
+ public void setKeystorePath(final String keystorePath) {
+ this.keystorePath = keystorePath;
+ }
+
+ public void setKeystorePassword(final String keystorePassword) {
+ this.keystorePassword = keystorePassword;
+ }
+
+ public void setPrivateKeyPassword(final String privateKeyPassword) {
+ this.privateKeyPassword = privateKeyPassword;
+ }
+
+ public String printClientMetadata() {
+ init();
+ return spMetadata;
+ }
+
+}
diff --git a/pac4j-saml/src/main/java/org/pac4j/saml/context/ExtendedSAMLMessageContext.java b/pac4j-saml/src/main/java/org/pac4j/saml/context/ExtendedSAMLMessageContext.java
new file mode 100644
index 0000000000..8fa1c33952
--- /dev/null
+++ b/pac4j-saml/src/main/java/org/pac4j/saml/context/ExtendedSAMLMessageContext.java
@@ -0,0 +1,63 @@
+/*
+ Copyright 2012 -2014 Michael Remond
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package org.pac4j.saml.context;
+
+import org.opensaml.common.binding.BasicSAMLMessageContext;
+import org.opensaml.saml2.core.Assertion;
+
+/**
+ * Allow to store additional information for SAML processing.
+ *
+ * @author Michael Remond
+ *
+ */
+public class ExtendedSAMLMessageContext extends BasicSAMLMessageContext {
+
+ /* valid subject assertion */
+ private Assertion subjectAssertion;
+
+ /* id of the authn request */
+ private String requestId;
+
+ /* endpoint location */
+ private String assertionConsumerUrl;
+
+ public Assertion getSubjectAssertion() {
+ return subjectAssertion;
+ }
+
+ public void setSubjectAssertion(final Assertion subjectAssertion) {
+ this.subjectAssertion = subjectAssertion;
+ }
+
+ public String getRequestId() {
+ return requestId;
+ }
+
+ public void setRequestId(final String requestId) {
+ this.requestId = requestId;
+ }
+
+ public String getAssertionConsumerUrl() {
+ return assertionConsumerUrl;
+ }
+
+ public void setAssertionConsumerUrl(final String assertionConsumerUrl) {
+ this.assertionConsumerUrl = assertionConsumerUrl;
+ }
+
+}
diff --git a/pac4j-saml/src/main/java/org/pac4j/saml/context/Saml2ContextProvider.java b/pac4j-saml/src/main/java/org/pac4j/saml/context/Saml2ContextProvider.java
new file mode 100644
index 0000000000..fc0f407916
--- /dev/null
+++ b/pac4j-saml/src/main/java/org/pac4j/saml/context/Saml2ContextProvider.java
@@ -0,0 +1,136 @@
+/*
+ Copyright 2012 -2014 Michael Remond
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package org.pac4j.saml.context;
+
+import org.opensaml.common.binding.BasicSAMLMessageContext;
+import org.opensaml.common.xml.SAMLConstants;
+import org.opensaml.saml2.metadata.EntityDescriptor;
+import org.opensaml.saml2.metadata.IDPSSODescriptor;
+import org.opensaml.saml2.metadata.RoleDescriptor;
+import org.opensaml.saml2.metadata.SPSSODescriptor;
+import org.opensaml.saml2.metadata.provider.MetadataProvider;
+import org.opensaml.saml2.metadata.provider.MetadataProviderException;
+import org.pac4j.core.context.WebContext;
+import org.pac4j.saml.exceptions.SamlException;
+import org.pac4j.saml.transport.SimpleRequestAdapter;
+import org.pac4j.saml.transport.SimpleResponseAdapter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Responsible for building a {@link ExtendedSAMLMessageContext} from given SAML2 properties
+ * (idpEntityId, spEntityId and metadata manager) and current {@link WebContext}.
+ *
+ * @author Michael Remond
+ * @since 1.5.0
+ *
+ */
+@SuppressWarnings("rawtypes")
+public class Saml2ContextProvider {
+
+ protected final static Logger logger = LoggerFactory.getLogger(Saml2ContextProvider.class);
+
+ protected MetadataProvider metadata;
+
+ protected String idpEntityId;
+
+ protected String spEntityId;
+
+ public Saml2ContextProvider(final MetadataProvider metadata, final String idpEntityId, final String spEntityId) {
+ this.metadata = metadata;
+ this.idpEntityId = idpEntityId;
+ this.spEntityId = spEntityId;
+ }
+
+ public ExtendedSAMLMessageContext buildSpContext(final WebContext webContext) {
+
+ ExtendedSAMLMessageContext context = new ExtendedSAMLMessageContext();
+ context.setMetadataProvider(metadata);
+ addTransportContext(webContext, context);
+ addSPContext(context);
+
+ return context;
+ }
+
+ public ExtendedSAMLMessageContext buildSpAndIdpContext(final WebContext webContext) {
+
+ ExtendedSAMLMessageContext context = new ExtendedSAMLMessageContext();
+ context.setMetadataProvider(metadata);
+ addTransportContext(webContext, context);
+ addSPContext(context);
+ addIDPContext(context);
+
+ return context;
+ }
+
+ protected void addTransportContext(final WebContext webContext, final BasicSAMLMessageContext context) {
+
+ SimpleRequestAdapter inTransport = new SimpleRequestAdapter(webContext);
+ SimpleResponseAdapter outTransport = new SimpleResponseAdapter();
+
+ context.setInboundMessageTransport(inTransport);
+ context.setOutboundMessageTransport(outTransport);
+ }
+
+ protected void addSPContext(final BasicSAMLMessageContext context) {
+ context.setLocalEntityId(spEntityId);
+ context.setLocalEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
+
+ EntityDescriptor entityDescriptor = null;
+ RoleDescriptor roleDescriptor = null;
+ try {
+ entityDescriptor = metadata.getEntityDescriptor(spEntityId);
+ roleDescriptor = metadata.getRole(spEntityId, SPSSODescriptor.DEFAULT_ELEMENT_NAME,
+ SAMLConstants.SAML20P_NS);
+ } catch (MetadataProviderException e) {
+ throw new SamlException("An error occured while getting SP descriptors", e);
+ }
+
+ if (entityDescriptor == null || roleDescriptor == null) {
+ throw new SamlException("Cannot find entity " + spEntityId + " or role "
+ + SPSSODescriptor.DEFAULT_ELEMENT_NAME + " in metadata provider");
+ }
+
+ context.setLocalEntityMetadata(entityDescriptor);
+ context.setLocalEntityRoleMetadata(roleDescriptor);
+ }
+
+ protected void addIDPContext(final BasicSAMLMessageContext context) {
+
+ context.setPeerEntityId(idpEntityId);
+ context.setPeerEntityRole(IDPSSODescriptor.DEFAULT_ELEMENT_NAME);
+
+ EntityDescriptor entityDescriptor = null;
+ RoleDescriptor roleDescriptor = null;
+ try {
+ entityDescriptor = metadata.getEntityDescriptor(idpEntityId);
+ roleDescriptor = metadata.getRole(idpEntityId, IDPSSODescriptor.DEFAULT_ELEMENT_NAME,
+ SAMLConstants.SAML20P_NS);
+ } catch (MetadataProviderException e) {
+ throw new SamlException("An error occured while getting IDP descriptors", e);
+ }
+
+ if (entityDescriptor == null || roleDescriptor == null) {
+ throw new SamlException("Cannot find entity " + idpEntityId + " or role "
+ + IDPSSODescriptor.DEFAULT_ELEMENT_NAME + " in metadata provider");
+ }
+
+ context.setPeerEntityMetadata(entityDescriptor);
+ context.setPeerEntityRoleMetadata(roleDescriptor);
+ }
+
+}
diff --git a/pac4j-saml/src/main/java/org/pac4j/saml/credentials/Saml2Credentials.java b/pac4j-saml/src/main/java/org/pac4j/saml/credentials/Saml2Credentials.java
new file mode 100644
index 0000000000..076ee2dd8b
--- /dev/null
+++ b/pac4j-saml/src/main/java/org/pac4j/saml/credentials/Saml2Credentials.java
@@ -0,0 +1,59 @@
+/*
+ Copyright 2012 -2014 Michael Remond
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package org.pac4j.saml.credentials;
+
+import java.util.List;
+
+import org.opensaml.saml2.core.Attribute;
+import org.opensaml.saml2.core.NameID;
+import org.pac4j.core.credentials.Credentials;
+
+/**
+ * Credentials containing the nameId of the SAML subject and all of its attributes.
+ *
+ * @author Michael Remond
+ * @since 1.5.0
+ *
+ */
+public class Saml2Credentials extends Credentials {
+
+ private static final long serialVersionUID = 5040516205957826527L;
+
+ private final NameID nameId;
+
+ private final List attributes;
+
+ public Saml2Credentials(final NameID nameId, final List attributes, final String clientName) {
+ this.nameId = nameId;
+ this.attributes = attributes;
+ setClientName(clientName);
+ }
+
+ public NameID getNameId() {
+ return nameId;
+ }
+
+ public List getAttributes() {
+ return attributes;
+ }
+
+ @Override
+ public String toString() {
+ return "SAMLCredential [nameId=" + nameId + ", attributes=" + attributes + "]";
+ }
+
+}
diff --git a/pac4j-saml/src/main/java/org/pac4j/saml/crypto/CredentialProvider.java b/pac4j-saml/src/main/java/org/pac4j/saml/crypto/CredentialProvider.java
new file mode 100644
index 0000000000..05439c83d1
--- /dev/null
+++ b/pac4j-saml/src/main/java/org/pac4j/saml/crypto/CredentialProvider.java
@@ -0,0 +1,106 @@
+/*
+ Copyright 2012 -2014 Michael Remond
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package org.pac4j.saml.crypto;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.opensaml.xml.security.CriteriaSet;
+import org.opensaml.xml.security.credential.Credential;
+import org.opensaml.xml.security.credential.CredentialResolver;
+import org.opensaml.xml.security.credential.KeyStoreCredentialResolver;
+import org.opensaml.xml.security.criteria.EntityIDCriteria;
+import org.pac4j.saml.exceptions.SamlException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class responsible for loading a private key from a JKS keystore and returning
+ * the corresponding {@link Credential} opensaml object.
+ *
+ * @author Michael Remond
+ * @since 1.5.0
+ *
+ */
+public class CredentialProvider {
+
+ private final Logger logger = LoggerFactory.getLogger(CredentialProvider.class);
+
+ private final CredentialResolver credentialResolver;
+
+ private final String privateKey;
+
+ public CredentialProvider(final String name, final String storePasswd, final String privateKeyPasswd) {
+ KeyStore keyStore = loadKeyStore(name, storePasswd);
+ this.privateKey = getPrivateKeyAlias(keyStore);
+ Map passwords = new HashMap();
+ passwords.put(privateKey, privateKeyPasswd);
+ this.credentialResolver = new KeyStoreCredentialResolver(keyStore, passwords);
+ }
+
+ public Credential getCredential() {
+ try {
+ CriteriaSet cs = new CriteriaSet();
+ EntityIDCriteria criteria = new EntityIDCriteria(privateKey);
+ cs.add(criteria);
+ return credentialResolver.resolveSingle(cs);
+ } catch (org.opensaml.xml.security.SecurityException e) {
+ throw new SamlException("Can't obtain SP private key", e);
+ }
+ }
+
+ private KeyStore loadKeyStore(final String name, final String storePasswd) {
+ InputStream inputStream = null;
+ try {
+ inputStream = new FileInputStream(name);
+ KeyStore ks = KeyStore.getInstance("JKS");
+ ks.load(inputStream, storePasswd == null ? null : storePasswd.toCharArray());
+ return ks;
+ } catch (Exception e) {
+ logger.error("Error loading keystore", e);
+ throw new SamlException("Error loading keystore", e);
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ logger.debug("Error closing input stream of keystore", e);
+ }
+ }
+ }
+ }
+
+ private String getPrivateKeyAlias(final KeyStore keyStore) {
+ try {
+ Enumeration aliases = keyStore.aliases();
+ if (aliases.hasMoreElements()) {
+ return aliases.nextElement();
+ } else {
+ throw new SamlException("Keystore has no private keys");
+ }
+ } catch (KeyStoreException e) {
+ throw new SamlException("Unable to get aliases from keyStore", e);
+ }
+ }
+
+}
diff --git a/pac4j-saml/src/main/java/org/pac4j/saml/crypto/EncryptionProvider.java b/pac4j-saml/src/main/java/org/pac4j/saml/crypto/EncryptionProvider.java
new file mode 100644
index 0000000000..4668aa5c25
--- /dev/null
+++ b/pac4j-saml/src/main/java/org/pac4j/saml/crypto/EncryptionProvider.java
@@ -0,0 +1,60 @@
+/*
+ Copyright 2012 -2014 Michael Remond
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package org.pac4j.saml.crypto;
+
+import org.opensaml.saml2.encryption.Decrypter;
+import org.opensaml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver;
+import org.opensaml.xml.encryption.ChainingEncryptedKeyResolver;
+import org.opensaml.xml.encryption.InlineEncryptedKeyResolver;
+import org.opensaml.xml.encryption.SimpleRetrievalMethodEncryptedKeyResolver;
+import org.opensaml.xml.security.credential.Credential;
+import org.opensaml.xml.security.keyinfo.KeyInfoCredentialResolver;
+import org.opensaml.xml.security.keyinfo.StaticKeyInfoCredentialResolver;
+
+/**
+ * Provider returning well configured decrypter instances.
+ *
+ * @author Michael Remond
+ * @since 1.5.0
+ *
+ */
+public class EncryptionProvider {
+
+ private static ChainingEncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver();
+
+ static {
+ encryptedKeyResolver.getResolverChain().add(new InlineEncryptedKeyResolver());
+ encryptedKeyResolver.getResolverChain().add(new EncryptedElementTypeEncryptedKeyResolver());
+ encryptedKeyResolver.getResolverChain().add(new SimpleRetrievalMethodEncryptedKeyResolver());
+ }
+
+ private final CredentialProvider credentialProvider;
+
+ public EncryptionProvider(final CredentialProvider credentialProvider) {
+ this.credentialProvider = credentialProvider;
+ }
+
+ public Decrypter buildDecrypter() {
+ Credential encryptionCredential = credentialProvider.getCredential();
+ KeyInfoCredentialResolver resolver = new StaticKeyInfoCredentialResolver(encryptionCredential);
+ Decrypter decrypter = new Decrypter(null, resolver, encryptedKeyResolver);
+ decrypter.setRootInNewDocument(true);
+
+ return decrypter;
+ }
+
+}
diff --git a/pac4j-saml/src/main/java/org/pac4j/saml/crypto/SignatureTrustEngineProvider.java b/pac4j-saml/src/main/java/org/pac4j/saml/crypto/SignatureTrustEngineProvider.java
new file mode 100644
index 0000000000..086535aa22
--- /dev/null
+++ b/pac4j-saml/src/main/java/org/pac4j/saml/crypto/SignatureTrustEngineProvider.java
@@ -0,0 +1,46 @@
+/*
+ Copyright 2012 -2014 Michael Remond
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package org.pac4j.saml.crypto;
+
+import org.opensaml.saml2.metadata.provider.MetadataProvider;
+import org.opensaml.security.MetadataCredentialResolver;
+import org.opensaml.xml.Configuration;
+import org.opensaml.xml.signature.SignatureTrustEngine;
+import org.opensaml.xml.signature.impl.ExplicitKeySignatureTrustEngine;
+
+/**
+ * Provider returning well configured {@link SignatureTrustEngine} instances.
+ *
+ * @author Michael Remond
+ * @since 1.5.0
+ *
+ */
+public class SignatureTrustEngineProvider {
+
+ private final MetadataProvider metadataProvider;
+
+ public SignatureTrustEngineProvider(final MetadataProvider metadataProvider) {
+ this.metadataProvider = metadataProvider;
+ }
+
+ public SignatureTrustEngine build() {
+ MetadataCredentialResolver metadataResolver = new MetadataCredentialResolver(metadataProvider);
+ return new ExplicitKeySignatureTrustEngine(metadataResolver, Configuration.getGlobalSecurityConfiguration()
+ .getDefaultKeyInfoCredentialResolver());
+ }
+
+}
diff --git a/pac4j-saml/src/main/java/org/pac4j/saml/exceptions/SamlException.java b/pac4j-saml/src/main/java/org/pac4j/saml/exceptions/SamlException.java
new file mode 100644
index 0000000000..eee1934ac9
--- /dev/null
+++ b/pac4j-saml/src/main/java/org/pac4j/saml/exceptions/SamlException.java
@@ -0,0 +1,44 @@
+/*
+ Copyright 2012 -2014 Michael Remond
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package org.pac4j.saml.exceptions;
+
+import org.pac4j.core.exception.TechnicalException;
+
+/**
+ * Root exception for SAML Client.
+ *
+ * @author Michael Remond
+ * @since 1.5.0
+ *
+ */
+public class SamlException extends TechnicalException {
+
+ private static final long serialVersionUID = -2963580056603469743L;
+
+ public SamlException(final String message) {
+ super(message);
+ }
+
+ public SamlException(final Throwable t) {
+ super(t);
+ }
+
+ public SamlException(final String message, final Throwable t) {
+ super(message, t);
+ }
+
+}
diff --git a/pac4j-saml/src/main/java/org/pac4j/saml/metadata/Saml2MetadataGenerator.java b/pac4j-saml/src/main/java/org/pac4j/saml/metadata/Saml2MetadataGenerator.java
new file mode 100644
index 0000000000..c01adf86f6
--- /dev/null
+++ b/pac4j-saml/src/main/java/org/pac4j/saml/metadata/Saml2MetadataGenerator.java
@@ -0,0 +1,248 @@
+/*
+ Copyright 2012 -2014 Michael Remond
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package org.pac4j.saml.metadata;
+
+import java.util.Collection;
+import java.util.LinkedList;
+
+import org.opensaml.Configuration;
+import org.opensaml.common.SAMLObjectBuilder;
+import org.opensaml.common.xml.SAMLConstants;
+import org.opensaml.saml2.core.NameIDType;
+import org.opensaml.saml2.metadata.AssertionConsumerService;
+import org.opensaml.saml2.metadata.EntityDescriptor;
+import org.opensaml.saml2.metadata.KeyDescriptor;
+import org.opensaml.saml2.metadata.NameIDFormat;
+import org.opensaml.saml2.metadata.SPSSODescriptor;
+import org.opensaml.saml2.metadata.SingleLogoutService;
+import org.opensaml.saml2.metadata.provider.AbstractMetadataProvider;
+import org.opensaml.saml2.metadata.provider.MetadataProviderException;
+import org.opensaml.xml.XMLObject;
+import org.opensaml.xml.XMLObjectBuilderFactory;
+import org.opensaml.xml.io.MarshallerFactory;
+import org.opensaml.xml.io.MarshallingException;
+import org.opensaml.xml.security.SecurityHelper;
+import org.opensaml.xml.security.credential.Credential;
+import org.opensaml.xml.security.credential.UsageType;
+import org.opensaml.xml.security.keyinfo.KeyInfoGenerator;
+import org.opensaml.xml.signature.KeyInfo;
+import org.opensaml.xml.util.XMLHelper;
+import org.pac4j.saml.client.Saml2Client;
+import org.pac4j.saml.crypto.CredentialProvider;
+import org.pac4j.saml.exceptions.SamlException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Element;
+
+/**
+ * Generates metadata object with standard values and overriden user defined values.
+ *
+ * @author Michael Remond
+ * @since 1.5.0
+ *
+ */
+@SuppressWarnings("unchecked")
+public class Saml2MetadataGenerator {
+
+ protected final static Logger logger = LoggerFactory.getLogger(Saml2MetadataGenerator.class);
+
+ protected XMLObjectBuilderFactory builderFactory = Configuration.getBuilderFactory();
+
+ protected MarshallerFactory marshallerFactory = Configuration.getMarshallerFactory();
+
+ protected CredentialProvider credentialProvider;
+
+ protected String entityId;
+
+ protected String assertionConsumerServiceUrl;
+
+ protected String singleLogoutServiceUrl;
+
+ protected boolean authnRequestSigned = true;
+
+ protected boolean wantAssertionSigned = true;
+
+ protected int defaultACSIndex = 0;
+
+ public AbstractMetadataProvider buildMetadataProvider() {
+ final EntityDescriptor md = buildMetadata();
+ return new AbstractMetadataProvider() {
+
+ @Override
+ protected XMLObject doGetMetadata() throws MetadataProviderException {
+ return md;
+ }
+ };
+ }
+
+ public String printMetadata() throws MarshallingException {
+ EntityDescriptor md = buildMetadata();
+ Element entityDescriptorElement = marshallerFactory.getMarshaller(md).marshall(md);
+ return XMLHelper.nodeToString(entityDescriptorElement);
+ }
+
+ public EntityDescriptor buildMetadata() {
+
+ SAMLObjectBuilder builder = (SAMLObjectBuilder) builderFactory
+ .getBuilder(EntityDescriptor.DEFAULT_ELEMENT_NAME);
+ EntityDescriptor descriptor = builder.buildObject();
+ descriptor.setEntityID(entityId);
+ descriptor.getRoleDescriptors().add(buildSPSSODescriptor());
+
+ return descriptor;
+
+ }
+
+ protected KeyInfo generateKeyInfoForCredential(final Credential credential) {
+ try {
+ KeyInfoGenerator keyInfoGenerator = SecurityHelper.getKeyInfoGenerator(credential, null,
+ Saml2Client.SAML_METADATA_KEY_INFO_GENERATOR);
+ return keyInfoGenerator.generate(credential);
+ } catch (org.opensaml.xml.security.SecurityException e) {
+ throw new SamlException("Unable to generate keyInfo from given credential", e);
+ }
+ }
+
+ protected SPSSODescriptor buildSPSSODescriptor() {
+
+ SAMLObjectBuilder builder = (SAMLObjectBuilder) builderFactory
+ .getBuilder(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
+ SPSSODescriptor spDescriptor = builder.buildObject();
+
+ spDescriptor.setAuthnRequestsSigned(authnRequestSigned);
+ spDescriptor.setWantAssertionsSigned(wantAssertionSigned);
+ spDescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS);
+
+ spDescriptor.getNameIDFormats().addAll(buildNameIDFormat());
+
+ int index = 0;
+ spDescriptor.getAssertionConsumerServices().add(
+ getAssertionConsumerService(SAMLConstants.SAML2_POST_BINDING_URI, index++, defaultACSIndex == index));
+
+ spDescriptor.getKeyDescriptors().add(getKeyDescriptor(UsageType.SIGNING, getKeyInfo()));
+ spDescriptor.getKeyDescriptors().add(getKeyDescriptor(UsageType.ENCRYPTION, getKeyInfo()));
+
+ return spDescriptor;
+
+ }
+
+ protected Collection buildNameIDFormat() {
+
+ SAMLObjectBuilder builder = (SAMLObjectBuilder) builderFactory
+ .getBuilder(NameIDFormat.DEFAULT_ELEMENT_NAME);
+ Collection formats = new LinkedList();
+ NameIDFormat transientNameID = builder.buildObject();
+ transientNameID.setFormat(NameIDType.TRANSIENT);
+ formats.add(transientNameID);
+ NameIDFormat persistentNameID = builder.buildObject();
+ persistentNameID.setFormat(NameIDType.PERSISTENT);
+ formats.add(persistentNameID);
+ NameIDFormat emailNameID = builder.buildObject();
+ emailNameID.setFormat(NameIDType.EMAIL);
+ formats.add(emailNameID);
+ NameIDFormat unspecNameID = builder.buildObject();
+ unspecNameID.setFormat(NameIDType.UNSPECIFIED);
+ formats.add(unspecNameID);
+ return formats;
+ }
+
+ protected AssertionConsumerService getAssertionConsumerService(final String binding, final int index,
+ final boolean isDefault) {
+ SAMLObjectBuilder builder = (SAMLObjectBuilder) builderFactory
+ .getBuilder(AssertionConsumerService.DEFAULT_ELEMENT_NAME);
+ AssertionConsumerService consumer = builder.buildObject();
+ consumer.setLocation(assertionConsumerServiceUrl);
+ consumer.setBinding(binding);
+ if (isDefault) {
+ consumer.setIsDefault(true);
+ }
+ consumer.setIndex(index);
+ return consumer;
+ }
+
+ protected SingleLogoutService getSingleLogoutService(final String binding) {
+ SAMLObjectBuilder builder = (SAMLObjectBuilder) builderFactory
+ .getBuilder(SingleLogoutService.DEFAULT_ELEMENT_NAME);
+ SingleLogoutService logoutService = builder.buildObject();
+ logoutService.setLocation(singleLogoutServiceUrl);
+ logoutService.setBinding(binding);
+ return logoutService;
+ }
+
+ protected KeyDescriptor getKeyDescriptor(final UsageType type, final KeyInfo key) {
+ SAMLObjectBuilder builder = (SAMLObjectBuilder) Configuration.getBuilderFactory()
+ .getBuilder(KeyDescriptor.DEFAULT_ELEMENT_NAME);
+ KeyDescriptor descriptor = builder.buildObject();
+ descriptor.setUse(type);
+ descriptor.setKeyInfo(key);
+ return descriptor;
+ }
+
+ protected KeyInfo getKeyInfo() {
+ Credential serverCredential = credentialProvider.getCredential();
+ return generateKeyInfoForCredential(serverCredential);
+ }
+
+ public CredentialProvider getCredentialProvider() {
+ return credentialProvider;
+ }
+
+ public void setCredentialProvider(final CredentialProvider credentialProvider) {
+ this.credentialProvider = credentialProvider;
+ }
+
+ public String getEntityId() {
+ return entityId;
+ }
+
+ public void setEntityId(final String entityId) {
+ this.entityId = entityId;
+ }
+
+ public boolean isAuthnRequestSigned() {
+ return authnRequestSigned;
+ }
+
+ public void setAuthnRequestSigned(final boolean authnRequestSigned) {
+ this.authnRequestSigned = authnRequestSigned;
+ }
+
+ public boolean isWantAssertionSigned() {
+ return wantAssertionSigned;
+ }
+
+ public void setWantAssertionSigned(final boolean wantAssertionSigned) {
+ this.wantAssertionSigned = wantAssertionSigned;
+ }
+
+ public int getDefaultACSIndex() {
+ return defaultACSIndex;
+ }
+
+ public void setDefaultACSIndex(final int defaultACSIndex) {
+ this.defaultACSIndex = defaultACSIndex;
+ }
+
+ public void setAssertionConsumerServiceUrl(final String assertionConsumerServiceUrl) {
+ this.assertionConsumerServiceUrl = assertionConsumerServiceUrl;
+ }
+
+ public void setSingleLogoutServiceUrl(final String singleLogoutServiceUrl) {
+ this.singleLogoutServiceUrl = singleLogoutServiceUrl;
+ }
+
+}
diff --git a/pac4j-saml/src/main/java/org/pac4j/saml/profile/Saml2Profile.java b/pac4j-saml/src/main/java/org/pac4j/saml/profile/Saml2Profile.java
new file mode 100644
index 0000000000..424371e979
--- /dev/null
+++ b/pac4j-saml/src/main/java/org/pac4j/saml/profile/Saml2Profile.java
@@ -0,0 +1,32 @@
+/*
+ Copyright 2012 -2014 Michael Remond
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package org.pac4j.saml.profile;
+
+import org.pac4j.core.profile.CommonProfile;
+
+/**
+ * This class is the user profile for sites using SAML2 protocol.
+ * It is returned by the {@link org.pac4j.cas.client.Saml2Client}.
+ *
+ * @author Michael Remond
+ *
+ */
+public class Saml2Profile extends CommonProfile {
+
+ private static final long serialVersionUID = -7811733390277407623L;
+
+}
diff --git a/pac4j-saml/src/main/java/org/pac4j/saml/sso/Saml2AuthnRequestBuilder.java b/pac4j-saml/src/main/java/org/pac4j/saml/sso/Saml2AuthnRequestBuilder.java
new file mode 100644
index 0000000000..b816c0a430
--- /dev/null
+++ b/pac4j-saml/src/main/java/org/pac4j/saml/sso/Saml2AuthnRequestBuilder.java
@@ -0,0 +1,98 @@
+/*
+ Copyright 2012 -2014 Michael Remond
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package org.pac4j.saml.sso;
+
+import java.util.Random;
+
+import org.joda.time.DateTime;
+import org.opensaml.Configuration;
+import org.opensaml.common.SAMLObjectBuilder;
+import org.opensaml.common.SAMLVersion;
+import org.opensaml.common.binding.SAMLMessageContext;
+import org.opensaml.common.xml.SAMLConstants;
+import org.opensaml.saml2.core.AuthnRequest;
+import org.opensaml.saml2.core.Issuer;
+import org.opensaml.saml2.metadata.AssertionConsumerService;
+import org.opensaml.saml2.metadata.IDPSSODescriptor;
+import org.opensaml.saml2.metadata.SPSSODescriptor;
+import org.opensaml.saml2.metadata.SingleSignOnService;
+import org.opensaml.xml.XMLObjectBuilderFactory;
+import org.pac4j.saml.util.SamlUtils;
+
+/**
+ * Build a SAML2 Authn Request from the given {@link SAMLMessageContext}.
+ *
+ * @author Michael Remond
+ * @since 1.5.0
+ *
+ */
+@SuppressWarnings("rawtypes")
+public class Saml2AuthnRequestBuilder {
+
+ private final XMLObjectBuilderFactory builderFactory = Configuration.getBuilderFactory();
+
+ public AuthnRequest build(final SAMLMessageContext context) {
+
+ SPSSODescriptor spDescriptor = (SPSSODescriptor) context.getLocalEntityRoleMetadata();
+ IDPSSODescriptor idpssoDescriptor = (IDPSSODescriptor) context.getPeerEntityRoleMetadata();
+
+ SingleSignOnService ssoService = SamlUtils.getSingleSignOnService(idpssoDescriptor,
+ SAMLConstants.SAML2_POST_BINDING_URI);
+ AssertionConsumerService assertionConsumerService = SamlUtils.getAssertionConsumerService(spDescriptor, null);
+
+ return buildAuthnRequest(context, assertionConsumerService, ssoService);
+ }
+
+ @SuppressWarnings("unchecked")
+ protected AuthnRequest buildAuthnRequest(final SAMLMessageContext context,
+ final AssertionConsumerService assertionConsumerService, final SingleSignOnService ssoService) {
+
+ SAMLObjectBuilder builder = (SAMLObjectBuilder) builderFactory
+ .getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME);
+ AuthnRequest request = builder.buildObject();
+
+ request.setID(generateID());
+ request.setIssuer(getIssuer(context.getLocalEntityId()));
+ request.setIssueInstant(new DateTime());
+ request.setVersion(SAMLVersion.VERSION_20);
+ request.setIsPassive(false);
+ request.setForceAuthn(false);
+ request.setProviderName("pac4j-saml");
+
+ request.setDestination(ssoService.getLocation());
+ request.setAssertionConsumerServiceURL(assertionConsumerService.getLocation());
+ request.setProtocolBinding(assertionConsumerService.getBinding());
+
+ return request;
+
+ }
+
+ @SuppressWarnings("unchecked")
+ protected Issuer getIssuer(final String spEntityId) {
+ SAMLObjectBuilder issuerBuilder = (SAMLObjectBuilder) builderFactory
+ .getBuilder(Issuer.DEFAULT_ELEMENT_NAME);
+ Issuer issuer = issuerBuilder.buildObject();
+ issuer.setValue(spEntityId);
+ return issuer;
+ }
+
+ protected String generateID() {
+ Random r = new Random();
+ return '_' + Long.toString(Math.abs(r.nextLong()), 16) + Long.toString(Math.abs(r.nextLong()), 16);
+ }
+
+}
diff --git a/pac4j-saml/src/main/java/org/pac4j/saml/sso/Saml2ResponseValidator.java b/pac4j-saml/src/main/java/org/pac4j/saml/sso/Saml2ResponseValidator.java
new file mode 100644
index 0000000000..82de68dc07
--- /dev/null
+++ b/pac4j-saml/src/main/java/org/pac4j/saml/sso/Saml2ResponseValidator.java
@@ -0,0 +1,467 @@
+/*
+ Copyright 2012 -2014 Michael Remond
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package org.pac4j.saml.sso;
+
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.opensaml.common.SAMLObject;
+import org.opensaml.common.xml.SAMLConstants;
+import org.opensaml.saml2.core.Assertion;
+import org.opensaml.saml2.core.Audience;
+import org.opensaml.saml2.core.AudienceRestriction;
+import org.opensaml.saml2.core.AuthnStatement;
+import org.opensaml.saml2.core.Conditions;
+import org.opensaml.saml2.core.EncryptedAssertion;
+import org.opensaml.saml2.core.Issuer;
+import org.opensaml.saml2.core.NameID;
+import org.opensaml.saml2.core.NameIDType;
+import org.opensaml.saml2.core.Response;
+import org.opensaml.saml2.core.StatusCode;
+import org.opensaml.saml2.core.Subject;
+import org.opensaml.saml2.core.SubjectConfirmation;
+import org.opensaml.saml2.core.SubjectConfirmationData;
+import org.opensaml.saml2.encryption.Decrypter;
+import org.opensaml.saml2.metadata.IDPSSODescriptor;
+import org.opensaml.saml2.metadata.SPSSODescriptor;
+import org.opensaml.security.MetadataCriteria;
+import org.opensaml.security.SAMLSignatureProfileValidator;
+import org.opensaml.xml.encryption.DecryptionException;
+import org.opensaml.xml.security.CriteriaSet;
+import org.opensaml.xml.security.SecurityException;
+import org.opensaml.xml.security.credential.UsageType;
+import org.opensaml.xml.security.criteria.EntityIDCriteria;
+import org.opensaml.xml.security.criteria.UsageCriteria;
+import org.opensaml.xml.signature.Signature;
+import org.opensaml.xml.signature.SignatureTrustEngine;
+import org.opensaml.xml.validation.ValidationException;
+import org.pac4j.saml.context.ExtendedSAMLMessageContext;
+import org.pac4j.saml.exceptions.SamlException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class responsible for executing every required checks for validating a SAML response.
+ * The method validateSamlResponse populates the given {@link ExtendedSAMLMessageContext}
+ * with the correct SAML assertion and the corresponding nameID's Bearer subject if every checks succeeds.
+ *
+ * @author Michael Remond
+ * @since 1.5.0
+ *
+ */
+public class Saml2ResponseValidator {
+
+ private final static Logger logger = LoggerFactory.getLogger(Saml2ResponseValidator.class);
+
+ /* maximum skew in seconds between SP and IDP clocks */
+ private int acceptedSkew = 60;
+
+ /**
+ * Validates the SAML protocol response and the SAML SSO response.
+ * The method decrypt encrypted assertions if any.
+ *
+ * @param context
+ * @param engine
+ * @param decrypter
+ */
+ public void validateSamlResponse(final ExtendedSAMLMessageContext context, final SignatureTrustEngine engine,
+ final Decrypter decrypter) {
+
+ SAMLObject message = context.getInboundSAMLMessage();
+
+ if (!(message instanceof Response)) {
+ throw new SamlException("Response instance is an unsupported type");
+ }
+ Response response = (Response) message;
+
+ validateSamlProtocolResponse(response, context, engine);
+
+ decryptEncryptedAssertions(response, decrypter);
+
+ validateSamlSSOResponse(response, context, engine, decrypter);
+
+ }
+
+ /**
+ * Validates the SAML protocol response:
+ * - IssueInstant
+ * - Issuer
+ * - StatusCode
+ * - Signature
+ *
+ * @param response
+ * @param context
+ * @param engine
+ */
+ public void validateSamlProtocolResponse(final Response response, final ExtendedSAMLMessageContext context,
+ final SignatureTrustEngine engine) {
+
+ if (!isIssueInstantValid(response.getIssueInstant())) {
+ throw new SamlException("Response issue instant is too old or in the future");
+ }
+
+ // TODO add Destination and inResponseTo Validation
+
+ if (response.getIssuer() != null) {
+ validateIssuer(response.getIssuer(), context);
+ }
+
+ if (!StatusCode.SUCCESS_URI.equals(response.getStatus().getStatusCode().getValue())) {
+ String status = response.getStatus().getStatusCode().getValue();
+ if (response.getStatus().getStatusMessage() != null) {
+ status += " / " + response.getStatus().getStatusMessage().getMessage();
+ }
+ throw new SamlException("Authentication response is not success ; actual " + status);
+ }
+
+ if (response.getSignature() != null) {
+ validateSignature(response.getSignature(), context.getPeerEntityId(), engine);
+ context.setInboundSAMLMessageAuthenticated(true);
+ }
+
+ }
+
+ /**
+ * Validates the SAML SSO response by finding a valid assertion with authn statements.
+ * Populates the {@link ExtendedSAMLMessageContext} with a subjectAssertion and a subjectNameIdentifier.
+ *
+ * @param response
+ * @param context
+ * @param engine
+ * @param decrypter
+ */
+ public void validateSamlSSOResponse(final Response response, final ExtendedSAMLMessageContext context,
+ final SignatureTrustEngine engine, final Decrypter decrypter) {
+
+ for (Assertion assertion : response.getAssertions()) {
+ if (assertion.getAuthnStatements().size() > 0) {
+ try {
+ validateAssertion(assertion, context, engine, decrypter);
+ } catch (SamlException e) {
+ logger.error("Current assertion validation failed, continue with the next one", e);
+ continue;
+ }
+ context.setSubjectAssertion(assertion);
+ break;
+ }
+ }
+
+ if (context.getSubjectAssertion() == null) {
+ throw new SamlException("No valid subject assertion found in response");
+ }
+ if (context.getSubjectNameIdentifier() == null) {
+ throw new SamlException("Subject NameID cannot be null");
+ }
+ }
+
+ /**
+ * Decrypt encrypted assertions and add them to the assertions list of the response.
+ *
+ * @param response
+ * @param decrypter
+ */
+ protected void decryptEncryptedAssertions(final Response response, final Decrypter decrypter) {
+
+ for (EncryptedAssertion encryptedAssertion : response.getEncryptedAssertions()) {
+ try {
+ Assertion decryptedAssertion = decrypter.decrypt(encryptedAssertion);
+ response.getAssertions().add(decryptedAssertion);
+ } catch (DecryptionException e) {
+ logger.error("Decryption of assertion failed, continue with the next one", e);
+ }
+ }
+
+ }
+
+ /**
+ * Validate issuer format and value.
+ *
+ * @param issuer
+ * @param context
+ */
+ protected void validateIssuer(final Issuer issuer, final ExtendedSAMLMessageContext context) {
+ if (issuer.getFormat() != null && !issuer.getFormat().equals(NameIDType.ENTITY)) {
+ throw new SamlException("Issuer type is not entity but " + issuer.getFormat());
+ }
+ if (!context.getPeerEntityMetadata().getEntityID().equals(issuer.getValue())) {
+ throw new SamlException("Issuer " + issuer.getValue() + " does not match idp entityId "
+ + context.getPeerEntityMetadata().getEntityID());
+ }
+ }
+
+ /**
+ * Validate the given assertion:
+ * - issueInstant
+ * - issuer
+ * - subject
+ * - conditions
+ * - authnStatements
+ * - signature
+ *
+ * @param assertion
+ * @param context
+ * @param engine
+ * @param decrypter
+ */
+ protected void validateAssertion(final Assertion assertion, final ExtendedSAMLMessageContext context,
+ final SignatureTrustEngine engine, final Decrypter decrypter) {
+
+ if (!isIssueInstantValid(assertion.getIssueInstant())) {
+ throw new SamlException("Assertion issue instant is too old or in the future");
+ }
+
+ validateIssuer(assertion.getIssuer(), context);
+
+ if (assertion.getSubject() != null) {
+ validateSubject(assertion.getSubject(), context, decrypter);
+ } else {
+ throw new SamlException("Assertion subject cannot be null");
+ }
+
+ validateAssertionConditions(assertion.getConditions(), context);
+
+ validateAuthenticationStatements(assertion.getAuthnStatements(), context);
+
+ validateAssertionSignature(assertion.getSignature(), context, engine);
+
+ }
+
+ /**
+ * Validate the given subject by finding a valid Bearer confirmation. If the subject is valid,
+ * put its nameID in the context.
+ *
+ * @param subject
+ * @param context
+ * @param decrypter
+ */
+ @SuppressWarnings("unchecked")
+ protected void validateSubject(final Subject subject, final ExtendedSAMLMessageContext context,
+ final Decrypter decrypter) {
+
+ for (SubjectConfirmation confirmation : subject.getSubjectConfirmations()) {
+ if (SubjectConfirmation.METHOD_BEARER.equals(confirmation.getMethod())) {
+ if (isValidBearerSubjectConfirmationData(confirmation.getSubjectConfirmationData(), context)) {
+ NameID nameID = null;
+ if (subject.getEncryptedID() != null) {
+ try {
+ nameID = (NameID) decrypter.decrypt(subject.getEncryptedID());
+ } catch (DecryptionException e) {
+ throw new SamlException("Decryption of nameID's subject failed", e);
+ }
+ } else {
+ nameID = subject.getNameID();
+ }
+ context.setSubjectNameIdentifier(nameID);
+ return;
+ }
+ }
+ }
+
+ throw new SamlException("Subject confirmation validation failed");
+ }
+
+ /**
+ * Validate Bearer subject confirmation data
+ * - notBefore
+ * - NotOnOrAfter
+ * - recipient
+ *
+ * @param data
+ * @param context
+ * @return true if all Bearer subject checks are passing
+ */
+ protected boolean isValidBearerSubjectConfirmationData(final SubjectConfirmationData data,
+ final ExtendedSAMLMessageContext context) {
+ if (data == null) {
+ logger.debug("SubjectConfirmationData cannot be null for Bearer confirmation");
+ return false;
+ }
+
+ // TODO Validate inResponseTo
+
+ if (data.getNotBefore() != null) {
+ logger.debug("SubjectConfirmationData notBefore must be null for Bearer confirmation");
+ return false;
+ }
+
+ if (data.getNotOnOrAfter() == null) {
+ logger.debug("SubjectConfirmationData notOnOrAfter cannot be null for Bearer confirmation");
+ return false;
+ }
+
+ if (data.getNotOnOrAfter().plusSeconds(acceptedSkew).isBeforeNow()) {
+ logger.debug("SubjectConfirmationData notOnOrAfter is too old");
+ return false;
+ }
+
+ if (data.getRecipient() == null) {
+ logger.debug("SubjectConfirmationData recipient cannot be null for Bearer confirmation");
+ return false;
+ } else {
+ if (!data.getRecipient().equals(context.getAssertionConsumerUrl())) {
+ logger.debug("SubjectConfirmationData recipient {} does not match SP assertion consumer URL, found",
+ data.getRecipient());
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Validate assertionConditions
+ * - notBefore
+ * - notOnOrAfter
+ *
+ * @param conditions
+ * @param context
+ */
+ protected void validateAssertionConditions(final Conditions conditions, final ExtendedSAMLMessageContext context) {
+
+ if (conditions == null) {
+ throw new SamlException("Assertion conditions cannot be null");
+ }
+
+ if (conditions.getNotBefore() != null) {
+ if (conditions.getNotBefore().minusSeconds(acceptedSkew).isAfterNow()) {
+ throw new SamlException("Assertion condition notBefore is not valid");
+ }
+ }
+
+ if (conditions.getNotOnOrAfter() != null) {
+ if (conditions.getNotOnOrAfter().plusSeconds(acceptedSkew).isBeforeNow()) {
+ throw new SamlException("Assertion condition notOnOrAfter is not valid");
+ }
+ }
+
+ validateAudienceRestrictions(conditions.getAudienceRestrictions(), context.getLocalEntityId());
+
+ }
+
+ /**
+ * Validate audience by matching the SP entityId.
+ *
+ * @param audienceRestrictions
+ * @param spEntityId
+ */
+ protected void validateAudienceRestrictions(final List audienceRestrictions,
+ final String spEntityId) {
+
+ if (audienceRestrictions == null || audienceRestrictions.size() == 0) {
+ throw new SamlException("Audience restrictions cannot be null or empty");
+ }
+ if (!matchAudienceRestriction(audienceRestrictions, spEntityId)) {
+ throw new SamlException("Assertion audience does not match SP configuration");
+ }
+
+ }
+
+ /**
+ * Validate the given authnStatements:
+ * - authnInstant
+ * - sessionNotOnOrAfter
+ *
+ * @param authnStatements
+ * @param context
+ */
+ protected void validateAuthenticationStatements(final List authnStatements,
+ final ExtendedSAMLMessageContext context) {
+
+ for (AuthnStatement statement : authnStatements) {
+ if (!isIssueInstantValid(statement.getAuthnInstant())) {
+ throw new SamlException("Authentication issue instant is too old or in the future");
+ }
+ if (statement.getSessionNotOnOrAfter() != null && statement.getSessionNotOnOrAfter().isBeforeNow()) {
+ throw new SamlException("Authentication session between IDP and subject has ended");
+ }
+ // TODO implement authnContext validation
+ }
+ }
+
+ /**
+ * Validate assertion signature. If none is found and the SAML response did not have one and the SP requires
+ * the assertions to be signed, the validation fails.
+ *
+ * @param signature
+ * @param context
+ * @param engine
+ */
+ protected void validateAssertionSignature(final Signature signature, final ExtendedSAMLMessageContext context,
+ final SignatureTrustEngine engine) {
+ if (signature != null) {
+ validateSignature(signature, context.getPeerEntityMetadata().getEntityID(), engine);
+ } else if (((SPSSODescriptor) context.getLocalEntityRoleMetadata()).getWantAssertionsSigned()
+ && !context.isInboundSAMLMessageAuthenticated()) {
+ throw new SamlException("Assertion or response must be signed");
+ }
+ }
+
+ /**
+ * Validate the given digital signature by checking its profile and value.
+ *
+ * @param signature
+ * @param idpEntityId
+ * @param trustEngine
+ */
+ protected void validateSignature(final Signature signature, final String idpEntityId,
+ final SignatureTrustEngine trustEngine) {
+
+ SAMLSignatureProfileValidator validator = new SAMLSignatureProfileValidator();
+ try {
+ validator.validate(signature);
+ } catch (ValidationException e) {
+ throw new SamlException("SAMLSignatureProfileValidator failed to validate signature", e);
+ }
+
+ CriteriaSet criteriaSet = new CriteriaSet();
+ criteriaSet.add(new UsageCriteria(UsageType.SIGNING));
+ criteriaSet.add(new MetadataCriteria(IDPSSODescriptor.DEFAULT_ELEMENT_NAME, SAMLConstants.SAML20P_NS));
+ criteriaSet.add(new EntityIDCriteria(idpEntityId));
+
+ boolean valid = false;
+ try {
+ valid = trustEngine.validate(signature, criteriaSet);
+ } catch (SecurityException e) {
+ throw new SamlException("An error occured during signature validation", e);
+ }
+ if (!valid) {
+ throw new SamlException("Signature is not trusted");
+ }
+ }
+
+ private boolean matchAudienceRestriction(final List audienceRestrictions,
+ final String spEntityId) {
+ for (AudienceRestriction audienceRestriction : audienceRestrictions) {
+ if (audienceRestriction.getAudiences() != null) {
+ for (Audience audience : audienceRestriction.getAudiences()) {
+ if (spEntityId.equals(audience.getAudienceURI())) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean isIssueInstantValid(final DateTime issueInstant) {
+ long now = System.currentTimeMillis();
+ return issueInstant.isBefore(now + acceptedSkew * 1000) && issueInstant.isAfter(now - acceptedSkew * 1000);
+ }
+
+ public void setAcceptedSkew(final int acceptedSkew) {
+ this.acceptedSkew = acceptedSkew;
+ }
+
+}
diff --git a/pac4j-saml/src/main/java/org/pac4j/saml/sso/Saml2WebSSOProfileHandler.java b/pac4j-saml/src/main/java/org/pac4j/saml/sso/Saml2WebSSOProfileHandler.java
new file mode 100644
index 0000000000..e4371960df
--- /dev/null
+++ b/pac4j-saml/src/main/java/org/pac4j/saml/sso/Saml2WebSSOProfileHandler.java
@@ -0,0 +1,128 @@
+/*
+ Copyright 2012 -2014 Michael Remond
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package org.pac4j.saml.sso;
+
+import org.opensaml.common.binding.SAMLMessageContext;
+import org.opensaml.common.binding.security.SAMLProtocolMessageXMLSignatureSecurityPolicyRule;
+import org.opensaml.common.xml.SAMLConstants;
+import org.opensaml.saml2.binding.security.SAML2HTTPPostSimpleSignRule;
+import org.opensaml.saml2.core.AuthnRequest;
+import org.opensaml.saml2.metadata.IDPSSODescriptor;
+import org.opensaml.saml2.metadata.SPSSODescriptor;
+import org.opensaml.saml2.metadata.SingleSignOnService;
+import org.opensaml.ws.message.decoder.MessageDecoder;
+import org.opensaml.ws.message.decoder.MessageDecodingException;
+import org.opensaml.ws.message.encoder.MessageEncoder;
+import org.opensaml.ws.message.encoder.MessageEncodingException;
+import org.opensaml.ws.security.SecurityPolicy;
+import org.opensaml.ws.security.provider.BasicSecurityPolicy;
+import org.opensaml.ws.security.provider.StaticSecurityPolicyResolver;
+import org.opensaml.xml.parse.StaticBasicParserPool;
+import org.opensaml.xml.security.SecurityException;
+import org.opensaml.xml.signature.SignatureTrustEngine;
+import org.pac4j.saml.crypto.CredentialProvider;
+import org.pac4j.saml.exceptions.SamlException;
+import org.pac4j.saml.util.SamlUtils;
+
+/**
+ * Handler capable of sending and receiving SAML messages according to the SAML2 SSO Browser profile.
+ *
+ * @author Michael Remond
+ * @since 1.5.0
+ *
+ */
+@SuppressWarnings("rawtypes")
+public class Saml2WebSSOProfileHandler {
+
+ private final CredentialProvider credentialProvider;
+
+ private final MessageEncoder encoder;
+
+ private final MessageDecoder decoder;
+
+ private final StaticBasicParserPool parserPool;
+
+ // SAML2 SSO browser profile because not available in opensaml constants
+ public static final String SAML2_WEBSSO_PROFILE_URI = "urn:oasis:names:tc:SAML:2.0:profiles:SSO:browser";
+
+ public Saml2WebSSOProfileHandler(final CredentialProvider credentialProvider, final MessageEncoder encoder,
+ final MessageDecoder decoder, final StaticBasicParserPool parserPool) {
+ this.credentialProvider = credentialProvider;
+ this.encoder = encoder;
+ this.decoder = decoder;
+ this.parserPool = parserPool;
+ }
+
+ @SuppressWarnings("unchecked")
+ public void sendMessage(final SAMLMessageContext context, final AuthnRequest authnRequest, final String relayState) {
+
+ SPSSODescriptor spDescriptor = (SPSSODescriptor) context.getLocalEntityRoleMetadata();
+ IDPSSODescriptor idpssoDescriptor = (IDPSSODescriptor) context.getPeerEntityRoleMetadata();
+ SingleSignOnService ssoService = SamlUtils.getSingleSignOnService(idpssoDescriptor,
+ SAMLConstants.SAML2_POST_BINDING_URI);
+
+ context.setCommunicationProfileId(SAML2_WEBSSO_PROFILE_URI);
+ context.setOutboundMessage(authnRequest);
+ context.setOutboundSAMLMessage(authnRequest);
+ context.setPeerEntityEndpoint(ssoService);
+
+ if (relayState != null) {
+ context.setRelayState(relayState);
+ }
+
+ boolean sign = spDescriptor.isAuthnRequestsSigned() || idpssoDescriptor.getWantAuthnRequestsSigned();
+
+ if (sign) {
+ context.setOutboundSAMLMessageSigningCredential(credentialProvider.getCredential());
+ }
+
+ try {
+ encoder.encode(context);
+ } catch (MessageEncodingException e) {
+ throw new SamlException("Error encoding saml message", e);
+ }
+
+ }
+
+ public void receiveMessage(final SAMLMessageContext context, final SignatureTrustEngine engine) {
+
+ context.setPeerEntityRole(IDPSSODescriptor.DEFAULT_ELEMENT_NAME);
+ context.setInboundSAMLProtocol(SAMLConstants.SAML20P_NS);
+
+ SecurityPolicy policy = new BasicSecurityPolicy();
+ policy.getPolicyRules().add(new SAML2HTTPPostSimpleSignRule(engine, parserPool, engine.getKeyInfoResolver()));
+ policy.getPolicyRules().add(new SAMLProtocolMessageXMLSignatureSecurityPolicyRule(engine));
+ StaticSecurityPolicyResolver resolver = new StaticSecurityPolicyResolver(policy);
+ context.setSecurityPolicyResolver(resolver);
+
+ try {
+ decoder.decode(context);
+ } catch (MessageDecodingException e) {
+ throw new SamlException("Error decoding saml message", e);
+ } catch (SecurityException e) {
+ throw new SamlException("Error decoding saml message", e);
+ }
+
+ if (context.getPeerEntityMetadata() == null) {
+ throw new SamlException("IDP Metadata cannot be null");
+ }
+
+ context.setPeerEntityId(context.getPeerEntityMetadata().getEntityID());
+ context.setCommunicationProfileId(SAML2_WEBSSO_PROFILE_URI);
+ }
+
+}
diff --git a/pac4j-saml/src/main/java/org/pac4j/saml/transport/Pac4jHTTPPostDecoder.java b/pac4j-saml/src/main/java/org/pac4j/saml/transport/Pac4jHTTPPostDecoder.java
new file mode 100644
index 0000000000..f485a6f3cd
--- /dev/null
+++ b/pac4j-saml/src/main/java/org/pac4j/saml/transport/Pac4jHTTPPostDecoder.java
@@ -0,0 +1,53 @@
+/*
+ Copyright 2012 -2014 Michael Remond
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package org.pac4j.saml.transport;
+
+import org.opensaml.common.binding.SAMLMessageContext;
+import org.opensaml.saml2.binding.decoding.HTTPPostDecoder;
+import org.opensaml.ws.message.decoder.MessageDecodingException;
+import org.opensaml.ws.transport.InTransport;
+import org.opensaml.ws.transport.http.HttpServletRequestAdapter;
+import org.opensaml.xml.parse.StaticBasicParserPool;
+import org.pac4j.core.context.WebContext;
+
+/**
+ * Extends {@link HTTPPostDecoder} to override getActualReceiverEndpointURI() because we
+ * do not have an {@link HttpServletRequestAdapter} as input in pac4j.
+ *
+ * @author Michael Remond
+ * @since 1.5.0
+ *
+ */
+public class Pac4jHTTPPostDecoder extends HTTPPostDecoder {
+
+ public Pac4jHTTPPostDecoder(final StaticBasicParserPool parserPool) {
+ super(parserPool);
+ }
+
+ @Override
+ protected String getActualReceiverEndpointURI(final SAMLMessageContext messageContext)
+ throws MessageDecodingException {
+ InTransport inTransport = messageContext.getInboundMessageTransport();
+ if (!(inTransport instanceof SimpleRequestAdapter)) {
+ throw new MessageDecodingException("Message context InTransport instance was an unsupported type");
+ }
+ WebContext webContext = ((SimpleRequestAdapter) inTransport).getWebContext();
+
+ return webContext.getFullRequestURL();
+ }
+
+}
diff --git a/pac4j-saml/src/main/java/org/pac4j/saml/transport/SimpleRequestAdapter.java b/pac4j-saml/src/main/java/org/pac4j/saml/transport/SimpleRequestAdapter.java
new file mode 100644
index 0000000000..54b5cb16c6
--- /dev/null
+++ b/pac4j-saml/src/main/java/org/pac4j/saml/transport/SimpleRequestAdapter.java
@@ -0,0 +1,126 @@
+/*
+ Copyright 2012 -2014 Michael Remond
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package org.pac4j.saml.transport;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.List;
+
+import org.opensaml.ws.transport.http.HTTPInTransport;
+import org.opensaml.xml.security.credential.Credential;
+import org.pac4j.core.context.WebContext;
+
+/**
+ * Basic RequestAdapter returning an inputStream from the input content of
+ * the {@link WebContext}.
+ *
+ * @author Michael Remond
+ * @since 1.5.0
+ *
+ */
+public class SimpleRequestAdapter implements HTTPInTransport {
+
+ private final ByteArrayInputStream inputStream;
+
+ private final WebContext wc;
+
+ public WebContext getWebContext() {
+ return wc;
+ }
+
+ public SimpleRequestAdapter(final WebContext wc) {
+ this.wc = wc;
+ this.inputStream = new ByteArrayInputStream(wc.readRequestContent().getBytes());
+ }
+
+ public InputStream getIncomingStream() {
+ return inputStream;
+ }
+
+ public Object getAttribute(final String arg0) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public String getCharacterEncoding() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public Credential getLocalCredential() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public Credential getPeerCredential() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public boolean isAuthenticated() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public boolean isConfidential() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public boolean isIntegrityProtected() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public void setAuthenticated(final boolean arg0) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public void setConfidential(final boolean arg0) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public void setIntegrityProtected(final boolean arg0) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public String getHTTPMethod() {
+ return wc.getRequestMethod();
+ }
+
+ public String getHeaderValue(final String arg0) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public String getParameterValue(final String arg0) {
+ return wc.getRequestParameter(arg0);
+ }
+
+ public List getParameterValues(final String arg0) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public int getStatusCode() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public HTTP_VERSION getVersion() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public String getPeerAddress() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public String getPeerDomainName() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+}
diff --git a/pac4j-saml/src/main/java/org/pac4j/saml/transport/SimpleResponseAdapter.java b/pac4j-saml/src/main/java/org/pac4j/saml/transport/SimpleResponseAdapter.java
new file mode 100644
index 0000000000..d75888c9b6
--- /dev/null
+++ b/pac4j-saml/src/main/java/org/pac4j/saml/transport/SimpleResponseAdapter.java
@@ -0,0 +1,138 @@
+/*
+ Copyright 2012 -2014 Michael Remond
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package org.pac4j.saml.transport;
+
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+import org.opensaml.ws.transport.http.HTTPOutTransport;
+import org.opensaml.xml.security.credential.Credential;
+
+/**
+ * Empty response adapter containing a {@link ByteArrayOutputStream} in order opensaml can write
+ * the saml messages. The content can be retrieved as a String from getOutgoingContent().
+ *
+ * @author Michael Remond
+ * @since 1.5.0
+ *
+ */
+public class SimpleResponseAdapter implements HTTPOutTransport {
+
+ private final OutputStream outputStream = new ByteArrayOutputStream();
+
+ public String getOutgoingContent() {
+ return outputStream.toString();
+ }
+
+ public OutputStream getOutgoingStream() {
+ return outputStream;
+ }
+
+ public void setAttribute(final String arg0, final Object arg1) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public void setCharacterEncoding(final String arg0) {
+ // TODO implement
+ }
+
+ public Object getAttribute(final String arg0) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public String getCharacterEncoding() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public Credential getLocalCredential() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public Credential getPeerCredential() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public boolean isAuthenticated() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public boolean isConfidential() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public boolean isIntegrityProtected() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public void setAuthenticated(final boolean arg0) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public void setConfidential(final boolean arg0) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public void setIntegrityProtected(final boolean arg0) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public String getHTTPMethod() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public String getHeaderValue(final String arg0) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public String getParameterValue(final String arg0) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public List getParameterValues(final String arg0) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public int getStatusCode() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public HTTP_VERSION getVersion() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public void addParameter(final String arg0, final String arg1) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public void sendRedirect(final String arg0) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public void setHeader(final String arg0, final String arg1) {
+ // TODO implement
+ }
+
+ public void setStatusCode(final int arg0) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public void setVersion(final HTTP_VERSION arg0) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+}
diff --git a/pac4j-saml/src/main/java/org/pac4j/saml/util/SamlUtils.java b/pac4j-saml/src/main/java/org/pac4j/saml/util/SamlUtils.java
new file mode 100644
index 0000000000..6a355bbd52
--- /dev/null
+++ b/pac4j-saml/src/main/java/org/pac4j/saml/util/SamlUtils.java
@@ -0,0 +1,80 @@
+/*
+ Copyright 2012 -2014 Michael Remond
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package org.pac4j.saml.util;
+
+import java.util.List;
+
+import org.opensaml.saml2.metadata.AssertionConsumerService;
+import org.opensaml.saml2.metadata.IDPSSODescriptor;
+import org.opensaml.saml2.metadata.SPSSODescriptor;
+import org.opensaml.saml2.metadata.SingleSignOnService;
+import org.pac4j.saml.exceptions.SamlException;
+
+/**
+ * Utility class for SAML operations.
+ *
+ * @author Michael Remond
+ * @since 1.5.0
+ *
+ */
+public class SamlUtils {
+
+ public static SingleSignOnService getSingleSignOnService(final IDPSSODescriptor idpssoDescriptor,
+ final String binding) {
+
+ List services = idpssoDescriptor.getSingleSignOnServices();
+ for (SingleSignOnService service : services) {
+ if (service.getBinding().equals(binding)) {
+ return service;
+ }
+ }
+ throw new SamlException("Identity provider has no single sign on service available for the selected profile"
+ + idpssoDescriptor);
+
+ }
+
+ public static AssertionConsumerService getAssertionConsumerService(final SPSSODescriptor spDescriptor,
+ final Integer acsIndex) {
+
+ List services = spDescriptor.getAssertionConsumerServices();
+
+ // Get by index
+ if (acsIndex != null) {
+ for (AssertionConsumerService service : services) {
+ if (acsIndex.equals(service.getIndex())) {
+ return service;
+ }
+ }
+ throw new SamlException("Assertion consumer service with index " + acsIndex
+ + " could not be found for spDescriptor " + spDescriptor);
+ }
+
+ // Get default
+ if (spDescriptor.getDefaultAssertionConsumerService() != null) {
+ return spDescriptor.getDefaultAssertionConsumerService();
+ }
+
+ // Get first
+ if (services.size() > 0) {
+ return services.iterator().next();
+ }
+
+ throw new SamlException("No assertion consumer services could be found for " + spDescriptor);
+
+ }
+
+}
diff --git a/pac4j-saml/src/main/java/org/pac4j/saml/util/VelocityEngineFactory.java b/pac4j-saml/src/main/java/org/pac4j/saml/util/VelocityEngineFactory.java
new file mode 100644
index 0000000000..95627ce6d0
--- /dev/null
+++ b/pac4j-saml/src/main/java/org/pac4j/saml/util/VelocityEngineFactory.java
@@ -0,0 +1,51 @@
+/*
+ Copyright 2012 -2014 Michael Remond
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package org.pac4j.saml.util;
+
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.log.JdkLogChute;
+import org.pac4j.core.exception.TechnicalException;
+
+/**
+ * Factory returning a well configured {@link VelocityEngine} instance required for
+ * generating an HTML form used to POST SAML messages.
+ *
+ * @author Michael Remond
+ *
+ */
+public class VelocityEngineFactory {
+
+ public static VelocityEngine getEngine() {
+
+ try {
+ VelocityEngine velocityEngine = new VelocityEngine();
+ velocityEngine.setProperty(RuntimeConstants.ENCODING_DEFAULT, "UTF-8");
+ velocityEngine.setProperty(RuntimeConstants.OUTPUT_ENCODING, "UTF-8");
+ velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
+ velocityEngine.setProperty("classpath.resource.loader.class",
+ "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
+ velocityEngine.setProperty(VelocityEngine.RUNTIME_LOG_LOGSYSTEM, new JdkLogChute());
+ velocityEngine.init();
+ return velocityEngine;
+ } catch (Exception e) {
+ throw new TechnicalException("Error configuring velocity", e);
+ }
+
+ }
+
+}
diff --git a/pac4j-saml/src/test/java/org/pac4j/saml/client/TestSaml2Client.java b/pac4j-saml/src/test/java/org/pac4j/saml/client/TestSaml2Client.java
new file mode 100644
index 0000000000..c547b61bdd
--- /dev/null
+++ b/pac4j-saml/src/test/java/org/pac4j/saml/client/TestSaml2Client.java
@@ -0,0 +1,42 @@
+package org.pac4j.saml.client;
+
+import junit.framework.TestCase;
+
+import org.pac4j.core.client.RedirectAction;
+import org.pac4j.core.context.HttpConstants;
+import org.pac4j.core.context.MockWebContext;
+import org.pac4j.core.exception.RequiresHttpAction;
+import org.pac4j.core.util.TestsConstants;
+import org.pac4j.saml.client.Saml2Client;
+
+public final class TestSaml2Client extends TestCase implements TestsConstants {
+
+ public void testInit() {
+ final Saml2Client saml2Client = new Saml2Client();
+
+ MockWebContext wc = MockWebContext.create();
+
+
+ saml2Client.setKeystorePath(this.getClass().getResource("samlKeystore.jks").getFile());
+ saml2Client.setKeystorePassword("pac4j-demo-passwd");
+ saml2Client.setPrivateKeyPassword("pac4j-demo-passwd");
+ saml2Client.setIdpMetadataPath(this.getClass().getResource("testshib-providers.xml").getFile());
+ saml2Client.setCallbackUrl("http://localhost:8080/callback");
+
+ saml2Client.init();
+
+ // TODO make some assert on SP metadata
+ String spMetadata = saml2Client.printClientMetadata();
+
+ try {
+ saml2Client.redirect(wc, false, false);
+ } catch (RequiresHttpAction e) {
+ fail();
+ }
+ assertEquals(HttpConstants.OK, wc.getResponseStatus());
+ String content = wc.getResponseContent();
+ // TODO make some assert on Authn Request
+
+ }
+
+}
diff --git a/pac4j-saml/src/test/resources/org/pac4j/saml/client/samlKeystore.jks b/pac4j-saml/src/test/resources/org/pac4j/saml/client/samlKeystore.jks
new file mode 100644
index 0000000000..5b7c04a19b
Binary files /dev/null and b/pac4j-saml/src/test/resources/org/pac4j/saml/client/samlKeystore.jks differ
diff --git a/pac4j-saml/src/test/resources/org/pac4j/saml/client/testshib-providers.xml b/pac4j-saml/src/test/resources/org/pac4j/saml/client/testshib-providers.xml
new file mode 100644
index 0000000000..840d0f9294
--- /dev/null
+++ b/pac4j-saml/src/test/resources/org/pac4j/saml/client/testshib-providers.xml
@@ -0,0 +1,310 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ testshib.org
+
+ TestShib Test IdP
+ TestShib IdP. Use this
+ as a source of attributes
+ for your test SP.
+ https://www.testshib.org/testshibtwo.jpg
+
+
+
+
+
+
+
+ MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzEV
+ MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMREwDwYD
+ VQQKEwhUZXN0U2hpYjEZMBcGA1UEAxMQaWRwLnRlc3RzaGliLm9yZzAeFw0wNjA4
+ MzAyMTEyMjVaFw0xNjA4MjcyMTEyMjVaMGcxCzAJBgNVBAYTAlVTMRUwEwYDVQQI
+ EwxQZW5uc3lsdmFuaWExEzARBgNVBAcTClBpdHRzYnVyZ2gxETAPBgNVBAoTCFRl
+ c3RTaGliMRkwFwYDVQQDExBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0B
+ AQEFAAOCAQ8AMIIBCgKCAQEArYkCGuTmJp9eAOSGHwRJo1SNatB5ZOKqDM9ysg7C
+ yVTDClcpu93gSP10nH4gkCZOlnESNgttg0r+MqL8tfJC6ybddEFB3YBo8PZajKSe
+ 3OQ01Ow3yT4I+Wdg1tsTpSge9gEz7SrC07EkYmHuPtd71CHiUaCWDv+xVfUQX0aT
+ NPFmDixzUjoYzbGDrtAyCqA8f9CN2txIfJnpHE6q6CmKcoLADS4UrNPlhHSzd614
+ kR/JYiks0K4kbRqCQF0Dv0P5Di+rEfefC6glV8ysC8dB5/9nb0yh/ojRuJGmgMWH
+ gWk6h0ihjihqiu4jACovUZ7vVOCgSE5Ipn7OIwqd93zp2wIDAQABo4HEMIHBMB0G
+ A1UdDgQWBBSsBQ869nh83KqZr5jArr4/7b+QazCBkQYDVR0jBIGJMIGGgBSsBQ86
+ 9nh83KqZr5jArr4/7b+Qa6FrpGkwZzELMAkGA1UEBhMCVVMxFTATBgNVBAgTDFBl
+ bm5zeWx2YW5pYTETMBEGA1UEBxMKUGl0dHNidXJnaDERMA8GA1UEChMIVGVzdFNo
+ aWIxGTAXBgNVBAMTEGlkcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN
+ BgkqhkiG9w0BAQUFAAOCAQEAjR29PhrCbk8qLN5MFfSVk98t3CT9jHZoYxd8QMRL
+ I4j7iYQxXiGJTT1FXs1nd4Rha9un+LqTfeMMYqISdDDI6tv8iNpkOAvZZUosVkUo
+ 93pv1T0RPz35hcHHYq2yee59HJOco2bFlcsH8JBXRSRrJ3Q7Eut+z9uo80JdGNJ4
+ /SJy5UorZ8KazGj16lfJhOBXldgrhppQBb0Nq6HKHguqmwRfJ+WkxemZXzhediAj
+ Geka8nz8JjwxpUjAiSWYKLtJhGEaTqCYxCCX2Dw+dOTqUzHOZ7WKv4JXPK5G/Uhr
+ 8K/qhmFT2nIQi538n6rVYLeWj8Bbnl+ev0peYzxFyF5sQA==
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ urn:mace:shibboleth:1.0:nameIdentifier
+ urn:oasis:names:tc:SAML:2.0:nameid-format:transient
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzEV
+ MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMREwDwYD
+ VQQKEwhUZXN0U2hpYjEZMBcGA1UEAxMQaWRwLnRlc3RzaGliLm9yZzAeFw0wNjA4
+ MzAyMTEyMjVaFw0xNjA4MjcyMTEyMjVaMGcxCzAJBgNVBAYTAlVTMRUwEwYDVQQI
+ EwxQZW5uc3lsdmFuaWExEzARBgNVBAcTClBpdHRzYnVyZ2gxETAPBgNVBAoTCFRl
+ c3RTaGliMRkwFwYDVQQDExBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0B
+ AQEFAAOCAQ8AMIIBCgKCAQEArYkCGuTmJp9eAOSGHwRJo1SNatB5ZOKqDM9ysg7C
+ yVTDClcpu93gSP10nH4gkCZOlnESNgttg0r+MqL8tfJC6ybddEFB3YBo8PZajKSe
+ 3OQ01Ow3yT4I+Wdg1tsTpSge9gEz7SrC07EkYmHuPtd71CHiUaCWDv+xVfUQX0aT
+ NPFmDixzUjoYzbGDrtAyCqA8f9CN2txIfJnpHE6q6CmKcoLADS4UrNPlhHSzd614
+ kR/JYiks0K4kbRqCQF0Dv0P5Di+rEfefC6glV8ysC8dB5/9nb0yh/ojRuJGmgMWH
+ gWk6h0ihjihqiu4jACovUZ7vVOCgSE5Ipn7OIwqd93zp2wIDAQABo4HEMIHBMB0G
+ A1UdDgQWBBSsBQ869nh83KqZr5jArr4/7b+QazCBkQYDVR0jBIGJMIGGgBSsBQ86
+ 9nh83KqZr5jArr4/7b+Qa6FrpGkwZzELMAkGA1UEBhMCVVMxFTATBgNVBAgTDFBl
+ bm5zeWx2YW5pYTETMBEGA1UEBxMKUGl0dHNidXJnaDERMA8GA1UEChMIVGVzdFNo
+ aWIxGTAXBgNVBAMTEGlkcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN
+ BgkqhkiG9w0BAQUFAAOCAQEAjR29PhrCbk8qLN5MFfSVk98t3CT9jHZoYxd8QMRL
+ I4j7iYQxXiGJTT1FXs1nd4Rha9un+LqTfeMMYqISdDDI6tv8iNpkOAvZZUosVkUo
+ 93pv1T0RPz35hcHHYq2yee59HJOco2bFlcsH8JBXRSRrJ3Q7Eut+z9uo80JdGNJ4
+ /SJy5UorZ8KazGj16lfJhOBXldgrhppQBb0Nq6HKHguqmwRfJ+WkxemZXzhediAj
+ Geka8nz8JjwxpUjAiSWYKLtJhGEaTqCYxCCX2Dw+dOTqUzHOZ7WKv4JXPK5G/Uhr
+ 8K/qhmFT2nIQi538n6rVYLeWj8Bbnl+ev0peYzxFyF5sQA==
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ urn:mace:shibboleth:1.0:nameIdentifier
+ urn:oasis:names:tc:SAML:2.0:nameid-format:transient
+
+
+
+
+ TestShib Two Identity
+ Provider
+ TestShib
+ Two
+ http://www.testshib.org/testshib-two/
+
+
+ Nate
+ Klingenstein
+ ndk@internet2.edu
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TestShib Test SP
+ TestShib SP. Log into
+ this to test your machine.
+ Once logged in check that all attributes that you expected have been
+ released.
+ https://www.testshib.org/testshibtwo.jpg
+
+
+
+
+
+
+
+ MIIEPjCCAyagAwIBAgIBADANBgkqhkiG9w0BAQUFADB3MQswCQYDVQQGEwJVUzEV
+ MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMSIwIAYD
+ VQQKExlUZXN0U2hpYiBTZXJ2aWNlIFByb3ZpZGVyMRgwFgYDVQQDEw9zcC50ZXN0
+ c2hpYi5vcmcwHhcNMDYwODMwMjEyNDM5WhcNMTYwODI3MjEyNDM5WjB3MQswCQYD
+ VQQGEwJVUzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1
+ cmdoMSIwIAYDVQQKExlUZXN0U2hpYiBTZXJ2aWNlIFByb3ZpZGVyMRgwFgYDVQQD
+ Ew9zcC50ZXN0c2hpYi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+ AQDJyR6ZP6MXkQ9z6RRziT0AuCabDd3x1m7nLO9ZRPbr0v1LsU+nnC363jO8nGEq
+ sqkgiZ/bSsO5lvjEt4ehff57ERio2Qk9cYw8XCgmYccVXKH9M+QVO1MQwErNobWb
+ AjiVkuhWcwLWQwTDBowfKXI87SA7KR7sFUymNx5z1aoRvk3GM++tiPY6u4shy8c7
+ vpWbVfisfTfvef/y+galxjPUQYHmegu7vCbjYP3On0V7/Ivzr+r2aPhp8egxt00Q
+ XpilNai12LBYV3Nv/lMsUzBeB7+CdXRVjZOHGuQ8mGqEbsj8MBXvcxIKbcpeK5Zi
+ JCVXPfarzuriM1G5y5QkKW+LAgMBAAGjgdQwgdEwHQYDVR0OBBYEFKB6wPDxwYrY
+ StNjU5P4b4AjBVQVMIGhBgNVHSMEgZkwgZaAFKB6wPDxwYrYStNjU5P4b4AjBVQV
+ oXukeTB3MQswCQYDVQQGEwJVUzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYD
+ VQQHEwpQaXR0c2J1cmdoMSIwIAYDVQQKExlUZXN0U2hpYiBTZXJ2aWNlIFByb3Zp
+ ZGVyMRgwFgYDVQQDEw9zcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN
+ BgkqhkiG9w0BAQUFAAOCAQEAc06Kgt7ZP6g2TIZgMbFxg6vKwvDL0+2dzF11Onpl
+ 5sbtkPaNIcj24lQ4vajCrrGKdzHXo9m54BzrdRJ7xDYtw0dbu37l1IZVmiZr12eE
+ Iay/5YMU+aWP1z70h867ZQ7/7Y4HW345rdiS6EW663oH732wSYNt9kr7/0Uer3KD
+ 9CuPuOidBacospDaFyfsaJruE99Kd6Eu/w5KLAGG+m0iqENCziDGzVA47TngKz2v
+ PVA+aokoOyoz3b53qeti77ijatSEoKjxheBWpO+eoJeGq/e49Um3M2ogIX/JAlMa
+ Inh+vYSYngQB2sx9LGkR9KHaMKNIGCDehk93Xla4pWJx1w==
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ urn:oasis:names:tc:SAML:2.0:nameid-format:transient
+ urn:mace:shibboleth:1.0:nameIdentifier
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TestShib Two Service
+ Provider
+ TestShib
+ Two
+ http://www.testshib.org/testshib-two/
+
+
+ Nate
+ Klingenstein
+ ndk@internet2.edu
+
+
+
+
+
+
+
diff --git a/pom.xml b/pom.xml
index 258174d8b9..8be3cb01bf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -60,6 +60,7 @@
pac4j-openid
pac4j-test-cas
pac4j-http
+ pac4j-saml