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", "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", "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 location = handleMfa(httpClientAdapter, logonMethod, username, clientRequestId);
248 }
249
250 if (location == null || !location.toString().startsWith(redirectUri)) {
251
252 config = extractConfig(logonMethod.getResponseBodyAsString());
253 if (config.optJSONArray("arrScopes") != null || config.optJSONArray("urlPostRedirect") != null) {
254 LOGGER.warn("Authentication successful but user consent or validation needed, please open the following url in a browser");
255 LOGGER.warn(url);
256 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
257 } else if ("ConvergedChangePassword".equals(config.optString("pgid"))) {
258 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_PASSWORD_EXPIRED");
259 } else if ("50126".equals(config.optString("sErrorCode"))) {
260 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
261 } else if ("50125".equals(config.optString("sErrorCode"))) {
262 throw new DavMailAuthenticationException("LOG_MESSAGE", "Your organization needs more information to keep your account secure, authenticate once in a web browser and try again");
263 } else if ("50128".equals(config.optString("sErrorCode"))) {
264 throw new DavMailAuthenticationException("LOG_MESSAGE", "Invalid domain name - No tenant-identifying information found in either the request or implied by any provided credentials.");
265 } else if (config.optString("strServiceExceptionMessage", null) != null) {
266 LOGGER.debug("O365 returned error: " + config.optString("strServiceExceptionMessage"));
267 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
268 } else {
269 throw new DavMailAuthenticationException("LOG_MESSAGE", "Authentication failed, unknown error: " + config);
270 }
271 }
272 String query = location.toString();
273 if (query.contains("code=")) {
274 code = query.substring(query.indexOf("code=") + 5, query.indexOf("&session_state="));
275 } else {
276 throw new DavMailAuthenticationException("LOG_MESSAGE", "Authentication failed, unknown error: " + query);
277 }
278 }
279 }
280 LOGGER.debug("Authentication Code: " + code);
281
282 token = O365Token.build(tenantId, clientId, redirectUri, code, password);
283
284 LOGGER.debug("Authenticated username: " + token.getUsername());
285 if (!username.equalsIgnoreCase(token.getUsername())) {
286 throw new IOException("Authenticated username " + token.getUsername() + " does not match " + username);
287 }
288
289 } catch (JSONException e) {
290 throw new IOException(e + " " + e.getMessage());
291 }
292
293 }
294
295 private void checkConfigErrors(JSONObject config) throws DavMailAuthenticationException {
296 if (config.optString("strServiceExceptionMessage", null) != null) {
297 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_REASON", config.optString("strServiceExceptionMessage"));
298 }
299 }
300
301 private String authenticateLive(HttpClientAdapter httpClientAdapter, JSONObject config, String referer) throws JSONException, IOException {
302 String urlPost = config.getString("urlPost");
303 PostRequest logonMethod = new PostRequest(urlPost);
304 logonMethod.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
305 logonMethod.setRequestHeader("Referer", referer);
306 String sFTTag = config.optString("sFTTag");
307 String ppft = "";
308 if (sFTTag.contains("value=")) {
309 ppft = sFTTag.substring(sFTTag.indexOf("value=\"")+7, sFTTag.indexOf("\"/>"));
310 }
311
312 logonMethod.setParameter("PPFT", ppft);
313
314 logonMethod.setParameter("login", username);
315 logonMethod.setParameter("loginfmt", username);
316
317 logonMethod.setParameter("passwd", password);
318
319 String responseBodyAsString = httpClientAdapter.executePostRequest(logonMethod);
320 URI location = logonMethod.getRedirectLocation();
321 if (location == null) {
322 if (responseBodyAsString.contains("ServerData =")) {
323 String errorMessage = extractServerData(responseBodyAsString).optString("sErrTxt");
324 throw new IOException("Live.com authentication failure: "+errorMessage);
325 }
326 } else {
327 String query = location.getQuery();
328 if (query.contains("code=")) {
329 String code = query.substring(query.indexOf("code=") + 5);
330 LOGGER.debug("Authentication Code: " + code);
331 return code;
332 }
333 }
334 throw new IOException("Unknown Live.com authentication failure");
335 }
336
337 private String authenticateRedirectADFS(HttpClientAdapter httpClientAdapter, String federationRedirectUrl, String authorizeUrl) throws IOException, JSONException {
338
339 GetRequest logonFormMethod = new GetRequest(federationRedirectUrl);
340 logonFormMethod = httpClientAdapter.executeFollowRedirect(logonFormMethod);
341 String responseBodyAsString = logonFormMethod.getResponseBodyAsString();
342 return authenticateADFS(httpClientAdapter, responseBodyAsString, authorizeUrl);
343 }
344
345 private String authenticateADFS(HttpClientAdapter httpClientAdapter, String responseBodyAsString, String authorizeUrl) throws IOException, JSONException {
346 URI location;
347
348 if (responseBodyAsString.contains(Settings.getO365LoginUrl())) {
349 LOGGER.info("Already authenticated through Basic or NTLM");
350 } else {
351
352 PostRequest logonMethod = new PostRequest(extract("method=\"post\" action=\"([^\"]+)\"", responseBodyAsString));
353 logonMethod.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
354
355 logonMethod.setParameter("UserName", userid);
356 logonMethod.setParameter("Password", password);
357 logonMethod.setParameter("AuthMethod", "FormsAuthentication");
358
359 httpClientAdapter.executePostRequest(logonMethod);
360 location = logonMethod.getRedirectLocation();
361 if (location == null) {
362 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
363 }
364
365 GetRequest redirectMethod = new GetRequest(location);
366 responseBodyAsString = httpClientAdapter.executeGetRequest(redirectMethod);
367 }
368
369 if (!responseBodyAsString.contains(Settings.getO365LoginUrl())) {
370 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
371 }
372 String targetUrl = extract("action=\"([^\"]+)\"", responseBodyAsString);
373 String wa = extract("name=\"wa\" value=\"([^\"]+)\"", responseBodyAsString);
374 String wresult = extract("name=\"wresult\" value=\"([^\"]+)\"", responseBodyAsString);
375
376 wresult = wresult.replaceAll(""", "\"");
377 wresult = wresult.replaceAll("<", "<");
378 wresult = wresult.replaceAll(">", ">");
379 String wctx = extract("name=\"wctx\" value=\"([^\"]+)\"", responseBodyAsString);
380 wctx = wctx.replaceAll("&", "&");
381
382 PostRequest targetMethod = new PostRequest(targetUrl);
383 targetMethod.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
384 targetMethod.setParameter("wa", wa);
385 targetMethod.setParameter("wresult", wresult);
386 targetMethod.setParameter("wctx", wctx);
387
388 responseBodyAsString = httpClientAdapter.executePostRequest(targetMethod);
389 location = targetMethod.getRedirectLocation();
390
391 LOGGER.debug(targetMethod.getURI().toString());
392 LOGGER.debug(targetMethod.getReasonPhrase());
393 LOGGER.debug(responseBodyAsString);
394
395 if (targetMethod.getStatusCode() == HttpStatus.SC_OK) {
396 JSONObject config = extractConfig(responseBodyAsString);
397 if (config.optJSONArray("arrScopes") != null || config.optJSONArray("urlPostRedirect") != null) {
398 LOGGER.warn("Authentication successful but user consent or validation needed, please open the following url in a browser");
399 LOGGER.warn(authorizeUrl);
400 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
401 }
402 } else if (targetMethod.getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY || location == null) {
403 throw new IOException("Unknown ADFS authentication failure");
404 }
405
406 if (location.getHost().startsWith("device")) {
407 location = processDeviceLogin(httpClientAdapter, location);
408 }
409 String query = location.getQuery();
410 if (query == null) {
411
412 query = location.getSchemeSpecificPart();
413 }
414
415 if (query.contains("code=") && query.contains("&session_state=")) {
416 String code = query.substring(query.indexOf("code=") + 5, query.indexOf("&session_state="));
417 LOGGER.debug("Authentication Code: " + code);
418 return code;
419 }
420 throw new IOException("Unknown ADFS authentication failure");
421 }
422
423 private URI processDeviceLogin(HttpClientAdapter httpClient, URI location) throws IOException, JSONException {
424 URI result = location;
425 LOGGER.debug("Proceed to device authentication, must have access to a client certificate signed by MS-Organization-Access");
426 if (Settings.isWindows() &&
427 (System.getProperty("java.version").compareTo("13") < 0
428 || !"MSCAPI".equals(Settings.getProperty("davmail.ssl.clientKeystoreType")))
429 ) {
430 LOGGER.warn("MSCAPI and Java version 13 or higher required to access TPM protected client certificate on Windows");
431 }
432 GetRequest deviceLoginMethod = new GetRequest(location);
433
434 String responseBodyAsString = httpClient.executeGetRequest(deviceLoginMethod);
435
436 if (responseBodyAsString.contains(Settings.getO365LoginUrl())) {
437 String ctx = extract("name=\"ctx\" value=\"([^\"]+)\"", responseBodyAsString);
438 String flowtoken = extract("name=\"flowtoken\" value=\"([^\"]+)\"", responseBodyAsString);
439
440 PostRequest processMethod = new PostRequest(extract("action=\"([^\"]+)\"", responseBodyAsString));
441 processMethod.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
442
443 processMethod.setParameter("ctx", ctx);
444 processMethod.setParameter("flowtoken", flowtoken);
445
446 responseBodyAsString = httpClient.executePostRequest(processMethod);
447 result = processMethod.getRedirectLocation();
448
449
450 if (result == null && responseBodyAsString != null && responseBodyAsString.contains("arrUserProofs")) {
451 result = handleMfa(httpClient, processMethod, username, null);
452 }
453
454 if (result == null) {
455 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
456 }
457
458 }
459 return result;
460 }
461
462 private URI handleMfa(HttpClientAdapter httpClientAdapter, PostRequest logonMethod, String username, String clientRequestId) throws IOException, JSONException {
463 JSONObject config = extractConfig(logonMethod.getResponseBodyAsString());
464 LOGGER.debug("Config=" + config);
465
466 String urlBeginAuth = config.getString("urlBeginAuth");
467 String urlEndAuth = config.getString("urlEndAuth");
468
469 String urlProcessAuth = config.optString("urlPost", Settings.getO365LoginUrl() + "/" + tenantId + "/SAS/ProcessAuth");
470
471 boolean isMFAMethodSupported = false;
472 String chosenAuthMethodId = null;
473 String chosenAuthMethodPrompt = null;
474
475 for (int i = 0; i < config.getJSONArray("arrUserProofs").length(); i++) {
476 JSONObject authMethod = (JSONObject) config.getJSONArray("arrUserProofs").get(i);
477 String authMethodId = authMethod.getString("authMethodId");
478 LOGGER.debug("Authentication method: " + authMethodId);
479 if ("PhoneAppNotification".equals(authMethodId)) {
480 LOGGER.debug("Found phone app auth method " + authMethod.getString("display"));
481 isMFAMethodSupported = true;
482 chosenAuthMethodId = authMethodId;
483 chosenAuthMethodPrompt = authMethod.getString("display");
484
485 break;
486 }
487 if ("OneWaySMS".equals(authMethodId)) {
488 LOGGER.debug("Found OneWaySMS auth method " + authMethod.getString("display"));
489 chosenAuthMethodId = authMethodId;
490 chosenAuthMethodPrompt = authMethod.getString("display");
491 isMFAMethodSupported = true;
492 }
493 }
494
495 if (!isMFAMethodSupported) {
496 throw new IOException("MFA authentication methods not supported");
497 }
498
499 String context = config.getString("sCtx");
500 String flowToken = config.getString("sFT");
501
502 String canary = config.getString("canary");
503 String apiCanary = config.getString("apiCanary");
504
505 String hpgrequestid = logonMethod.getResponseHeader("x-ms-request-id").getValue();
506 String hpgact = config.getString("hpgact");
507 String hpgid = config.getString("hpgid");
508
509
510 String correlationId = clientRequestId;
511 if (correlationId == null) {
512 correlationId = config.getString("correlationId");
513 }
514
515 RestRequest beginAuthMethod = new RestRequest(urlBeginAuth);
516 beginAuthMethod.setRequestHeader("Accept", "application/json");
517 beginAuthMethod.setRequestHeader("canary", apiCanary);
518 beginAuthMethod.setRequestHeader("client-request-id", correlationId);
519 beginAuthMethod.setRequestHeader("hpgact", hpgact);
520 beginAuthMethod.setRequestHeader("hpgid", hpgid);
521 beginAuthMethod.setRequestHeader("hpgrequestid", hpgrequestid);
522
523
524 JSONObject beginAuthJson = new JSONObject();
525 beginAuthJson.put("AuthMethodId", chosenAuthMethodId);
526 beginAuthJson.put("Ctx", context);
527 beginAuthJson.put("FlowToken", flowToken);
528 beginAuthJson.put("Method", "BeginAuth");
529 beginAuthMethod.setJsonBody(beginAuthJson);
530
531 config = httpClientAdapter.executeRestRequest(beginAuthMethod);
532 LOGGER.debug(config);
533
534 if (!config.getBoolean("Success")) {
535 throw new IOException("Authentication failed: " + config);
536 }
537
538
539 String entropy = config.optString("Entropy", null);
540
541
542 NumberMatchingFrame numberMatchingFrame = null;
543 if (entropy != null && !"0".equals(entropy)) {
544 LOGGER.info("Number matching value for " + username + ": " + entropy);
545 if (!Settings.getBooleanProperty("davmail.server") && !GraphicsEnvironment.isHeadless()) {
546 numberMatchingFrame = new NumberMatchingFrame(entropy);
547 }
548 }
549
550 String smsCode = retrieveSmsCode(chosenAuthMethodId, chosenAuthMethodPrompt);
551
552 context = config.getString("Ctx");
553 flowToken = config.getString("FlowToken");
554 String sessionId = config.getString("SessionId");
555
556 int i = 0;
557 boolean success = false;
558 try {
559 while (!success && i++ < 12) {
560
561 try {
562 Thread.sleep(5000);
563 } catch (InterruptedException e) {
564 LOGGER.debug("Interrupted");
565 Thread.currentThread().interrupt();
566 }
567
568 RestRequest endAuthMethod = new RestRequest(urlEndAuth);
569 endAuthMethod.setRequestHeader("Accept", "application/json");
570 endAuthMethod.setRequestHeader("canary", apiCanary);
571 endAuthMethod.setRequestHeader("client-request-id", clientRequestId);
572 endAuthMethod.setRequestHeader("hpgact", hpgact);
573 endAuthMethod.setRequestHeader("hpgid", hpgid);
574 endAuthMethod.setRequestHeader("hpgrequestid", hpgrequestid);
575
576 JSONObject endAuthJson = new JSONObject();
577 endAuthJson.put("AuthMethodId", chosenAuthMethodId);
578 endAuthJson.put("Ctx", context);
579 endAuthJson.put("FlowToken", flowToken);
580 endAuthJson.put("Method", "EndAuth");
581 endAuthJson.put("PollCount", "1");
582 endAuthJson.put("SessionId", sessionId);
583
584
585
586 endAuthJson.put("AdditionalAuthData", smsCode);
587
588 endAuthMethod.setJsonBody(endAuthJson);
589
590 config = httpClientAdapter.executeRestRequest(endAuthMethod);
591 LOGGER.debug(config);
592 String resultValue = config.getString("ResultValue");
593 if ("PhoneAppDenied".equals(resultValue) || "PhoneAppNoResponse".equals(resultValue)) {
594 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_REASON", resultValue);
595 }
596 if ("SMSAuthFailedWrongCodeEntered".equals(resultValue)) {
597 smsCode = retrieveSmsCode(chosenAuthMethodId, chosenAuthMethodPrompt);
598 }
599 if (config.getBoolean("Success")) {
600 success = true;
601 }
602 }
603 } finally {
604
605 if (numberMatchingFrame != null && numberMatchingFrame.isVisible()) {
606 final JFrame finalNumberMatchingFrame = numberMatchingFrame;
607 SwingUtilities.invokeLater(() -> {
608 finalNumberMatchingFrame.setVisible(false);
609 finalNumberMatchingFrame.dispose();
610 });
611 }
612
613 }
614 if (!success) {
615 throw new IOException("Authentication failed: " + config);
616 }
617
618 String authMethod = "PhoneAppOTP";
619 String type = "22";
620
621 context = config.getString("Ctx");
622 flowToken = config.getString("FlowToken");
623
624
625 PostRequest processAuthMethod = new PostRequest(urlProcessAuth);
626 processAuthMethod.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
627 processAuthMethod.setParameter("type", type);
628 processAuthMethod.setParameter("request", context);
629 processAuthMethod.setParameter("mfaAuthMethod", authMethod);
630 processAuthMethod.setParameter("canary", canary);
631 processAuthMethod.setParameter("login", username);
632 processAuthMethod.setParameter("flowToken", flowToken);
633 processAuthMethod.setParameter("hpgrequestid", hpgrequestid);
634
635 httpClientAdapter.executePostRequest(processAuthMethod);
636 return processAuthMethod.getRedirectLocation();
637
638 }
639
640 private String retrieveSmsCode(String chosenAuthMethodId, String chosenAuthMethodPrompt) throws IOException {
641 String smsCode = null;
642 if ("OneWaySMS".equals(chosenAuthMethodId)) {
643 LOGGER.info("Need to retrieve SMS verification code for " + username);
644 if (Settings.getBooleanProperty("davmail.server") || GraphicsEnvironment.isHeadless()) {
645
646 System.out.print(BundleMessage.format("UI_SMS_PHONE_CODE", chosenAuthMethodPrompt));
647 BufferedReader inReader = new BufferedReader(new InputStreamReader(System.in));
648 smsCode = inReader.readLine();
649 } else {
650 PasswordPromptDialog passwordPromptDialog = new PasswordPromptDialog(BundleMessage.format("UI_SMS_PHONE_CODE", chosenAuthMethodPrompt));
651 smsCode = String.valueOf(passwordPromptDialog.getPassword());
652 }
653 }
654 return smsCode;
655 }
656
657 private String executeFollowRedirect(HttpClientAdapter httpClientAdapter, GetRequest getRequest) throws IOException {
658 LOGGER.debug(getRequest.getURI());
659 ResponseWrapper responseWrapper = httpClientAdapter.executeFollowRedirect(getRequest);
660 String responseHost = responseWrapper.getURI().getHost();
661 if (responseHost.endsWith("okta.com")) {
662 throw new DavMailAuthenticationException("LOG_MESSAGE", "Okta authentication not supported, please try O365Interactive");
663 }
664 return responseWrapper.getResponseBodyAsString();
665 }
666
667 public JSONObject extractConfig(String content) throws IOException {
668 try {
669 return new JSONObject(extract("Config=([^\n]+);", content));
670 } catch (JSONException e1) {
671 LOGGER.debug(content);
672 throw new IOException("Unable to extract config from response body");
673 }
674 }
675
676
677
678
679
680
681
682 public JSONObject extractServerData(String content) throws IOException {
683 try {
684 return new JSONObject(extract("ServerData =([^\n]+);", content));
685 } catch (JSONException e1) {
686 LOGGER.debug(content);
687 throw new IOException("Unable to extract config from response body");
688 }
689 }
690
691 public String extract(String pattern, String content) throws IOException {
692 String value;
693 Matcher matcher = Pattern.compile(pattern).matcher(content);
694 if (matcher.find()) {
695 value = matcher.group(1);
696 } else {
697 throw new IOException("pattern not found");
698 }
699 return value;
700 }
701
702 }