mirror of
https://github.com/moparisthebest/davmail
synced 2025-01-05 18:58:02 -05:00
Kerberos authentication implementation: SpNegoScheme to implement Negotiate authentication scheme, KerberosHelper to handle ticket access and KerberosLoginConfiguration to replace JAAS configuration file
git-svn-id: http://svn.code.sf.net/p/davmail/code/trunk@2057 3d1905a2-6b24-0410-a738-b14d5a86fcbd
This commit is contained in:
parent
76058edc6d
commit
4d8c6d4c52
130
src/java/davmail/http/KerberosHelper.java
Normal file
130
src/java/davmail/http/KerberosHelper.java
Normal file
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
|
||||
* Copyright (C) 2012 Mickael Guessant
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
package davmail.http;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
import org.ietf.jgss.*;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.callback.*;
|
||||
import javax.security.auth.login.LoginContext;
|
||||
import javax.security.auth.login.LoginException;
|
||||
import java.io.IOException;
|
||||
import java.security.PrivilegedAction;
|
||||
import java.security.Security;
|
||||
|
||||
|
||||
/**
|
||||
* Kerberos helper class.
|
||||
*/
|
||||
public class KerberosHelper {
|
||||
protected static final Logger LOGGER = Logger.getLogger(KerberosHelper.class);
|
||||
protected static final Object LOCK = new Object();
|
||||
protected static KerberosCallbackHandler kerberosCallbackHandler;
|
||||
protected static LoginContext loginContext;
|
||||
|
||||
static {
|
||||
Security.setProperty("login.configuration.provider", "davmail.http.KerberosLoginConfiguration");
|
||||
kerberosCallbackHandler = new KerberosCallbackHandler();
|
||||
}
|
||||
|
||||
protected static class KerberosCallbackHandler implements CallbackHandler {
|
||||
String principal;
|
||||
String password;
|
||||
|
||||
protected KerberosCallbackHandler() {
|
||||
}
|
||||
|
||||
protected KerberosCallbackHandler(String principal, String password) {
|
||||
this.principal = principal;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
|
||||
for (int i = 0; i < callbacks.length; i++) {
|
||||
if (callbacks[i] instanceof NameCallback) {
|
||||
if (principal == null) {
|
||||
throw new UnsupportedCallbackException(callbacks[i]);
|
||||
}
|
||||
final NameCallback nameCallback = (NameCallback) callbacks[i];
|
||||
nameCallback.setName(principal);
|
||||
} else if (callbacks[i] instanceof PasswordCallback) {
|
||||
if (password == null) {
|
||||
throw new UnsupportedCallbackException(callbacks[i]);
|
||||
}
|
||||
final PasswordCallback passCallback = (PasswordCallback) callbacks[i];
|
||||
passCallback.setPassword(password.toCharArray());
|
||||
} else {
|
||||
throw new UnsupportedCallbackException(callbacks[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] getToken(final String host, final byte[] token) throws GSSException, LoginException {
|
||||
LOGGER.debug("KerberosHelper.getToken " + host + " " + token.length + " bytes token");
|
||||
|
||||
LoginContext loginContext = login();
|
||||
|
||||
Object result = Subject.doAs(loginContext.getSubject(), new PrivilegedAction() {
|
||||
|
||||
public Object run() {
|
||||
Object result;
|
||||
try {
|
||||
GSSManager manager = GSSManager.getInstance();
|
||||
GSSName serverName = manager.createName("HTTP/" + host, null);
|
||||
// Kerberos v5 OID
|
||||
Oid krb5Oid = new Oid("1.2.840.113554.1.2.2");
|
||||
|
||||
GSSContext context = manager.createContext(serverName, krb5Oid, null,
|
||||
GSSContext.DEFAULT_LIFETIME);
|
||||
|
||||
//context.requestMutualAuth(true);
|
||||
context.requestCredDeleg(true);
|
||||
|
||||
result = context.initSecContext(token, 0, token.length);
|
||||
} catch (GSSException e) {
|
||||
result = e;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
});
|
||||
if (result instanceof GSSException) {
|
||||
throw (GSSException) result;
|
||||
}
|
||||
|
||||
LOGGER.debug("KerberosHelper.getToken return " + ((byte[]) result).length + " bytes token");
|
||||
return (byte[]) result;
|
||||
}
|
||||
|
||||
public static void setCredentials(String principal, String password) {
|
||||
kerberosCallbackHandler = new KerberosCallbackHandler(principal, password);
|
||||
}
|
||||
|
||||
public static LoginContext login() throws LoginException {
|
||||
synchronized (LOCK) {
|
||||
if (loginContext == null) {
|
||||
final LoginContext localLoginContext = new LoginContext("spnego-client", kerberosCallbackHandler);
|
||||
localLoginContext.login();
|
||||
loginContext = localLoginContext;
|
||||
}
|
||||
}
|
||||
return loginContext;
|
||||
}
|
||||
}
|
56
src/java/davmail/http/KerberosLoginConfiguration.java
Normal file
56
src/java/davmail/http/KerberosLoginConfiguration.java
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
|
||||
* Copyright (C) 2012 Mickael Guessant
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
package davmail.http;
|
||||
|
||||
import javax.security.auth.login.AppConfigurationEntry;
|
||||
import javax.security.auth.login.Configuration;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* Custom JAAS login configuration.
|
||||
* Equivalent to the following configuration:
|
||||
* spnego-client {
|
||||
* com.sun.security.auth.module.Krb5LoginModule required;
|
||||
* };
|
||||
* <p/>
|
||||
*/
|
||||
public class KerberosLoginConfiguration extends Configuration {
|
||||
protected static final AppConfigurationEntry[] CLIENT_LOGIN_MODULE;
|
||||
|
||||
static {
|
||||
HashMap<String, String> loginModuleOptions = new HashMap<String, String>();
|
||||
loginModuleOptions.put("useTicketCache", "true");
|
||||
//loginModuleOptions.put("doNotPrompt", "true");
|
||||
CLIENT_LOGIN_MODULE = new AppConfigurationEntry[]{new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, loginModuleOptions)};
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
|
||||
if ("spnego-client".equals(name)) {
|
||||
return CLIENT_LOGIN_MODULE;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
// nothing to do
|
||||
}
|
||||
}
|
217
src/java/davmail/http/SpNegoScheme.java
Normal file
217
src/java/davmail/http/SpNegoScheme.java
Normal file
@ -0,0 +1,217 @@
|
||||
/*
|
||||
* DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
|
||||
* Copyright (C) 2012 Mickael Guessant
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
package davmail.http;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.httpclient.Credentials;
|
||||
import org.apache.commons.httpclient.Header;
|
||||
import org.apache.commons.httpclient.HttpMethod;
|
||||
import org.apache.commons.httpclient.URIException;
|
||||
import org.apache.commons.httpclient.auth.*;
|
||||
import org.apache.commons.httpclient.util.EncodingUtil;
|
||||
import org.ietf.jgss.GSSException;
|
||||
|
||||
import javax.security.auth.login.LoginException;
|
||||
|
||||
/**
|
||||
* Implement spnego (Negotiate) authentication scheme.
|
||||
*/
|
||||
public class SpNegoScheme implements AuthScheme {
|
||||
private static final int UNINITIATED = 0;
|
||||
private static final int INITIATED = 1;
|
||||
private static final int TYPE1_MSG_GENERATED = 2;
|
||||
private static final int TYPE2_MSG_RECEIVED = 3;
|
||||
private static final int TYPE3_MSG_GENERATED = 4;
|
||||
private static final int FAILED = Integer.MAX_VALUE;
|
||||
|
||||
private byte[] serverToken;
|
||||
/**
|
||||
* Authentication process state
|
||||
*/
|
||||
private int state;
|
||||
|
||||
/**
|
||||
* Processes the Negotiate challenge.
|
||||
*
|
||||
* @param challenge the challenge string
|
||||
* @throws MalformedChallengeException is thrown if the authentication challenge is malformed
|
||||
*/
|
||||
public void processChallenge(final String challenge) throws MalformedChallengeException {
|
||||
String authScheme = AuthChallengeParser.extractScheme(challenge);
|
||||
if (!authScheme.equalsIgnoreCase(getSchemeName())) {
|
||||
throw new MalformedChallengeException("Invalid Negotiate challenge: " + challenge);
|
||||
}
|
||||
int spaceIndex = challenge.indexOf(' ');
|
||||
if (spaceIndex != -1) {
|
||||
// step 2: received server challenge
|
||||
serverToken = Base64.decodeBase64(EncodingUtil.getBytes(
|
||||
challenge.substring(spaceIndex, challenge.length()).trim(), "ASCII"));
|
||||
this.state = TYPE2_MSG_RECEIVED;
|
||||
} else {
|
||||
this.serverToken = null;
|
||||
if (this.state == UNINITIATED) {
|
||||
this.state = INITIATED;
|
||||
} else {
|
||||
this.state = FAILED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns textual designation of the Negotiate authentication scheme.
|
||||
*
|
||||
* @return <code>Negotiate</code>
|
||||
*/
|
||||
public String getSchemeName() {
|
||||
return "Negotiate";
|
||||
}
|
||||
|
||||
/**
|
||||
* Not used with Negotiate.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public String getParameter(String s) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Not used with Negotiate.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public String getRealm() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated.
|
||||
*/
|
||||
@Deprecated
|
||||
public String getID() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Negotiate is connection based.
|
||||
*
|
||||
* @return true
|
||||
*/
|
||||
public boolean isConnectionBased() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the Negotiate authentication process has been completed.
|
||||
*
|
||||
* @return <tt>true</tt> if authorization has been processed
|
||||
*/
|
||||
public boolean isComplete() {
|
||||
return state == TYPE3_MSG_GENERATED || state == FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Not implemented.
|
||||
*
|
||||
* @param credentials user credentials
|
||||
* @param method method name
|
||||
* @param uri URI
|
||||
* @return an Negotiate authorization string
|
||||
* @throws org.apache.commons.httpclient.auth.InvalidCredentialsException
|
||||
* if authentication credentials
|
||||
* are not valid or not applicable for this authentication scheme
|
||||
* @throws org.apache.commons.httpclient.auth.AuthenticationException
|
||||
* if authorization string cannot
|
||||
* be generated due to an authentication failure
|
||||
*/
|
||||
@Deprecated
|
||||
public String authenticate(final Credentials credentials, String method, String uri) throws AuthenticationException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces Negotiate authorization string for the given set of
|
||||
* {@link Credentials}.
|
||||
*
|
||||
* @param credentials The set of credentials to be used for authentication
|
||||
* @param httpMethod The method being authenticated
|
||||
* @return an Negotiate authorization string
|
||||
* @throws org.apache.commons.httpclient.auth.InvalidCredentialsException
|
||||
* if authentication credentials
|
||||
* are not valid or not applicable for this authentication scheme
|
||||
* @throws AuthenticationException if authorization string cannot
|
||||
* be generated due to an authentication failure
|
||||
*/
|
||||
public String authenticate(Credentials credentials, HttpMethod httpMethod) throws AuthenticationException {
|
||||
if (this.state == UNINITIATED) {
|
||||
throw new IllegalStateException("Negotiate authentication process has not been initiated");
|
||||
}
|
||||
String host = null;
|
||||
try {
|
||||
host = httpMethod.getURI().getHost();
|
||||
} catch (URIException e) {
|
||||
// ignore
|
||||
}
|
||||
if (host == null) {
|
||||
Header header = httpMethod.getRequestHeader("Host");
|
||||
if (header != null) {
|
||||
host = header.getValue();
|
||||
if (host.indexOf(':') >= 0) {
|
||||
host = host.substring(0, host.indexOf(':'));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (host == null) {
|
||||
throw new IllegalStateException("Negotiate authentication failed: empty host");
|
||||
}
|
||||
|
||||
// no credentials needed
|
||||
String response;
|
||||
try {
|
||||
if (this.state == INITIATED || this.state == FAILED) {
|
||||
// send initial token to server
|
||||
response = EncodingUtil.getAsciiString(Base64.encodeBase64(KerberosHelper.getToken(host, new byte[0])));
|
||||
this.state = TYPE1_MSG_GENERATED;
|
||||
} else {
|
||||
// send challenge response
|
||||
response = EncodingUtil.getAsciiString(Base64.encodeBase64(KerberosHelper.getToken(host, serverToken)));
|
||||
this.state = TYPE3_MSG_GENERATED;
|
||||
}
|
||||
} catch (GSSException gsse) {
|
||||
state = FAILED;
|
||||
if (gsse.getMajor() == GSSException.DEFECTIVE_CREDENTIAL
|
||||
|| gsse.getMajor() == GSSException.CREDENTIALS_EXPIRED)
|
||||
throw new InvalidCredentialsException(gsse.getMessage(), gsse);
|
||||
if (gsse.getMajor() == GSSException.NO_CRED)
|
||||
throw new CredentialsNotAvailableException(gsse.getMessage(), gsse);
|
||||
if (gsse.getMajor() == GSSException.DEFECTIVE_TOKEN
|
||||
|| gsse.getMajor() == GSSException.DUPLICATE_TOKEN
|
||||
|| gsse.getMajor() == GSSException.OLD_TOKEN)
|
||||
throw new AuthChallengeException(gsse.getMessage(), gsse);
|
||||
// other error
|
||||
throw new AuthenticationException(gsse.getMessage(), gsse);
|
||||
} catch (LoginException e) {
|
||||
state = FAILED;
|
||||
throw new InvalidCredentialsException(e.getMessage(), e);
|
||||
}
|
||||
return "Negotiate " + response;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user