View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2012  Mickael Guessant
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18   */
19  package davmail.http;
20  
21  import davmail.Settings;
22  import davmail.ui.CredentialPromptDialog;
23  import org.apache.log4j.Logger;
24  import org.ietf.jgss.*;
25  
26  import javax.security.auth.RefreshFailedException;
27  import javax.security.auth.Subject;
28  import javax.security.auth.callback.*;
29  import javax.security.auth.kerberos.KerberosTicket;
30  import javax.security.auth.login.LoginContext;
31  import javax.security.auth.login.LoginException;
32  import java.awt.*;
33  import java.io.BufferedReader;
34  import java.io.IOException;
35  import java.io.InputStreamReader;
36  import java.security.PrivilegedAction;
37  import java.security.Security;
38  
39  
40  /**
41   * Kerberos helper class.
42   */
43  @SuppressWarnings( {"removal"})
44  public class KerberosHelper {
45      protected static final Logger LOGGER = Logger.getLogger(KerberosHelper.class);
46      protected static final Object LOCK = new Object();
47      protected static final KerberosCallbackHandler KERBEROS_CALLBACK_HANDLER;
48      private static LoginContext clientLoginContext;
49  
50      static {
51          // Load Jaas configuration from class
52          Security.setProperty("login.configuration.provider", "davmail.http.KerberosLoginConfiguration");
53          // Kerberos callback handler singleton
54          KERBEROS_CALLBACK_HANDLER = new KerberosCallbackHandler();
55      }
56  
57      private KerberosHelper() {
58      }
59  
60      @SuppressWarnings("UseOfSystemOutOrSystemErr")
61      protected static class KerberosCallbackHandler implements CallbackHandler {
62          String principal;
63          String password;
64  
65          public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
66              for (Callback callback : callbacks) {
67                  if (callback instanceof NameCallback) {
68                      if (principal == null) {
69                          // if we get there kerberos token is missing or invalid
70                          if (Settings.getBooleanProperty("davmail.server") || GraphicsEnvironment.isHeadless()) {
71                              // headless or server mode
72                              System.out.print(((NameCallback) callback).getPrompt());
73                              BufferedReader inReader = new BufferedReader(new InputStreamReader(System.in));
74                              principal = inReader.readLine();
75                          } else {
76                              CredentialPromptDialog credentialPromptDialog = new CredentialPromptDialog(((NameCallback) callback).getPrompt());
77                              principal = credentialPromptDialog.getPrincipal();
78                              password = String.valueOf(credentialPromptDialog.getPassword());
79                          }
80                      }
81                      if (principal == null) {
82                          throw new IOException("KerberosCallbackHandler: failed to retrieve principal");
83                      }
84                      ((NameCallback) callback).setName(principal);
85  
86                  } else if (callback instanceof PasswordCallback) {
87                      if (password == null) {
88                          // if we get there kerberos token is missing or invalid
89                          if (Settings.getBooleanProperty("davmail.server") || GraphicsEnvironment.isHeadless()) {
90                              // headless or server mode
91                              System.out.print(((PasswordCallback) callback).getPrompt());
92                              BufferedReader inReader = new BufferedReader(new InputStreamReader(System.in));
93                              password = inReader.readLine();
94                          }
95                      }
96                      if (password == null) {
97                          throw new IOException("KerberosCallbackHandler: failed to retrieve password");
98                      }
99                      ((PasswordCallback) callback).setPassword(password.toCharArray());
100 
101                 } else {
102                     throw new UnsupportedCallbackException(callback);
103                 }
104             }
105         }
106     }
107 
108     /**
109      * Force client principal in callback handler
110      *
111      * @param principal client principal
112      */
113     public static void setClientPrincipal(String principal) {
114         KERBEROS_CALLBACK_HANDLER.principal = principal;
115     }
116 
117     /**
118      * Force client password in callback handler
119      *
120      * @param password client password
121      */
122     public static void setClientPassword(String password) {
123         KERBEROS_CALLBACK_HANDLER.password = password;
124     }
125 
126     /**
127      * Get response Kerberos token for host with provided token.
128      *
129      * @param protocol target protocol
130      * @param host     target host
131      * @param token    input token
132      * @return response token
133      * @throws GSSException   on error
134      * @throws LoginException on error
135      */
136     public static byte[] initSecurityContext(final String protocol, final String host, final byte[] token) throws GSSException, LoginException {
137         return initSecurityContext(protocol, host, null, token);
138     }
139 
140     /**
141      * Get response Kerberos token for host with provided token, use client provided delegation credentials.
142      * Used to authenticate with target host on a gateway server with client credentials,
143      * gateway must have its own principal authorized for delegation
144      *
145      * @param protocol             target protocol
146      * @param host                 target host
147      * @param delegatedCredentials client delegated credentials
148      * @param token                input token
149      * @return response token
150      * @throws GSSException   on error
151      * @throws LoginException on error
152      */
153     public static byte[] initSecurityContext(final String protocol, final String host, final GSSCredential delegatedCredentials, final byte[] token) throws GSSException, LoginException {
154         LOGGER.debug("KerberosHelper.initSecurityContext " + protocol + '@' + host + ' ' + token.length + " bytes token");
155 
156         synchronized (LOCK) {
157             // check cached TGT
158             if (clientLoginContext != null) {
159                 for (KerberosTicket kerberosTicket : clientLoginContext.getSubject().getPrivateCredentials(KerberosTicket.class)) {
160                     if (kerberosTicket.getServer().getName().startsWith("krbtgt") && !kerberosTicket.isCurrent()) {
161                         LOGGER.debug("KerberosHelper.clientLogin cached TGT expired, try to relogin");
162                         clientLoginContext = null;
163                     }
164                 }
165             }
166             // create client login context
167             if (clientLoginContext == null) {
168                 final LoginContext localLoginContext = new LoginContext("spnego-client", KERBEROS_CALLBACK_HANDLER);
169                 localLoginContext.login();
170                 clientLoginContext = localLoginContext;
171             }
172             // try to renew almost expired tickets
173             for (KerberosTicket kerberosTicket : clientLoginContext.getSubject().getPrivateCredentials(KerberosTicket.class)) {
174                 LOGGER.debug("KerberosHelper.clientLogin ticket for " + kerberosTicket.getServer().getName() + " expires at " + kerberosTicket.getEndTime());
175                 if (kerberosTicket.getEndTime().getTime() < System.currentTimeMillis() + 10000) {
176                     if (kerberosTicket.isRenewable()) {
177                         try {
178                             kerberosTicket.refresh();
179                         } catch (RefreshFailedException e) {
180                             LOGGER.debug("KerberosHelper.clientLogin failed to renew ticket " + kerberosTicket);
181                         }
182                     } else {
183                         LOGGER.debug("KerberosHelper.clientLogin ticket is not renewable");
184                     }
185                 }
186             }
187 
188             Object result = internalInitSecContext(protocol, host, delegatedCredentials, token);
189             if (result instanceof GSSException) {
190                 LOGGER.info("KerberosHelper.initSecurityContext exception code " + ((GSSException) result).getMajor() + " minor code " + ((GSSException) result).getMinor() + " message " + ((Throwable) result).getMessage());
191                 throw (GSSException) result;
192             }
193 
194             LOGGER.debug("KerberosHelper.initSecurityContext return " + ((byte[]) result).length + " bytes token");
195             return (byte[]) result;
196         }
197     }
198 
199     protected static Object internalInitSecContext(final String protocol, final String host, final GSSCredential delegatedCredentials, final byte[] token) {
200         return Subject.doAs(clientLoginContext.getSubject(), (PrivilegedAction<Object>) () -> {
201             Object result;
202             GSSContext context = null;
203             try {
204                 GSSManager manager = GSSManager.getInstance();
205                 GSSName serverName = manager.createName(protocol + '@' + host, GSSName.NT_HOSTBASED_SERVICE);
206                 // Kerberos v5 OID
207                 Oid krb5Oid = new Oid("1.2.840.113554.1.2.2");
208 
209                 context = manager.createContext(serverName, krb5Oid, delegatedCredentials, GSSContext.DEFAULT_LIFETIME);
210 
211                 //context.requestMutualAuth(true);
212                 // TODO: used by IIS to pass token to Exchange ?
213                 context.requestCredDeleg(true);
214 
215                 result = context.initSecContext(token, 0, token.length);
216             } catch (GSSException e) {
217                 result = e;
218             } finally {
219                 if (context != null) {
220                     try {
221                         context.dispose();
222                     } catch (GSSException e) {
223                         LOGGER.debug("KerberosHelper.internalInitSecContext " + e + ' ' + e.getMessage());
224                     }
225                 }
226             }
227             return result;
228         });
229     }
230 
231     /**
232      * Create server side Kerberos login context for provided credentials.
233      *
234      * @param serverPrincipal server principal
235      * @param serverPassword  server password
236      * @return LoginContext server login context
237      * @throws LoginException on error
238      */
239     public static LoginContext serverLogin(final String serverPrincipal, final String serverPassword) throws LoginException {
240         LoginContext serverLoginContext = new LoginContext("spnego-server", callbacks -> {
241             for (Callback callback : callbacks) {
242                 if (callback instanceof NameCallback) {
243                     final NameCallback nameCallback = (NameCallback) callback;
244                     nameCallback.setName(serverPrincipal);
245                 } else if (callback instanceof PasswordCallback) {
246                     final PasswordCallback passCallback = (PasswordCallback) callback;
247                     passCallback.setPassword(serverPassword.toCharArray());
248                 } else {
249                     throw new UnsupportedCallbackException(callback);
250                 }
251             }
252 
253         });
254         serverLoginContext.login();
255         return serverLoginContext;
256     }
257 
258     /**
259      * Contains server Kerberos context information in server mode.
260      */
261     public static class SecurityContext {
262         /**
263          * response token
264          */
265         public byte[] token;
266         /**
267          * authenticated principal
268          */
269         public String principal;
270         /**
271          * client delegated credential
272          */
273         public GSSCredential clientCredential;
274     }
275 
276     /**
277      * Check client provided Kerberos token in server login context
278      *
279      * @param serverLoginContext server login context
280      * @param token              Kerberos client token
281      * @return result with client principal and optional returned Kerberos token
282      * @throws GSSException on error
283      */
284     public static SecurityContext acceptSecurityContext(LoginContext serverLoginContext, final byte[] token) throws GSSException {
285         Object result = Subject.doAs(serverLoginContext.getSubject(), (PrivilegedAction<Object>) () -> {
286             Object innerResult;
287             SecurityContext securityContext = new SecurityContext();
288             GSSContext context = null;
289             try {
290                 GSSManager manager = GSSManager.getInstance();
291 
292                 // get server credentials from context
293                 Oid krb5oid = new Oid("1.2.840.113554.1.2.2");
294                 GSSCredential serverCreds = manager.createCredential(null/* use name from login context*/,
295                         GSSCredential.DEFAULT_LIFETIME,
296                         krb5oid,
297                         GSSCredential.ACCEPT_ONLY/* server mode */);
298                 context = manager.createContext(serverCreds);
299 
300                 securityContext.token = context.acceptSecContext(token, 0, token.length);
301                 if (context.isEstablished()) {
302                     securityContext.principal = context.getSrcName().toString();
303                     LOGGER.debug("Authenticated user: " + securityContext.principal);
304                     if (!context.getCredDelegState()) {
305                         LOGGER.debug("Credentials can not be delegated");
306                     } else {
307                         // Get client delegated credentials from context (gateway mode)
308                         securityContext.clientCredential = context.getDelegCred();
309                     }
310                 }
311                 innerResult = securityContext;
312             } catch (GSSException e) {
313                 innerResult = e;
314             } finally {
315                 if (context != null) {
316                     try {
317                         context.dispose();
318                     } catch (GSSException e) {
319                         LOGGER.debug("KerberosHelper.acceptSecurityContext " + e + ' ' + e.getMessage());
320                     }
321                 }
322             }
323             return innerResult;
324         });
325         if (result instanceof GSSException) {
326             LOGGER.info("KerberosHelper.acceptSecurityContext exception code " + ((GSSException) result).getMajor() + " minor code " + ((GSSException) result).getMinor() + " message " + ((Throwable) result).getMessage());
327             throw (GSSException) result;
328         }
329         return (SecurityContext) result;
330     }
331 }