Problem
You would like to enable Container-Managed Authentication in a Java EE web application fronted by a Shibboleth Native SP, in order to, for instance, leverage the platform's authorization capabilities, such as the EJB security model, from within your application's code. Your Application Server (AS) is, however, unaware of authentication performed by Shibboleth, effectively treating your users as anonymous. Thus, such API calls do not typically work right out of the box. What needs to be done is to somehow propagate the user identity at the SP to the AS, or in simple terms, a "caller" Principal
representing the authenticated user needs to be established at there.
Solution
In order to communicate the fact that a user should be considered authenticated to your AS, you may either implement a container-specific JAAS LoginModule
, or what is loosely termed its standard, Java EE counterpart, a JASPIC ServerAuthModule
(SAM) . This how-to employs the standardized over the proprietary approach.
Implement the SAM
The following example is a simplistic "bridge" SAM, which, for every --which is obviously not ideal-- incoming request:
- Probes the received
HttpServletRequest
for presence of the "eduPersonPrincipalName" and "eduPersonAffiliation" attributes. - If both are present, registers a "caller"
Principal
and one or multiple "group" Principal
s --enclosing the values of "eduPersonPrincipalName" and "eduPersonAffiliation" as their respective names-- at the AS to represent the authenticated user. - Otherwise:
- If the targeted request is "public", it treats the user as a guest.
- Otherwise, it redirects the request to the SP's
SessionInitiator
.
Step 3.b. also occurs whenever the user explicitly asks to be authenticated, e.g. by clicking a "Login" button in the UI. Similarly, when the user wishes to be logged out, the SAM redirects her request to the SP's LogoutInitiator
.
package org.my;
import java.io.IOException;
import java.security.Principal;
import java.util.Map;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.message.AuthException;
import javax.security.auth.message.AuthStatus;
import javax.security.auth.message.MessageInfo;
import javax.security.auth.message.MessagePolicy;
import javax.security.auth.message.callback.CallerPrincipalCallback;
import javax.security.auth.message.callback.GroupPrincipalCallback;
import javax.security.auth.message.module.ServerAuthModule;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class SpAsBridgeSam implements ServerAuthModule {
static class MyOrgPrincipal implements Principal {
public MyOrgPrincipal(String name) {
this.name = name;
}
private final String name;
@Override
public String getName() {
return name;
}
}
private static final Class<?>[] SUPPORTED_MESSAGE_TYPES = new Class<?>[] { HttpServletRequest.class, HttpServletResponse.class };
private static final String LOGIN_URL = "http://my.org/Shibboleth.sso/Login";
private static final String LOGOUT_URL = "http://my.org/Shibboleth.sso/Logout";
private boolean isProtectedResource;
private CallbackHandler ch;
// Post-construct initialization goes here
@Override
public void initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy, CallbackHandler ch, Map options) throws AuthException {
this.ch = ch;
}
// Authentication logic and optional request pre-processing goes here
@Override
public AuthStatus validateRequest(MessageInfo mi, Subject client, Subject service) throws AuthException {
HttpServletRequest hreq = (HttpServletRequest) mi.getRequestMessage();
HttpServletResponse hres = (HttpServletResponse) mi.getResponseMessage();
String username = getUsername(hreq);
String[] groups = getUsergroups(hreq);
if ((username != null) && (!username.trim().isEmpty()) && (groups != null)) {
try {
ch.handle(new Callback[] {
new CallerPrincipalCallback(client, new MyOrgPrincipal(username)),
new GroupPrincipalCallback(client, groups) });
}
catch (UnsupportedCallbackException | IOException e) {
e.printStackTrace();
throw new AuthException("Could not authenticate user.");
}
return AuthStatus.SUCCESS;
}
else if (!isProtectedResource) {
return AuthStatus.SUCCESS;
}
else {
try {
hres.sendRedirect(LOGIN_URL);
}
catch (IOException ioe) {
ioe.printStackTrace();
throw new AuthException("Could not authenticate user.");
}
return AuthStatus.SEND_CONTINUE;
}
}
// Validation of response and optional post-processing goes here.
// Not typically used, as --unless it had initially been wrapped-- the response is committed at this point.
@Override
public AuthStatus secureResponse(MessageInfo messageInfo, Subject service) throws AuthException {
return AuthStatus.SEND_SUCCESS;
}
@Override
public void cleanSubject(MessageInfo mi, Subject client) throws AuthException {
HttpServletRequest hreq = (HttpServletRequest) mi.getRequestMessage();
HttpServletResponse hres = (HttpServletResponse) mi.getResponseMessage();
HttpSession hs = hreq.getSession(false);
if (hs != null) {
try {
hs.invalidate();
}
catch (IllegalStateException ise) {
ise.printStackTrace();
throw new AuthException("Could not invalidate user session.");
}
}
try {
hreq.logout();
hres.sendRedirect(LOGOUT_URL);
}
catch (ServletException | IOException e) {
e.printStackTrace();
throw new AuthException("Could not complete user logout.");
}
}
@Override
public Class<?>[] getSupportedMessageTypes() {
return SUPPORTED_MESSAGE_TYPES;
}
private String getUsername(HttpServletRequest hreq) {
return (String) hreq.getAttribute("eduPersonPrincipalName");
}
private String[] getUsergroups(HttpServletRequest hreq) {
String groupsAttribute = (String) hreq.getAttribute("eduPersonAffiliation");
String[] groups = null;
if (groupsAttribute != null) {
groups = groupsAttribute.split(";");
for (int i = 0; i < groups.length; i++) {
groups[i] = groups[i].trim();
}
}
return groups;
}
}
Step-by-step guide
1 Java EE mostly disregards javax.security.auth.Subject
s; all what's left of them, that is, what most Java EE specifications tend to use, is this one "caller" Principal
, along with potential "group" Principal
s. A blog entry by Arjan Tijms titled "JAAS in Java EE is not the universal standard you may think it is" examines some common misconceptions surrounding JAAS in this scope.
2 JSR 196, Authentication Service Provider Interface for Containers (JASPIC, JASPI) is available in the Full Java EE 6+ Profile, and Servlet 3+ Containers included in compatible implementations, have been mandated to support JASPIC's Servlet Profile.