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.exception.DavMailAuthenticationException;
24 import davmail.exception.DavMailException;
25 import davmail.exception.WebdavNotAvailableException;
26 import davmail.http.DavGatewayOTPPrompt;
27 import davmail.http.HttpClientAdapter;
28 import davmail.http.URIUtil;
29 import davmail.http.request.GetRequest;
30 import davmail.http.request.PostRequest;
31 import davmail.http.request.ResponseWrapper;
32 import davmail.util.StringUtil;
33 import org.apache.http.HttpStatus;
34 import org.apache.http.client.methods.CloseableHttpResponse;
35 import org.apache.http.client.methods.HttpGet;
36 import org.apache.http.client.methods.HttpRequestBase;
37 import org.apache.http.client.protocol.HttpClientContext;
38 import org.apache.http.client.utils.URIBuilder;
39 import org.apache.http.cookie.Cookie;
40 import org.apache.http.impl.client.BasicCookieStore;
41 import org.apache.http.impl.cookie.BasicClientCookie;
42 import org.apache.log4j.Logger;
43 import org.htmlcleaner.BaseToken;
44 import org.htmlcleaner.CommentNode;
45 import org.htmlcleaner.ContentNode;
46 import org.htmlcleaner.HtmlCleaner;
47 import org.htmlcleaner.TagNode;
48
49 import javax.imageio.ImageIO;
50 import java.awt.image.BufferedImage;
51 import java.io.ByteArrayInputStream;
52 import java.io.IOException;
53 import java.net.ConnectException;
54 import java.net.URI;
55 import java.net.URISyntaxException;
56 import java.net.UnknownHostException;
57 import java.nio.charset.StandardCharsets;
58 import java.util.ArrayList;
59 import java.util.HashSet;
60 import java.util.List;
61 import java.util.Set;
62
63
64
65
66 public class ExchangeFormAuthenticator implements ExchangeAuthenticator {
67 protected static final Logger LOGGER = Logger.getLogger("davmail.exchange.ExchangeSession");
68
69
70
71
72 protected static final Set<String> USER_NAME_FIELDS = new HashSet<>();
73
74 static {
75 USER_NAME_FIELDS.add("username");
76 USER_NAME_FIELDS.add("txtusername");
77 USER_NAME_FIELDS.add("userid");
78 USER_NAME_FIELDS.add("SafeWordUser");
79 USER_NAME_FIELDS.add("user_name");
80 USER_NAME_FIELDS.add("login");
81 USER_NAME_FIELDS.add("UserName");
82 }
83
84
85
86
87 protected static final Set<String> PASSWORD_FIELDS = new HashSet<>();
88
89 static {
90 PASSWORD_FIELDS.add("password");
91 PASSWORD_FIELDS.add("txtUserPass");
92 PASSWORD_FIELDS.add("pw");
93 PASSWORD_FIELDS.add("basicPassword");
94 PASSWORD_FIELDS.add("passwd");
95 PASSWORD_FIELDS.add("Password");
96 }
97
98
99
100
101
102 protected static final Set<String> TOKEN_FIELDS = new HashSet<>();
103
104 static {
105 TOKEN_FIELDS.add("SafeWordPassword");
106 TOKEN_FIELDS.add("passcode");
107 }
108
109
110
111
112
113
114
115
116 private String username;
117
118
119
120 private String password;
121
122
123
124 private String url;
125
126
127
128 private HttpClientAdapter httpClientAdapter;
129
130
131
132 private String preAuthusername;
133
134
135
136
137 private final List<String> usernameInputs = new ArrayList<>();
138
139
140
141 private String passwordInput = null;
142
143
144
145 private boolean otpPreAuthFound = false;
146
147
148
149 private int otpPreAuthRetries = 0;
150
151
152
153 private static final int MAX_OTP_RETRIES = 3;
154
155
156
157
158 private java.net.URI exchangeUri;
159
160 @Override
161 public void setUsername(String username) {
162 this.username = username;
163 }
164
165 @Override
166 public void setPassword(String password) {
167 this.password = password;
168 }
169
170 public void setUrl(String url) {
171 this.url = url;
172 }
173
174 @Override
175 public void authenticate() throws DavMailException {
176 try {
177
178 httpClientAdapter = new HttpClientAdapter(url, true);
179 boolean isHttpAuthentication = isHttpAuthentication(httpClientAdapter, url);
180
181
182
183
184 if (preAuthusername == null) {
185
186
187 int doubleQuoteIndex = this.username.indexOf('"');
188 if (doubleQuoteIndex > 0) {
189 preAuthusername = this.username.substring(0, doubleQuoteIndex);
190 this.username = this.username.substring(doubleQuoteIndex + 1);
191 } else {
192
193 preAuthusername = this.username;
194 }
195 }
196
197
198 httpClientAdapter.setCredentials(username, password);
199
200
201
202
203 GetRequest getRequest = httpClientAdapter.executeFollowRedirect(new GetRequest(url));
204
205 if (!this.isAuthenticated(getRequest)) {
206 if (isHttpAuthentication) {
207 int status = getRequest.getStatusCode();
208
209 if (status == HttpStatus.SC_UNAUTHORIZED) {
210 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
211 } else if (status != HttpStatus.SC_OK) {
212 throw HttpClientAdapter.buildHttpResponseException(getRequest, getRequest.getHttpResponse());
213 }
214
215 if ("/owa/auth/logon.aspx".equals(getRequest.getURI().getPath())) {
216 formLogin(httpClientAdapter, getRequest, password);
217 }
218 } else {
219 formLogin(httpClientAdapter, getRequest, password);
220 }
221 }
222
223 } catch (DavMailAuthenticationException exc) {
224 close();
225 LOGGER.error(exc.getMessage());
226 throw exc;
227 } catch (ConnectException | UnknownHostException exc) {
228 close();
229 BundleMessage message = new BundleMessage("EXCEPTION_CONNECT", exc.getClass().getName(), exc.getMessage());
230 LOGGER.error(message);
231 throw new DavMailException("EXCEPTION_DAVMAIL_CONFIGURATION", message);
232 } catch (WebdavNotAvailableException exc) {
233 close();
234 throw exc;
235 } catch (IOException exc) {
236 close();
237 LOGGER.error(BundleMessage.formatLog("EXCEPTION_EXCHANGE_LOGIN_FAILED", exc));
238 throw new DavMailException("EXCEPTION_EXCHANGE_LOGIN_FAILED", exc);
239 }
240 LOGGER.debug("Successfully authenticated to " + exchangeUri);
241 }
242
243
244
245
246
247
248
249
250 protected boolean isHttpAuthentication(HttpClientAdapter httpClient, String url) {
251 boolean isHttpAuthentication = false;
252 HttpGet httpGet = new HttpGet(url);
253
254 HttpClientContext context = HttpClientContext.create();
255 context.setCookieStore(new BasicCookieStore());
256 try (CloseableHttpResponse response = httpClient.execute(httpGet, context)) {
257 isHttpAuthentication = response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED;
258 } catch (IOException e) {
259
260 }
261 return isHttpAuthentication;
262 }
263
264
265
266
267
268
269 protected boolean isAuthenticated(ResponseWrapper getRequest) {
270 boolean authenticated = false;
271 if (getRequest.getStatusCode() == HttpStatus.SC_OK
272 && "/ews/services.wsdl".equalsIgnoreCase(getRequest.getURI().getPath())) {
273
274 authenticated = true;
275 } else {
276
277 for (Cookie cookie : httpClientAdapter.getCookies()) {
278
279 if (cookie.getName().startsWith("cadata") || "sessionid".equals(cookie.getName())
280
281 || "UserContext".equals(cookie.getName())
282
283 || "TimeWindowSig".equals(cookie.getName())
284 ) {
285 authenticated = true;
286 break;
287 }
288 }
289 }
290 return authenticated;
291 }
292
293 protected void formLogin(HttpClientAdapter httpClient, ResponseWrapper initRequest, String password) throws IOException {
294 LOGGER.debug("Form based authentication detected");
295
296 PostRequest postRequest = buildLogonMethod(httpClient, initRequest);
297 if (postRequest == null) {
298 LOGGER.debug("Authentication form not found at " + initRequest.getURI() + ", trying default url");
299 postRequest = new PostRequest("/owa/auth/owaauth.dll");
300 }
301
302 exchangeUri = postLogonMethod(httpClient, postRequest, password).getURI();
303 }
304
305
306
307
308
309
310
311
312 protected PostRequest buildLogonMethod(HttpClientAdapter httpClient, ResponseWrapper responseWrapper) {
313 PostRequest logonMethod = null;
314
315
316 HtmlCleaner cleaner = new HtmlCleaner();
317
318 cleaner.getProperties().setAllowHtmlInsideAttributes(true);
319
320
321 usernameInputs.clear();
322
323 try {
324 URI uri = responseWrapper.getURI();
325 String responseBody = responseWrapper.getResponseBodyAsString();
326 TagNode node = cleaner.clean(new ByteArrayInputStream(responseBody.getBytes(StandardCharsets.UTF_8)));
327 List<? extends TagNode> forms = node.getElementListByName("form", true);
328 TagNode logonForm = null;
329
330 if (forms.size() == 1) {
331 logonForm = forms.get(0);
332 } else if (forms.size() > 1) {
333 for (TagNode form : forms) {
334 if ("logonForm".equals(form.getAttributeByName("name"))) {
335 logonForm = form;
336 } else if ("loginForm".equals(form.getAttributeByName("id"))) {
337 logonForm = form;
338 }
339
340 }
341 }
342 if (logonForm != null) {
343 String logonMethodPath = logonForm.getAttributeByName("action");
344
345
346 if (logonMethodPath != null && logonMethodPath.isEmpty()) {
347 logonMethodPath = "/owa/auth.owa";
348 }
349
350 logonMethod = new PostRequest(getAbsoluteUri(uri, logonMethodPath));
351
352
353 List<? extends TagNode> inputList = node.getElementListByName("input", true);
354
355 for (TagNode input : inputList) {
356 String type = input.getAttributeByName("type");
357 String name = input.getAttributeByName("name");
358 String value = input.getAttributeByName("value");
359 if ("hidden".equalsIgnoreCase(type) && name != null && value != null) {
360
361 if ("wresult".equals(name)) {
362 String decoded = value.replaceAll(""","\"").replaceAll("<","<");
363 logonMethod.setParameter(name, decoded);
364
365 logonMethod.setRequestHeader("Referer", url);
366 } else if ("wctx".equals(name)) {
367 String decoded = value.replaceAll("&","&");
368 logonMethod.setParameter(name, decoded);
369 } else {
370 logonMethod.setParameter(name, value);
371 }
372 }
373
374 if (USER_NAME_FIELDS.contains(name) && !usernameInputs.contains(name)) {
375 usernameInputs.add(name);
376 } else if (PASSWORD_FIELDS.contains(name)) {
377 passwordInput = name;
378 } else if ("addr".equals(name)) {
379
380 logonMethod = buildLogonMethod(httpClient, httpClient.executeFollowRedirect(logonMethod));
381 } else if (TOKEN_FIELDS.contains(name)) {
382
383 logonMethod.setParameter(name, DavGatewayOTPPrompt.getOneTimePassword());
384 } else if ("otc".equals(name)) {
385
386 String pinsafeUser = getAliasFromLogin();
387 if (pinsafeUser == null) {
388 pinsafeUser = username;
389 }
390 HttpGet pinRequest = new HttpGet("/PINsafeISAFilter.dll?username=" + pinsafeUser);
391 try (CloseableHttpResponse pinResponse = httpClient.execute(pinRequest)) {
392 int status = pinResponse.getStatusLine().getStatusCode();
393 if (status != HttpStatus.SC_OK) {
394 throw HttpClientAdapter.buildHttpResponseException(pinRequest, pinResponse.getStatusLine());
395 }
396 BufferedImage captchaImage = ImageIO.read(pinResponse.getEntity().getContent());
397 logonMethod.setParameter(name, DavGatewayOTPPrompt.getCaptchaValue(captchaImage));
398 }
399 }
400 }
401 } else {
402 List<? extends TagNode> frameList = node.getElementListByName("frame", true);
403 if (frameList.size() == 1) {
404 String src = frameList.get(0).getAttributeByName("src");
405 if (src != null) {
406 LOGGER.debug("Frames detected in form page, try frame content");
407 logonMethod = buildLogonMethod(httpClient, httpClient.executeFollowRedirect(new GetRequest(src)));
408 }
409 } else {
410
411 List<? extends TagNode> scriptList = node.getElementListByName("script", true);
412 for (TagNode script : scriptList) {
413 List<? extends BaseToken> contents = script.getAllChildren();
414 for (Object content : contents) {
415 if (content instanceof CommentNode) {
416 String scriptValue = ((CommentNode) content).getCommentedContent();
417 String sUrl = StringUtil.getToken(scriptValue, "var a_sUrl = \"", "\"");
418 String sLgn = StringUtil.getToken(scriptValue, "var a_sLgnQS = \"", "\"");
419 if (sLgn == null) {
420 sLgn = StringUtil.getToken(scriptValue, "var a_sLgn = \"", "\"");
421 }
422 if (sUrl != null && sLgn != null) {
423 URI src = getScriptBasedFormURL(uri, sLgn + sUrl);
424 LOGGER.debug("Detected script based logon, redirect to form at " + src);
425 logonMethod = buildLogonMethod(httpClient, httpClient.executeFollowRedirect(new GetRequest(src)));
426 }
427
428 } else if (content instanceof ContentNode) {
429
430 String scriptValue = ((ContentNode) content).getContent();
431 String location = StringUtil.getToken(scriptValue, "window.location.replace(\"", "\"");
432 if (location != null) {
433 LOGGER.debug("Post logon redirect to: " + location);
434 logonMethod = buildLogonMethod(httpClient, httpClient.executeFollowRedirect(new GetRequest(location)));
435 }
436 }
437 }
438 }
439 }
440 }
441 } catch (IOException | URISyntaxException e) {
442 LOGGER.error("Error parsing login form at " + responseWrapper.getURI());
443 }
444
445 return logonMethod;
446 }
447
448
449 protected ResponseWrapper postLogonMethod(HttpClientAdapter httpClient, PostRequest logonMethod, String password) throws IOException {
450
451 setAuthFormFields(logonMethod, httpClient, password);
452
453
454 BasicClientCookie pBackCookie = new BasicClientCookie("PBack", "0");
455 pBackCookie.setPath("/");
456 pBackCookie.setDomain(httpClientAdapter.getHost());
457 httpClient.addCookie(pBackCookie);
458
459 ResponseWrapper resultRequest = httpClient.executeFollowRedirect(logonMethod);
460
461
462 checkFormLoginQueryString(resultRequest);
463
464
465 if (!isAuthenticated(resultRequest)) {
466
467 logonMethod = buildLogonMethod(httpClient, resultRequest);
468
469 if (logonMethod != null) {
470 if (otpPreAuthFound && otpPreAuthRetries < MAX_OTP_RETRIES) {
471
472
473
474
475 return postLogonMethod(httpClient, logonMethod, password);
476 }
477
478
479 resultRequest = httpClient.executeFollowRedirect(logonMethod);
480
481 checkFormLoginQueryString(resultRequest);
482
483 if (!isAuthenticated(resultRequest)) {
484 throwAuthenticationFailed();
485 }
486 } else {
487
488 throwAuthenticationFailed();
489 }
490 }
491
492
493 if ("/owa/languageselection.aspx".equals(resultRequest.getURI().getPath())) {
494
495 resultRequest = submitLanguageSelectionForm(resultRequest.getURI(), resultRequest.getResponseBodyAsString());
496 }
497 return resultRequest;
498 }
499
500 protected ResponseWrapper submitLanguageSelectionForm(URI uri, String responseBodyAsString) throws IOException {
501 PostRequest postLanguageFormMethod;
502
503 HtmlCleaner cleaner = new HtmlCleaner();
504
505 try {
506 TagNode node = cleaner.clean(responseBodyAsString);
507 List<? extends TagNode> forms = node.getElementListByName("form", true);
508 TagNode languageForm;
509
510 if (forms.size() == 1) {
511 languageForm = forms.get(0);
512 } else {
513 throw new IOException("Form not found");
514 }
515 String languageMethodPath = languageForm.getAttributeByName("action");
516
517 postLanguageFormMethod = new PostRequest(getAbsoluteUri(uri, languageMethodPath));
518
519 List<? extends TagNode> inputList = languageForm.getElementListByName("input", true);
520 for (TagNode input : inputList) {
521 String name = input.getAttributeByName("name");
522 String value = input.getAttributeByName("value");
523 if (name != null && value != null) {
524 postLanguageFormMethod.setParameter(name, value);
525 }
526 }
527 List<? extends TagNode> selectList = languageForm.getElementListByName("select", true);
528 for (TagNode select : selectList) {
529 String name = select.getAttributeByName("name");
530 List<? extends TagNode> optionList = select.getElementListByName("option", true);
531 String value = null;
532 for (TagNode option : optionList) {
533 if (option.getAttributeByName("selected") != null) {
534 value = option.getAttributeByName("value");
535 break;
536 }
537 }
538 if (name != null && value != null) {
539 postLanguageFormMethod.setParameter(name, value);
540 }
541 }
542 } catch (IOException | URISyntaxException e) {
543 String errorMessage = "Error parsing language selection form at " + uri;
544 LOGGER.error(errorMessage);
545 throw new IOException(errorMessage);
546 }
547
548 return httpClientAdapter.executeFollowRedirect(postLanguageFormMethod);
549 }
550
551 protected void setAuthFormFields(HttpRequestBase logonMethod, HttpClientAdapter httpClient, String password) throws IllegalArgumentException {
552 String usernameInput;
553 if (usernameInputs.size() == 2) {
554 String userid;
555
556 int pipeIndex = username.indexOf('|');
557 if (pipeIndex < 0) {
558 LOGGER.debug("Multiple user fields detected, please use userid|username as user name in client, except when userid is username");
559 userid = username;
560 } else {
561 userid = username.substring(0, pipeIndex);
562 username = username.substring(pipeIndex + 1);
563
564 httpClient.setCredentials(username, password);
565 }
566 ((PostRequest) logonMethod).removeParameter("userid");
567 ((PostRequest) logonMethod).setParameter("userid", userid);
568
569 usernameInput = "username";
570 } else if (usernameInputs.size() == 1) {
571
572 usernameInput = usernameInputs.get(0);
573 } else {
574
575 usernameInput = "username";
576 }
577
578 ((PostRequest) logonMethod).removeParameter(usernameInput);
579 if (passwordInput != null) {
580 ((PostRequest) logonMethod).removeParameter(passwordInput);
581 }
582 ((PostRequest) logonMethod).removeParameter("trusted");
583 ((PostRequest) logonMethod).removeParameter("flags");
584
585 if (passwordInput == null) {
586
587 otpPreAuthFound = true;
588 otpPreAuthRetries++;
589 ((PostRequest) logonMethod).setParameter(usernameInput, preAuthusername);
590 } else {
591 otpPreAuthFound = false;
592 otpPreAuthRetries = 0;
593
594 ((PostRequest) logonMethod).setParameter(usernameInput, username);
595 ((PostRequest) logonMethod).setParameter(passwordInput, password);
596 ((PostRequest) logonMethod).setParameter("trusted", "4");
597 ((PostRequest) logonMethod).setParameter("flags", "4");
598 }
599 }
600
601 protected URI getAbsoluteUri(URI uri, String path) throws URISyntaxException {
602 URIBuilder uriBuilder = new URIBuilder(uri);
603 if (path != null) {
604
605 uriBuilder.clearParameters();
606 if (path.startsWith("/")) {
607
608 uriBuilder.setPath(path);
609 } else if (path.startsWith("http://") || path.startsWith("https://")) {
610 return URI.create(path);
611 } else {
612
613 String currentPath = uri.getPath();
614 int end = currentPath.lastIndexOf('/');
615 if (end >= 0) {
616 uriBuilder.setPath(currentPath.substring(0, end + 1) + path);
617 } else {
618 throw new URISyntaxException(uriBuilder.build().toString(), "Invalid path");
619 }
620 }
621 }
622 return uriBuilder.build();
623 }
624
625 protected URI getScriptBasedFormURL(URI uri, String pathQuery) throws URISyntaxException, IOException {
626 URIBuilder uriBuilder = new URIBuilder(uri);
627 int queryIndex = pathQuery.indexOf('?');
628 if (queryIndex >= 0) {
629 if (queryIndex > 0) {
630
631 String newPath = pathQuery.substring(0, queryIndex);
632 if (newPath.startsWith("/")) {
633
634 uriBuilder.setPath(newPath);
635 } else {
636 String currentPath = uriBuilder.getPath();
637 int folderIndex = currentPath.lastIndexOf('/');
638 if (folderIndex >= 0) {
639
640 uriBuilder.setPath(currentPath.substring(0, folderIndex + 1) + newPath);
641 } else {
642
643 uriBuilder.setPath('/' + newPath);
644 }
645 }
646 }
647 uriBuilder.setCustomQuery(URIUtil.decode(pathQuery.substring(queryIndex + 1)));
648 }
649 return uriBuilder.build();
650 }
651
652 protected void checkFormLoginQueryString(ResponseWrapper logonMethod) throws DavMailAuthenticationException {
653 String queryString = logonMethod.getURI().getRawQuery();
654 if (queryString != null && (queryString.contains("reason=2") || queryString.contains("reason=4"))) {
655 throwAuthenticationFailed();
656 }
657 }
658
659 protected void throwAuthenticationFailed() throws DavMailAuthenticationException {
660 if (this.username != null && this.username.contains("\\")) {
661 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
662 } else {
663 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_RETRY");
664 }
665 }
666
667
668
669
670
671
672 public String getAliasFromLogin() {
673
674 if (this.username.indexOf('@') >= 0) {
675 return null;
676 }
677 String result = this.username;
678
679 int index = Math.max(result.indexOf('\\'), result.indexOf('/'));
680 if (index >= 0) {
681 result = result.substring(index + 1);
682 }
683 return result;
684 }
685
686
687
688
689
690 public void close() {
691 httpClientAdapter.close();
692 }
693
694
695
696
697
698
699
700 @Override
701 public O365Token getToken() {
702 throw new UnsupportedOperationException();
703 }
704
705
706
707
708
709
710
711 @Override
712 public java.net.URI getExchangeUri() {
713 return exchangeUri;
714 }
715
716
717
718
719
720
721 public HttpClientAdapter getHttpClientAdapter() {
722 return httpClientAdapter;
723 }
724
725
726
727
728
729
730
731 public String getUsername() {
732 return username;
733 }
734 }
735