├── .classpath ├── .gitignore ├── .project ├── .settings ├── org.eclipse.core.resources.prefs └── org.eclipse.jdt.core.prefs ├── LICENSE ├── README.md ├── libs ├── commons-codec-1.10-shaded.jar ├── commons-logging-1.2-shaded.jar ├── httpclient-4.5.3-shaded.jar ├── httpcore-4.4.6-shaded.jar ├── jsonbeans-0.9.jar └── minlog-1.2.jar └── src └── com └── esotericsoftware └── oauth └── OAuth.java /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | oauth 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.jdt.core.javanature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.settings/org.eclipse.core.resources.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | encoding/=Cp1252 3 | -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled 3 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 4 | org.eclipse.jdt.core.compiler.compliance=1.8 5 | org.eclipse.jdt.core.compiler.problem.assertIdentifier=error 6 | org.eclipse.jdt.core.compiler.problem.enumIdentifier=error 7 | org.eclipse.jdt.core.compiler.release=enabled 8 | org.eclipse.jdt.core.compiler.source=1.8 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Esoteric Software 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple OAuth for Java 2 | 3 | This library is a single, ~150 LOC class which makes using OAuth 2.0 from Java very easy. 4 | 5 | ## Examples 6 | 7 | For simplicity, these examples set the client secret. Keep in mind that embedding the client secret has [security implications](#Security), though it still makes sense for some scenarios. 8 | 9 | ### Slack 10 | 11 | Create the OAuth instance: 12 | 13 | ```java 14 | OAuth oauth = new OAuth("slack", "yourClientID", 15 | "yourRedirectURL", 16 | "https://slack.com/oauth/authorize", // authorize URL 17 | "https://slack.com/api/oauth.access", // access token URL 18 | "dnd:write", // scopes (for "do not disturb") 19 | 4); // concurrent HTTP requests 20 | oauth.setClientSecret("yourClientSecret"); // Note security implications below. 21 | ``` 22 | 23 | Create/load and initialize a `Token` instance: 24 | 25 | ```java 26 | Token token = ... // Load token from disk. 27 | if (oauth.authorize(token)) { 28 | // Save token to disk. 29 | } 30 | ``` 31 | 32 | Finally, use the access token to make requests: 33 | 34 | ```java 35 | // Slack access tokens never expire, so just use the access token. 36 | HttpGet request = new HttpGet("https://slack.com/api/dnd.setSnooze?num_minutes=60"); 37 | request.setHeader("Authorization", "Bearer " + token.accessToken); 38 | httpRequest(request); 39 | ``` 40 | 41 | ### Spotify 42 | 43 | ```java 44 | oauth = new OAuth("spotify", "yourClientID", 45 | "yourRedirectURL", 46 | "https://accounts.spotify.com/authorize", // authorize URL 47 | "https://accounts.spotify.com/api/token", // access token URL 48 | "user-modify-playback-state", // scopes 49 | 4); // concurrent HTTP requests 50 | oauth.setClientSecret("yourClientSecret"); // Note security implications below. 51 | 52 | ... 53 | 54 | Token token = ... // Load token from disk. 55 | if (oauth.authorize(token)) { 56 | // Save token to disk. 57 | } 58 | 59 | ... 60 | 61 | // Spotify access tokens expire, so call refreshAccessToken just before using the access token. 62 | if (oauth.refreshAccessToken(token)) { 63 | // Save token to disk. 64 | } 65 | HttpPut request = new HttpPut("https://api.spotify.com/v1/me/player/play"); 66 | request.setHeader("Authorization", "Bearer " + accessToken); 67 | httpRequest(request); 68 | ``` 69 | 70 | ### Google 71 | 72 | ```java 73 | oauth = new OAuth("google", "yourClientID", 74 | "urn:ietf:wg:oauth:2.0:oob", // redirect URL 75 | "https://accounts.google.com/o/oauth2/v2/auth", // authorize URL 76 | "https://www.googleapis.com/oauth2/v4/token", // access token URL 77 | "https://www.googleapis.com/auth/assistant-sdk-prototype", // scopes (for Google Assistant) 78 | 4); // concurrent HTTP requests 79 | oauth.setClientSecret("yourClientSecret"); // Note security implications below. 80 | 81 | ... 82 | 83 | Token token = ... // Load token from disk. 84 | if (oauth.authorize(token)) { 85 | // Save token to disk. 86 | } 87 | 88 | ... 89 | 90 | EmbeddedAssistantGrpc.EmbeddedAssistantStub client; 91 | client = EmbeddedAssistantGrpc.newStub(ManagedChannelBuilder.forAddress("embeddedassistant.googleapis.com", 443).build()); 92 | 93 | if (oauth.refreshAccessToken(token)) { 94 | // Save token to disk. 95 | } 96 | // Google's stuff to set the access token. 97 | OAuth2Credentials credentials = new OAuth2Credentials(new AccessToken(accessToken, new Date(expirationTime))); 98 | client = client.withCallCredentials(MoreCallCredentials.from(credentials)); 99 | // Google's stuff to use the Google Assistant API. 100 | StreamObserver observer = ... 101 | client.converse(observer); 102 | ``` 103 | 104 | ## Security 105 | 106 | The examples above embed the client secret in the application, which only makes sense if the application is used in a secure enviroment. For example, when the client secret is owned by the user running the application and the application containing the client secret is not distributed to others. Otherwise, the client secret can be extracted and used to impersonate the application. 107 | 108 | If a client secret has been set, the default implementation opens the specified URL in a browser and prompts the user to paste the authorization code at the command line. Next the authorization code and client secret are used to obtain an access token, which is ready for the application to use. 109 | 110 | If a client secret has not been set, then the `obtainAccessToken` method must be overridden: 111 | 112 | ```java 113 | oauth = new OAuth("someService", "yourClientID", 114 | "yourRedirectURL", 115 | "serviceAuthorizeURL", 116 | "serviceAccessTokenURL", 117 | "serviceScopes", 118 | 4 119 | ) { 120 | protected void obtainAccessToken (Token token, String url) throws IOException { 121 | // your code here 122 | } 123 | } 124 | ``` 125 | 126 | The `obtainAccessToken` method should have the user visit the specified `url` and allow access. Then the user is forwarded with a one-time use authorization code to `yourRedirectURL`, which is your web service that uses the authorization code to obtain an access token, refresh token, and expiration milliseconds. The application should retrieve those from your web service and set the corresponding 3 fields on the specified `token`. The web service should only give the access token to authenticated users. 127 | 128 | The web service obtains the access token, refresh token, and expiration milliseconds by doing an HTTP POST to `serviceAccessTokenURL` with a POST body of: 129 | 130 | ``` 131 | code=usersAuthorizationCode&redirect_uri=yourRedirectURL&client_id=yourClientID&client_secret=yourClientSecret&grant_type=authorization_code 132 | ``` 133 | 134 | As you can see, obtaining the access token requires your client secret, which ensures only your app can approve access. Your web service ensures the client secret is not leaked and only gives an access token to users it has authenticated. 135 | 136 | ## Utilities 137 | 138 | ### JsonBeans 139 | 140 | [JsonBeans](https://github.com/EsotericSoftware/jsonbeans/) is used by the `OAuth` class and makes it easy to save/load the `Token` instance: 141 | 142 | ```java 143 | File file = ... 144 | Json json = new Json(); 145 | Token token = file.exists() ? json.fromJson(Token.class, file) : new Token(); 146 | if (oauth.authorize(token)) { 147 | json.toJson(token, new FileWriter(file)); 148 | } 149 | ``` 150 | 151 | ### HttpClient 152 | 153 | Most of the examples above use [Apache HttpClient](https://hc.apache.org/httpcomponents-client-ga/), which the `OAuth` class depends on. Here is the `httpRequest` method: 154 | 155 | ```java 156 | int connectionPoolSize = 4; 157 | PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); 158 | connectionManager.setMaxTotal(connectionPoolSize); 159 | connectionManager.setDefaultMaxPerRoute(connectionPoolSize); 160 | CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager).build(); 161 | 162 | ... 163 | 164 | public String httpRequest (HttpUriRequest request) throws IOException { 165 | HttpEntity entity = null; 166 | try (CloseableHttpResponse response = httpClient.execute(request)) { 167 | String body = ""; 168 | entity = response.getEntity(); 169 | if (entity != null) { 170 | InputStream input = entity.getContent(); 171 | if (input != null) { 172 | try (Scanner scanner = new Scanner(input, "UTF-8").useDelimiter("\\A")) { 173 | if (scanner.hasNext()) body = scanner.next().trim(); 174 | } 175 | } 176 | } 177 | 178 | int status = response.getStatusLine().getStatusCode(); 179 | if (status < 200 || status >= 300) 180 | throw new IOException(response.getStatusLine().toString() + (body.length() > 0 ? "\n" + body : "")); 181 | return body; 182 | } finally { 183 | if (entity != null) EntityUtils.consumeQuietly(entity); 184 | } 185 | } 186 | ``` 187 | 188 | If using HttpClient in your app like this, `httpClient` can be passed to the `OAuth` constructor to share the same instance. 189 | 190 | ### Logging 191 | 192 | [MinLog](https://github.com/EsotericSoftware/minlog/) is used for logging, which is easily disabled or redirected. 193 | -------------------------------------------------------------------------------- /libs/commons-codec-1.10-shaded.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EsotericSoftware/oauth/5b25668f82538a7972f10877174c011da8543f20/libs/commons-codec-1.10-shaded.jar -------------------------------------------------------------------------------- /libs/commons-logging-1.2-shaded.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EsotericSoftware/oauth/5b25668f82538a7972f10877174c011da8543f20/libs/commons-logging-1.2-shaded.jar -------------------------------------------------------------------------------- /libs/httpclient-4.5.3-shaded.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EsotericSoftware/oauth/5b25668f82538a7972f10877174c011da8543f20/libs/httpclient-4.5.3-shaded.jar -------------------------------------------------------------------------------- /libs/httpcore-4.4.6-shaded.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EsotericSoftware/oauth/5b25668f82538a7972f10877174c011da8543f20/libs/httpcore-4.4.6-shaded.jar -------------------------------------------------------------------------------- /libs/jsonbeans-0.9.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EsotericSoftware/oauth/5b25668f82538a7972f10877174c011da8543f20/libs/jsonbeans-0.9.jar -------------------------------------------------------------------------------- /libs/minlog-1.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EsotericSoftware/oauth/5b25668f82538a7972f10877174c011da8543f20/libs/minlog-1.2.jar -------------------------------------------------------------------------------- /src/com/esotericsoftware/oauth/OAuth.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2017, Nathan Sweet 2 | * All rights reserved. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following 5 | * conditions are met: 6 | * 7 | * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 9 | * disclaimer in the documentation and/or other materials provided with the distribution. 10 | * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived 11 | * from this software without specific prior written permission. 12 | * 13 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, 14 | * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 15 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 16 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 17 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 18 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ 19 | 20 | package com.esotericsoftware.oauth; 21 | 22 | import static com.esotericsoftware.minlog.Log.*; 23 | 24 | import java.awt.Desktop; 25 | import java.io.BufferedReader; 26 | import java.io.IOException; 27 | import java.io.InputStreamReader; 28 | import java.net.URI; 29 | import java.net.URLEncoder; 30 | import java.util.Scanner; 31 | import java.util.regex.Matcher; 32 | import java.util.regex.Pattern; 33 | 34 | import com.esotericsoftware.jsonbeans.JsonReader; 35 | import com.esotericsoftware.jsonbeans.JsonValue; 36 | 37 | import shaded.org.apache.http.HttpEntity; 38 | import shaded.org.apache.http.client.methods.CloseableHttpResponse; 39 | import shaded.org.apache.http.client.methods.HttpPost; 40 | import shaded.org.apache.http.entity.StringEntity; 41 | import shaded.org.apache.http.impl.client.CloseableHttpClient; 42 | import shaded.org.apache.http.impl.client.HttpClients; 43 | import shaded.org.apache.http.impl.conn.PoolingHttpClientConnectionManager; 44 | import shaded.org.apache.http.util.EntityUtils; 45 | 46 | /** @author Nathan Sweet */ 47 | public class OAuth { 48 | private final String category; 49 | private final CloseableHttpClient http; 50 | private final String clientID; 51 | private final String redirectURL, authorizeURL, accessTokenURL; 52 | private final String scopes; 53 | private String clientSecret; 54 | 55 | /** @param connectionPoolSize The number of threads that can make HTTP requests concurrently. */ 56 | public OAuth (String category, String clientID, String redirectURL, String authorizeURL, String accessTokenURL, String scopes, 57 | int connectionPoolSize) { 58 | this.category = category; 59 | this.clientID = clientID; 60 | this.redirectURL = redirectURL; 61 | this.authorizeURL = authorizeURL; 62 | this.accessTokenURL = accessTokenURL; 63 | this.scopes = scopes; 64 | 65 | PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); 66 | connectionManager.setMaxTotal(connectionPoolSize); 67 | connectionManager.setDefaultMaxPerRoute(connectionPoolSize); 68 | http = HttpClients.custom().setConnectionManager(connectionManager).build(); 69 | } 70 | 71 | public OAuth (String category, String clientID, String redirectURL, String authorizeURL, String accessTokenURL, String scopes, 72 | CloseableHttpClient http) { 73 | this.category = category; 74 | this.clientID = clientID; 75 | this.redirectURL = redirectURL; 76 | this.authorizeURL = authorizeURL; 77 | this.accessTokenURL = accessTokenURL; 78 | this.scopes = scopes; 79 | this.http = http; 80 | } 81 | 82 | /** Initializes the specified token, if necessary. 83 | * @return true when a new access token was needed. */ 84 | public boolean authorize (Token token) throws IOException { 85 | if (token.accessToken != null) return false; 86 | obtainAccessToken(token, authorizeURL // 87 | + "?client_id=" + URLEncoder.encode(clientID, "UTF-8") // 88 | + "&response_type=code" // 89 | + "&redirect_uri=" + URLEncoder.encode(redirectURL, "UTF-8") // 90 | + "&scope=" + URLEncoder.encode(scopes, "UTF-8")); 91 | return true; 92 | } 93 | 94 | /** Called when a new access token is needed. The default implementation throws UnsupportedOperationException unless a 95 | * {@link #setClientSecret(String) client secret} has been set. 96 | *

97 | * Override to have the user visit the specified URL and allow access. Then the user is forwarded with a one-time use 98 | * authorization code to the {@link #redirectURL}, which is your web service that uses the authorization code to obtain an 99 | * accessToken, refreshToken, and expirationMillis. Retrieve those from your web service and set the fields on the specified 100 | * token. The web service should only give the access token to authenticated users. 101 | *

102 | * The web service obtains the access token by doing an HTTP POST to {@link #accessTokenURL} with a POST body of: 103 | * code=usersAuthCode&redirect_uri=yourRedirectURL&client_id=yourClientID&client_secret=yourClientSecret&grant_type=authorization_code 104 | * Obtaining the access token requires your client secret (bold), which ensures only your app can approve access. Your web 105 | * service ensures the client secret is not leaked and only gives an access token to users it has authenticated. 106 | *

107 | * Distributing the client secret inside your app can result in the client secret being extracted and used to impersonate your 108 | * app. Only use {@link #setClientSecret(String)} if the application is used in a secure enviroment. For example, when the 109 | * client secret is owned by the user running the application and the application containing the client secret is not 110 | * distributed to others. 111 | *

112 | * If a client secret has been set, the default implementation opens the specified URL in a browser and prompts the user to 113 | * paste the authorization code at the command line. Next the authorization code and client secret are used to obtain an access 114 | * token. The specified token is updated and ready to use when this method returns. */ 115 | protected void obtainAccessToken (Token token, String url) throws IOException { 116 | if (clientSecret == null) throw new UnsupportedOperationException(); 117 | 118 | if (INFO) info(category, "Visit this URL, allow access, and paste the new URL:\n" + url); 119 | try { 120 | Desktop.getDesktop().browse(new URI(url)); 121 | } catch (Exception ignored) { 122 | } 123 | String authorizationCode = new BufferedReader(new InputStreamReader(System.in)).readLine(); 124 | if (authorizationCode.contains("code=")) { 125 | Pattern pattern = Pattern.compile("code=([^&]+)&?"); 126 | Matcher matcher = pattern.matcher(authorizationCode); 127 | if (matcher.find()) authorizationCode = matcher.group(1); 128 | } 129 | 130 | if (TRACE) trace(category, "Requesting access token."); 131 | JsonValue json = post(accessTokenURL, // 132 | "code=" + URLEncoder.encode(authorizationCode, "UTF-8") // 133 | + "&redirect_uri=" + URLEncoder.encode(redirectURL, "UTF-8") // 134 | + "&client_id=" + URLEncoder.encode(clientID, "UTF-8") // 135 | + "&client_secret=" + URLEncoder.encode(clientSecret, "UTF-8") // 136 | + "&grant_type=authorization_code"); 137 | try { 138 | token.refreshToken = json.getString("refresh_token", null); 139 | token.accessToken = json.getString("access_token"); 140 | token.expirationMillis = System.currentTimeMillis() + json.getInt("expires_in", Integer.MAX_VALUE) * 1000; 141 | } catch (Throwable ex) { 142 | throw new IOException("Invalid access token response" + (json != null ? ": " + json : "."), ex); 143 | } 144 | 145 | if (INFO) info(category, "Access token stored."); 146 | } 147 | 148 | /** Refreshes the access token, if necessary. Call this method just before each use of the access token. Some OAuth access 149 | * tokens never expire and do not provide a refresh token, in which case this method is not needed. 150 | * @return true if the token was refreshed. */ 151 | public boolean refreshAccessToken (Token token) { 152 | if (!token.isExpired()) return false; 153 | if (TRACE) trace(category, "Refreshing access token."); 154 | 155 | if (token.refreshToken == null) { 156 | if (ERROR) error(category, "Refresh token is missing."); 157 | return false; 158 | } 159 | 160 | JsonValue json = null; 161 | try { 162 | json = post(accessTokenURL, // 163 | "refresh_token=" + URLEncoder.encode(token.refreshToken, "UTF-8") // 164 | + "&client_id=" + URLEncoder.encode(clientID, "UTF-8") // 165 | + "&client_secret=" + URLEncoder.encode(clientSecret, "UTF-8") // 166 | + "&grant_type=refresh_token"); 167 | token.accessToken = json.getString("access_token"); 168 | token.expirationMillis = System.currentTimeMillis() + json.getInt("expires_in") * 1000; 169 | if (DEBUG) debug(category, "Access token refreshed."); 170 | return true; 171 | } catch (Throwable ex) { 172 | if (ERROR) error(category, "Error refreshing access token" + (json != null ? ": " + json : "."), ex); 173 | return false; 174 | } 175 | } 176 | 177 | private JsonValue post (String url, String postBody) throws IOException { 178 | HttpPost request = new HttpPost(url); 179 | request.setEntity(new StringEntity(postBody)); 180 | request.setHeader("Content-Type", "application/x-www-form-urlencoded"); 181 | 182 | HttpEntity entity = null; 183 | CloseableHttpResponse response = null; 184 | try { 185 | response = http.execute(request); 186 | String body = ""; 187 | entity = response.getEntity(); 188 | if (entity != null) { 189 | Scanner scanner = null; 190 | try { 191 | scanner = new Scanner(entity.getContent(), "UTF-8").useDelimiter("\\A"); 192 | if (scanner.hasNext()) body = scanner.next().trim(); 193 | } finally { 194 | if (scanner != null) { 195 | try { 196 | scanner.close(); 197 | } catch (Throwable ignored) { 198 | } 199 | } 200 | } 201 | } 202 | 203 | int status = response.getStatusLine().getStatusCode(); 204 | if (status < 200 || status >= 300) 205 | throw new IOException(response.getStatusLine().toString() + (body.length() > 0 ? "\n" + body : "")); 206 | return new JsonReader().parse(body); 207 | } finally { 208 | if (entity != null) EntityUtils.consumeQuietly(entity); 209 | if (response != null) { 210 | try { 211 | response.close(); 212 | } catch (Throwable ignored) { 213 | } 214 | } 215 | } 216 | } 217 | 218 | public String getClientID () { 219 | return clientID; 220 | } 221 | 222 | /** @return May be null. */ 223 | public String getClientSecret () { 224 | return clientSecret; 225 | } 226 | 227 | /** Sets the client secret for obtaining an access token. See {@link #obtainAccessToken(Token, String)} for the security 228 | * implications of embedded the client secret in your application. */ 229 | public void setClientSecret (String clientSecret) { 230 | this.clientSecret = clientSecret; 231 | } 232 | 233 | public String getRedirectURL () { 234 | return redirectURL; 235 | } 236 | 237 | public String getAuthorizeURL () { 238 | return authorizeURL; 239 | } 240 | 241 | public String getAccessTokenURL () { 242 | return accessTokenURL; 243 | } 244 | 245 | public String getScopes () { 246 | return scopes; 247 | } 248 | 249 | static public class Token { 250 | public String refreshToken; 251 | public String accessToken; 252 | public long expirationMillis; 253 | 254 | public boolean isExpired () { 255 | return expirationMillis < System.currentTimeMillis(); 256 | } 257 | } 258 | } 259 | --------------------------------------------------------------------------------