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