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  public class KerberosHelper {
44      protected static final Logger LOGGER = Logger.getLogger(KerberosHelper.class);
45      protected static final Object LOCK = new Object();
46      protected static final KerberosCallbackHandler KERBEROS_CALLBACK_HANDLER;
47      private static LoginContext clientLoginContext;
48  
49      static {
50          // Load Jaas configuration from class
51          Security.setProperty("login.configuration.provider", "davmail.http.KerberosLoginConfiguration");
52          // Kerberos callback handler singleton
53          KERBEROS_CALLBACK_HANDLER = new KerberosCallbackHandler();
54      }
55  
56      private KerberosHelper() {
57      }
58  
59      @SuppressWarnings("UseOfSystemOutOrSystemErr")
60      protected static class KerberosCallbackHandler implements CallbackHandler {
61          String principal;
62          String password;
63  
64          public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
65              for (Callback callback : callbacks) {
66                  if (callback instanceof NameCallback) {
67                      if (principal == null) {
68                          // if we get there kerberos token is missing or invalid
69                          if (Settings.getBooleanProperty("davmail.server") || GraphicsEnvironment.isHeadless()) {
70                              // headless or server mode
71                              System.out.print(((NameCallback) callback).getPrompt());
72                              BufferedReader inReader = new BufferedReader(new InputStreamReader(System.in));
73                              principal = inReader.readLine();
74                          } else {
75                              CredentialPromptDialog credentialPromptDialog = new CredentialPromptDialog(((NameCallback) callback).getPrompt());
76                              principal = credentialPromptDialog.getPrincipal();
77                              password = String.valueOf(credentialPromptDialog.getPassword());
78                          }
79                      }
80                      if (principal == null) {
81                          throw new IOException("KerberosCallbackHandler: failed to retrieve principal");
82                      }
83                      ((NameCallback) callback).setName(principal);
84  
85                  } else if (callback instanceof PasswordCallback) {
86                      if (password == null) {
87                          // if we get there kerberos token is missing or invalid
88                          if (Settings.getBooleanProperty("davmail.server") || GraphicsEnvironment.isHeadless()) {
89                              // headless or server mode
90                              System.out.print(((PasswordCallback) callback).getPrompt());
91                              BufferedReader inReader = new BufferedReader(new InputStreamReader(System.in));
92                              password = inReader.readLine();
93                          }
94                      }
95                      if (password == null) {
96                          throw new IOException("KerberosCallbackHandler: failed to retrieve password");
97                      }
98                      ((PasswordCallback) callback).setPassword(password.toCharArray());
99  
100                 } else {
101                     throw new UnsupportedCallbackException(callback);
102                 }
103             }
104         }
105     }
106 
107     /**
108      * Force client principal in callback handler
109      *
110      * @param principal client principal
111      */
112     public static void setClientPrincipal(String principal) {
113         KERBEROS_CALLBACK_HANDLER.principal = principal;
114     }
115 
116     /**
117      * Force client password in callback handler
118      *
119      * @param password client password
120      */
121     public static void setClientPassword(String password) {
122         KERBEROS_CALLBACK_HANDLER.password = password;
123     }
124 
125     /**
126      * Get response Kerberos token for host with provided token.
127      *
128      * @param protocol target protocol
129      * @param host     target host
130      * @param token    input token
131      * @return response token
132      * @throws GSSException   on error
133      * @throws LoginException on error
134      */
135     public static byte[] initSecurityContext(final String protocol, final String host, final byte[] token) throws GSSException, LoginException {
136         return initSecurityContext(protocol, host, null, token);
137     }
138 
139     /**
140      * Get response Kerberos token for host with provided token, use client provided delegation credentials.
141      * Used to authenticate with target host on a gateway server with client credentials,
142      * gateway must have its own principal authorized for delegation
143      *
144      * @param protocol             target protocol
145      * @param host                 target host
146      * @param delegatedCredentials client delegated credentials
147      * @param token                input token
148      * @return response token
149      * @throws GSSException   on error
150      * @throws LoginException on error
151      */
152     public static byte[] initSecurityContext(final String protocol, final String host, final GSSCredential delegatedCredentials, final byte[] token) throws GSSException, LoginException {
153         LOGGER.debug("KerberosHelper.initSecurityContext " + protocol + '@' + host + ' ' + token.length + " bytes token");
154 
155         synchronized (LOCK) {
156             // check cached TGT
157             if (clientLoginContext != null) {
158                 for (Object ticket : clientLoginContext.getSubject().getPrivateCredentials(KerberosTicket.class)) {
159                     KerberosTicket kerberosTicket = (KerberosTicket) ticket;
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 (Object ticket : clientLoginContext.getSubject().getPrivateCredentials(KerberosTicket.class)) {
174                 KerberosTicket kerberosTicket = (KerberosTicket) ticket;
175                 LOGGER.debug("KerberosHelper.clientLogin ticket for " + kerberosTicket.getServer().getName() + " expires at " + kerberosTicket.getEndTime());
176                 if (kerberosTicket.getEndTime().getTime() < System.currentTimeMillis() + 10000) {
177                     if (kerberosTicket.isRenewable()) {
178                         try {
179                             kerberosTicket.refresh();
180                         } catch (RefreshFailedException e) {
181                             LOGGER.debug("KerberosHelper.clientLogin failed to renew ticket " + kerberosTicket.toString());
182                         }
183                     } else {
184                         LOGGER.debug("KerberosHelper.clientLogin ticket is not renewable");
185                     }
186                 }
187             }
188 
189             Object result = internalInitSecContext(protocol, host, delegatedCredentials, token);
190             if (result instanceof GSSException) {
191                 LOGGER.info("KerberosHelper.initSecurityContext exception code " + ((GSSException) result).getMajor() + " minor code " + ((GSSException) result).getMinor() + " message " + ((Throwable) result).getMessage());
192                 throw (GSSException) result;
193             }
194 
195             LOGGER.debug("KerberosHelper.initSecurityContext return " + ((byte[]) result).length + " bytes token");
196             return (byte[]) result;
197         }
198     }
199 
200     protected static Object internalInitSecContext(final String protocol, final String host, final GSSCredential delegatedCredentials, final byte[] token) {
201         return Subject.doAs(clientLoginContext.getSubject(), new PrivilegedAction() {
202 
203             public Object run() {
204                 Object result;
205                 GSSContext context = null;
206                 try {
207                     GSSManager manager = GSSManager.getInstance();
208                     GSSName serverName = manager.createName(protocol + '@' + host, GSSName.NT_HOSTBASED_SERVICE);
209                     // Kerberos v5 OID
210                     Oid krb5Oid = new Oid("1.2.840.113554.1.2.2");
211 
212                     context = manager.createContext(serverName, krb5Oid, delegatedCredentials, GSSContext.DEFAULT_LIFETIME);
213 
214                     //context.requestMutualAuth(true);
215                     // TODO: used by IIS to pass token to Exchange ?
216                     context.requestCredDeleg(true);
217 
218                     result = context.initSecContext(token, 0, token.length);
219                 } catch (GSSException e) {
220                     result = e;
221                 } finally {
222                     if (context != null) {
223                         try {
224                             context.dispose();
225                         } catch (GSSException e) {
226                             LOGGER.debug("KerberosHelper.internalInitSecContext " + e + ' ' + e.getMessage());
227                         }
228                     }
229                 }
230                 return result;
231             }
232         });
233     }
234 
235     /**
236      * Create server side Kerberos login context for provided credentials.
237      *
238      * @param serverPrincipal server principal
239      * @param serverPassword  server passsword
240      * @return LoginContext server login context
241      * @throws LoginException on error
242      */
243     public static LoginContext serverLogin(final String serverPrincipal, final String serverPassword) throws LoginException {
244         LoginContext serverLoginContext = new LoginContext("spnego-server", new CallbackHandler() {
245 
246             public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
247                 for (Callback callback : callbacks) {
248                     if (callback instanceof NameCallback) {
249                         final NameCallback nameCallback = (NameCallback) callback;
250                         nameCallback.setName(serverPrincipal);
251                     } else if (callback instanceof PasswordCallback) {
252                         final PasswordCallback passCallback = (PasswordCallback) callback;
253                         passCallback.setPassword(serverPassword.toCharArray());
254                     } else {
255                         throw new UnsupportedCallbackException(callback);
256                     }
257                 }
258 
259             }
260         });
261         serverLoginContext.login();
262         return serverLoginContext;
263     }
264 
265     /**
266      * Contains server Kerberos context information in server mode.
267      */
268     public static class SecurityContext {
269         /**
270          * response token
271          */
272         public byte[] token;
273         /**
274          * authenticated principal
275          */
276         public String principal;
277         /**
278          * client delegated credential
279          */
280         public GSSCredential clientCredential;
281     }
282 
283     /**
284      * Check client provided Kerberos token in server login context
285      *
286      * @param serverLoginContext server login context
287      * @param token              Kerberos client token
288      * @return result with client principal and optional returned Kerberos token
289      * @throws GSSException on error
290      */
291     public static SecurityContext acceptSecurityContext(LoginContext serverLoginContext, final byte[] token) throws GSSException {
292         Object result = Subject.doAs(serverLoginContext.getSubject(), new PrivilegedAction() {
293 
294             public Object run() {
295                 Object innerResult;
296                 SecurityContext securityContext = new SecurityContext();
297                 GSSContext context = null;
298                 try {
299                     GSSManager manager = GSSManager.getInstance();
300 
301                     // get server credentials from context
302                     Oid krb5oid = new Oid("1.2.840.113554.1.2.2");
303                     GSSCredential serverCreds = manager.createCredential(null/* use name from login context*/,
304                             GSSCredential.DEFAULT_LIFETIME,
305                             krb5oid,
306                             GSSCredential.ACCEPT_ONLY/* server mode */);
307                     context = manager.createContext(serverCreds);
308 
309                     securityContext.token = context.acceptSecContext(token, 0, token.length);
310                     if (context.isEstablished()) {
311                         securityContext.principal = context.getSrcName().toString();
312                         LOGGER.debug("Authenticated user: " + securityContext.principal);
313                         if (!context.getCredDelegState()) {
314                             LOGGER.debug("Credentials can not be delegated");
315                         } else {
316                             // Get client delegated credentials from context (gateway mode)
317                             securityContext.clientCredential = context.getDelegCred();
318                         }
319                     }
320                     innerResult = securityContext;
321                 } catch (GSSException e) {
322                     innerResult = e;
323                 } finally {
324                     if (context != null) {
325                         try {
326                             context.dispose();
327                         } catch (GSSException e) {
328                             LOGGER.debug("KerberosHelper.acceptSecurityContext " + e + ' ' + e.getMessage());
329                         }
330                     }
331                 }
332                 return innerResult;
333             }
334         });
335         if (result instanceof GSSException) {
336             LOGGER.info("KerberosHelper.acceptSecurityContext exception code " + ((GSSException) result).getMajor() + " minor code " + ((GSSException) result).getMinor() + " message " + ((Throwable) result).getMessage());
337             throw (GSSException) result;
338         }
339         return (SecurityContext) result;
340     }
341 }