View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2010  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.exchange.auth;
20  
21  import davmail.Settings;
22  import davmail.exception.DavMailAuthenticationException;
23  import davmail.exception.DavMailException;
24  import davmail.exchange.ews.BaseShape;
25  import davmail.exchange.ews.DistinguishedFolderId;
26  import davmail.exchange.ews.GetFolderMethod;
27  import davmail.exchange.ews.GetUserConfigurationMethod;
28  import davmail.http.HttpClientAdapter;
29  import org.apache.http.client.methods.CloseableHttpResponse;
30  import org.apache.log4j.Logger;
31  
32  import javax.swing.*;
33  import java.io.IOException;
34  import java.lang.reflect.InvocationTargetException;
35  import java.net.Authenticator;
36  import java.net.PasswordAuthentication;
37  import java.net.URI;
38  import java.security.Security;
39  
40  public class O365InteractiveAuthenticator implements ExchangeAuthenticator {
41  
42      private static final int MAX_COUNT = 300;
43      private static final Logger LOGGER = Logger.getLogger(O365InteractiveAuthenticator.class);
44  
45      static {
46          // disable HTTP/2 loader on Java 14 and later to enable custom socket factory
47          System.setProperty("com.sun.webkit.useHTTP2Loader", "false");
48      }
49  
50      boolean isAuthenticated = false;
51      String errorCode = null;
52      String code = null;
53  
54      URI ewsUrl = URI.create(Settings.getO365Url());
55  
56      private O365InteractiveAuthenticatorFrame o365InteractiveAuthenticatorFrame;
57      private O365InteractiveAuthenticatorSWT o365InteractiveAuthenticatorSWT;
58      private O365ManualAuthenticatorDialog o365ManualAuthenticatorDialog;
59  
60      private String username;
61      private String password;
62      private O365Token token;
63  
64      public O365Token getToken() {
65          return token;
66      }
67  
68      @Override
69      public URI getExchangeUri() {
70          return ewsUrl;
71      }
72  
73      public String getUsername() {
74          return username;
75      }
76  
77      public void setUsername(String username) {
78          this.username = username;
79      }
80  
81      public void setPassword(String password) {
82          this.password = password;
83      }
84  
85      /**
86       * Return a pool enabled HttpClientAdapter instance to access O365
87       *
88       * @return HttpClientAdapter instance
89       */
90      @Override
91      public HttpClientAdapter getHttpClientAdapter() {
92          return new HttpClientAdapter(getExchangeUri(), username, password, true);
93      }
94  
95      public void authenticate() throws IOException {
96  
97          // allow cross domain requests for Okta form support
98          System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
99          // enable NTLM for ADFS support
100         System.setProperty("jdk.http.ntlm.transparentAuth", "allHosts");
101 
102         // common DavMail client id
103         final String clientId = Settings.getProperty("davmail.oauth.clientId", "facd6cff-a294-4415-b59f-c5b01937d7bd");
104         // standard native app redirectUri
105         final String redirectUri = Settings.getProperty("davmail.oauth.redirectUri", "https://localhost/common/oauth2/nativeclient");
106         // company tenantId or common
107         String tenantId = Settings.getProperty("davmail.oauth.tenantId", "common");
108 
109         // first try to load stored token
110         token = O365Token.load(tenantId, clientId, redirectUri, username, password);
111         if (token != null) {
112             isAuthenticated = true;
113             return;
114         }
115 
116         final String initUrl = O365Authenticator.buildAuthorizeUrl(tenantId, clientId, redirectUri, username);
117 
118         // set default authenticator
119         Authenticator.setDefault(new Authenticator() {
120             @Override
121             public PasswordAuthentication getPasswordAuthentication() {
122                 if (getRequestorType() == RequestorType.PROXY) {
123                     String proxyUser = Settings.getProperty("davmail.proxyUser");
124                     String proxyPassword = Settings.getProperty("davmail.proxyPassword");
125                     if (proxyUser != null && proxyPassword != null) {
126                         LOGGER.debug("Proxy authentication with user " + proxyUser);
127                         return new PasswordAuthentication(proxyUser, proxyPassword.toCharArray());
128                     } else {
129                         LOGGER.debug("Missing proxy credentials ");
130                         return null;
131                     }
132                 } else {
133                     LOGGER.debug("Password authentication with user " + username);
134                     return new PasswordAuthentication(username, password.toCharArray());
135                 }
136             }
137         });
138 
139         // Check if SWT is available
140         boolean isSWTAvailable = Settings.isSWTAvailable();
141         boolean isDocker = Settings.isDocker();
142         boolean isJFXAvailable = Settings.isJFXAvailable();
143 
144         if (isSWTAvailable && !isDocker) {
145             LOGGER.debug("Open SWT browser");
146             try {
147                 o365InteractiveAuthenticatorSWT = new O365InteractiveAuthenticatorSWT();
148                 o365InteractiveAuthenticatorSWT.setO365InteractiveAuthenticator(O365InteractiveAuthenticator.this);
149                 o365InteractiveAuthenticatorSWT.authenticate(initUrl, redirectUri);
150             } catch (Error e) {
151                 LOGGER.warn("Unable to load SWT browser: "+e.getMessage());
152                 if (o365InteractiveAuthenticatorSWT != null) {
153                     try {
154                         o365InteractiveAuthenticatorSWT.dispose();
155                     } catch (Throwable t) {
156                         LOGGER.warn("Error disposing SWT frame: "+t.getMessage());
157                     }
158                 }
159                 o365InteractiveAuthenticatorSWT = null;
160             }
161         }
162 
163         if (o365InteractiveAuthenticatorSWT == null && isJFXAvailable
164                 // do not try failover on linux, SWT gtk browser breaks JFX
165                 && (!(isSWTAvailable && Settings.isLinux() && !isDocker))) {
166             LOGGER.info("Open JavaFX (OpenJFX) browser");
167             SwingUtilities.invokeLater(() -> {
168                 try {
169                     o365InteractiveAuthenticatorFrame = new O365InteractiveAuthenticatorFrame();
170                     o365InteractiveAuthenticatorFrame.setO365InteractiveAuthenticator(O365InteractiveAuthenticator.this);
171                     o365InteractiveAuthenticatorFrame.authenticate(initUrl, redirectUri);
172                 } catch (NoClassDefFoundError e) {
173                     LOGGER.warn("Unable to load JavaFX (OpenJFX)");
174                 } catch (IllegalAccessError e) {
175                     LOGGER.warn("Unable to load JavaFX (OpenJFX), append --add-exports java.base/sun.net.www.protocol.https=ALL-UNNAMED to java options");
176                 }
177 
178             });
179         } else {
180             if (o365InteractiveAuthenticatorFrame == null && o365InteractiveAuthenticatorSWT == null) {
181                 try {
182                     SwingUtilities.invokeAndWait(() -> o365ManualAuthenticatorDialog = new O365ManualAuthenticatorDialog(initUrl));
183                 } catch (InterruptedException e) {
184                     Thread.currentThread().interrupt();
185                 } catch (InvocationTargetException e) {
186                     throw new IOException(e);
187                 }
188                 code = o365ManualAuthenticatorDialog.getCode();
189                 isAuthenticated = code != null;
190                 if (!isAuthenticated) {
191                     errorCode = "User did not provide authentication code";
192                 }
193             }
194         }
195 
196         int count = 0;
197 
198         while (!isAuthenticated && errorCode == null && count++ < MAX_COUNT) {
199             try {
200                 Thread.sleep(1000);
201             } catch (InterruptedException e) {
202                 Thread.currentThread().interrupt();
203             }
204         }
205 
206         if (count > MAX_COUNT) {
207             errorCode = "Timed out waiting for interactive authentication";
208         }
209 
210         if (o365InteractiveAuthenticatorFrame != null && o365InteractiveAuthenticatorFrame.isVisible()) {
211             o365InteractiveAuthenticatorFrame.close();
212         }
213 
214         if (o365InteractiveAuthenticatorSWT != null) {
215             o365InteractiveAuthenticatorSWT.dispose();
216         }
217 
218         if (isAuthenticated) {
219             token = O365Token.build(tenantId, clientId, redirectUri, code, password);
220 
221             LOGGER.debug("Authenticated username: " + token.getUsername());
222             if (username != null && !username.isEmpty() && !username.equalsIgnoreCase(token.getUsername())) {
223                 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_MISMATCH", token.getUsername(), username);
224             }
225 
226         } else {
227             LOGGER.error("Authentication failed " + errorCode);
228             throw new DavMailException("EXCEPTION_AUTHENTICATION_FAILED_REASON", errorCode);
229         }
230     }
231 
232     /**
233      * Extract code from redirect location matching redirectUri
234      * @param location redirect location
235      */
236     public void handleCode(String location) {
237         isAuthenticated = location.contains("code=");
238         if (!isAuthenticated && location.contains("error=")) {
239             errorCode = location.substring(location.indexOf("error="));
240         }
241 
242         if (isAuthenticated) {
243             LOGGER.debug("Authenticated location: " + location);
244             if (location.contains("&session_state=")) {
245                 code = location.substring(location.indexOf("code=") + 5, location.indexOf("&session_state="));
246             } else {
247                 code = location.substring(location.indexOf("code=") + 5);
248             }
249             String sessionState = location.substring(location.lastIndexOf('=') + 1);
250 
251             LOGGER.debug("Authentication Code: " + code);
252             LOGGER.debug("Authentication session state: " + sessionState);
253         }
254     }
255 
256     public static void main(String[] argv) {
257 
258         try {
259             // set custom factory before loading OpenJFX
260             Security.setProperty("ssl.SocketFactory.provider", "davmail.http.DavGatewaySSLSocketFactory");
261 
262             Settings.setDefaultSettings();
263             Settings.setConfigFilePath("davmail-interactive.properties");
264             Settings.load();
265 
266             O365InteractiveAuthenticator authenticator = new O365InteractiveAuthenticator();
267             authenticator.setUsername("demo@demo.onmicrosoft.com");
268             authenticator.authenticate();
269 
270             try (
271                     HttpClientAdapter httpClientAdapter = new HttpClientAdapter(authenticator.getExchangeUri(), true)
272             ) {
273 
274                 // switch to EWS url
275                 GetFolderMethod checkMethod = new GetFolderMethod(BaseShape.ID_ONLY, DistinguishedFolderId.getInstance(null, DistinguishedFolderId.Name.root), null);
276                 checkMethod.setHeader("Authorization", "Bearer " + authenticator.getToken().getAccessToken());
277                 try (
278                         CloseableHttpResponse response = httpClientAdapter.execute(checkMethod)
279                 ) {
280                     checkMethod.handleResponse(response);
281                     checkMethod.checkSuccess();
282                 }
283                 LOGGER.info("Retrieved folder id " + checkMethod.getResponseItem().get("FolderId"));
284 
285                 // loop to check expiration
286                 int i = 0;
287                 while (i++ < 12 * 60 * 2) {
288                     GetUserConfigurationMethod getUserConfigurationMethod = new GetUserConfigurationMethod();
289                     getUserConfigurationMethod.setHeader("Authorization", "Bearer " + authenticator.getToken().getAccessToken());
290                     try (
291                             CloseableHttpResponse response = httpClientAdapter.execute(getUserConfigurationMethod)
292                     ) {
293                         getUserConfigurationMethod.handleResponse(response);
294                         getUserConfigurationMethod.checkSuccess();
295                     }
296                     LOGGER.info(getUserConfigurationMethod.getResponseItem());
297 
298                     Thread.sleep(5000);
299                 }
300             }
301         } catch (InterruptedException e) {
302             LOGGER.warn("Thread interrupted", e);
303             Thread.currentThread().interrupt();
304         } catch (Exception e) {
305             LOGGER.error(e + " " + e.getMessage(), e);
306         }
307         System.exit(0);
308     }
309 }