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 O365ManualAuthenticatorDialog o365ManualAuthenticatorDialog;
58  
59      private String username;
60      private String password;
61      private O365Token token;
62  
63      public O365Token getToken() {
64          return token;
65      }
66  
67      @Override
68      public URI getExchangeUri() {
69          return ewsUrl;
70      }
71  
72      public String getUsername() {
73          return username;
74      }
75  
76      public void setUsername(String username) {
77          this.username = username;
78      }
79  
80      public void setPassword(String password) {
81          this.password = password;
82      }
83  
84      /**
85       * Return a pool enabled HttpClientAdapter instance to access O365
86       *
87       * @return HttpClientAdapter instance
88       */
89      @Override
90      public HttpClientAdapter getHttpClientAdapter() {
91          return new HttpClientAdapter(getExchangeUri(), username, password, true);
92      }
93  
94      public void authenticate() throws IOException {
95  
96          // allow cross domain requests for Okta form support
97          System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
98          // enable NTLM for ADFS support
99          System.setProperty("jdk.http.ntlm.transparentAuth", "allHosts");
100 
101         // common DavMail client id
102         final String clientId = Settings.getProperty("davmail.oauth.clientId", "facd6cff-a294-4415-b59f-c5b01937d7bd");
103         // standard native app redirectUri
104         final String redirectUri = Settings.getProperty("davmail.oauth.redirectUri", Settings.getO365LoginUrl()+"/common/oauth2/nativeclient");
105         // company tenantId or common
106         String tenantId = Settings.getProperty("davmail.oauth.tenantId", "common");
107 
108         // first try to load stored token
109         token = O365Token.load(tenantId, clientId, redirectUri, username, password);
110         if (token != null) {
111             isAuthenticated = true;
112             return;
113         }
114 
115         final String initUrl = O365Authenticator.buildAuthorizeUrl(tenantId, clientId, redirectUri, username);
116 
117         // set default authenticator
118         Authenticator.setDefault(new Authenticator() {
119             @Override
120             public PasswordAuthentication getPasswordAuthentication() {
121                 if (getRequestorType() == RequestorType.PROXY) {
122                     String proxyUser = Settings.getProperty("davmail.proxyUser");
123                     String proxyPassword = Settings.getProperty("davmail.proxyPassword");
124                     if (proxyUser != null && proxyPassword != null) {
125                         LOGGER.debug("Proxy authentication with user " + proxyUser);
126                         return new PasswordAuthentication(proxyUser, proxyPassword.toCharArray());
127                     } else {
128                         LOGGER.debug("Missing proxy credentials ");
129                         return null;
130                     }
131                 } else {
132                     LOGGER.debug("Password authentication with user " + username);
133                     return new PasswordAuthentication(username, password.toCharArray());
134                 }
135             }
136         });
137 
138         boolean isJFXAvailable = true;
139         try {
140             Class.forName("javafx.application.Platform");
141         } catch (ClassNotFoundException e) {
142             LOGGER.warn("Unable to load JavaFX (OpenJFX), switch to manual mode");
143             isJFXAvailable = false;
144         }
145 
146         if (isJFXAvailable) {
147             SwingUtilities.invokeLater(() -> {
148                 try {
149                     o365InteractiveAuthenticatorFrame = new O365InteractiveAuthenticatorFrame();
150                     o365InteractiveAuthenticatorFrame.setO365InteractiveAuthenticator(O365InteractiveAuthenticator.this);
151                     o365InteractiveAuthenticatorFrame.authenticate(initUrl, redirectUri);
152                 } catch (NoClassDefFoundError e) {
153                     LOGGER.warn("Unable to load JavaFX (OpenJFX)");
154                 } catch (IllegalAccessError e) {
155                     LOGGER.warn("Unable to load JavaFX (OpenJFX), append --add-exports java.base/sun.net.www.protocol.https=ALL-UNNAMED to java options");
156                 }
157 
158             });
159         } else {
160             if (o365InteractiveAuthenticatorFrame == null) {
161                 try {
162                     SwingUtilities.invokeAndWait(() -> o365ManualAuthenticatorDialog = new O365ManualAuthenticatorDialog(initUrl));
163                 } catch (InterruptedException e) {
164                     Thread.currentThread().interrupt();
165                 } catch (InvocationTargetException e) {
166                     throw new IOException(e);
167                 }
168                 code = o365ManualAuthenticatorDialog.getCode();
169                 isAuthenticated = code != null;
170                 if (!isAuthenticated) {
171                     errorCode = "User did not provide authentication code";
172                 }
173             }
174         }
175 
176         int count = 0;
177 
178         while (!isAuthenticated && errorCode == null && count++ < MAX_COUNT) {
179             try {
180                 Thread.sleep(1000);
181             } catch (InterruptedException e) {
182                 Thread.currentThread().interrupt();
183             }
184         }
185 
186         if (count > MAX_COUNT) {
187             errorCode = "Timed out waiting for interactive authentication";
188         }
189 
190         if (o365InteractiveAuthenticatorFrame != null && o365InteractiveAuthenticatorFrame.isVisible()) {
191             o365InteractiveAuthenticatorFrame.close();
192         }
193 
194         if (isAuthenticated) {
195             token = O365Token.build(tenantId, clientId, redirectUri, code, password);
196 
197             LOGGER.debug("Authenticated username: " + token.getUsername());
198             if (username != null && !username.isEmpty() && !username.equalsIgnoreCase(token.getUsername())) {
199                 throw new DavMailAuthenticationException("Authenticated username " + token.getUsername() + " does not match " + username);
200             }
201 
202         } else {
203             LOGGER.error("Authentication failed " + errorCode);
204             throw new DavMailException("EXCEPTION_AUTHENTICATION_FAILED_REASON", errorCode);
205         }
206     }
207 
208     public static void main(String[] argv) {
209 
210         try {
211             // set custom factory before loading OpenJFX
212             Security.setProperty("ssl.SocketFactory.provider", "davmail.http.DavGatewaySSLSocketFactory");
213 
214             Settings.setDefaultSettings();
215             Settings.setConfigFilePath("davmail-interactive.properties");
216             Settings.load();
217 
218             O365InteractiveAuthenticator authenticator = new O365InteractiveAuthenticator();
219             authenticator.setUsername("");
220             authenticator.authenticate();
221 
222             try (
223                     HttpClientAdapter httpClientAdapter = new HttpClientAdapter(authenticator.getExchangeUri(), true)
224             ) {
225 
226                 // switch to EWS url
227                 GetFolderMethod checkMethod = new GetFolderMethod(BaseShape.ID_ONLY, DistinguishedFolderId.getInstance(null, DistinguishedFolderId.Name.root), null);
228                 checkMethod.setHeader("Authorization", "Bearer " + authenticator.getToken().getAccessToken());
229                 try (
230                         CloseableHttpResponse response = httpClientAdapter.execute(checkMethod)
231                 ) {
232                     checkMethod.handleResponse(response);
233                     checkMethod.checkSuccess();
234                 }
235                 LOGGER.info("Retrieved folder id " + checkMethod.getResponseItem().get("FolderId"));
236 
237                 // loop to check expiration
238                 int i = 0;
239                 while (i++ < 12 * 60 * 2) {
240                     GetUserConfigurationMethod getUserConfigurationMethod = new GetUserConfigurationMethod();
241                     getUserConfigurationMethod.setHeader("Authorization", "Bearer " + authenticator.getToken().getAccessToken());
242                     try (
243                             CloseableHttpResponse response = httpClientAdapter.execute(getUserConfigurationMethod)
244                     ) {
245                         getUserConfigurationMethod.handleResponse(response);
246                         getUserConfigurationMethod.checkSuccess();
247                     }
248                     LOGGER.info(getUserConfigurationMethod.getResponseItem());
249 
250                     Thread.sleep(5000);
251                 }
252             }
253         } catch (InterruptedException e) {
254             LOGGER.warn("Thread interrupted", e);
255             Thread.currentThread().interrupt();
256         } catch (Exception e) {
257             LOGGER.error(e + " " + e.getMessage(), e);
258         }
259         System.exit(0);
260     }
261 }