/* * This 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 2.1 of * the License, or (at your option) any later version. * * This software 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 software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.mobicents.servlet.sip.security.authentication; import java.io.IOException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.text.ParseException; import java.util.StringTokenizer; import javax.sip.header.AuthorizationHeader; import javax.sip.header.HeaderFactory; import javax.sip.header.ProxyAuthenticateHeader; import javax.sip.header.WWWAuthenticateHeader; import org.apache.catalina.Realm; import org.apache.catalina.authenticator.Constants; import org.apache.catalina.util.MD5Encoder; import org.apache.log4j.Logger; import org.mobicents.servlet.sip.SipFactories; import org.mobicents.servlet.sip.message.SipServletRequestImpl; import org.mobicents.servlet.sip.message.SipServletResponseImpl; import org.mobicents.servlet.sip.startup.loading.SipLoginConfig; /** * An <b>Authenticator</b> and <b>Valve</b> implementation of HTTP DIGEST * Authentication (see RFC 2069). Modified for SIP authentication. * * @author Craig R. McClanahan * @author Remy Maucherat * @author Vladimir Ralev */ public class DigestAuthenticator extends AuthenticatorBase { private static transient Logger log = Logger.getLogger(DigestAuthenticator.class); // -------------------------------------------------------------- Constants /** * The MD5 helper object for this class. */ static final MD5Encoder md5Encoder = new MD5Encoder(); /** * Descriptive information about this implementation. */ protected static final String info = "org.apache.catalina.authenticator.DigestAuthenticator/1.0"; // ----------------------------------------------------------- Constructors public DigestAuthenticator() { super(); } // ----------------------------------------------------- Instance Variables /** * MD5 message digest provider. */ protected volatile static MessageDigest md5Helper; static { try { md5Helper = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(e); } } /** * Private key. */ protected String key = "Catalina"; /* * Principal */ private Principal principal; // ------------------------------------------------------------- Properties /** * Return descriptive information about this Valve implementation. */ public String getInfo() { return (info); } // --------------------------------------------------------- Public Methods /** * Authenticate the user making this request, based on the specified * login configuration. Return <code>true</code> if any specified * constraint has been satisfied, or <code>false</code> if we have * created a response challenge already. * * @param request Request we are processing * @param response Response we are creating * @param config Login configuration describing how authentication * should be performed * * @exception IOException if an input/output error occurs */ public boolean authenticate(SipServletRequestImpl request, SipServletResponseImpl response, SipLoginConfig config) throws IOException { principal = null; // Have we already authenticated someone? principal = request.getUserPrincipal(); //String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE); if (principal != null) { if (log.isDebugEnabled()) log.debug("Already authenticated '" + principal.getName() + "'"); return (true); } // Validate any credentials already included with this request String authorization = request.getHeader("authorization"); if (authorization != null) { principal = findPrincipal(request, authorization, context.getRealm()); if (principal != null) { String username = parseUsername(authorization); register(request, response, principal, Constants.DIGEST_METHOD, username, null); return (true); } } // Send an "unauthorized" response and an appropriate challenge // Next, generate a nOnce token (that is a token which is supposed // to be unique). String nOnce = generateNOnce(request); setAuthenticateHeader(request, response, config, nOnce); response.send(); // hres.flushBuffer(); return (false); } // ------------------------------------------------------ Protected Methods /** * Parse the specified authorization credentials, and return the * associated Principal that these credentials authenticate (if any) * from the specified Realm. If there is no such Principal, return * <code>null</code>. * * @param request HTTP servlet request * @param authorization Authorization credentials from this request * @param realm Realm used to authenticate Principals */ protected static Principal findPrincipal(SipServletRequestImpl request, String authorization, Realm realm) { //System.out.println("Authorization token : " + authorization); // Validate the authorization credentials format if (authorization == null) return (null); if (!authorization.startsWith("Digest ")) return (null); authorization = authorization.substring(7).trim(); // Bugzilla 37132: http://issues.apache.org/bugzilla/show_bug.cgi?id=37132 // The solution of 37132 doesn't work with : response="2d05f1206becab904c1f311f205b405b",cnonce="5644k1k670",username="admin",nc=00000001,qop=auth,nonce="b6c73ab509830b8c0897984f6b0526e8",realm="sip-servlets-realm",opaque="9ed6d115d11f505f9ee20f6a68654cc2",uri="sip:192.168.1.142",algorithm=MD5 // That's why I am going back to simple comma (Vladimir). TODO: Review this. String[] tokens = authorization.split(",");//(?=(?:[^\"]*\"[^\"]*\")+$)"); String userName = null; String realmName = null; String nOnce = null; String nc = null; String cnonce = null; String qop = null; String uri = null; String response = null; String method = request.getMethod(); for (int i = 0; i < tokens.length; i++) { String currentToken = tokens[i]; if (currentToken.length() == 0) continue; int equalSign = currentToken.indexOf('='); if (equalSign < 0) return null; String currentTokenName = currentToken.substring(0, equalSign).trim(); String currentTokenValue = currentToken.substring(equalSign + 1).trim(); if ("username".equals(currentTokenName)) userName = removeQuotes(currentTokenValue); if ("realm".equals(currentTokenName)) realmName = removeQuotes(currentTokenValue, true); if ("nonce".equals(currentTokenName)) nOnce = removeQuotes(currentTokenValue); if ("nc".equals(currentTokenName)) nc = removeQuotes(currentTokenValue); if ("cnonce".equals(currentTokenName)) cnonce = removeQuotes(currentTokenValue); if ("qop".equals(currentTokenName)) qop = removeQuotes(currentTokenValue); if ("uri".equals(currentTokenName)) uri = removeQuotes(currentTokenValue); if ("response".equals(currentTokenName)) response = removeQuotes(currentTokenValue); } if ( (userName == null) || (realmName == null) || (nOnce == null) || (uri == null) || (response == null) ) return null; // Second MD5 digest used to calculate the digest : // MD5(Method + ":" + uri) String a2 = method + ":" + uri; //System.out.println("A2:" + a2); byte[] buffer = null; synchronized (md5Helper) { buffer = md5Helper.digest(a2.getBytes()); } String md5a2 = md5Encoder.encode(buffer); return (realm.authenticate(userName, response, nOnce, nc, cnonce, qop, realmName, md5a2)); } /** * Parse the username from the specified authorization string. If none * can be identified, return <code>null</code> * * @param authorization Authorization string to be parsed */ protected String parseUsername(String authorization) { //System.out.println("Authorization token : " + authorization); // Validate the authorization credentials format if (authorization == null) return (null); if (!authorization.startsWith("Digest ")) return (null); authorization = authorization.substring(7).trim(); StringTokenizer commaTokenizer = new StringTokenizer(authorization, ","); while (commaTokenizer.hasMoreTokens()) { String currentToken = commaTokenizer.nextToken(); int equalSign = currentToken.indexOf('='); if (equalSign < 0) return null; String currentTokenName = currentToken.substring(0, equalSign).trim(); String currentTokenValue = currentToken.substring(equalSign + 1).trim(); if ("username".equals(currentTokenName)) return (removeQuotes(currentTokenValue)); } return (null); } /** * Removes the quotes on a string. RFC2617 states quotes are optional for * all parameters except realm. */ protected static String removeQuotes(String quotedString, boolean quotesRequired) { //support both quoted and non-quoted if (quotedString.length() > 0 && quotedString.charAt(0) != '"' && !quotesRequired) { return quotedString; } else if (quotedString.length() > 2) { return quotedString.substring(1, quotedString.length() - 1); } else { return ""; } } /** * Removes the quotes on a string. */ protected static String removeQuotes(String quotedString) { return removeQuotes(quotedString, false); } /** * Generate a unique token. The token is generated according to the * following pattern. NOnceToken = Base64 ( MD5 ( client-IP ":" * time-stamp ":" private-key ) ). * * @param request HTTP Servlet request */ protected String generateNOnce(SipServletRequestImpl request) { long currentTime = System.currentTimeMillis(); String nOnceValue = request.getRemoteAddr() + ":" + currentTime + ":" + key; byte[] buffer = null; synchronized (md5Helper) { buffer = md5Helper.digest(nOnceValue.getBytes()); } nOnceValue = md5Encoder.encode(buffer); return nOnceValue; } /** * Generates the WWW-Authenticate header. * <p> * The header MUST follow this template : * <pre> * WWW-Authenticate = "WWW-Authenticate" ":" "Digest" * digest-challenge * * digest-challenge = 1#( realm | [ domain ] | nOnce | * [ digest-opaque ] |[ stale ] | [ algorithm ] ) * * realm = "realm" "=" realm-value * realm-value = quoted-string * domain = "domain" "=" <"> 1#URI <"> * nonce = "nonce" "=" nonce-value * nonce-value = quoted-string * opaque = "opaque" "=" quoted-string * stale = "stale" "=" ( "true" | "false" ) * algorithm = "algorithm" "=" ( "MD5" | token ) * </pre> * * @param request HTTP Servlet request * @param response HTTP Servlet response * @param config Login configuration describing how authentication * should be performed * @param nOnce nonce token */ protected void setAuthenticateHeader(SipServletRequestImpl request, SipServletResponseImpl response, SipLoginConfig config, String nOnce) { // Get the realm name String realmName = config.getRealmName(); if (realmName == null) realmName = request.getServerName() + ":" + request.getServerPort(); byte[] buffer = null; synchronized (md5Helper) { buffer = md5Helper.digest(nOnce.getBytes()); } String authenticateHeader = "Digest realm=\"" + realmName + "\", " + "qop=\"auth\", nonce=\"" + nOnce + "\", " + "opaque=\"" + md5Encoder.encode(buffer) + "\""; // There are different headers for different types of auth if(response.getStatus() == SipServletResponseImpl.SC_PROXY_AUTHENTICATION_REQUIRED) { response.setHeader("Proxy-Authenticate", authenticateHeader); } else { response.setHeader("WWW-Authenticate", authenticateHeader); } } /** * Generates an authorisation header in response to wwwAuthHeader. * * @param method method of the request being authenticated * @param uri digest-uri * @param requestBody the body of the request. * @param authHeader the challenge that we should respond to * @param username * @param password * * @return an authorisation header in response to authHeader. * * @throws OperationFailedException if auth header was malformated. */ public static AuthorizationHeader getAuthorizationHeader( String method, String uri, String requestBody, WWWAuthenticateHeader authHeader, String username, String password) { String response = null; HeaderFactory headerFactory = SipFactories.headerFactory; // JvB: authHeader.getQop() is a quoted _list_ of qop values // (e.g. "auth,auth-int") Client is supposed to pick one String qopList = authHeader.getQop(); String qop = (qopList != null) ? "auth" : null; String nc_value = "00000001"; String cnonce = "xyz"; try { response = MessageDigestResponseAlgorithm.calculateResponse( authHeader.getAlgorithm(), username, authHeader.getRealm(), password, authHeader.getNonce(), nc_value, // JvB added cnonce, // JvB added method, uri, requestBody, qop);//jvb changed } catch (NullPointerException exc) { throw new IllegalStateException( "The authenticate header was malformatted", exc); } AuthorizationHeader authorization = null; try { if (authHeader instanceof ProxyAuthenticateHeader) { authorization = headerFactory.createProxyAuthorizationHeader( authHeader.getScheme()); } else { authorization = headerFactory.createAuthorizationHeader( authHeader.getScheme()); } authorization.setUsername(username); authorization.setRealm(authHeader.getRealm()); authorization.setNonce(authHeader.getNonce()); authorization.setParameter("uri", uri); authorization.setResponse(response); if (authHeader.getAlgorithm() != null) { authorization.setAlgorithm(authHeader.getAlgorithm()); } if (authHeader.getOpaque() != null && authHeader.getOpaque().length() > 0) { authorization.setOpaque(authHeader.getOpaque()); } // jvb added if (qop!=null) { authorization.setQop(qop); authorization.setCNonce(cnonce); authorization.setNonceCount( Integer.parseInt(nc_value) ); } authorization.setResponse(response); } catch (ParseException ex) { throw new SecurityException( "Failed to create an authorization header!"); } return authorization; } public Principal getPrincipal() { return principal; } }