blob: 5a48954daa2ea1a314efe5e912296d95e4666caa [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.geronimo.components.jaspi.modules.openid;
import java.io.IOException;
import java.net.URL;
import java.util.List;
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.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.openid4java.consumer.ConsumerException;
import org.openid4java.consumer.ConsumerManager;
import org.openid4java.consumer.InMemoryConsumerAssociationStore;
import org.openid4java.consumer.InMemoryNonceVerifier;
import org.openid4java.consumer.VerificationResult;
import org.openid4java.discovery.DiscoveryException;
import org.openid4java.discovery.DiscoveryInformation;
import org.openid4java.discovery.Identifier;
import org.openid4java.message.AuthRequest;
import org.openid4java.message.MessageException;
import org.openid4java.message.ParameterList;
/**
* @version $Rev$ $Date$
*/
public class OpenIDServerAuthModule implements ServerAuthModule {
private static final Class[] SUPPORTED_MESSAGE_TYPES = new Class[]{HttpServletRequest.class, HttpServletResponse.class};
public static final String MANDATORY_KEY = "javax.security.auth.message.MessagePolicy.isMandatory";
public static final String AUTH_METHOD_KEY = "javax.servlet.http.authType";
private static final String OPENID_IDENTIFIER = "openid_identifier";
private static final String DISCOVERY_SESSION_KEY = "openid-disc";
private static final String RETURN_ADDRESS = "/_openid_security_check";
private static final String ORIGINAL_URI_KEY = "org.apache.geronimo.components.jaspi.openid.URI";
private static final String RETURN_ADDRESS_KEY = "org.apache.geronimo.components.jaspi.openid.return.address";
public static final String LOGIN_PAGE_KEY = "org.apache.geronimo.security.jaspi.openid.LoginPage";
public static final String ERROR_PAGE_KEY = "org.apache.geronimo.security.jaspi.openid.ErrorPage";
private String errorPage;
private String errorPath;
private String loginPage;
private String loginPath;
private CallbackHandler callbackHandler;
private ConsumerManager consumerManager;
private static final String ID_KEY = "org.apache.geronimo.components.jaspi.openid.ID";
public Class[] getSupportedMessageTypes() {
return SUPPORTED_MESSAGE_TYPES;
}
public void initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy, CallbackHandler handler, Map options) throws AuthException {
if (options == null) {
throw new AuthException("No options supplied");
}
this.callbackHandler = handler;
try {
consumerManager = new ConsumerManager();
} catch (ConsumerException e) {
throw (AuthException) new AuthException("Unable to create ConsumerManager").initCause(e);
}
consumerManager.setAssociations(new InMemoryConsumerAssociationStore());
consumerManager.setNonceVerifier(new InMemoryNonceVerifier(5000));
//??
consumerManager.getRealmVerifier().setEnforceRpId(false);
setLoginPage((String) options.get(LOGIN_PAGE_KEY));
setErrorPage((String) options.get(ERROR_PAGE_KEY));
}
private void setLoginPage(String path) throws AuthException {
if (path == null) {
throw new AuthException("No login page specified with key " + LOGIN_PAGE_KEY);
}
if (!path.startsWith("/")) {
path = "/" + path;
}
loginPage = path;
loginPath = path;
if (loginPath.indexOf('?') > 0) {
loginPath = loginPath.substring(0, loginPath.indexOf('?'));
}
}
private void setErrorPage(String path) throws AuthException {
if (path == null) {
throw new AuthException("No error page specified with key " + ERROR_PAGE_KEY);
}
if (path == null || path.trim().length() == 0) {
errorPath = null;
errorPage = null;
} else {
if (!path.startsWith("/")) {
path = "/" + path;
}
errorPage = path;
errorPath = path;
if (errorPath.indexOf('?') > 0) {
errorPath = errorPath.substring(0, errorPath.indexOf('?'));
}
}
}
public void cleanSubject(MessageInfo messageInfo, Subject subject) throws AuthException {
}
public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject) throws AuthException {
HttpServletRequest request = (HttpServletRequest) messageInfo.getRequestMessage();
HttpServletResponse response = (HttpServletResponse) messageInfo.getResponseMessage();
boolean isMandatory = isMandatory(messageInfo);
HttpSession session = request.getSession(isMandatory);
String uri = request.getRequestURI();
if (session == null || isLoginOrErrorPage(uri)) {
//auth not mandatory and not logged in.
return AuthStatus.SUCCESS;
}
//are we returning from the OP redirect?
if (uri.endsWith(RETURN_ADDRESS)) {
ParameterList parameterList = new ParameterList(request.getParameterMap());
DiscoveryInformation discovered = (DiscoveryInformation) session.getAttribute(DISCOVERY_SESSION_KEY);
String returnAddress = (String) session.getAttribute(RETURN_ADDRESS_KEY);
session.removeAttribute(RETURN_ADDRESS_KEY);
try {
VerificationResult verification = consumerManager.verify(returnAddress, parameterList, discovered);
Identifier identifier = verification.getVerifiedId();
if (identifier != null) {
session.setAttribute(ID_KEY, identifier);
//redirect back to original page
response.setContentLength(0);
String originalURI = (String) session.getAttribute(ORIGINAL_URI_KEY);
session.removeAttribute(ORIGINAL_URI_KEY);
if (originalURI == null || originalURI.length() == 0) {
originalURI = request.getContextPath();
if (originalURI.length() == 0) {
originalURI = "/";
}
}
response.sendRedirect(response.encodeRedirectURL(originalURI));
return AuthStatus.SEND_CONTINUE;
}
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Response verification failed: " + verification.getStatusMsg());
// } catch (MessageException e) {
//
// } catch (DiscoveryException e) {
//
// } catch (AssociationException e) {
//
// } catch (IOException e) {
} catch (Exception e) {
try {
//TODO redirect to error page or just send error
response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());
} catch (IOException e1) {
}
}
return AuthStatus.SEND_FAILURE;
}
//are we already logged in, and not expired?
Identifier identifier = (Identifier) session.getAttribute(ID_KEY);
if (identifier != null) {
//TODO set up subject and callback handler.
final IdentifierPrincipal principal = new IdentifierPrincipal(identifier.getIdentifier());
clientSubject.getPrincipals().add(principal);
clientSubject.getPrincipals().add(new AuthenticatedPrincipal());
DiscoveryInformation discovered = (DiscoveryInformation) session.getAttribute(DISCOVERY_SESSION_KEY);
URL opEndpoint = discovered.getOPEndpoint();
clientSubject.getPrincipals().add(new OpenIDProviderPrincipal(opEndpoint.toString()));
CallerPrincipalCallback cpCallback = new CallerPrincipalCallback(clientSubject, principal);
GroupPrincipalCallback gpCallback = new GroupPrincipalCallback(clientSubject, new String[]{"authenticated"});
try {
callbackHandler.handle(new Callback[]{cpCallback, gpCallback});
} catch (IOException e) {
} catch (UnsupportedCallbackException e) {
}
return AuthStatus.SUCCESS;
}
//if request is not mandatory, we don't authenticate.
if (!isMandatory) {
return AuthStatus.SUCCESS;
}
//assume not...
String openidIdentifier = request.getParameter(OPENID_IDENTIFIER);
try {
//redirect to login page here...
if (openidIdentifier == null) {
// redirect to login page
session.setAttribute(ORIGINAL_URI_KEY, getFullRequestURI(request).toString());
response.setContentLength(0);
response.sendRedirect(response.encodeRedirectURL(addPaths(request.getContextPath(), loginPage)));
return AuthStatus.SEND_CONTINUE;
}
List<DiscoveryInformation> discoveries = consumerManager.discover(openidIdentifier);
//associate with one OP
DiscoveryInformation discovered = consumerManager.associate(discoveries);
//save association info in session
session.setAttribute(DISCOVERY_SESSION_KEY, discovered);
String returnAddress = request.getRequestURL().append(RETURN_ADDRESS).toString();
AuthRequest authRequest = consumerManager.authenticate(discovered, returnAddress);
session.setAttribute(RETURN_ADDRESS_KEY, authRequest.getReturnTo());
//save original uri in response, to be retrieved after redirect returns
if (session.getAttribute(ORIGINAL_URI_KEY) == null) {
session.setAttribute(ORIGINAL_URI_KEY, getFullRequestURI(request).toString());
}
//TODO openid 2.0 form redirect
response.sendRedirect(authRequest.getDestinationUrl(true));
return AuthStatus.SEND_CONTINUE;
} catch (DiscoveryException e) {
throw (AuthException) new AuthException("Could not authenticate").initCause(e);
} catch (ConsumerException e) {
throw (AuthException) new AuthException("Could not authenticate").initCause(e);
} catch (MessageException e) {
throw (AuthException) new AuthException("Could not authenticate").initCause(e);
} catch (IOException e) {
throw (AuthException) new AuthException("Could not authenticate").initCause(e);
}
// return null;
}
private boolean isLoginOrErrorPage(String uri) {
return (uri != null &&
(uri.equals(loginPage) || uri.equals(errorPage)));
}
private String addPaths(String p1, String p2) {
StringBuilder b = new StringBuilder(p1);
if (p1.endsWith("/")) {
if (p2.startsWith("/")) {
b.append(p2, 1, p2.length() - 1);
} else {
b.append(p2);
}
} else {
if (!p2.startsWith("/")) {
b.append("/");
}
b.append(p2);
}
return b.toString();
}
private StringBuilder getFullRequestURI(HttpServletRequest request) {
StringBuilder builder = new StringBuilder();
builder.append(request.getScheme()).append("://");
builder.append(request.getServerName()).append(":");
builder.append(request.getServerPort());
//TODO jetty combines this with the uri and query string. Can this have query params?
builder.append(request.getContextPath());
builder.append(request.getPathInfo());
if (request.getQueryString() != null && request.getQueryString().length() > 0) {
builder.append("?").append(request.getQueryString());
}
return builder;
}
private boolean isMandatory(MessageInfo messageInfo) {
String mandatory = (String) messageInfo.getMap().get(MANDATORY_KEY);
if (mandatory == null) {
return false;
}
return Boolean.valueOf(mandatory);
}
public AuthStatus secureResponse(MessageInfo messageInfo, Subject serviceSubject) throws AuthException {
return AuthStatus.SEND_SUCCESS;
}
}