/* * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited * * 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.springframework.security.openid; import org.openid4java.consumer.ConsumerException; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.util.*; /** * Filter which processes OpenID authentication requests. * <p> * The OpenID authentication involves two stages. * * <h2>Submission of OpenID identity</h2> * * The user's OpenID identity is submitted via a login form, just as it would be for a * normal form login. At this stage the filter will extract the identity from the * submitted request (by default, the parameter is called <tt>openid_identifier</tt>, as * recommended by the OpenID 2.0 Specification). It then passes the identity to the * configured <tt>OpenIDConsumer</tt>, which returns the URL to which the request should * be redirected for authentication. A "return_to" URL is also supplied, which matches the * URL processed by this filter, to allow the filter to handle the request once the user * has been successfully authenticated. The OpenID server will then authenticate the user * and redirect back to the application. * * <h2>Processing the Redirect from the OpenID Server</h2> * * Once the user has been authenticated externally, the redirected request will be passed * to the <tt>OpenIDConsumer</tt> again for validation. The returned * <tt>OpenIDAuthentication</tt> will be passed to the <tt>AuthenticationManager</tt> * where it should (normally) be processed by an <tt>OpenIDAuthenticationProvider</tt> in * order to load the authorities for the user. * * @author Robin Bramley * @author Ray Krueger * @author Luke Taylor * @since 2.0 * @see OpenIDAuthenticationProvider */ public class OpenIDAuthenticationFilter extends AbstractAuthenticationProcessingFilter { // ~ Static fields/initializers // ===================================================================================== public static final String DEFAULT_CLAIMED_IDENTITY_FIELD = "openid_identifier"; // ~ Instance fields // ================================================================================================ private OpenIDConsumer consumer; private String claimedIdentityFieldName = DEFAULT_CLAIMED_IDENTITY_FIELD; private Map<String, String> realmMapping = Collections.emptyMap(); private Set<String> returnToUrlParameters = Collections.emptySet(); // ~ Constructors // =================================================================================================== public OpenIDAuthenticationFilter() { super("/login/openid"); } // ~ Methods // ======================================================================================================== @Override public void afterPropertiesSet() { super.afterPropertiesSet(); if (consumer == null) { try { consumer = new OpenID4JavaConsumer(); } catch (ConsumerException e) { throw new IllegalArgumentException("Failed to initialize OpenID", e); } } if (returnToUrlParameters.isEmpty() && getRememberMeServices() instanceof AbstractRememberMeServices) { returnToUrlParameters = new HashSet<String>(); returnToUrlParameters .add(((AbstractRememberMeServices) getRememberMeServices()) .getParameter()); } } /** * Authentication has two phases. * <ol> * <li>The initial submission of the claimed OpenID. A redirect to the URL returned * from the consumer will be performed and null will be returned.</li> * <li>The redirection from the OpenID server to the return_to URL, once it has * authenticated the user</li> * </ol> */ @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException { OpenIDAuthenticationToken token; String identity = request.getParameter("openid.identity"); if (!StringUtils.hasText(identity)) { String claimedIdentity = obtainUsername(request); try { String returnToUrl = buildReturnToUrl(request); String realm = lookupRealm(returnToUrl); String openIdUrl = consumer.beginConsumption(request, claimedIdentity, returnToUrl, realm); if (logger.isDebugEnabled()) { logger.debug("return_to is '" + returnToUrl + "', realm is '" + realm + "'"); logger.debug("Redirecting to " + openIdUrl); } response.sendRedirect(openIdUrl); // Indicate to parent class that authentication is continuing. return null; } catch (OpenIDConsumerException e) { logger.debug("Failed to consume claimedIdentity: " + claimedIdentity, e); throw new AuthenticationServiceException( "Unable to process claimed identity '" + claimedIdentity + "'"); } } if (logger.isDebugEnabled()) { logger.debug("Supplied OpenID identity is " + identity); } try { token = consumer.endConsumption(request); } catch (OpenIDConsumerException oice) { throw new AuthenticationServiceException("Consumer error", oice); } token.setDetails(authenticationDetailsSource.buildDetails(request)); // delegate to the authentication provider Authentication authentication = this.getAuthenticationManager().authenticate( token); return authentication; } protected String lookupRealm(String returnToUrl) { String mapping = realmMapping.get(returnToUrl); if (mapping == null) { try { URL url = new URL(returnToUrl); int port = url.getPort(); StringBuilder realmBuffer = new StringBuilder(returnToUrl.length()) .append(url.getProtocol()).append("://").append(url.getHost()); if (port > 0) { realmBuffer.append(":").append(port); } realmBuffer.append("/"); mapping = realmBuffer.toString(); } catch (MalformedURLException e) { logger.warn("returnToUrl was not a valid URL: [" + returnToUrl + "]", e); } } return mapping; } /** * Builds the <tt>return_to</tt> URL that will be sent to the OpenID service provider. * By default returns the URL of the current request. * * @param request the current request which is being processed by this filter * @return The <tt>return_to</tt> URL. */ protected String buildReturnToUrl(HttpServletRequest request) { StringBuffer sb = request.getRequestURL(); Iterator<String> iterator = returnToUrlParameters.iterator(); boolean isFirst = true; while (iterator.hasNext()) { String name = iterator.next(); // Assume for simplicity that there is only one value String value = request.getParameter(name); if (value == null) { continue; } if (isFirst) { sb.append("?"); isFirst = false; } sb.append(utf8UrlEncode(name)).append("=").append(utf8UrlEncode(value)); if (iterator.hasNext()) { sb.append("&"); } } return sb.toString(); } /** * Reads the <tt>claimedIdentityFieldName</tt> from the submitted request. */ protected String obtainUsername(HttpServletRequest req) { String claimedIdentity = req.getParameter(claimedIdentityFieldName); if (!StringUtils.hasText(claimedIdentity)) { logger.error("No claimed identity supplied in authentication request"); return ""; } return claimedIdentity.trim(); } /** * Maps the <tt>return_to url</tt> to a realm, for example: * * <pre> * http://www.example.com/login/openid -> http://www.example.com/realm * </pre> * * If no mapping is provided then the returnToUrl will be parsed to extract the * protocol, hostname and port followed by a trailing slash. This means that * <tt>http://www.example.com/login/openid</tt> will automatically become * <tt>http://www.example.com:80/</tt> * * @param realmMapping containing returnToUrl -> realm mappings */ public void setRealmMapping(Map<String, String> realmMapping) { this.realmMapping = realmMapping; } /** * The name of the request parameter containing the OpenID identity, as submitted from * the initial login form. * * @param claimedIdentityFieldName defaults to "openid_identifier" */ public void setClaimedIdentityFieldName(String claimedIdentityFieldName) { this.claimedIdentityFieldName = claimedIdentityFieldName; } public void setConsumer(OpenIDConsumer consumer) { this.consumer = consumer; } /** * Specifies any extra parameters submitted along with the identity field which should * be appended to the {@code return_to} URL which is assembled by * {@link #buildReturnToUrl}. * * @param returnToUrlParameters the set of parameter names. If not set, it will * default to the parameter name used by the {@code RememberMeServices} obtained from * the parent class (if one is set). */ public void setReturnToUrlParameters(Set<String> returnToUrlParameters) { Assert.notNull(returnToUrlParameters, "returnToUrlParameters cannot be null"); this.returnToUrlParameters = returnToUrlParameters; } /** * Performs URL encoding with UTF-8 * * @param value the value to URL encode * @return the encoded value */ private String utf8UrlEncode(String value) { try { return URLEncoder.encode(value, "UTF-8"); } catch (UnsupportedEncodingException e) { Error err = new AssertionError( "The Java platform guarantees UTF-8 support, but it seemingly is not present."); err.initCause(e); throw err; } } }