/** * Copyright 2014 Liverpool John Moores University <http://www.ljmu.ac.uk/cmp/> * Aniketos Project FP7-ICT-257930 <http://www.aniketos.eu> * David Llewellyn-Jones <D.Llewellyn-Jones@ljmu.ac.uk> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see <http://www.gnu.org/licenses/>. * */ package eu.aniketos.components.verification.compositionsecurityvalidation; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.Arrays; import java.util.Hashtable; import java.util.List; import java.util.Map; import org.apache.cxf.binding.Binding; import org.apache.cxf.binding.soap.interceptor.SoapHeaderInterceptor; import org.apache.cxf.configuration.security.AuthorizationPolicy; import org.apache.cxf.endpoint.Endpoint; import org.apache.cxf.interceptor.Fault; import org.apache.cxf.message.Exchange; import org.apache.cxf.message.Message; import org.apache.cxf.transport.Conduit; import org.apache.cxf.ws.addressing.EndpointReferenceType; import org.apache.log4j.Logger; import org.osgi.framework.BundleContext; import arlut.csd.crypto.MD5Crypt; /** * CXF Interceptor that provides HTTP Basic Authentication validation. * For use with Aniketos DOSGi Web Services * * Based on the concepts outline here: * http://chrisdail.com/2008/03/31/apache-cxf-with-http-basic-authentication * * Requires log4j (although this can be commented out to print output on stdout) * Requires org.apache.cxf * * The class initialiser will try to load in details from the file configure/users.xts * If the file doesn't exist, authentication will not be used * * Can use the following for additional maven dependencies * * <dependency> * <groupId>log4j</groupId> * <artifactId>log4j</artifactId> * <version>1.2.17</version> * </dependency> * <dependency> * <groupId>org.apache.cxf</groupId> * <artifactId>cxf-rt-frontend-jaxws</artifactId> * <version>${cxf.version}</version> * </dependency> * * @author CDail * @author David Llewellyn-Jones, Liverpool John Moores University */ public class BasicAuthAuthorizationInterceptor extends SoapHeaderInterceptor { /** * Enumerates the various authorisation levels * Both NONE and FAIL are liable to generate HTTP error responses depending on the value of blockPage */ public enum AuthLevel { NONE, FAIL, NORMAL, FULL } /** * Returns the authentication level depending on the HTTP BASIC authentication * credentials provided by the client. * This method is intended to be called by the CXF-DOSGi-called implementation method * * @return the authorisation level */ public static AuthLevel getAuthLevel () { AuthLevel authLevel = AuthLevel.NONE; // Collect the message details, so we can extract the authentication level Message message = org.apache.cxf.phase.PhaseInterceptorChain.getCurrentMessage(); if (message != null) { // Determine the authentication level, which was recorded by the interceptor if (message.containsKey("authentication")) { authLevel = (AuthLevel) message.get("authentication"); } } /* // Output all of the message properties for (String key : message.keySet()) { System.out.println("Key: " + key); } */ return authLevel; } /** * Logger class for logging requests */ protected Logger log = Logger.getLogger(getClass()); /** * Specifies whether authentication should be used. * true - authentication is required * false - no authenatication needed */ private boolean authenticationActive; /** * Specifies whether a HTTP 401 Unauthorised/403 Forbidden message should be returned to unauthenticated users * true - return 401/403 where authentication is missing/fails * false - continue to page in all cases */ private boolean blockPage; /** * Map of users and their passwords hashed using the Apache MD5 algorithm. * These represent normal authenticated users. */ private Map<String,String> usersNormal; /** * Map of users and their passwords hashed using the Apache MD5 algorithm. * These represent users authenticated with elevated privileges */ private Map<String,String> usersFull; /** * Class constructor sets up the initial username and password list */ public BasicAuthAuthorizationInterceptor() { authenticationActive = true; blockPage = true; usersNormal = new Hashtable<String,String>(); usersNormal.clear(); usersFull = new Hashtable<String,String>(); usersFull.clear(); // Password uses the Apache MD5 hashing algorithm //addUser(usersFull, "Aniketos", "$apr1$BnkINIkJ$j66rMhBdb7plJ4q472ohd/"); try { LoadUserFilesDefault(); } catch (IOException e) { authenticationActive = false; } if (authenticationActive == false) { System.out.println("Service is public: No HTTP authentication in use."); } } /** * Log a message if logging is enabled * @param message the string to log */ private void Log(String message) { if (log.isDebugEnabled()) { log.debug(message); } System.out.println(message); } /** * Add a single user to the authentication list * Use httpasswd to generate password hashes. * For info about the hashing algorithm, see https://httpd.apache.org/docs/2.2/misc/password_encryptions.html * @param users the list of users to add the user to * @param user the user to add * @param password an Apache MD5-hash of the uer's password */ public void addUser(Map<String,String> users, String user, String password) { users.put(user, password); } /** * Load the username and passwords from the files in Apache hash format * For info about the hashing algorithm, see https://httpd.apache.org/docs/2.2/misc/password_encryptions.html * @throws IOException */ public void LoadUserFilesDefault() throws IOException { LoadUserFile(usersNormal, "configure/users-normal.txt"); LoadUserFile(usersFull, "configure/users-full.txt"); // If there are no registered users, turn off the authentication if ((usersNormal.size() == 0) && (usersFull.size() == 0)) { authenticationActive = false; } } /** * Load the username and passwords from a given file in Apache hash format * For info about the hashing algorithm, see https://httpd.apache.org/docs/2.2/misc/password_encryptions.html * @param users the list of users to store the result to * @param file the file to load the user info from * @throws IOException */ public void LoadUserFile(Map<String,String> users, String file) throws IOException { BundleContext context = Activator.getContext(); // Load data from the configuration file if there is one System.out.println("Reading users from: " + file); URL configURL = context.getBundle().getEntry(file); if (configURL != null) { users.clear(); BufferedReader input = new BufferedReader(new InputStreamReader(configURL.openStream())); try { // Read in each user String userPass = ""; while (userPass != null) { userPass = input.readLine(); if ((userPass != null) && (userPass.startsWith("#") == false)) { int separator = userPass.indexOf(':'); if (separator >= 0) { String user = userPass.substring(0, separator); String pass = userPass.substring(separator + 1); // Add the user to the list of authorised users addUser(users, user, pass); } } } } finally { input.close(); } } } /* (non-Javadoc) * @see org.apache.cxf.binding.soap.interceptor.SoapHeaderInterceptor#handleMessage(org.apache.cxf.message.Message) */ @Override public void handleMessage(Message message) throws Fault { // Store the authentication level so it can be extracted by the CXF-DOSGi-called implementation method message.put("authentication", AuthLevel.NONE); // If authentication is turned off we don't have to do anything if (authenticationActive) { // This is set by CXF AuthorizationPolicy policy = message.get(AuthorizationPolicy.class); // If the policy is not set, the user did not specify credentials // A 401 is sent to the client to indicate that authentication is required if (policy == null) { Log("User attempted to log in with no credentials"); // Store the authentication level so it can be extracted by the CXF-DOSGi-called implementation method message.put("authentication", AuthLevel.NONE); if (blockPage) { // Block access sendErrorResponse(message, HttpURLConnection.HTTP_UNAUTHORIZED); Log("HTTP 401 Unauthorized returned"); } } else { // Verify the password Log("Authenticating user: " + policy.getUserName() + " (" + this.getClass().getPackage() + ")"); // Check the full users String realPasswordCrypt = usersFull.get(policy.getUserName()); if (realPasswordCrypt == null || !MD5Crypt.verifyPassword(policy.getPassword(), realPasswordCrypt)) { // Check the normal users realPasswordCrypt = usersNormal.get(policy.getUserName()); if (realPasswordCrypt == null || !MD5Crypt.verifyPassword(policy.getPassword(), realPasswordCrypt)) { Log("Invalid username or password for user: " + policy.getUserName()); // Store the authentication level so it can be extracted by the CXF-DOSGi-called implementation method message.put("authentication", AuthLevel.FAIL); if (blockPage) { // Block access sendErrorResponse(message, HttpURLConnection.HTTP_FORBIDDEN); Log("HTTP 403 Forbidden returned"); } } else { // Store the authentication level so it can be extracted by the CXF-DOSGi-called implementation method message.put("authentication", AuthLevel.NORMAL); } } else { // Store the authentication level so it can be extracted by the CXF-DOSGi-called implementation method message.put("authentication", AuthLevel.FULL); } } } } /** * Send an error response if the user failed to gain access * @param message error message to send * @param responseCode error code to return */ private void sendErrorResponse(Message message, int responseCode) { Message outMessage = getOutMessage(message); outMessage.put(Message.RESPONSE_CODE, responseCode); // Set the response headers @SuppressWarnings("unchecked") Map<String, List<String>> responseHeaders = (Map<String, List<String>>)message.get(Message.PROTOCOL_HEADERS); if (responseHeaders != null) { responseHeaders.put("WWW-Authenticate", Arrays.asList(new String[]{"Basic realm=realm"})); responseHeaders.put("Content-Length", Arrays.asList(new String[]{"0"})); } message.getInterceptorChain().abort(); try { getConduit(message).prepare(outMessage); close(outMessage); } catch (IOException e) { Log(e.getMessage()); } } /** * Get the message to output * @param inMessage the input message * @return */ private Message getOutMessage(Message inMessage) { Exchange exchange = inMessage.getExchange(); Message outMessage = exchange.getOutMessage(); if (outMessage == null) { Endpoint endpoint = exchange.get(Endpoint.class); Binding binding = endpoint.getBinding(); outMessage = binding.createMessage(); exchange.setOutMessage(outMessage); } outMessage.putAll(inMessage); return outMessage; } /** * Get the conduit for returning the error message * @param inMessage the message to return * @return the conduit to return the message on * @throws IOException */ private Conduit getConduit(Message inMessage) throws IOException { Exchange exchange = inMessage.getExchange(); EndpointReferenceType target = exchange.get(EndpointReferenceType.class); Conduit conduit = exchange.getDestination().getBackChannel(inMessage, null, target); exchange.setConduit(conduit); return conduit; } /** * Send the message to the output stream * @param outMessage the message to output * @throws IOException */ private void close(Message outMessage) throws IOException { OutputStream os = outMessage.getContent(OutputStream.class); os.flush(); os.close(); } }