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