1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
53 private String username;
54
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
70
71
72 if ("https://outlook.live.com".equals(Settings.getOutlookUrl())) {
73
74 String liveAuthorizeUrl = "https://login.live.com/oauth20_authorize.srf";
75 uriBuilder = new URIBuilder(liveAuthorizeUrl)
76 .addParameter("client_id", clientId)
77 .addParameter("response_type", "code")
78 .addParameter("redirect_uri", redirectUri)
79 .addParameter("response_mode", "query")
80 .addParameter("login_hint", username)
81 .addParameter("scope", "openid offline_access https://outlook.live.com/EWS.AccessAsUser.All Mail.ReadWrite MailboxSettings.Read")
82 .addParameter("resource", "https://outlook.live.com")
83
84 ;
85
86 } else if (Settings.getBooleanProperty("davmail.enableGraph", false)) {
87 if (Settings.getBooleanProperty("davmail.enableOidc", false)) {
88
89 uriBuilder.setPath("/" + tenantId + "/oauth2/v2.0/authorize")
90 .addParameter("scope", Settings.getProperty("davmail.oauth.scope", "openid profile offline_access Mail.ReadWrite Calendars.ReadWrite MailboxSettings.Read Mail.ReadWrite.Shared Contacts.ReadWrite Tasks.ReadWrite Mail.Send"));
91
92
93
94
95 } else {
96
97
98 uriBuilder.setPath("/" + tenantId + "/oauth2/authorize")
99 .addParameter("resource", Settings.getGraphUrl());
100 }
101
102 } else if (Settings.getBooleanProperty("davmail.enableOidc", false)) {
103 uriBuilder.setPath("/" + tenantId + "/oauth2/v2.0/authorize")
104 .addParameter("scope", Settings.getProperty("davmail.oauth.scope", "openid profile offline_access " + Settings.getOutlookUrl() + "/EWS.AccessAsUser.All"));
105 } else {
106 uriBuilder.setPath("/" + tenantId + "/oauth2/authorize")
107 .addParameter("resource", Settings.getOutlookUrl());
108 }
109
110 uri = uriBuilder.build();
111 } catch (URISyntaxException e) {
112 throw new IOException(e);
113 }
114 return uri.toString();
115 }
116
117 public void setUsername(String username) {
118 if (username.contains("|")) {
119 this.userid = username.substring(0, username.indexOf("|"));
120 this.username = username.substring(username.indexOf("|") + 1);
121 } else {
122 this.username = username;
123 this.userid = username;
124 }
125 }
126
127 public void setPassword(String password) {
128 this.password = password;
129 }
130
131 public O365Token getToken() {
132 return token;
133 }
134
135 public URI getExchangeUri() {
136 return URI.create(Settings.getO365Url());
137 }
138
139
140
141
142
143
144 @Override
145 public HttpClientAdapter getHttpClientAdapter() {
146 return new HttpClientAdapter(getExchangeUri(), username, password, true);
147 }
148
149 public void authenticate() throws IOException {
150
151 String clientId = Settings.getProperty("davmail.oauth.clientId", "facd6cff-a294-4415-b59f-c5b01937d7bd");
152
153 String redirectUri = Settings.getProperty("davmail.oauth.redirectUri", Settings.getO365LoginUrl() + "/common/oauth2/nativeclient");
154
155 tenantId = Settings.getProperty("davmail.oauth.tenantId", "common");
156
157
158 token = O365Token.load(tenantId, clientId, redirectUri, username, password);
159 if (token != null) {
160 return;
161 }
162
163 String url = O365Authenticator.buildAuthorizeUrl(tenantId, clientId, redirectUri, username);
164
165 try (
166 HttpClientAdapter httpClientAdapter = new HttpClientAdapter(url, userid, password)
167 ) {
168
169 GetRequest getRequest = new GetRequest(url);
170 String responseBodyAsString = executeFollowRedirect(httpClientAdapter, getRequest);
171 String code;
172 if (!responseBodyAsString.contains("Config=") && responseBodyAsString.contains("ServerData =")) {
173
174 JSONObject config = extractServerData(responseBodyAsString);
175
176 String referer = getRequest.getURI().toString();
177 code = authenticateLive(httpClientAdapter, config, referer);
178 } else if (!responseBodyAsString.contains("Config=")) {
179
180 code = authenticateADFS(httpClientAdapter, responseBodyAsString, url);
181 } else {
182 JSONObject config = extractConfig(responseBodyAsString);
183
184 checkConfigErrors(config);
185
186 String context = config.getString("sCtx");
187 String apiCanary = config.getString("apiCanary");
188 String clientRequestId = config.getString("correlationId");
189 String hpgact = config.getString("hpgact");
190 String hpgid = config.getString("hpgid");
191 String flowToken = config.getString("sFT");
192 String canary = config.getString("canary");
193 String sessionId = config.getString("sessionId");
194
195 String referer = getRequest.getURI().toString();
196
197 RestRequest getCredentialMethod = new RestRequest(Settings.getO365LoginUrl() + "/" + tenantId + "/GetCredentialType");
198 getCredentialMethod.setRequestHeader("Accept", "application/json");
199 getCredentialMethod.setRequestHeader("canary", apiCanary);
200 getCredentialMethod.setRequestHeader("client-request-id", clientRequestId);
201 getCredentialMethod.setRequestHeader("hpgact", hpgact);
202 getCredentialMethod.setRequestHeader("hpgid", hpgid);
203 getCredentialMethod.setRequestHeader("hpgrequestid", sessionId);
204 getCredentialMethod.setRequestHeader("Referer", referer);
205
206 final JSONObject jsonObject = new JSONObject();
207 jsonObject.put("username", username);
208 jsonObject.put("isOtherIdpSupported", true);
209 jsonObject.put("checkPhones", false);
210 jsonObject.put("isRemoteNGCSupported", false);
211 jsonObject.put("isCookieBannerShown", false);
212 jsonObject.put("isFidoSupported", false);
213 jsonObject.put("flowToken", flowToken);
214 jsonObject.put("originalRequest", context);
215
216 getCredentialMethod.setJsonBody(jsonObject);
217
218 JSONObject credentialType = httpClientAdapter.executeRestRequest(getCredentialMethod);
219
220 LOGGER.debug("CredentialType=" + credentialType);
221
222 JSONObject credentials = credentialType.getJSONObject("Credentials");
223 String federationRedirectUrl = credentials.optString("FederationRedirectUrl");
224
225 if (federationRedirectUrl != null && !federationRedirectUrl.isEmpty()) {
226 LOGGER.debug("Detected ADFS, redirecting to " + federationRedirectUrl);
227 code = authenticateRedirectADFS(httpClientAdapter, federationRedirectUrl, url);
228 } else {
229 PostRequest logonMethod = new PostRequest(Settings.getO365LoginUrl() + "/" + tenantId + "/login");
230 logonMethod.setRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
231 logonMethod.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
232
233 logonMethod.setRequestHeader("Referer", referer);
234
235 logonMethod.setParameter("canary", canary);
236 logonMethod.setParameter("ctx", context);
237 logonMethod.setParameter("flowToken", flowToken);
238 logonMethod.setParameter("hpgrequestid", sessionId);
239 logonMethod.setParameter("login", username);
240 logonMethod.setParameter("loginfmt", username);
241 logonMethod.setParameter("passwd", password);
242
243 responseBodyAsString = httpClientAdapter.executePostRequest(logonMethod);
244 URI location = logonMethod.getRedirectLocation();
245
246 if (responseBodyAsString != null && responseBodyAsString.contains("arrUserProofs")) {
247 logonMethod = handleMfa(httpClientAdapter, logonMethod, username, clientRequestId);
248 location = logonMethod.getRedirectLocation();
249 }
250
251 if (location == null || !location.toString().startsWith(redirectUri)) {
252
253 config = extractConfig(logonMethod.getResponseBodyAsString());
254 if (config.optJSONArray("arrScopes") != null || config.optJSONArray("urlPostRedirect") != null) {
255 LOGGER.warn("Authentication successful but user consent or validation needed, please open the following url in a browser");
256 LOGGER.warn(url);
257 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
258 } else if ("ConvergedChangePassword".equals(config.optString("pgid"))) {
259 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_PASSWORD_EXPIRED");
260 } else if ("50126".equals(config.optString("sErrorCode"))) {
261 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
262 } else if ("50125".equals(config.optString("sErrorCode"))) {
263 throw new DavMailAuthenticationException("LOG_MESSAGE", "Your organization needs more information to keep your account secure, authenticate once in a web browser and try again");
264 } else if ("50128".equals(config.optString("sErrorCode"))) {
265 throw new DavMailAuthenticationException("LOG_MESSAGE", "Invalid domain name - No tenant-identifying information found in either the request or implied by any provided credentials.");
266 } else if (config.optString("strServiceExceptionMessage", null) != null) {
267 LOGGER.debug("O365 returned error: " + config.optString("strServiceExceptionMessage"));
268 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
269 } else {
270 throw new DavMailAuthenticationException("LOG_MESSAGE", "Authentication failed, unknown error: " + config);
271 }
272 }
273 String query = location.toString();
274 if (query.contains("code=")) {
275 code = query.substring(query.indexOf("code=") + 5, query.indexOf("&session_state="));
276 } else {
277 throw new DavMailAuthenticationException("LOG_MESSAGE", "Authentication failed, unknown error: " + query);
278 }
279 }
280 }
281 LOGGER.debug("Authentication Code: " + code);
282
283 token = O365Token.build(tenantId, clientId, redirectUri, code, password);
284
285 LOGGER.debug("Authenticated username: " + token.getUsername());
286 if (!username.equalsIgnoreCase(token.getUsername())) {
287 throw new IOException("Authenticated username " + token.getUsername() + " does not match " + username);
288 }
289
290 } catch (JSONException e) {
291 throw new IOException(e + " " + e.getMessage());
292 }
293
294 }
295
296 private void checkConfigErrors(JSONObject config) throws DavMailAuthenticationException {
297 if (config.optString("strServiceExceptionMessage", null) != null) {
298 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_REASON", config.optString("strServiceExceptionMessage"));
299 }
300 }
301
302 private String authenticateLive(HttpClientAdapter httpClientAdapter, JSONObject config, String referer) throws JSONException, IOException {
303 String urlPost = config.getString("urlPost");
304 PostRequest logonMethod = new PostRequest(urlPost);
305 logonMethod.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
306 logonMethod.setRequestHeader("Referer", referer);
307 String sFTTag = config.optString("sFTTag");
308 String ppft = "";
309 if (sFTTag.contains("value=")) {
310 ppft = sFTTag.substring(sFTTag.indexOf("value=\"")+7, sFTTag.indexOf("\"/>"));
311 }
312
313 logonMethod.setParameter("PPFT", ppft);
314
315 logonMethod.setParameter("login", username);
316 logonMethod.setParameter("loginfmt", username);
317
318 logonMethod.setParameter("passwd", password);
319
320 String responseBodyAsString = httpClientAdapter.executePostRequest(logonMethod);
321 URI location = logonMethod.getRedirectLocation();
322 if (location == null) {
323 if (responseBodyAsString.contains("ServerData =")) {
324 String errorMessage = extractServerData(responseBodyAsString).optString("sErrTxt");
325 throw new IOException("Live.com authentication failure: "+errorMessage);
326 }
327 } else {
328 String query = location.getQuery();
329 if (query.contains("code=")) {
330 String code = query.substring(query.indexOf("code=") + 5);
331 LOGGER.debug("Authentication Code: " + code);
332 return code;
333 }
334 }
335 throw new IOException("Unknown Live.com authentication failure");
336 }
337
338 private String authenticateRedirectADFS(HttpClientAdapter httpClientAdapter, String federationRedirectUrl, String authorizeUrl) throws IOException, JSONException {
339
340 GetRequest logonFormMethod = new GetRequest(federationRedirectUrl);
341 logonFormMethod = httpClientAdapter.executeFollowRedirect(logonFormMethod);
342 String responseBodyAsString = logonFormMethod.getResponseBodyAsString();
343 return authenticateADFS(httpClientAdapter, responseBodyAsString, authorizeUrl);
344 }
345
346 private String authenticateADFS(HttpClientAdapter httpClientAdapter, String responseBodyAsString, String authorizeUrl) throws IOException, JSONException {
347 URI location;
348
349 if (responseBodyAsString.contains(Settings.getO365LoginUrl())) {
350 LOGGER.info("Already authenticated through Basic or NTLM");
351 } else {
352
353 PostRequest logonMethod = new PostRequest(extract("method=\"post\" action=\"([^\"]+)\"", responseBodyAsString));
354 logonMethod.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
355
356 logonMethod.setParameter("UserName", userid);
357 logonMethod.setParameter("Password", password);
358 logonMethod.setParameter("AuthMethod", "FormsAuthentication");
359
360 httpClientAdapter.executePostRequest(logonMethod);
361 location = logonMethod.getRedirectLocation();
362 if (location == null) {
363 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
364 }
365
366 GetRequest redirectMethod = new GetRequest(location);
367 responseBodyAsString = httpClientAdapter.executeGetRequest(redirectMethod);
368 }
369
370 if (!responseBodyAsString.contains(Settings.getO365LoginUrl())) {
371 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
372 }
373 String targetUrl = extract("action=\"([^\"]+)\"", responseBodyAsString);
374 String wa = extract("name=\"wa\" value=\"([^\"]+)\"", responseBodyAsString);
375 String wresult = extract("name=\"wresult\" value=\"([^\"]+)\"", responseBodyAsString);
376
377 wresult = wresult.replaceAll(""", "\"");
378 wresult = wresult.replaceAll("<", "<");
379 wresult = wresult.replaceAll(">", ">");
380 String wctx = extract("name=\"wctx\" value=\"([^\"]+)\"", responseBodyAsString);
381 wctx = wctx.replaceAll("&", "&");
382
383 PostRequest targetMethod = new PostRequest(targetUrl);
384 targetMethod.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
385 targetMethod.setParameter("wa", wa);
386 targetMethod.setParameter("wresult", wresult);
387 targetMethod.setParameter("wctx", wctx);
388
389 responseBodyAsString = httpClientAdapter.executePostRequest(targetMethod);
390 location = targetMethod.getRedirectLocation();
391
392 LOGGER.debug(targetMethod.getURI().toString());
393 LOGGER.debug(targetMethod.getReasonPhrase());
394 LOGGER.debug(responseBodyAsString);
395
396 if (targetMethod.getStatusCode() == HttpStatus.SC_OK) {
397 JSONObject config = extractConfig(responseBodyAsString);
398 if (config.optJSONArray("arrScopes") != null || config.optJSONArray("urlPostRedirect") != null) {
399 LOGGER.warn("Authentication successful but user consent or validation needed, please open the following url in a browser");
400 LOGGER.warn(authorizeUrl);
401 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
402 }
403 } else if (targetMethod.getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY || location == null) {
404 throw new IOException("Unknown ADFS authentication failure");
405 }
406
407 if (location.getHost().startsWith("device")) {
408 location = processDeviceLogin(httpClientAdapter, location);
409 }
410 String query = location.getQuery();
411 if (query == null) {
412
413 query = location.getSchemeSpecificPart();
414 }
415
416 if (query.contains("code=") && query.contains("&session_state=")) {
417 String code = query.substring(query.indexOf("code=") + 5, query.indexOf("&session_state="));
418 LOGGER.debug("Authentication Code: " + code);
419 return code;
420 }
421 throw new IOException("Unknown ADFS authentication failure");
422 }
423
424 private URI processDeviceLogin(HttpClientAdapter httpClient, URI location) throws IOException, JSONException {
425 URI result = location;
426 LOGGER.debug("Proceed to device authentication, must have access to a client certificate signed by MS-Organization-Access");
427 if (Settings.isWindows() &&
428 (System.getProperty("java.version").compareTo("13") < 0
429 || !"MSCAPI".equals(Settings.getProperty("davmail.ssl.clientKeystoreType")))
430 ) {
431 LOGGER.warn("MSCAPI and Java version 13 or higher required to access TPM protected client certificate on Windows");
432 }
433 GetRequest deviceLoginMethod = new GetRequest(location);
434
435 String responseBodyAsString = httpClient.executeGetRequest(deviceLoginMethod);
436
437 if (responseBodyAsString.contains(Settings.getO365LoginUrl())) {
438 String ctx = extract("name=\"ctx\" value=\"([^\"]+)\"", responseBodyAsString);
439 String flowtoken = extract("name=\"flowtoken\" value=\"([^\"]+)\"", responseBodyAsString);
440
441 PostRequest processMethod = new PostRequest(extract("action=\"([^\"]+)\"", responseBodyAsString));
442 processMethod.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
443
444 processMethod.setParameter("ctx", ctx);
445 processMethod.setParameter("flowtoken", flowtoken);
446
447 responseBodyAsString = httpClient.executePostRequest(processMethod);
448 result = processMethod.getRedirectLocation();
449
450
451 if (result == null && responseBodyAsString != null && responseBodyAsString.contains("arrUserProofs")) {
452 processMethod = handleMfa(httpClient, processMethod, username, null);
453 result = processMethod.getRedirectLocation();
454 }
455
456 if (result == null) {
457 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
458 }
459
460 }
461 return result;
462 }
463
464 private PostRequest handleMfa(HttpClientAdapter httpClientAdapter, PostRequest logonMethod, String username, String clientRequestId) throws IOException, JSONException {
465 JSONObject config = extractConfig(logonMethod.getResponseBodyAsString());
466 LOGGER.debug("Config=" + config);
467
468 String urlBeginAuth = config.getString("urlBeginAuth");
469 String urlEndAuth = config.getString("urlEndAuth");
470
471 String urlProcessAuth = config.optString("urlPost", Settings.getO365LoginUrl() + "/" + tenantId + "/SAS/ProcessAuth");
472
473 boolean isMFAMethodSupported = false;
474 String chosenAuthMethodId = null;
475 String chosenAuthMethodPrompt = null;
476
477 for (int i = 0; i < config.getJSONArray("arrUserProofs").length(); i++) {
478 JSONObject authMethod = (JSONObject) config.getJSONArray("arrUserProofs").get(i);
479 String authMethodId = authMethod.getString("authMethodId");
480 LOGGER.debug("Authentication method: " + authMethodId);
481 if ("PhoneAppNotification".equals(authMethodId)) {
482 LOGGER.debug("Found phone app auth method " + authMethod.getString("display"));
483 isMFAMethodSupported = true;
484 chosenAuthMethodId = authMethodId;
485 chosenAuthMethodPrompt = authMethod.getString("display");
486
487 break;
488 }
489 if ("OneWaySMS".equals(authMethodId)) {
490 LOGGER.debug("Found OneWaySMS auth method " + authMethod.getString("display"));
491 chosenAuthMethodId = authMethodId;
492 chosenAuthMethodPrompt = authMethod.getString("display");
493 isMFAMethodSupported = true;
494 }
495 }
496
497 if (!isMFAMethodSupported) {
498 throw new IOException("MFA authentication methods not supported");
499 }
500
501 String context = config.getString("sCtx");
502 String flowToken = config.getString("sFT");
503
504 String canary = config.getString("canary");
505 String apiCanary = config.getString("apiCanary");
506
507 String hpgrequestid = logonMethod.getResponseHeader("x-ms-request-id").getValue();
508 String hpgact = config.getString("hpgact");
509 String hpgid = config.getString("hpgid");
510
511
512 String correlationId = clientRequestId;
513 if (correlationId == null) {
514 correlationId = config.getString("correlationId");
515 }
516
517 RestRequest beginAuthMethod = new RestRequest(urlBeginAuth);
518 beginAuthMethod.setRequestHeader("Accept", "application/json");
519 beginAuthMethod.setRequestHeader("canary", apiCanary);
520 beginAuthMethod.setRequestHeader("client-request-id", correlationId);
521 beginAuthMethod.setRequestHeader("hpgact", hpgact);
522 beginAuthMethod.setRequestHeader("hpgid", hpgid);
523 beginAuthMethod.setRequestHeader("hpgrequestid", hpgrequestid);
524
525
526 JSONObject beginAuthJson = new JSONObject();
527 beginAuthJson.put("AuthMethodId", chosenAuthMethodId);
528 beginAuthJson.put("Ctx", context);
529 beginAuthJson.put("FlowToken", flowToken);
530 beginAuthJson.put("Method", "BeginAuth");
531 beginAuthMethod.setJsonBody(beginAuthJson);
532
533 config = httpClientAdapter.executeRestRequest(beginAuthMethod);
534 LOGGER.debug(config);
535
536 if (!config.getBoolean("Success")) {
537 throw new IOException("Authentication failed: " + config);
538 }
539
540
541 String entropy = config.optString("Entropy", null);
542
543
544 NumberMatchingFrame numberMatchingFrame = null;
545 if (entropy != null && !"0".equals(entropy)) {
546 LOGGER.info("Number matching value for " + username + ": " + entropy);
547 if (!Settings.getBooleanProperty("davmail.server") && !GraphicsEnvironment.isHeadless()) {
548 numberMatchingFrame = new NumberMatchingFrame(entropy);
549 }
550 }
551
552 String smsCode = retrieveSmsCode(chosenAuthMethodId, chosenAuthMethodPrompt);
553
554 context = config.getString("Ctx");
555 flowToken = config.getString("FlowToken");
556 String sessionId = config.getString("SessionId");
557
558 int i = 0;
559 boolean success = false;
560 try {
561 while (!success && i++ < 12) {
562
563 try {
564 Thread.sleep(5000);
565 } catch (InterruptedException e) {
566 LOGGER.debug("Interrupted");
567 Thread.currentThread().interrupt();
568 }
569
570 RestRequest endAuthMethod = new RestRequest(urlEndAuth);
571 endAuthMethod.setRequestHeader("Accept", "application/json");
572 endAuthMethod.setRequestHeader("canary", apiCanary);
573 endAuthMethod.setRequestHeader("client-request-id", clientRequestId);
574 endAuthMethod.setRequestHeader("hpgact", hpgact);
575 endAuthMethod.setRequestHeader("hpgid", hpgid);
576 endAuthMethod.setRequestHeader("hpgrequestid", hpgrequestid);
577
578 JSONObject endAuthJson = new JSONObject();
579 endAuthJson.put("AuthMethodId", chosenAuthMethodId);
580 endAuthJson.put("Ctx", context);
581 endAuthJson.put("FlowToken", flowToken);
582 endAuthJson.put("Method", "EndAuth");
583 endAuthJson.put("PollCount", "1");
584 endAuthJson.put("SessionId", sessionId);
585
586
587
588 endAuthJson.put("AdditionalAuthData", smsCode);
589
590 endAuthMethod.setJsonBody(endAuthJson);
591
592 config = httpClientAdapter.executeRestRequest(endAuthMethod);
593 LOGGER.debug(config);
594 String resultValue = config.getString("ResultValue");
595 if ("PhoneAppDenied".equals(resultValue) || "PhoneAppNoResponse".equals(resultValue)) {
596 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_REASON", resultValue);
597 }
598 if ("SMSAuthFailedWrongCodeEntered".equals(resultValue)) {
599 smsCode = retrieveSmsCode(chosenAuthMethodId, chosenAuthMethodPrompt);
600 }
601 if (config.getBoolean("Success")) {
602 success = true;
603 }
604 }
605 } finally {
606
607 if (numberMatchingFrame != null && numberMatchingFrame.isVisible()) {
608 final JFrame finalNumberMatchingFrame = numberMatchingFrame;
609 SwingUtilities.invokeLater(() -> {
610 finalNumberMatchingFrame.setVisible(false);
611 finalNumberMatchingFrame.dispose();
612 });
613 }
614
615 }
616 if (!success) {
617 throw new IOException("Authentication failed: " + config);
618 }
619
620 String authMethod = chosenAuthMethodId;
621 String type = "22";
622
623 context = config.getString("Ctx");
624 flowToken = config.getString("FlowToken");
625
626
627 PostRequest processAuthMethod = new PostRequest(urlProcessAuth);
628 processAuthMethod.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
629 processAuthMethod.setParameter("type", type);
630 processAuthMethod.setParameter("request", context);
631 processAuthMethod.setParameter("mfaAuthMethod", authMethod);
632 processAuthMethod.setParameter("canary", canary);
633 processAuthMethod.setParameter("login", username);
634 processAuthMethod.setParameter("flowToken", flowToken);
635 processAuthMethod.setParameter("hpgrequestid", hpgrequestid);
636
637 httpClientAdapter.executePostRequest(processAuthMethod);
638 return processAuthMethod;
639
640 }
641
642 private String retrieveSmsCode(String chosenAuthMethodId, String chosenAuthMethodPrompt) throws IOException {
643 String smsCode = null;
644 if ("OneWaySMS".equals(chosenAuthMethodId)) {
645 LOGGER.info("Need to retrieve SMS verification code for " + username);
646 if (Settings.getBooleanProperty("davmail.server") || GraphicsEnvironment.isHeadless()) {
647
648 System.out.print(BundleMessage.format("UI_SMS_PHONE_CODE", chosenAuthMethodPrompt));
649 BufferedReader inReader = new BufferedReader(new InputStreamReader(System.in));
650 smsCode = inReader.readLine();
651 } else {
652 PasswordPromptDialog passwordPromptDialog = new PasswordPromptDialog(BundleMessage.format("UI_SMS_PHONE_CODE", chosenAuthMethodPrompt));
653 smsCode = String.valueOf(passwordPromptDialog.getPassword());
654 }
655 }
656 return smsCode;
657 }
658
659 private String executeFollowRedirect(HttpClientAdapter httpClientAdapter, GetRequest getRequest) throws IOException {
660 LOGGER.debug(getRequest.getURI());
661 ResponseWrapper responseWrapper = httpClientAdapter.executeFollowRedirect(getRequest);
662 String responseHost = responseWrapper.getURI().getHost();
663 if (responseHost.endsWith("okta.com")) {
664 throw new DavMailAuthenticationException("LOG_MESSAGE", "Okta authentication not supported, please try O365Interactive");
665 }
666 return responseWrapper.getResponseBodyAsString();
667 }
668
669 public JSONObject extractConfig(String content) throws IOException {
670 try {
671 return new JSONObject(extract("Config=([^\n]+);", content));
672 } catch (JSONException e1) {
673 LOGGER.debug(content);
674 throw new IOException("Unable to extract config from response body");
675 }
676 }
677
678
679
680
681
682
683
684 public JSONObject extractServerData(String content) throws IOException {
685 try {
686 return new JSONObject(extract("ServerData =([^\n]+);", content));
687 } catch (JSONException e1) {
688 LOGGER.debug(content);
689 throw new IOException("Unable to extract config from response body");
690 }
691 }
692
693 public String extract(String pattern, String content) throws IOException {
694 String value;
695 Matcher matcher = Pattern.compile(pattern).matcher(content);
696 if (matcher.find()) {
697 value = matcher.group(1);
698 } else {
699 throw new IOException("pattern not found");
700 }
701 return value;
702 }
703
704 }