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.Settings;
23 import davmail.exception.DavMailAuthenticationException;
24 import davmail.exception.DavMailException;
25 import davmail.exchange.NetworkDownException;
26 import davmail.http.HttpClientAdapter;
27 import davmail.http.request.RestRequest;
28 import davmail.util.IOUtil;
29 import davmail.util.StringEncryptor;
30 import org.apache.http.Consts;
31 import org.apache.http.NameValuePair;
32 import org.apache.http.client.entity.UrlEncodedFormEntity;
33 import org.apache.http.client.methods.CloseableHttpResponse;
34 import org.apache.http.message.BasicNameValuePair;
35 import org.apache.log4j.Logger;
36 import org.codehaus.jettison.json.JSONException;
37 import org.codehaus.jettison.json.JSONObject;
38
39 import java.io.IOException;
40 import java.net.UnknownHostException;
41 import java.util.ArrayList;
42 import java.util.Date;
43
44
45
46
47 public class O365Token {
48
49 protected static final Logger LOGGER = Logger.getLogger(O365Token.class);
50
51 private String clientId;
52 private final String tokenUrl;
53 private final String password;
54 private String redirectUri;
55 private String username;
56 private String refreshToken;
57 private String accessToken;
58 private long expiresOn;
59
60 public O365Token(String tenantId, String clientId, String redirectUri, String password) {
61 this.clientId = clientId;
62 this.redirectUri = redirectUri;
63 this.tokenUrl = buildTokenUrl(tenantId);
64 this.password = password;
65 }
66
67 public O365Token(String tenantId, String clientId, String redirectUri, String code, String password) throws IOException {
68 this.clientId = clientId;
69 this.redirectUri = redirectUri;
70 this.tokenUrl = buildTokenUrl(tenantId);
71 this.password = password;
72
73 ArrayList<NameValuePair> parameters = new ArrayList<>();
74 parameters.add(new BasicNameValuePair("grant_type", "authorization_code"));
75 parameters.add(new BasicNameValuePair("code", code));
76 parameters.add(new BasicNameValuePair("redirect_uri", redirectUri));
77 parameters.add(new BasicNameValuePair("client_id", clientId));
78
79 RestRequest tokenRequest = new RestRequest(tokenUrl, new UrlEncodedFormEntity(parameters, Consts.UTF_8));
80
81 String origin = Settings.getProperty("davmail.oauth.refreshTokenOrigin");
82 if (origin != null && !origin.isEmpty()) {
83 tokenRequest.setRequestHeader("Origin", origin);
84 }
85
86 LOGGER.debug("Obtain token for clientId: " + clientId + " redirectUri: " + redirectUri + " tokenUrl: " + tokenUrl);
87 executeRequest(tokenRequest);
88 }
89
90 protected O365Token(String tenantId, String clientId, O365DeviceCodeAuthenticator.DeviceCode code, String password) throws IOException {
91 this.clientId = clientId;
92 this.tokenUrl = buildTokenUrl(tenantId);
93 this.password = password;
94
95 ArrayList<NameValuePair> parameters = new ArrayList<>();
96 parameters.add(new BasicNameValuePair("grant_type", "urn:ietf:params:oauth:grant-type:device_code"));
97 parameters.add(new BasicNameValuePair("code", code.getDeviceCode()));
98 parameters.add(new BasicNameValuePair("redirect_uri", redirectUri));
99 parameters.add(new BasicNameValuePair("client_id", clientId));
100 RestRequest tokenRequest = new RestRequest(tokenUrl, new UrlEncodedFormEntity(parameters, Consts.UTF_8));
101
102 executeRequest(tokenRequest);
103 }
104
105 protected String buildTokenUrl(String tenantId) {
106 if (Settings.getBooleanProperty("davmail.enableOidc", Settings.isGraphEnabled())) {
107
108 return Settings.getO365LoginUrl()+"/"+tenantId+"/oauth2/v2.0/token";
109 } else {
110 return Settings.getO365LoginUrl()+"/"+tenantId+"/oauth2/token";
111 }
112 }
113
114
115 public String getUsername() {
116 return username;
117 }
118
119 public void setJsonToken(JSONObject jsonToken) throws IOException {
120 String scope;
121 try {
122 final Object error = jsonToken.opt("error");
123 if (error != null) {
124 if (error.equals("authorization_pending"))
125 throw new O365AuthorizationPending();
126
127 if (error.equals("invalid_grant") && Settings.getProperty("davmail.oauth.scope") == null) {
128 LOGGER.warn("received invalid grant and scope is not set, if this is a live.com account please set davmail.oauth.scope=openid profile offline_access Mail.ReadWrite");
129 Settings.setProperty("davmail.oauth.scope", "openid profile offline_access Mail.ReadWrite");
130 }
131
132 if (error.equals("invalid_grant") && "openid profile offline_access Mail.ReadWrite".equals(Settings.getProperty("davmail.oauth.scope"))
133 && !Settings.isGraphEnabled()) {
134 LOGGER.warn("received invalid grant and graph is disabled, if this is a live.com account please set davmail.enableGraph=true");
135 }
136
137 throw new DavMailAuthenticationException("LOG_MESSAGE", jsonToken.optString("error") + " " + jsonToken.optString("error_description"));
138 }
139 scope = jsonToken.optString("scope", null);
140 LOGGER.debug("Obtained token for scopes: " + scope);
141
142 if (Settings.isGraphEnabled() && scope != null && !scope.contains("Mail.ReadWrite")) {
143 throw new DavMailAuthenticationException("LOG_MESSAGE", "Received a token without Mail.ReadWrite for a graph session");
144 }
145
146 accessToken = jsonToken.getString("access_token");
147
148 refreshToken = jsonToken.getString("refresh_token");
149
150 expiresOn = jsonToken.optLong("expires_on") * 1000;
151
152 if (expiresOn > 0) {
153 LOGGER.debug("Access token expires " + new Date(expiresOn));
154 } else {
155 long expiresIn = jsonToken.optLong("expires_in") * 1000;
156 if (expiresIn > 0) {
157 expiresOn = System.currentTimeMillis()+expiresIn;
158 }
159 }
160
161
162 String idToken = jsonToken.optString("id_token");
163 if (idToken != null && idToken.contains(".")) {
164 String decodedJwt = IOUtil.decodeBase64AsString(idToken.substring(idToken.indexOf("."), idToken.lastIndexOf(".")));
165 try {
166 JSONObject tokenBody = new JSONObject(decodedJwt);
167 LOGGER.debug("Token: " + tokenBody);
168 if ("https://login.live.com".equals(tokenBody.optString("iss"))) {
169
170 username = tokenBody.optString("email", null);
171 } else {
172 username = tokenBody.optString("unique_name", null);
173 if (username == null) {
174 username = tokenBody.optString("preferred_username");
175 }
176
177 final String liveDotCom = "live.com#";
178 if (username != null && username.startsWith(liveDotCom)) {
179 username = username.substring(liveDotCom.length());
180 }
181 }
182 } catch (JSONException e) {
183 LOGGER.warn("Invalid id_token " + e.getMessage(), e);
184 }
185 }
186
187 if (username == null && accessToken.contains(".")) {
188 String decodedBearer = IOUtil.decodeBase64AsString(accessToken.substring(accessToken.indexOf('.') + 1, accessToken.lastIndexOf('.')) + "==");
189 JSONObject tokenBody = new JSONObject(decodedBearer);
190 LOGGER.debug("Token: " + tokenBody);
191 username = tokenBody.getString("unique_name");
192 }
193
194
195 if (Settings.isGraphEnabled()) {
196
197 if (scope != null && (!scope.contains("Mail.ReadWrite") || scope.contains(Settings.getOutlookUrl()))) {
198 Settings.storeRefreshToken(username, "");
199 throw new IOException("Found EWS stored token, incompatible with Graph API");
200 }
201 } else {
202
203 if (scope != null && !scope.contains("EWS")) {
204
205 Settings.storeRefreshToken(username, "");
206 throw new IOException("Found Graph stored token, incompatible with EWS");
207 }
208 }
209
210
211 } catch (JSONException e) {
212 throw new DavMailException("LOG_MESSAGE", "Exception parsing token "+e.getMessage(), e);
213 }
214 }
215
216 public void setClientId(String clientId) {
217 this.clientId = clientId;
218 }
219
220 public void setRedirectUri(String redirectUri) {
221 this.redirectUri = redirectUri;
222 }
223
224 public String getAccessToken() throws IOException {
225
226 if (isTokenExpired()) {
227 LOGGER.debug("Access token expires soon, trying to refresh it");
228 refreshToken();
229 }
230
231 return accessToken;
232 }
233
234 private boolean isTokenExpired() {
235 return System.currentTimeMillis() > (expiresOn - 60000);
236 }
237
238 public void setAccessToken(String accessToken) {
239 this.accessToken = accessToken;
240
241 expiresOn = System.currentTimeMillis() + 1000 * 60 * 60;
242 }
243
244 public void setRefreshToken(String refreshToken) {
245 this.refreshToken = refreshToken;
246 }
247
248 public String getRefreshToken() {
249 return refreshToken;
250 }
251
252 public void refreshToken() throws IOException {
253 ArrayList<NameValuePair> parameters = new ArrayList<>();
254 parameters.add(new BasicNameValuePair("grant_type", "refresh_token"));
255 parameters.add(new BasicNameValuePair("refresh_token", refreshToken));
256 parameters.add(new BasicNameValuePair("redirect_uri", redirectUri));
257 parameters.add(new BasicNameValuePair("client_id", clientId));
258
259
260 if (!Settings.getBooleanProperty("davmail.enableOidc", Settings.isGraphEnabled())) {
261 parameters.add(new BasicNameValuePair("resource", Settings.isGraphEnabled() ? Settings.getGraphUrl() : Settings.getOutlookUrl()));
262 }
263
264 RestRequest tokenRequest = new RestRequest(tokenUrl, new UrlEncodedFormEntity(parameters, Consts.UTF_8));
265
266 String origin = Settings.getProperty("davmail.oauth.refreshTokenOrigin");
267 if (origin != null && !origin.isEmpty()) {
268 tokenRequest.setRequestHeader("Origin", origin);
269 }
270
271 try {
272 executeRequest(tokenRequest);
273 } catch (UnknownHostException e) {
274
275 LOGGER.debug("refresh token failed " + e.getMessage());
276 throw e;
277 }
278
279
280 persistToken();
281 }
282
283 private void executeRequest(RestRequest tokenMethod) throws IOException {
284
285 try (
286 HttpClientAdapter httpClientAdapter = new HttpClientAdapter(tokenUrl);
287 CloseableHttpResponse response = httpClientAdapter.execute(tokenMethod)
288 ) {
289 setJsonToken(tokenMethod.handleResponse(response));
290 }
291 }
292
293 static O365Token build(String tenantId, String clientId, String redirectUri, String code, String password) throws IOException {
294 O365Token token = new O365Token(tenantId, clientId, redirectUri, code, password);
295 token.persistToken();
296 return token;
297 }
298
299 static O365Token build(String tenantId, String clientId, O365DeviceCodeAuthenticator.DeviceCode code, String password) throws IOException {
300 O365Token token = new O365Token(tenantId, clientId, code, password);
301 token.persistToken();
302 return token;
303 }
304
305 static O365Token load(String tenantId, String clientId, String redirectUri, String username, String password) throws UnknownHostException {
306 O365Token token = null;
307 if (Settings.getBooleanProperty("davmail.oauth.persistToken", true)) {
308 String encryptedRefreshToken = Settings.loadRefreshToken(username);
309 if (encryptedRefreshToken != null) {
310 String refreshToken;
311 try {
312 refreshToken = decryptToken(encryptedRefreshToken, password);
313 LOGGER.debug("Loaded stored token for " + username);
314 O365Token localToken = new O365Token(tenantId, clientId, redirectUri, password);
315
316 localToken.setRefreshToken(refreshToken);
317 localToken.refreshToken();
318 LOGGER.debug("Authenticated user " + localToken.getUsername() + " from stored token");
319 token = localToken;
320
321 } catch (UnknownHostException e) {
322
323 throw e;
324 } catch (IOException e) {
325 LOGGER.error("refresh token failed " + e.getMessage());
326 }
327 }
328 }
329 return token;
330 }
331
332 private void persistToken() throws IOException {
333 if (Settings.getBooleanProperty("davmail.oauth.persistToken", true)) {
334 if (password == null || password.isEmpty()) {
335
336 Settings.storeRefreshToken(username, refreshToken);
337 } else {
338 Settings.storeRefreshToken(username, O365Token.encryptToken(refreshToken, password));
339 }
340 }
341 }
342
343 private static String decryptToken(String encryptedRefreshToken, String password) throws IOException {
344 return new StringEncryptor(password).decryptString(encryptedRefreshToken);
345 }
346
347 private static String encryptToken(String refreshToken, String password) throws IOException {
348 return new StringEncryptor(password).encryptString(refreshToken);
349 }
350 }