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