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", Settings.getO365LoginUrl() + "/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");
152                 if (o365InteractiveAuthenticatorSWT != null) {
153                     o365InteractiveAuthenticatorSWT.dispose();
154                 }
155                 o365InteractiveAuthenticatorSWT = null;
156             }
157         }
158 
159         if (o365InteractiveAuthenticatorSWT == null && isJFXAvailable) {
160             LOGGER.info("Open JavaFX (OpenJFX) browser");
161             SwingUtilities.invokeLater(() -> {
162                 try {
163                     o365InteractiveAuthenticatorFrame = new O365InteractiveAuthenticatorFrame();
164                     o365InteractiveAuthenticatorFrame.setO365InteractiveAuthenticator(O365InteractiveAuthenticator.this);
165                     o365InteractiveAuthenticatorFrame.authenticate(initUrl, redirectUri);
166                 } catch (NoClassDefFoundError e) {
167                     LOGGER.warn("Unable to load JavaFX (OpenJFX)");
168                 } catch (IllegalAccessError e) {
169                     LOGGER.warn("Unable to load JavaFX (OpenJFX), append --add-exports java.base/sun.net.www.protocol.https=ALL-UNNAMED to java options");
170                 }
171 
172             });
173         } else {
174             if (o365InteractiveAuthenticatorFrame == null && o365InteractiveAuthenticatorSWT == null) {
175                 try {
176                     SwingUtilities.invokeAndWait(() -> o365ManualAuthenticatorDialog = new O365ManualAuthenticatorDialog(initUrl));
177                 } catch (InterruptedException e) {
178                     Thread.currentThread().interrupt();
179                 } catch (InvocationTargetException e) {
180                     throw new IOException(e);
181                 }
182                 code = o365ManualAuthenticatorDialog.getCode();
183                 isAuthenticated = code != null;
184                 if (!isAuthenticated) {
185                     errorCode = "User did not provide authentication code";
186                 }
187             }
188         }
189 
190         int count = 0;
191 
192         while (!isAuthenticated && errorCode == null && count++ < MAX_COUNT) {
193             try {
194                 Thread.sleep(1000);
195             } catch (InterruptedException e) {
196                 Thread.currentThread().interrupt();
197             }
198         }
199 
200         if (count > MAX_COUNT) {
201             errorCode = "Timed out waiting for interactive authentication";
202         }
203 
204         if (o365InteractiveAuthenticatorFrame != null && o365InteractiveAuthenticatorFrame.isVisible()) {
205             o365InteractiveAuthenticatorFrame.close();
206         }
207 
208         if (o365InteractiveAuthenticatorSWT != null) {
209             o365InteractiveAuthenticatorSWT.dispose();
210         }
211 
212         if (isAuthenticated) {
213             token = O365Token.build(tenantId, clientId, redirectUri, code, password);
214 
215             LOGGER.debug("Authenticated username: " + token.getUsername());
216             if (username != null && !username.isEmpty() && !username.equalsIgnoreCase(token.getUsername())) {
217                 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_MISMATCH", token.getUsername(), username);
218             }
219 
220         } else {
221             LOGGER.error("Authentication failed " + errorCode);
222             throw new DavMailException("EXCEPTION_AUTHENTICATION_FAILED_REASON", errorCode);
223         }
224     }
225 
226     /**
227      * Extract code from redirect location matching redirectUri
228      * @param location redirect location
229      */
230     public void handleCode(String location) {
231         isAuthenticated = location.contains("code=");
232         if (!isAuthenticated && location.contains("error=")) {
233             errorCode = location.substring(location.indexOf("error="));
234         }
235 
236         if (isAuthenticated) {
237             LOGGER.debug("Authenticated location: " + location);
238             if (location.contains("&session_state=")) {
239                 code = location.substring(location.indexOf("code=") + 5, location.indexOf("&session_state="));
240             } else {
241                 code = location.substring(location.indexOf("code=") + 5);
242             }
243             String sessionState = location.substring(location.lastIndexOf('=') + 1);
244 
245             LOGGER.debug("Authentication Code: " + code);
246             LOGGER.debug("Authentication session state: " + sessionState);
247         }
248     }
249 
250     public static void main(String[] argv) {
251 
252         try {
253             // set custom factory before loading OpenJFX
254             Security.setProperty("ssl.SocketFactory.provider", "davmail.http.DavGatewaySSLSocketFactory");
255 
256             Settings.setDefaultSettings();
257             Settings.setConfigFilePath("davmail-interactive.properties");
258             Settings.load();
259 
260             O365InteractiveAuthenticator authenticator = new O365InteractiveAuthenticator();
261             authenticator.setUsername("demo@demo.onmicrosoft.com");
262             authenticator.authenticate();
263 
264             try (
265                     HttpClientAdapter httpClientAdapter = new HttpClientAdapter(authenticator.getExchangeUri(), true)
266             ) {
267 
268                 // switch to EWS url
269                 GetFolderMethod checkMethod = new GetFolderMethod(BaseShape.ID_ONLY, DistinguishedFolderId.getInstance(null, DistinguishedFolderId.Name.root), null);
270                 checkMethod.setHeader("Authorization", "Bearer " + authenticator.getToken().getAccessToken());
271                 try (
272                         CloseableHttpResponse response = httpClientAdapter.execute(checkMethod)
273                 ) {
274                     checkMethod.handleResponse(response);
275                     checkMethod.checkSuccess();
276                 }
277                 LOGGER.info("Retrieved folder id " + checkMethod.getResponseItem().get("FolderId"));
278 
279                 // loop to check expiration
280                 int i = 0;
281                 while (i++ < 12 * 60 * 2) {
282                     GetUserConfigurationMethod getUserConfigurationMethod = new GetUserConfigurationMethod();
283                     getUserConfigurationMethod.setHeader("Authorization", "Bearer " + authenticator.getToken().getAccessToken());
284                     try (
285                             CloseableHttpResponse response = httpClientAdapter.execute(getUserConfigurationMethod)
286                     ) {
287                         getUserConfigurationMethod.handleResponse(response);
288                         getUserConfigurationMethod.checkSuccess();
289                     }
290                     LOGGER.info(getUserConfigurationMethod.getResponseItem());
291 
292                     Thread.sleep(5000);
293                 }
294             }
295         } catch (InterruptedException e) {
296             LOGGER.warn("Thread interrupted", e);
297             Thread.currentThread().interrupt();
298         } catch (Exception e) {
299             LOGGER.error(e + " " + e.getMessage(), e);
300         }
301         System.exit(0);
302     }
303 }