/*
* Copyright (c) 1998-2011 Caucho Technology -- all rights reserved
*
* This file is part of Resin(R) Open Source
*
* Each copy or derived work must preserve the copyright notice and this
* notice unmodified.
*
* Resin Open Source is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* Resin Open Source 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, or any warranty
* of NON-INFRINGEMENT. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License
* along with Resin Open Source; if not, write to the
*
* Free Software Foundation, Inc.
* 59 Temple Place, Suite 330
* Boston, MA 02111-1307 USA
*
* @author Scott Ferguson
*/
package com.caucho.security;
import java.security.MessageDigest;
import java.security.Principal;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.servlet.ServletException;
import com.caucho.config.inject.HandleAware;
import com.caucho.server.security.PasswordDigest;
import com.caucho.util.Base64;
/**
* All applications should extend AbstractAuthenticator to implement
* their custom authenticators. While this isn't absolutely required,
* it protects implementations from API changes.
*
* <p>The AbstractAuthenticator provides a single-signon cache. Users
* logged into one web-app will share the same principal.
*/
@SuppressWarnings("serial")
public class AbstractAuthenticator
implements Authenticator, HandleAware, java.io.Serializable
{
private static final Logger log
= Logger.getLogger(AbstractAuthenticator.class.getName());
private static final SingleSignon NULL_SINGLE_SIGNON = new NullSingleSignon();
protected String _passwordDigestAlgorithm = "MD5-base64";
protected String _passwordDigestRealm = "resin";
protected PasswordDigest _passwordDigest;
private boolean _logoutOnTimeout = true;
private Object _serializationHandle;
private SingleSignon _singleSignon;
/**
* Returns the password digest
*/
public PasswordDigest getPasswordDigest()
{
return _passwordDigest;
}
/**
* Sets the password digest. The password digest of the form:
* "algorithm-format", e.g. "MD5-base64".
*/
public void setPasswordDigest(PasswordDigest digest)
{
_passwordDigest = digest;
}
/**
* Returns the password digest algorithm
*/
public String getPasswordDigestAlgorithm()
{
return _passwordDigestAlgorithm;
}
/**
* Sets the password digest algorithm. The password digest of the form:
* "algorithm-format", e.g. "MD5-base64".
*/
public void setPasswordDigestAlgorithm(String digest)
{
_passwordDigestAlgorithm = digest;
}
/**
* Returns the password digest realm
*/
public String getPasswordDigestRealm()
{
return _passwordDigestRealm;
}
/**
* Sets the password digest realm.
*/
public void setPasswordDigestRealm(String realm)
{
_passwordDigestRealm = realm;
}
/**
* Returns true if the user should be logged out on a session timeout.
*/
public boolean getLogoutOnSessionTimeout()
{
return _logoutOnTimeout;
}
/**
* Sets true if the principal should logout when the session times out.
*/
public void setLogoutOnSessionTimeout(boolean logout)
{
_logoutOnTimeout = logout;
}
/**
* Adds a role mapping.
*/
public void addRoleMapping(Principal principal, String role)
{
}
/**
* Initialize the authenticator with the application.
*/
@PostConstruct
public void init()
throws ServletException
{
if (_passwordDigest != null) {
if (_passwordDigest.getAlgorithm() == null
|| _passwordDigest.getAlgorithm().equals("none")) {
_passwordDigest = null;
_passwordDigestAlgorithm = "none";
}
}
else if (_passwordDigestAlgorithm == null
|| _passwordDigestAlgorithm.equals("none")) {
}
else {
int p = _passwordDigestAlgorithm.indexOf('-');
if (p > 0) {
String algorithm = _passwordDigestAlgorithm.substring(0, p);
String format = _passwordDigestAlgorithm.substring(p + 1);
_passwordDigest = new PasswordDigest();
_passwordDigest.setAlgorithm(algorithm);
_passwordDigest.setFormat(format);
_passwordDigest.setRealm(_passwordDigestRealm);
_passwordDigest.init();
}
}
/*
if (Server.getCurrent() != null) {
_singleSignon = _localSingleSignon.get();
// server/1al4 vs server/1ak1
if (_singleSignon == null) {
MemorySingleSignon memorySignon = new MemorySingleSignon();
memorySignon.init();
_singleSignon = memorySignon;
_localSingleSignon.set(_singleSignon);
}
}
*/
}
//
// Authenticator API
//
@Override
public String getAlgorithm(Principal user)
{
PasswordUser password = getPasswordUser(user);
if (password != null) {
String algorithm = getAlgorithm(password.getPassword());
if (algorithm != null)
return algorithm;
}
if (_passwordDigest != null)
return _passwordDigest.getType();
else
return "plain";
}
private String getAlgorithm(char []password)
{
return DigestBuilder.getAlgorithm(password);
}
/**
* Authenticator main call to login a user.
*
* @param user the Login's user, generally a BasicPrincipal just containing
* the name, but may contain an X.509 certificate
* @param credentials the login credentials
* @param details extra information, e.g. HttpServletRequest
*/
@Override
public Principal authenticate(Principal user,
Credentials credentials,
Object details)
{
if (credentials instanceof PasswordCredentials) {
return authenticate(user, (PasswordCredentials) credentials, details);
}
else if (credentials instanceof HttpDigestCredentials) {
return authenticate(user, (HttpDigestCredentials) credentials, details);
}
else if (credentials instanceof DigestCredentials) {
return authenticate(user, (DigestCredentials) credentials, details);
}
else
return null;
}
/**
* Returns true if the user plays the named role.
*
* @param user the user to test
* @param role the role to test
*/
@Override
public boolean isUserInRole(Principal user, String role)
{
PasswordUser passwordUser = getPasswordUser(user);
if (passwordUser != null)
return passwordUser.isUserInRole(role);
else
return false;
}
/**
* Logs the user out from the session.
*
* @param user the logged in user
*/
@Override
public void logout(Principal user)
{
if (log.isLoggable(Level.FINE))
log.fine(this + " logout " + user);
}
//
// basic password authentication
//
/**
* Main authenticator API.
*/
protected Principal authenticate(Principal principal,
PasswordCredentials cred,
Object details)
{
return authenticate(principal, cred.getPassword());
}
/**
* Password-based authenticator.
*/
protected Principal authenticate(Principal principal,
char []password)
{
PasswordUser user = getPasswordUser(principal);
if (user == null || user.isDisabled())
return null;
String algorithm = "";
if (! isMatch(principal, algorithm, password, user.getPassword())
&& ! user.isAnonymous()) {
user = null;
}
if (user != null)
return user.getPrincipal();
else
return null;
}
//
// http digest authentication
//
/**
* Validates the user when HTTP Digest authentication.
* The HTTP Digest authentication uses the following algorithm
* to calculate the digest. The digest is then compared to
* the client digest.
*
* <code><pre>
* A1 = MD5(username + ':' + realm + ':' + password)
* A2 = MD5(method + ':' + uri)
* digest = MD5(A1 + ':' + nonce + A2)
* </pre></code>
*
* @param principal the user trying to authenticate.
* @param cred the digest credentials
*
* @return the logged in principal if successful
*/
protected Principal authenticate(Principal principal,
HttpDigestCredentials cred,
Object details)
{
String cnonce = cred.getCnonce();
String method = cred.getMethod();
String nc = cred.getNc();
String nonce = cred.getNonce();
String qop = cred.getQop();
String realm = cred.getRealm();
byte []clientDigest = cred.getResponse();
String uri = cred.getUri();
try {
if (clientDigest == null)
return null;
MessageDigest digest = MessageDigest.getInstance("MD5");
byte []a1 = getDigestSecret(principal, realm);
if (a1 == null)
return null;
digestUpdateHex(digest, a1);
digest.update((byte) ':');
for (int i = 0; i < nonce.length(); i++)
digest.update((byte) nonce.charAt(i));
if (qop != null) {
digest.update((byte) ':');
for (int i = 0; i < nc.length(); i++)
digest.update((byte) nc.charAt(i));
digest.update((byte) ':');
for (int i = 0; cnonce != null && i < cnonce.length(); i++)
digest.update((byte) cnonce.charAt(i));
digest.update((byte) ':');
for (int i = 0; qop != null && i < qop.length(); i++)
digest.update((byte) qop.charAt(i));
}
digest.update((byte) ':');
byte []a2 = digest(method + ":" + uri);
digestUpdateHex(digest, a2);
byte []serverDigest = digest.digest();
if (isMatch(clientDigest, serverDigest))
return principal;
else
return null;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//
// http digest authentication
//
/**
* Validates the user when Resin's Digest authentication.
* The digest authentication uses the following algorithm
* to calculate the digest. The digest is then compared to
* the client digest.
*
* <code><pre>
* A1 = MD5(username + ':' + realm + ':' + password)
* digest = MD5(A1 + ':' + nonce)
* </pre></code>
*
* @param principal the user trying to authenticate.
* @param cred the digest credentials
*
* @return the logged in principal if successful
*/
protected Principal authenticate(Principal principal,
DigestCredentials cred,
Object details)
{
String nonce = cred.getNonce();
String realm = cred.getRealm();
String clientDigest = cred.getDigest();
try {
if (clientDigest == null)
return null;
PasswordUser user = getPasswordUser(principal);
if (user == null || user.isDisabled())
return null;
String signed = new String(user.getPassword());
String algorithm = getAlgorithm(principal);
char []digest = DigestBuilder.getDigest(principal,
"",
user.getPassword(),
user.getPassword());
if (digest != null)
signed = new String(signed);
else if (algorithm != null && ! "plain".equals(algorithm)) {
int p = algorithm.lastIndexOf('}');
if (p > 0)
signed = algorithm.substring(0, p + 1) + signed;
}
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(principal.getName().getBytes("UTF-8"));
md.update(nonce.getBytes("UTF-8"));
md.update(signed.getBytes("UTF-8"));
byte []serverDigest = md.digest();
if (clientDigest.equals(Base64.encode(serverDigest)))
return principal;
else
return null;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Returns the digest view of the password. The default
* uses the PasswordDigest class if available, and returns the
* plaintext password if not.
*/
protected char []getPasswordDigest(String user, char []password)
{
if (_passwordDigest != null) {
char []digest = _passwordDigest.getPasswordDigest(user, password);
if (digest != null)
return digest;
}
char []digest = new char[password.length];
System.arraycopy(password, 0, digest, 0, password.length);
return digest;
}
/**
* Returns the digest secret for Digest authentication.
*/
protected byte []getDigestSecret(Principal principal, String realm)
{
PasswordUser user = getPasswordUser(principal);
if (user == null || user.isDisabled())
return null;
return getDigestSecret(principal, realm, user.getPassword());
}
protected byte []getDigestSecret(Principal principal,
String realm,
char []userPassword)
{
if (userPassword == null)
return null;
if (_passwordDigest != null)
return _passwordDigest.stringToDigest(userPassword);
String username = principal.getName();
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
String string = username + ":" + realm + ":";
byte []data = string.getBytes("UTF8");
digest.update(data);
char []password = userPassword;
for (int i = 0; i < password.length; i++)
digest.update((byte) password[i]);
return digest.digest();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//
// abstract methods
//
/**
* Abstract method to return a user based on the name
*
* @param userName the string user name
*
* @return the populated PasswordUser value
*/
protected PasswordUser getPasswordUser(String userName)
{
if (log.isLoggable(Level.FINE)) {
log.fine(this + " getPasswordUser() is not implemented for "
+ userName);
}
return null;
}
/**
* Returns the user based on a principal
*/
protected PasswordUser getPasswordUser(Principal principal)
{
return getPasswordUser(principal.getName());
}
//
// Compatibility
//
/**
* Returns the scoped single-signon
*/
public SingleSignon getSingleSignon()
{
if (_singleSignon == null) {
_singleSignon = AbstractSingleSignon.getCurrent();
if (_singleSignon == null)
_singleSignon = NULL_SINGLE_SIGNON;
}
if (_singleSignon != NULL_SINGLE_SIGNON)
return _singleSignon;
else
return null;
}
//
// utilities
//
private void digestUpdateHex(MessageDigest digest, byte []bytes)
{
for (int i = 0; i < bytes.length; i++) {
int b = bytes[i];
int d1 = (b >> 4) & 0xf;
int d2 = b & 0xf;
if (d1 < 10)
digest.update((byte) (d1 + '0'));
else
digest.update((byte) (d1 + 'a' - 10));
if (d2 < 10)
digest.update((byte) (d2 + '0'));
else
digest.update((byte) (d2 + 'a' - 10));
}
}
protected byte []stringToDigest(String digest)
{
if (digest == null)
return null;
int len = (digest.length() + 1) / 2;
byte []clientDigest = new byte[len];
for (int i = 0; i + 1 < digest.length(); i += 2) {
int ch1 = digest.charAt(i);
int ch2 = digest.charAt(i + 1);
int b = 0;
if (ch1 >= '0' && ch1 <= '9')
b += ch1 - '0';
else if (ch1 >= 'a' && ch1 <= 'f')
b += ch1 - 'a' + 10;
b *= 16;
if (ch2 >= '0' && ch2 <= '9')
b += ch2 - '0';
else if (ch2 >= 'a' && ch2 <= 'f')
b += ch2 - 'a' + 10;
clientDigest[i / 2] = (byte) b;
}
return clientDigest;
}
protected byte []digest(String value)
throws ServletException
{
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
byte []data = value.getBytes("UTF8");
return digest.digest(data);
} catch (Exception e) {
throw new ServletException(e);
}
}
/**
* Tests passwords
*/
private boolean isMatch(Principal userName,
String algorithm,
char []testPassword,
char []systemDigest)
{
char []testDigest = getDigest(userName, algorithm,
testPassword, systemDigest);
boolean isMatch = isMatch(testDigest, systemDigest);
Arrays.fill(testDigest, 'a');
return isMatch;
}
protected char []getDigest(Principal user,
String algorithm,
char []testPassword,
char []systemDigest)
{
char []digest = DigestBuilder.getDigest(user, algorithm,
testPassword, systemDigest);
if (digest != null)
return digest;
return getPasswordDigest(user.getName(), testPassword);
}
/**
* Tests passwords
*/
private boolean isMatch(char []password, char []userPassword)
{
int len = password.length;
if (len != userPassword.length)
return false;
for (int i = 0; i < len; i++) {
if (password[i] != userPassword[i])
return false;
}
return true;
}
/**
* Tests passwords
*/
private boolean isMatch(byte []password, byte []userPassword)
{
int len = password.length;
if (len != userPassword.length)
return false;
for (int i = 0; i < len; i++) {
if (password[i] != userPassword[i])
return false;
}
return true;
}
/**
* Sets the serialization handle
*/
public void setSerializationHandle(Object handle)
{
_serializationHandle = handle;
}
/**
* Serialize to the handle
*/
public Object writeReplace()
{
return _serializationHandle;
}
@Override
public String toString()
{
if (_passwordDigest != null) {
return (getClass().getSimpleName()
+ "[" + _passwordDigest.getAlgorithm()
+ "," + _passwordDigest.getRealm() + "]");
}
else {
return (getClass().getSimpleName()
+ "[" + _passwordDigestAlgorithm
+ "," + _passwordDigestRealm + "]");
}
}
}