View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2010  Mickael Guessant
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
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   * O365 token wrapper
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 = Settings.getO365LoginUrl() + tenantId + "/oauth2/token";
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 = Settings.getO365LoginUrl() + tenantId + "/oauth2/token";
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  
82      public String getUsername() {
83          return username;
84      }
85  
86      public void setJsonToken(JSONObject jsonToken) throws IOException {
87          try {
88              if (jsonToken.opt("error") != null) {
89                  throw new IOException(jsonToken.optString("error") + " " + jsonToken.optString("error_description"));
90              }
91              // access token expires after one hour
92              accessToken = jsonToken.getString("access_token");
93              // precious refresh token
94              refreshToken = jsonToken.getString("refresh_token");
95              // expires_on is in second, not millisecond
96              expiresOn = jsonToken.getLong("expires_on") * 1000;
97  
98              LOGGER.debug("Access token expires " + new Date(expiresOn));
99  
100             // get username from id_token
101             String idToken = jsonToken.optString("id_token");
102             if (idToken != null && idToken.contains(".")) {
103                 String decodedJwt = IOUtil.decodeBase64AsString(idToken.substring(idToken.indexOf("."), idToken.lastIndexOf(".")));
104                 try {
105                     JSONObject tokenBody = new JSONObject(decodedJwt);
106                     LOGGER.debug("Token: " + tokenBody);
107                     username = tokenBody.getString("unique_name");
108                 } catch (JSONException e) {
109                     LOGGER.warn("Invalid id_token " + e.getMessage(), e);
110                 }
111             }
112             // failover: get username from bearer
113             if (username == null) {
114                 String decodedBearer = IOUtil.decodeBase64AsString(accessToken.substring(accessToken.indexOf('.') + 1, accessToken.lastIndexOf('.')) + "==");
115                 JSONObject tokenBody = new JSONObject(decodedBearer);
116                 LOGGER.debug("Token: " + tokenBody);
117                 username = tokenBody.getString("unique_name");
118             }
119 
120         } catch (JSONException e) {
121             throw new IOException("Exception parsing token", e);
122         }
123     }
124 
125     public void setClientId(String clientId) {
126         this.clientId = clientId;
127     }
128 
129     public void setRedirectUri(String redirectUri) {
130         this.redirectUri = redirectUri;
131     }
132 
133     public String getAccessToken() throws IOException {
134         // detect expiration and refresh token
135         if (isTokenExpired()) {
136             LOGGER.debug("Access token expires soon, trying to refresh it");
137             refreshToken();
138         }
139         //LOGGER.debug("Access token for " + username + " expires in " + (expiresOn - System.currentTimeMillis()) / 60000 + " minutes");
140         return accessToken;
141     }
142 
143     private boolean isTokenExpired() {
144         return System.currentTimeMillis() > (expiresOn - 60000);
145     }
146 
147     public void setAccessToken(String accessToken) {
148         this.accessToken = accessToken;
149         // assume unexpired token
150         expiresOn = System.currentTimeMillis() + 1000 * 60 * 60;
151     }
152 
153     public void setRefreshToken(String refreshToken) {
154         this.refreshToken = refreshToken;
155     }
156 
157     public String getRefreshToken() {
158         return refreshToken;
159     }
160 
161     public void refreshToken() throws IOException {
162         ArrayList<NameValuePair> parameters = new ArrayList<>();
163         parameters.add(new BasicNameValuePair("grant_type", "refresh_token"));
164         parameters.add(new BasicNameValuePair("refresh_token", refreshToken));
165         parameters.add(new BasicNameValuePair("redirect_uri", redirectUri));
166         parameters.add(new BasicNameValuePair("client_id", clientId));
167         parameters.add(new BasicNameValuePair("resource", Settings.OUTLOOK_URL));
168 
169         RestRequest tokenRequest = new RestRequest(tokenUrl, new UrlEncodedFormEntity(parameters, Consts.UTF_8));
170 
171         executeRequest(tokenRequest);
172 
173         // persist new refresh token
174         persistToken();
175     }
176 
177     private void executeRequest(RestRequest tokenMethod) throws IOException {
178         // do not keep login connections open (no pooling)
179         try (
180                 HttpClientAdapter httpClientAdapter = new HttpClientAdapter(tokenUrl);
181                 CloseableHttpResponse response = httpClientAdapter.execute(tokenMethod)
182         ) {
183             setJsonToken(tokenMethod.handleResponse(response));
184         }
185     }
186 
187     static O365Token build(String tenantId, String clientId, String redirectUri, String code, String password) throws IOException {
188         O365Token token = new O365Token(tenantId, clientId, redirectUri, code, password);
189         token.persistToken();
190         return token;
191     }
192 
193 
194     static O365Token load(String tenantId, String clientId, String redirectUri, String username, String password) throws UnknownHostException {
195         O365Token token = null;
196         if (Settings.getBooleanProperty("davmail.oauth.persistToken", true)) {
197             String encryptedRefreshToken = Settings.loadRefreshToken(username);
198             if (encryptedRefreshToken != null) {
199                 String refreshToken;
200                 try {
201                     refreshToken = decryptToken(encryptedRefreshToken, password);
202                     LOGGER.debug("Loaded stored token for " + username);
203                     O365Token localToken = new O365Token(tenantId, clientId, redirectUri, password);
204 
205                     localToken.setRefreshToken(refreshToken);
206                     localToken.refreshToken();
207                     LOGGER.debug("Authenticated user " + localToken.getUsername() + " from stored token");
208                     token = localToken;
209 
210                 } catch (UnknownHostException e) {
211                     // network down, rethrow to avoid invalidating token
212                     throw e;
213                 } catch (IOException e) {
214                     LOGGER.error("refresh token failed " + e.getMessage());
215                     // TODO detect network down and rethrow exception
216                 }
217             }
218         }
219         return token;
220     }
221 
222     private void persistToken() throws IOException {
223         if (Settings.getBooleanProperty("davmail.oauth.persistToken", true)) {
224             if (password == null || password.isEmpty()) {
225                 // no password provided, store token unencrypted
226                 Settings.storeRefreshToken(username, refreshToken);
227             } else {
228                 Settings.storeRefreshToken(username, O365Token.encryptToken(refreshToken, password));
229             }
230         }
231     }
232 
233     private static String decryptToken(String encryptedRefreshToken, String password) throws IOException {
234         return new StringEncryptor(password).decryptString(encryptedRefreshToken);
235     }
236 
237     private static String encryptToken(String refreshToken, String password) throws IOException {
238         return new StringEncryptor(password).encryptString(refreshToken);
239     }
240 }