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  
20  package davmail.exchange.auth;
21  
22  import davmail.BundleMessage;
23  import davmail.Settings;
24  import davmail.exception.DavMailAuthenticationException;
25  import davmail.http.HttpClientAdapter;
26  import davmail.http.request.GetRequest;
27  import davmail.http.request.PostRequest;
28  import davmail.http.request.ResponseWrapper;
29  import davmail.http.request.RestRequest;
30  import davmail.ui.NumberMatchingFrame;
31  import davmail.ui.PasswordPromptDialog;
32  import org.apache.http.HttpStatus;
33  import org.apache.http.client.utils.URIBuilder;
34  import org.apache.log4j.Logger;
35  import org.codehaus.jettison.json.JSONException;
36  import org.codehaus.jettison.json.JSONObject;
37  
38  import javax.swing.*;
39  import java.awt.*;
40  import java.io.BufferedReader;
41  import java.io.IOException;
42  import java.io.InputStreamReader;
43  import java.net.URI;
44  import java.net.URISyntaxException;
45  import java.util.regex.Matcher;
46  import java.util.regex.Pattern;
47  
48  public class O365Authenticator implements ExchangeAuthenticator {
49      protected static final Logger LOGGER = Logger.getLogger(O365Authenticator.class);
50  
51      private String tenantId;
52      // Office 365 username
53      private String username;
54      // authentication userid, can be different from username
55      private String userid;
56      private String password;
57      private O365Token token;
58  
59      public static String buildAuthorizeUrl(String tenantId, String clientId, String redirectUri, String username) throws IOException {
60          URI uri;
61          try {
62              URIBuilder uriBuilder = new URIBuilder(Settings.getO365LoginUrl())
63                      .addParameter("client_id", clientId)
64                      .addParameter("response_type", "code")
65                      .addParameter("redirect_uri", redirectUri)
66                      .addParameter("response_mode", "query")
67                      .addParameter("login_hint", username);
68  
69              // force consent
70              //uriBuilder.addParameter("prompt", "consent");
71              // switch to new v2.0 OIDC compliant endpoint https://docs.microsoft.com/en-us/azure/active-directory/develop/azure-ad-endpoint-comparison
72              if (Settings.getBooleanProperty("davmail.enableOidc", false)) {
73                  uriBuilder.setPath("/" + tenantId + "/oauth2/v2.0/authorize")
74                          .addParameter("scope", "openid " + Settings.getOutlookUrl() + "/EWS.AccessAsUser.All");
75              } else if (Settings.getBooleanProperty("davmail.enableGraph", false)) {
76                  uriBuilder.setPath("/" + tenantId + "/oauth2/authorize")
77                          .addParameter("resource", "https://graph.microsoft.com");
78                  // OIDC compliant
79                  //uriBuilder.setPath("/" + tenantId + "/oauth2/v2.0/authorize")
80                  //        .addParameter("scope", "Mail.ReadWrite Calendars.ReadWrite MailboxSettings.Read");
81                          //.addParameter("scope", "openid " + Settings.getOutlookUrl() + "/EWS.AccessAsUser.All AuditLog.Read.All Calendar.ReadWrite Calendars.Read.Shared Calendars.ReadWrite Contacts.ReadWrite DataLossPreventionPolicy.Evaluate Directory.AccessAsUser.All Directory.Read.All Files.Read Files.Read.All Files.ReadWrite.All Group.Read.All Group.ReadWrite.All InformationProtectionPolicy.Read Mail.ReadWrite Mail.Send Notes.Create Organization.Read.All People.Read People.Read.All Printer.Read.All PrintJob.ReadWriteBasic SensitiveInfoType.Detect SensitiveInfoType.Read.All SensitivityLabel.Evaluate Tasks.ReadWrite TeamMember.ReadWrite.All TeamsTab.ReadWriteForChat User.Read.All User.ReadBasic.All User.ReadWrite Users.Read");
82              } else {
83                  uriBuilder.setPath("/" + tenantId + "/oauth2/authorize")
84                          .addParameter("resource", Settings.getOutlookUrl());
85              }
86  
87              uri = uriBuilder.build();
88          } catch (URISyntaxException e) {
89              throw new IOException(e);
90          }
91          return uri.toString();
92      }
93  
94      public void setUsername(String username) {
95          if (username.contains("|")) {
96              this.userid = username.substring(0, username.indexOf("|"));
97              this.username = username.substring(username.indexOf("|") + 1);
98          } else {
99              this.username = username;
100             this.userid = username;
101         }
102     }
103 
104     public void setPassword(String password) {
105         this.password = password;
106     }
107 
108     public O365Token getToken() {
109         return token;
110     }
111 
112     public URI getExchangeUri() {
113         return URI.create(Settings.getO365Url());
114     }
115 
116     /**
117      * Return a pool enabled HttpClientAdapter instance to access O365
118      *
119      * @return HttpClientAdapter instance
120      */
121     @Override
122     public HttpClientAdapter getHttpClientAdapter() {
123         return new HttpClientAdapter(getExchangeUri(), username, password, true);
124     }
125 
126     public void authenticate() throws IOException {
127         // common DavMail client id
128         String clientId = Settings.getProperty("davmail.oauth.clientId", "facd6cff-a294-4415-b59f-c5b01937d7bd");
129         // standard native app redirectUri
130         String redirectUri = Settings.getProperty("davmail.oauth.redirectUri", Settings.getO365LoginUrl()+"/common/oauth2/nativeclient");
131         // company tenantId or common
132         tenantId = Settings.getProperty("davmail.oauth.tenantId", "common");
133 
134         // first try to load stored token
135         token = O365Token.load(tenantId, clientId, redirectUri, username, password);
136         if (token != null) {
137             return;
138         }
139 
140         String url = O365Authenticator.buildAuthorizeUrl(tenantId, clientId, redirectUri, username);
141 
142         try (
143                 HttpClientAdapter httpClientAdapter = new HttpClientAdapter(url, userid, password)
144         ) {
145 
146             GetRequest getRequest = new GetRequest(url);
147             String responseBodyAsString = executeFollowRedirect(httpClientAdapter, getRequest);
148             String code;
149             if (!responseBodyAsString.contains("Config=")) {
150                 // we are no longer on Microsoft, try ADFS
151                 code = authenticateADFS(httpClientAdapter, responseBodyAsString, url);
152             } else {
153                 JSONObject config = extractConfig(responseBodyAsString);
154 
155                 String context = config.getString("sCtx"); // csts request
156                 String apiCanary = config.getString("apiCanary"); // canary for API calls
157                 String clientRequestId = config.getString("correlationId");
158                 String hpgact = config.getString("hpgact");
159                 String hpgid = config.getString("hpgid");
160                 String flowToken = config.getString("sFT");
161                 String canary = config.getString("canary");
162                 String sessionId = config.getString("sessionId");
163 
164                 String referer = getRequest.getURI().toString();
165 
166                 RestRequest getCredentialMethod = new RestRequest(Settings.getO365LoginUrl()  + "/" + tenantId + "/GetCredentialType");
167                 getCredentialMethod.setRequestHeader("Accept", "application/json");
168                 getCredentialMethod.setRequestHeader("canary", apiCanary);
169                 getCredentialMethod.setRequestHeader("client-request-id", clientRequestId);
170                 getCredentialMethod.setRequestHeader("hpgact", hpgact);
171                 getCredentialMethod.setRequestHeader("hpgid", hpgid);
172                 getCredentialMethod.setRequestHeader("hpgrequestid", sessionId);
173                 getCredentialMethod.setRequestHeader("Referer", referer);
174 
175                 final JSONObject jsonObject = new JSONObject();
176                 jsonObject.put("username", username);
177                 jsonObject.put("isOtherIdpSupported", true);
178                 jsonObject.put("checkPhones", false);
179                 jsonObject.put("isRemoteNGCSupported", false);
180                 jsonObject.put("isCookieBannerShown", false);
181                 jsonObject.put("isFidoSupported", false);
182                 jsonObject.put("flowToken", flowToken);
183                 jsonObject.put("originalRequest", context);
184 
185                 getCredentialMethod.setJsonBody(jsonObject);
186 
187                 JSONObject credentialType = httpClientAdapter.executeRestRequest(getCredentialMethod);
188 
189                 LOGGER.debug("CredentialType=" + credentialType);
190 
191                 JSONObject credentials = credentialType.getJSONObject("Credentials");
192                 String federationRedirectUrl = credentials.optString("FederationRedirectUrl");
193 
194                 if (federationRedirectUrl != null && !federationRedirectUrl.isEmpty()) {
195                     LOGGER.debug("Detected ADFS, redirecting to " + federationRedirectUrl);
196                     code = authenticateRedirectADFS(httpClientAdapter, federationRedirectUrl, url);
197                 } else {
198                     PostRequest logonMethod = new PostRequest(Settings.getO365LoginUrl()  + "/" + tenantId + "/login");
199                     logonMethod.setRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
200                     logonMethod.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
201 
202                     logonMethod.setRequestHeader("Referer", referer);
203 
204                     logonMethod.setParameter("canary", canary);
205                     logonMethod.setParameter("ctx", context);
206                     logonMethod.setParameter("flowToken", flowToken);
207                     logonMethod.setParameter("hpgrequestid", sessionId);
208                     logonMethod.setParameter("login", username);
209                     logonMethod.setParameter("loginfmt", username);
210                     logonMethod.setParameter("passwd", password);
211 
212                     responseBodyAsString = httpClientAdapter.executePostRequest(logonMethod);
213                     URI location = logonMethod.getRedirectLocation();
214 
215                     if (responseBodyAsString != null && responseBodyAsString.contains("arrUserProofs")) {
216                         location = handleMfa(httpClientAdapter, logonMethod, username, clientRequestId);
217                     }
218 
219                     if (location == null || !location.toString().startsWith(redirectUri)) {
220                         // extract response
221                         config = extractConfig(logonMethod.getResponseBodyAsString());
222                         if (config.optJSONArray("arrScopes") != null || config.optJSONArray("urlPostRedirect") != null) {
223                             LOGGER.warn("Authentication successful but user consent or validation needed, please open the following url in a browser");
224                             LOGGER.warn(url);
225                             throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
226                         } else if ("50126".equals(config.optString("sErrorCode"))) {
227                             throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
228                         } else if ("50125".equals(config.optString("sErrorCode"))) {
229                             throw new DavMailAuthenticationException("LOG_MESSAGE", "Your organization needs more information to keep your account secure, authenticate once in a web browser and try again");
230                         } else if (config.optString("strServiceExceptionMessage") != null) {
231                             LOGGER.debug("O365 returned error: " + config.optString("strServiceExceptionMessage"));
232                             throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
233                         } else {
234                             throw new DavMailAuthenticationException("LOG_MESSAGE", "Authentication failed, unknown error: " + config);
235                         }
236                     }
237                     String query = location.toString();
238                     if (query.contains("code=")) {
239                         code = query.substring(query.indexOf("code=") + 5, query.indexOf("&session_state="));
240                     } else {
241                         throw new DavMailAuthenticationException("LOG_MESSAGE", "Authentication failed, unknown error: " + query);
242                     }
243                 }
244             }
245             LOGGER.debug("Authentication Code: " + code);
246 
247             token = O365Token.build(tenantId, clientId, redirectUri, code, password);
248 
249             LOGGER.debug("Authenticated username: " + token.getUsername());
250             if (!username.equalsIgnoreCase(token.getUsername())) {
251                 throw new IOException("Authenticated username " + token.getUsername() + " does not match " + username);
252             }
253 
254         } catch (JSONException e) {
255             throw new IOException(e + " " + e.getMessage());
256         }
257 
258     }
259 
260     private String authenticateRedirectADFS(HttpClientAdapter httpClientAdapter, String federationRedirectUrl, String authorizeUrl) throws IOException, JSONException {
261         // get ADFS login form
262         GetRequest logonFormMethod = new GetRequest(federationRedirectUrl);
263         logonFormMethod = httpClientAdapter.executeFollowRedirect(logonFormMethod);
264         String responseBodyAsString = logonFormMethod.getResponseBodyAsString();
265         return authenticateADFS(httpClientAdapter, responseBodyAsString, authorizeUrl);
266     }
267 
268     private String authenticateADFS(HttpClientAdapter httpClientAdapter, String responseBodyAsString, String authorizeUrl) throws IOException, JSONException {
269         URI location;
270 
271         if (responseBodyAsString.contains(Settings.getO365LoginUrl())) {
272             LOGGER.info("Already authenticated through Basic or NTLM");
273         } else {
274             // parse form to get target url, authenticate as userid
275             PostRequest logonMethod = new PostRequest(extract("method=\"post\" action=\"([^\"]+)\"", responseBodyAsString));
276             logonMethod.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
277 
278             logonMethod.setParameter("UserName", userid);
279             logonMethod.setParameter("Password", password);
280             logonMethod.setParameter("AuthMethod", "FormsAuthentication");
281 
282             httpClientAdapter.executePostRequest(logonMethod);
283             location = logonMethod.getRedirectLocation();
284             if (location == null) {
285                 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
286             }
287 
288             GetRequest redirectMethod = new GetRequest(location);
289             responseBodyAsString = httpClientAdapter.executeGetRequest(redirectMethod);
290         }
291 
292         if (!responseBodyAsString.contains(Settings.getO365LoginUrl())) {
293             throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
294         }
295         String targetUrl = extract("action=\"([^\"]+)\"", responseBodyAsString);
296         String wa = extract("name=\"wa\" value=\"([^\"]+)\"", responseBodyAsString);
297         String wresult = extract("name=\"wresult\" value=\"([^\"]+)\"", responseBodyAsString);
298         // decode wresult
299         wresult = wresult.replaceAll(""", "\"");
300         wresult = wresult.replaceAll("&lt;", "<");
301         wresult = wresult.replaceAll("&gt;", ">");
302         String wctx = extract("name=\"wctx\" value=\"([^\"]+)\"", responseBodyAsString);
303         wctx = wctx.replaceAll("&amp;", "&");
304 
305         PostRequest targetMethod = new PostRequest(targetUrl);
306         targetMethod.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
307         targetMethod.setParameter("wa", wa);
308         targetMethod.setParameter("wresult", wresult);
309         targetMethod.setParameter("wctx", wctx);
310 
311         responseBodyAsString = httpClientAdapter.executePostRequest(targetMethod);
312         location = targetMethod.getRedirectLocation();
313 
314         LOGGER.debug(targetMethod.getURI().toString());
315         LOGGER.debug(targetMethod.getReasonPhrase());
316         LOGGER.debug(responseBodyAsString);
317 
318         if (targetMethod.getStatusCode() == HttpStatus.SC_OK) {
319             JSONObject config = extractConfig(responseBodyAsString);
320             if (config.optJSONArray("arrScopes") != null || config.optJSONArray("urlPostRedirect") != null) {
321                 LOGGER.warn("Authentication successful but user consent or validation needed, please open the following url in a browser");
322                 LOGGER.warn(authorizeUrl);
323                 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
324             }
325         } else if (targetMethod.getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY || location == null) {
326             throw new IOException("Unknown ADFS authentication failure");
327         }
328 
329         if (location.getHost().startsWith("device")) {
330             location = processDeviceLogin(httpClientAdapter, location);
331         }
332         String query = location.getQuery();
333         if (query == null) {
334             // failover for null query with non https URI like urn:ietf:wg:oauth:2.0:oob?code=...
335             query = location.getSchemeSpecificPart();
336         }
337 
338         if (query.contains("code=") && query.contains("&session_state=")) {
339             String code = query.substring(query.indexOf("code=") + 5, query.indexOf("&session_state="));
340             LOGGER.debug("Authentication Code: " + code);
341             return code;
342         }
343         throw new IOException("Unknown ADFS authentication failure");
344     }
345 
346     private URI processDeviceLogin(HttpClientAdapter httpClient, URI location) throws IOException, JSONException {
347         URI result = location;
348         LOGGER.debug("Proceed to device authentication, must have access to a client certificate signed by MS-Organization-Access");
349         if (Settings.isWindows() &&
350                 (System.getProperty("java.version").compareTo("13") < 0
351                         || !"MSCAPI".equals(Settings.getProperty("davmail.ssl.clientKeystoreType")))
352         ) {
353             LOGGER.warn("MSCAPI and Java version 13 or higher required to access TPM protected client certificate on Windows");
354         }
355         GetRequest deviceLoginMethod = new GetRequest(location);
356 
357         String responseBodyAsString = httpClient.executeGetRequest(deviceLoginMethod);
358 
359         if (responseBodyAsString.contains(Settings.getO365LoginUrl())) {
360             String ctx = extract("name=\"ctx\" value=\"([^\"]+)\"", responseBodyAsString);
361             String flowtoken = extract("name=\"flowtoken\" value=\"([^\"]+)\"", responseBodyAsString);
362 
363             PostRequest processMethod = new PostRequest(extract("action=\"([^\"]+)\"", responseBodyAsString));
364             processMethod.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
365 
366             processMethod.setParameter("ctx", ctx);
367             processMethod.setParameter("flowtoken", flowtoken);
368 
369             responseBodyAsString = httpClient.executePostRequest(processMethod);
370             result = processMethod.getRedirectLocation();
371 
372             // MFA triggered after device authentication
373             if (result == null && responseBodyAsString != null && responseBodyAsString.contains("arrUserProofs")) {
374                 result = handleMfa(httpClient, processMethod, username, null);
375             }
376 
377             if (result == null) {
378                 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
379             }
380 
381         }
382         return result;
383     }
384 
385     private URI handleMfa(HttpClientAdapter httpClientAdapter, PostRequest logonMethod, String username, String clientRequestId) throws IOException, JSONException {
386         JSONObject config = extractConfig(logonMethod.getResponseBodyAsString());
387         LOGGER.debug("Config=" + config);
388 
389         String urlBeginAuth = config.getString("urlBeginAuth");
390         String urlEndAuth = config.getString("urlEndAuth");
391         // Get processAuth url from config
392         String urlProcessAuth = config.optString("urlPost", Settings.getO365LoginUrl()  + "/" + tenantId + "/SAS/ProcessAuth");
393 
394         boolean isMFAMethodSupported = false;
395         String chosenAuthMethodId = null;
396         String chosenAuthMethodPrompt = null;
397 
398         for (int i = 0; i < config.getJSONArray("arrUserProofs").length(); i++) {
399             JSONObject authMethod = (JSONObject) config.getJSONArray("arrUserProofs").get(i);
400             String authMethodId = authMethod.getString("authMethodId");
401             LOGGER.debug("Authentication method: " + authMethodId);
402             if ("PhoneAppNotification".equals(authMethodId)) {
403                 LOGGER.debug("Found phone app auth method " + authMethod.getString("display"));
404                 isMFAMethodSupported = true;
405                 chosenAuthMethodId = authMethodId;
406                 chosenAuthMethodPrompt = authMethod.getString("display");
407                 // prefer phone app
408                 break;
409             }
410             if ("OneWaySMS".equals(authMethodId)) {
411                 LOGGER.debug("Found OneWaySMS auth method " + authMethod.getString("display"));
412                 chosenAuthMethodId = authMethodId;
413                 chosenAuthMethodPrompt = authMethod.getString("display");
414                 isMFAMethodSupported = true;
415             }
416         }
417 
418         if (!isMFAMethodSupported) {
419             throw new IOException("MFA authentication methods not supported");
420         }
421 
422         String context = config.getString("sCtx");
423         String flowToken = config.getString("sFT");
424 
425         String canary = config.getString("canary");
426         String apiCanary = config.getString("apiCanary");
427 
428         String hpgrequestid = logonMethod.getResponseHeader("x-ms-request-id").getValue();
429         String hpgact = config.getString("hpgact");
430         String hpgid = config.getString("hpgid");
431 
432         // clientRequestId is null coming from device login
433         String correlationId = clientRequestId;
434         if (correlationId == null) {
435             correlationId = config.getString("correlationId");
436         }
437 
438         RestRequest beginAuthMethod = new RestRequest(urlBeginAuth);
439         beginAuthMethod.setRequestHeader("Accept", "application/json");
440         beginAuthMethod.setRequestHeader("canary", apiCanary);
441         beginAuthMethod.setRequestHeader("client-request-id", correlationId);
442         beginAuthMethod.setRequestHeader("hpgact", hpgact);
443         beginAuthMethod.setRequestHeader("hpgid", hpgid);
444         beginAuthMethod.setRequestHeader("hpgrequestid", hpgrequestid);
445 
446         // only support PhoneAppNotification
447         JSONObject beginAuthJson = new JSONObject();
448         beginAuthJson.put("AuthMethodId", chosenAuthMethodId);
449         beginAuthJson.put("Ctx", context);
450         beginAuthJson.put("FlowToken", flowToken);
451         beginAuthJson.put("Method", "BeginAuth");
452         beginAuthMethod.setJsonBody(beginAuthJson);
453 
454         config = httpClientAdapter.executeRestRequest(beginAuthMethod);
455         LOGGER.debug(config);
456 
457         if (!config.getBoolean("Success")) {
458             throw new IOException("Authentication failed: " + config);
459         }
460 
461         // look for number matching value
462         String entropy = config.optString("Entropy", null);
463 
464         // display number matching value to user
465         NumberMatchingFrame numberMatchingFrame = null;
466         if (entropy != null && !"0".equals(entropy)) {
467             LOGGER.info("Number matching value for " + username + ": " + entropy);
468             if (!Settings.getBooleanProperty("davmail.server") && !GraphicsEnvironment.isHeadless()) {
469                 numberMatchingFrame = new NumberMatchingFrame(entropy);
470             }
471         }
472 
473         String smsCode = retrieveSmsCode(chosenAuthMethodId, chosenAuthMethodPrompt);
474 
475         context = config.getString("Ctx");
476         flowToken = config.getString("FlowToken");
477         String sessionId = config.getString("SessionId");
478 
479         int i = 0;
480         boolean success = false;
481         try {
482             while (!success && i++ < 12) {
483 
484                 try {
485                     Thread.sleep(5000);
486                 } catch (InterruptedException e) {
487                     LOGGER.debug("Interrupted");
488                     Thread.currentThread().interrupt();
489                 }
490 
491                 RestRequest endAuthMethod = new RestRequest(urlEndAuth);
492                 endAuthMethod.setRequestHeader("Accept", "application/json");
493                 endAuthMethod.setRequestHeader("canary", apiCanary);
494                 endAuthMethod.setRequestHeader("client-request-id", clientRequestId);
495                 endAuthMethod.setRequestHeader("hpgact", hpgact);
496                 endAuthMethod.setRequestHeader("hpgid", hpgid);
497                 endAuthMethod.setRequestHeader("hpgrequestid", hpgrequestid);
498 
499                 JSONObject endAuthJson = new JSONObject();
500                 endAuthJson.put("AuthMethodId", chosenAuthMethodId);
501                 endAuthJson.put("Ctx", context);
502                 endAuthJson.put("FlowToken", flowToken);
503                 endAuthJson.put("Method", "EndAuth");
504                 endAuthJson.put("PollCount", "1");
505                 endAuthJson.put("SessionId", sessionId);
506 
507                 // When in beginAuthMethod is used 'AuthMethodId': 'OneWaySMS', then in endAuthMethod is send SMS code
508                 // via attribute 'AdditionalAuthData'
509                 endAuthJson.put("AdditionalAuthData", smsCode);
510 
511                 endAuthMethod.setJsonBody(endAuthJson);
512 
513                 config = httpClientAdapter.executeRestRequest(endAuthMethod);
514                 LOGGER.debug(config);
515                 String resultValue = config.getString("ResultValue");
516                 if ("PhoneAppDenied".equals(resultValue) || "PhoneAppNoResponse".equals(resultValue)) {
517                     throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_REASON", resultValue);
518                 }
519                 if ("SMSAuthFailedWrongCodeEntered".equals(resultValue)) {
520                     smsCode = retrieveSmsCode(chosenAuthMethodId, chosenAuthMethodPrompt);
521                 }
522                 if (config.getBoolean("Success")) {
523                     success = true;
524                 }
525             }
526         } finally {
527             // close number matching frame if exists
528             if (numberMatchingFrame != null && numberMatchingFrame.isVisible()) {
529                 final JFrame finalNumberMatchingFrame = numberMatchingFrame;
530                 SwingUtilities.invokeLater(() -> {
531                     finalNumberMatchingFrame.setVisible(false);
532                     finalNumberMatchingFrame.dispose();
533                 });
534             }
535 
536         }
537         if (!success) {
538             throw new IOException("Authentication failed: " + config);
539         }
540 
541         String authMethod = "PhoneAppOTP";
542         String type = "22";
543 
544         context = config.getString("Ctx");
545         flowToken = config.getString("FlowToken");
546 
547         // process auth
548         PostRequest processAuthMethod = new PostRequest(urlProcessAuth);
549         processAuthMethod.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
550         processAuthMethod.setParameter("type", type);
551         processAuthMethod.setParameter("request", context);
552         processAuthMethod.setParameter("mfaAuthMethod", authMethod);
553         processAuthMethod.setParameter("canary", canary);
554         processAuthMethod.setParameter("login", username);
555         processAuthMethod.setParameter("flowToken", flowToken);
556         processAuthMethod.setParameter("hpgrequestid", hpgrequestid);
557 
558         httpClientAdapter.executePostRequest(processAuthMethod);
559         return processAuthMethod.getRedirectLocation();
560 
561     }
562 
563     private String retrieveSmsCode(String chosenAuthMethodId, String chosenAuthMethodPrompt) throws IOException {
564         String smsCode = null;
565         if ("OneWaySMS".equals(chosenAuthMethodId)) {
566             LOGGER.info("Need to retrieve SMS verification code for " + username);
567             if (Settings.getBooleanProperty("davmail.server") || GraphicsEnvironment.isHeadless()) {
568                 // headless or server mode
569                 System.out.print(BundleMessage.format("UI_SMS_PHONE_CODE", chosenAuthMethodPrompt));
570                 BufferedReader inReader = new BufferedReader(new InputStreamReader(System.in));
571                 smsCode = inReader.readLine();
572             } else {
573                 PasswordPromptDialog passwordPromptDialog = new PasswordPromptDialog(BundleMessage.format("UI_SMS_PHONE_CODE", chosenAuthMethodPrompt));
574                 smsCode = String.valueOf(passwordPromptDialog.getPassword());
575             }
576         }
577         return smsCode;
578     }
579 
580     private String executeFollowRedirect(HttpClientAdapter httpClientAdapter, GetRequest getRequest) throws IOException {
581         LOGGER.debug(getRequest.getURI());
582         ResponseWrapper responseWrapper = httpClientAdapter.executeFollowRedirect(getRequest);
583         if (responseWrapper.getURI().getHost().endsWith("okta.com")) {
584             throw new DavMailAuthenticationException("LOG_MESSAGE", "Okta authentication not supported, please try O365Interactive");
585         }
586         return responseWrapper.getResponseBodyAsString();
587     }
588 
589     public JSONObject extractConfig(String content) throws IOException {
590         try {
591             return new JSONObject(extract("Config=([^\n]+);", content));
592         } catch (JSONException e1) {
593             LOGGER.debug(content);
594             throw new IOException("Unable to extract config from response body");
595         }
596     }
597 
598     public String extract(String pattern, String content) throws IOException {
599         String value;
600         Matcher matcher = Pattern.compile(pattern).matcher(content);
601         if (matcher.find()) {
602             value = matcher.group(1);
603         } else {
604             throw new IOException("pattern not found");
605         }
606         return value;
607     }
608 
609 }