/*
* 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.tomcat.security.authentication;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.StringTokenizer;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.util.MD5Encoder;
import org.apache.geronimo.security.jaspi.LoginService;
import org.apache.geronimo.security.jaspi.UserIdentity;
import org.apache.geronimo.tomcat.security.AuthResult;
import org.apache.geronimo.tomcat.security.TomcatAuthStatus;
import org.apache.geronimo.tomcat.security.Authenticator;
import org.apache.geronimo.tomcat.security.ServerAuthException;
/**
* @version $Rev: 1331047 $ $Date: 2012-04-27 03:40:50 +0800 (Fri, 27 Apr 2012) $
*/
public class DigestAuthenticator implements Authenticator {
private static final MD5Encoder md5Encoder = new MD5Encoder();
/**
* MD5 message digest provider.
*/
private static final MessageDigest md5Helper;
static {
try {
md5Helper = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
/**
* Private key.
*/
private static final String key = "Catalina";
private final LoginService loginService;
private final String realmName;
private final UserIdentity unauthenticatedIdentity;
public DigestAuthenticator(LoginService loginService, String realmName, UserIdentity unauthenticatedIdentity) {
this.loginService = loginService;
this.realmName = realmName;
this.unauthenticatedIdentity = unauthenticatedIdentity;
}
public AuthResult validateRequest(Request request, HttpServletResponse response, boolean isAuthMandatory, UserIdentity cachedIdentity) throws ServerAuthException {
String authorization = request.getHeader("authorization");
if (authorization != null) {
UserIdentity userIdentity = findPrincipal(request, authorization);
if (userIdentity != null) {
return new AuthResult(TomcatAuthStatus.SUCCESS, userIdentity, false);
}
}
// Send an "unauthorized" response and an appropriate challenge
// Next, generate a nOnce token (that is a token which is supposed
// to be unique).
if (isAuthMandatory) {
String nOnce = generateNOnce(request);
setAuthenticateHeader(response, nOnce);
try {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
} catch (IOException e) {
throw new ServerAuthException(e);
}
return new AuthResult(TomcatAuthStatus.SEND_CONTINUE, null, false);
}
return new AuthResult(TomcatAuthStatus.SUCCESS, unauthenticatedIdentity, false);
}
public boolean secureResponse(Request request, Response response, AuthResult authResult) throws ServerAuthException {
return true;
}
public String getAuthType() {
return HttpServletRequest.DIGEST_AUTH;
}
/**
* 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
*/
protected UserIdentity findPrincipal(Request request,
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();
// Bugzilla 37132: http://issues.apache.org/bugzilla/show_bug.cgi?id=37132
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);
//TODO this is totally wrong
return loginService.login(userName, 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(Request 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
* <p/>
* digest-challenge = 1#( realm | [ domain ] | nOnce |
* [ digest-opaque ] |[ stale ] | [ algorithm ] )
* <p/>
* 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 response HTTP Servlet response
* @param nOnce nonce token
*/
protected void setAuthenticateHeader(
HttpServletResponse response,
String nOnce) {
// Get the realm name
byte[] buffer;
synchronized (md5Helper) {
buffer = md5Helper.digest(nOnce.getBytes());
}
String authenticateHeader = "Digest realm=\"" + realmName + "\", "
+ "qop=\"auth\", nonce=\"" + nOnce + "\", " + "opaque=\""
+ md5Encoder.encode(buffer) + "\"";
response.setHeader("WWW-Authenticate", authenticateHeader);
}
@Override
public AuthResult login(String username, String password, Request request) throws ServletException {
UserIdentity userIdentity = loginService.login(username, password);
if (userIdentity != null) {
return new AuthResult(TomcatAuthStatus.SUCCESS, userIdentity, false);
}
return new AuthResult(TomcatAuthStatus.FAILURE, null, false);
}
@Override
public void logout(Request request) {
}
}