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