Skip to content

Commit

Permalink
Add SAML support
Browse files Browse the repository at this point in the history
  • Loading branch information
miremond committed Mar 29, 2014
1 parent 2b7658b commit c5306af
Show file tree
Hide file tree
Showing 46 changed files with 3,269 additions and 199 deletions.
53 changes: 46 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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()...)
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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:

<dependency>
Expand Down Expand Up @@ -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)...

Expand All @@ -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...

Expand Down Expand Up @@ -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...

Expand All @@ -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...

Expand All @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions pac4j-cas/src/main/java/org/pac4j/cas/client/CasClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
}

Expand Down
14 changes: 10 additions & 4 deletions pac4j-cas/src/test/java/org/pac4j/cas/client/TestCasClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,21 +72,27 @@ 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 {
final CasClient casClient = new CasClient();
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);
}
Expand Down
6 changes: 6 additions & 0 deletions pac4j-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@
<artifactId>kryo</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>

<!-- for testing -->
<dependency>
<groupId>junit</groupId>
Expand Down
35 changes: 27 additions & 8 deletions pac4j-core/src/main/java/org/pac4j/core/client/BaseClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,10 +36,10 @@
* <li>the callback url is handled through the {@link #setCallbackUrl(String)} and {@link #getCallbackUrl()} methods</li>
* <li>the name of the client is handled through the {@link #setName(String)} and {@link #getName()} methods</li>
* <li>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 <b>later</b> in the {@link #getCredentials(WebContext)} method.<br />
* To force a direct redirection, the {@link #getRedirectionUrl(WebContext, boolean, boolean)} must be used with <code>true</code> for the
* To force a direct redirection, the {@link #getRedirection(WebContext, boolean, boolean)} must be used with <code>true</code> for the
* <code>forceDirectRedirection</code> parameter</li>
* <li>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.</li>
Expand Down Expand Up @@ -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)
Expand All @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions pac4j-core/src/main/java/org/pac4j/core/client/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
* A client has a type accessible by the {@link #getName()} method.<br />
* A client supports the authentication process and user profile retrieval through :<br />
* <ul>
* <li>the {@link #getRedirectionUrl(WebContext,boolean,boolean)} method to get the url where to redirect the user for authentication (at
* <li>the {@link #getRedirection(WebContext,boolean,boolean)} method to get the url where to redirect the user for authentication (at
* the provider)</li>
* <li>the {@link #getCredentials(WebContext)} method to get the credentials (in the application) after the user has been successfully
* authenticated at the provider</li>
Expand All @@ -46,7 +46,7 @@ public interface Client<C extends Credentials, U extends UserProfile> {
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.
* <p />
* 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 <code>protectedTarget</code> parameter set to <code>true</code> enforces
Expand All @@ -64,14 +64,14 @@ public interface Client<C extends Credentials, U extends UserProfile> {
* @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:<br />
* <ul>
* <li>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)</li>
* {@link #getRedirection(WebContext, boolean, boolean)} one (302 HTTP status code)</li>
* <li>if the <code>CasClient</code> receives a logout request, it returns a 200 HTTP status code</li>
* <li>for the <code>BasicAuthClient</code>, if no credentials are sent to the callback url, an unauthorized response (401 HTTP status
* code) is returned to request credentials through a popup.</li>
Expand Down
72 changes: 72 additions & 0 deletions pac4j-core/src/main/java/org/pac4j/core/client/RedirectAction.java
Original file line number Diff line number Diff line change
@@ -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 :
* <ul>
* <li>REDIRECT (HTTP 302)</li>
* <li>SUCCESS (HTTP 200)</li>
* </ul>
*
* @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;
}

}
Loading

0 comments on commit c5306af

Please sign in to comment.