/* * File: AuthFilterJAAS.java * * Copyright 2009 Muradora * * 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.fcrepo.server.jaas; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.security.Principal; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import javax.security.auth.Subject; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.log4j.Logger; import org.fcrepo.server.jaas.auth.AuthHttpServletRequestWrapper; import org.fcrepo.server.jaas.auth.handler.UsernamePasswordCallbackHandler; import org.fcrepo.server.jaas.util.Base64; import org.fcrepo.server.jaas.util.SubjectUtils; import fedora.common.Constants; /** * A Servlet Filter for protecting resources. This filter uses JAAS for * performing user authentication. Once a user is authenticated, a user * principal object that is returned from the JAAS login module is created and * added to the servlet request. The parameters of this filter are as follows: * <ul> * <li> * <p> * <strong>jaas.config.location</strong> * </p> * <p> * This specifies the location of the jaas configuration file. The default is * $FEDORA_HOME/server/config/jaas.conf * </p> * </li> * <li> * <p> * <strong>jaas.config.name</strong> * </p> * <p> * The name of the jaas configuration to use. The default is fedora-auth * </p> * </li> * </ul> * * @author nish.naidoo@gmail.com */ public class AuthFilterJAAS implements Filter { private static Logger log = Logger.getLogger(AuthFilterJAAS.class); private static final String SESSION_SUBJECT_KEY = "javax.security.auth.subject"; private static final String JAAS_CONFIG_KEY = "java.security.auth.login.config"; private static final String JAAS_CONFIG_DEFAULT = "fedora-auth"; private static final String ROLE_KEY = "role"; private static final String FEDORA_ROLE_KEY = "fedoraRole"; private static final String FEDORA_ATTRIBUTES_KEY = "FEDORA_AUX_SUBJECT_ATTRIBUTES"; private String jaasConfigName = null; private FilterConfig filterConfig = null; private Set<String> userClassNames = null; private Set<String> roleClassNames = null; private Set<String> roleAttributeNames = null; private Set<String> excludedUris = null; public void init(FilterConfig filterConfig) throws ServletException { // get FEDORA_HOME. This being set is mandatory. String fedoraHome = Constants.FEDORA_HOME; if (fedoraHome == null || "".equals(fedoraHome)) { String msg = "FEDORA_HOME environment variable not set"; throw new ServletException(msg); } this.filterConfig = filterConfig; if (this.filterConfig == null) { log.info("No configuration for: " + this.getClass().getName()); } log.info("using FEDORA_HOME: " + fedoraHome); // Get the jaas.conf file to use and the config to use from the // jaas.conf file. This defaults to $FEDORA_HOME/server/config/jaas.conf // and 'fedora-auth' for the configuration. String jaasConfigLocation = fedoraHome + "/server/config/jaas.conf"; jaasConfigName = JAAS_CONFIG_DEFAULT; String tmp = null; tmp = filterConfig.getInitParameter("jaas.config.location"); if (tmp != null && !"".equals(tmp)) { jaasConfigLocation = tmp; if (log.isDebugEnabled()) { log.debug("using location from init file: " + jaasConfigLocation); } } tmp = filterConfig.getInitParameter("jaas.config.name"); if (tmp != null && !"".equals(tmp)) { jaasConfigName = tmp; if (log.isDebugEnabled()) { log.debug("using name from init file: " + jaasConfigName); } } tmp = filterConfig.getInitParameter("excludeUris"); excludedUris = new HashSet<String>(); if (tmp != null) { String[] names = tmp.split(" *, *"); if (names != null && names.length > 0) { for (String n : names) { excludedUris.add(n); } } } tmp = filterConfig.getInitParameter("userClassNames"); userClassNames = new HashSet<String>(); if (tmp != null) { String[] names = tmp.split(" *, *"); if (names != null && names.length > 0) { for (String n : names) { userClassNames.add(n); } } } tmp = filterConfig.getInitParameter("roleClassNames"); roleClassNames = new HashSet<String>(); if (tmp != null) { String[] names = tmp.split(" *, *"); if (names != null && names.length > 0) { for (String n : names) { roleClassNames.add(n); } } } tmp = filterConfig.getInitParameter("roleAttributeNames"); roleAttributeNames = new HashSet<String>(); roleAttributeNames.add(ROLE_KEY); roleAttributeNames.add(FEDORA_ROLE_KEY); if (tmp != null) { String[] names = tmp.split(" *, *"); if (names != null && names.length > 0) { for (String n : names) { roleAttributeNames.add(n); } } } File jaasConfig = new File(jaasConfigLocation); if (!jaasConfig.exists()) { String msg = "JAAS config file not at: " + jaasConfig.getAbsolutePath(); log.error(msg); throw new ServletException(msg); } System.setProperty(JAAS_CONFIG_KEY, jaasConfig.getAbsolutePath()); log.info("initialised servlet filter: " + this.getClass().getName()); } /* * (non-Javadoc) * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, * javax.servlet.ServletResponse, javax.servlet.FilterChain) */ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; // if the servlet is in our excluded list, just continue the // chain and return after processing the chain. if (excludedUris != null && excludedUris.size() > 0) { if (excludedUris.contains(req.getServletPath())) { if (log.isDebugEnabled()) { log.debug("skipping authentication on servlet: " + req.getServletPath()); } chain.doFilter(request, response); return; } } if (log.isDebugEnabled()) { log.debug("incoming filter: " + this.getClass().getName()); log.debug("session-id: " + req.getSession().getId()); } Subject subject = authenticate(req); if (subject == null) { loginForm(res); return; } // obtain the user principal from the subject and add to servlet. Principal userPrincipal = getUserPrincipal(subject); // obtain the user roles from the subject and add to servlet. Set<String> userRoles = getUserRoles(subject); // wrap the request in one that has the ability to store role // and principal information and store this information. AuthHttpServletRequestWrapper authRequest = new AuthHttpServletRequestWrapper(req); authRequest.setUserPrincipal(userPrincipal); authRequest.setUserRoles(userRoles); // add the roles that were obtained to the Subject. addRolesToSubject(subject, userRoles); // add the roles that were obtained to the Fedora attribute location. populateFedoraRoles(subject, userRoles, authRequest); chain.doFilter(authRequest, response); if (log.isDebugEnabled()) { log.debug("outgoing filter: " + this.getClass().getName()); } } public void destroy() { log.info("destroying servlet filter: " + this.getClass().getName()); filterConfig = null; } /** * Sends a 401 error to the browser. This forces a login box to be displayed * allowing the user to login. * * @param response * the response to set the headers and status */ private void loginForm(HttpServletResponse response) throws IOException { response.reset(); response.addHeader("WWW-Authenticate", "Basic realm=\"!!Fedora Repository Server\""); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); OutputStream out = response.getOutputStream(); out.write("Fedora: 401 ".getBytes()); out.flush(); out.close(); } /** * Performs the authentication. Once a Subject is obtained, it is stored in * the users session. Subsequent requests check for the existence of this * object before performing the authentication again. * * @param req * the servlet request. * @return a user principal that was extracted from the login context. */ private Subject authenticate(HttpServletRequest req) { String authorization = req.getHeader("authorization"); if (authorization == null || "".equals(authorization.trim())) { return null; } // subject from session instead of re-authenticating // can't change username/password for this session. Subject subject = (Subject) req.getSession().getAttribute(authorization); if (subject != null) { return subject; } String auth = null; try { byte[] data = Base64.decode(authorization.substring(6)); auth = new String(data); } catch (IOException e) { log.error(e.getMessage()); return null; } String username = auth.substring(0, auth.indexOf(':')); String password = auth.substring(auth.indexOf(':') + 1); if (log.isDebugEnabled()) { log.debug("auth username: " + username); } LoginContext loginContext = null; try { CallbackHandler handler = new UsernamePasswordCallbackHandler(username, password); loginContext = new LoginContext(jaasConfigName, handler); loginContext.login(); } catch (LoginException le) { log.error(le.getMessage()); return null; } // successfully logged in subject = loginContext.getSubject(); // object accessable by a fixed key for usage req.getSession().setAttribute(SESSION_SUBJECT_KEY, subject); // object accessable only by base64 encoded username:password that was // initially used - prevents some dodgy stuff req.getSession().setAttribute(authorization, subject); return subject; } /** * Given a subject, obtain the userPrincipal from it. The user principal is * defined by a Principal class that can be defined in the web.xml file. If * this is undefined, the first principal found is assumed to be the * userPrincipal. * * @param subject * the subject returned from authentication. * @return the userPrincipal associated with the given subject. */ private Principal getUserPrincipal(Subject subject) { Principal userPrincipal = null; Set<Principal> principals = subject.getPrincipals(); // try and get userPrincipal based on userClassNames if (userClassNames != null && userClassNames.size() > 0) { for (Principal p : principals) { if (userPrincipal == null && userClassNames.contains(p.getClass().getName())) { userPrincipal = p; } } } // no userPrincipal found using userClassNames, just grab first principal if (userPrincipal == null) { Iterator<Principal> i = principals.iterator(); // should always have 1 at least and 1st should be user principal if (i.hasNext()) { userPrincipal = i.next(); } } if (log.isDebugEnabled()) { log.debug("found userPrincipal [" + userPrincipal.getClass().getName() + "]: " + userPrincipal.getName()); } return userPrincipal; } /** * Obtains the roles for the user based on the class names and attribute * names provided in the web.xml file. * * @param subject * the subject returned from authentication. * @return a set of strings that represent the users roles. */ private Set<String> getUserRoles(Subject subject) { Set<String> userRoles = new HashSet<String>(); // get roles from specified classes Set<Principal> principals = subject.getPrincipals(); if (roleClassNames != null && roleClassNames.size() > 0) { for (Principal p : principals) { if (roleClassNames.contains(p.getClass().getName())) { userRoles.add(p.getName()); } } } // get roles from specified attributes Map<String, Set<String>> attributes = SubjectUtils.getAttributes(subject); if (attributes != null) { for (String key : attributes.keySet()) { if (roleAttributeNames.contains(key)) { userRoles.addAll(attributes.get(key)); } } } if (log.isDebugEnabled()) { for (String r : userRoles) { log.debug("found role: " + r); } } return userRoles; } /** * Adds roles to the Subject object. * * @param subject * the subject that was returned from authentication. * @param userRoles * the set of user roles that were found. */ private void addRolesToSubject(Subject subject, Set<String> userRoles) { if (userRoles == null) { userRoles = new HashSet<String>(); } Map<String, Set<String>> attributes = SubjectUtils.getAttributes(subject); Set<String> roles = attributes.get(ROLE_KEY); if (roles == null) { roles = new HashSet<String>(); attributes.put(ROLE_KEY, roles); } for (String role : userRoles) { roles.add(role); if (log.isDebugEnabled()) { log.debug("added role: " + role); } } } /** * Add roles to where Fedora expects them - * FEDORA_AUX_SUBJECT_ATTRIBUTES.fedoraRole. * * @param subject * the aubject from authentication. * @param userRoles * the set of user roles. * @param request * the request in which to place the roles for Fedora. */ private void populateFedoraRoles(Subject subject, Set<String> userRoles, HttpServletRequest request) { Map<String, Set<String>> attributes = SubjectUtils.getAttributes(subject); if (attributes == null) { attributes = new HashMap<String, Set<String>>(); } // get the fedoraRole attribute or create it. Set<String> roles = attributes.get(FEDORA_ROLE_KEY); if (roles == null) { roles = new HashSet<String>(); attributes.put(FEDORA_ROLE_KEY, roles); } roles.addAll(userRoles); request.setAttribute(FEDORA_ATTRIBUTES_KEY, attributes); } }