1 // Copyright (c) 2011-2012 Google Inc.
3 package com
.google
.appengine
.tools
.admin
;
5 import com
.google
.api
.client
.auth
.oauth2
.Credential
;
6 import com
.google
.api
.client
.auth
.oauth2
.CredentialStore
;
7 import com
.google
.api
.client
.auth
.oauth2
.TokenResponseException
;
8 import com
.google
.api
.client
.extensions
.java6
.auth
.oauth2
.FileCredentialStore
;
9 import com
.google
.api
.client
.googleapis
.auth
.oauth2
.GoogleAuthorizationCodeFlow
;
10 import com
.google
.api
.client
.googleapis
.auth
.oauth2
.GoogleClientSecrets
;
11 import com
.google
.api
.client
.googleapis
.auth
.oauth2
.GoogleTokenResponse
;
12 import com
.google
.api
.client
.http
.HttpTransport
;
13 import com
.google
.api
.client
.http
.javanet
.NetHttpTransport
;
14 import com
.google
.api
.client
.json
.JsonFactory
;
15 import com
.google
.api
.client
.json
.jackson
.JacksonFactory
;
16 import com
.google
.common
.base
.Preconditions
;
18 import java
.awt
.Desktop
;
19 import java
.awt
.Desktop
.Action
;
21 import java
.io
.IOException
;
23 import java
.util
.Arrays
;
24 import java
.util
.Collection
;
27 * Implements OAuth authentication "native" flow recommended for installed clients in which the end
28 * user must grant access in a web browser and then copy a code into the application.
31 * Warning: the client ID and secret are not secured and are plainly visible to users.
32 * It is a hard problem to secure client credentials in installed applications.
36 public class OAuth2Native
{
38 * Browser to open in case {@link Desktop#isDesktopSupported()} is {@code false} or {@code null}
39 * to prompt user to open the URL in their favorite browser.
41 private static final String BROWSER
= "google-chrome";
43 protected static final String OAUTH2_CLIENT_ID
= "550516889912.apps.googleusercontent.com";
44 protected static final String OAUTH2_CLIENT_SECRET
= "ykPq-0UYfKNprLRjVx1hBBar";
45 protected static final String OAUTH2_SCOPE
= "https://www.googleapis.com/auth/appengine.admin";
46 protected static final long MIN_EXPIRES_SECONDS
= 300;
48 /** Token store filename. */
49 protected static final String TOKEN_STORE_BASE
= ".appcfg_oauth2_tokens_java";
51 /** Google client secrets. */
52 private static final GoogleClientSecrets DEFAULT_CLIENT_SECRETS
=
53 createClientSecrets(OAUTH2_CLIENT_ID
, OAUTH2_CLIENT_SECRET
);
55 private GoogleClientSecrets clientSecrets
;
56 private String refreshTokenOverride
;
58 private GoogleAuthorizationCodeFlow flow
;
59 private final VerificationCodeReceiver receiver
;
60 private final String userId
;
63 * Creates the client secrets used for authentication
65 * @param clientIdOverride The client id to use
66 * @param clientSecretOverride The client secret to use
67 * @return The client secrets
69 private static GoogleClientSecrets
createClientSecrets(
70 final String clientIdOverride
, final String clientSecretOverride
) {
71 return new GoogleClientSecrets().setInstalled(new GoogleClientSecrets
.Details().setClientId(
72 clientIdOverride
).setClientSecret(clientSecretOverride
));
75 public OAuth2Native(boolean usePersistedCredentials
) {
76 this(usePersistedCredentials
, null, null, null);
80 * Initialize a native OAuth2 flow using the specific client id and client secret provided.
82 * @param usePersistedCredentials {@code true} to use a file to store credentials, {@code false}
84 * @param clientIdOverride A client id to use for authentication requests or {@code null} to use
85 * the default for this application. If provided, {@code clientSecretOverride} must also be
87 * @param clientSecretOverride A client secret to use for authentication requests or {@code null}
88 * to use the default for this application. If provided, {@code clientIdOverride} must also
90 * @param refreshTokenOverride An alternate oauth2 refresh token to use for authorization or
91 * {@code null} to use the default token from the credential store.
93 public OAuth2Native(boolean usePersistedCredentials
, String clientIdOverride
,
94 String clientSecretOverride
, String refreshTokenOverride
) {
95 this(new PromptReceiver(), System
.getProperty("user.name"), null);
97 Preconditions
.checkArgument(!(clientIdOverride
== null ^ clientSecretOverride
== null),
98 "If either is given, both a client id and a client secret must be provided");
100 if (clientIdOverride
!= null) {
101 clientSecrets
= createClientSecrets(clientIdOverride
, clientSecretOverride
);
104 Preconditions
.checkArgument(refreshTokenOverride
== null || !usePersistedCredentials
,
105 "A credential store cannot be used when overriding the refresh token");
107 this.refreshTokenOverride
= refreshTokenOverride
;
110 flow
= getAuthorizationCodeFlow(usePersistedCredentials
);
111 } catch (IOException e
) {
112 System
.err
.println("Error creating the Authorization Flow: " + e
);
113 } catch (IllegalArgumentException e
) {
114 if (exceptionMentionsJson(e
)) {
115 System
.err
.format("The credentials file is malformed. Please delete the file '%s'.%n",
116 getTokenStoreFile());
123 public OAuth2Native(VerificationCodeReceiver receiver
, String userId
,
124 GoogleAuthorizationCodeFlow flow
) {
125 this.receiver
= receiver
;
126 this.userId
= userId
;
128 this.clientSecrets
= DEFAULT_CLIENT_SECRETS
;
129 this.refreshTokenOverride
= null;
133 * @return the Google client secrets.
135 public GoogleClientSecrets
getClientSecrets() {
136 return clientSecrets
;
140 * @return a File representing the token store file name in the user's home directory.
142 protected File
getTokenStoreFile() {
143 String userDir
= System
.getProperty("user.home");
144 return new File(userDir
, TOKEN_STORE_BASE
);
148 * Returns the CredentialStore to be used when calling
149 * {@link OAuth2Native#getAuthorizationCodeFlow(boolean)} with parameter {@code true}.
151 protected CredentialStore
getCredentialStore(JsonFactory jsonFactory
) throws IOException
{
152 return new FileCredentialStore(getTokenStoreFile(), jsonFactory
);
156 * Creates an authorization code flow with the right CredentialStore based on the argument.
158 * @param usePersistedCredentials whether or not to persist the credentials
159 * @return a GoogleAuthorizationCodeFlow
161 protected GoogleAuthorizationCodeFlow
getAuthorizationCodeFlow(boolean usePersistedCredentials
)
163 HttpTransport httpTransport
= new NetHttpTransport();
164 JsonFactory jsonFactory
= new JacksonFactory();
165 Collection
<String
> scopes
= Arrays
.asList(OAUTH2_SCOPE
);
166 GoogleClientSecrets clientSecrets
= getClientSecrets();
167 GoogleAuthorizationCodeFlow flow
;
168 if (usePersistedCredentials
) {
169 flow
= new GoogleAuthorizationCodeFlow
.Builder(
170 httpTransport
, jsonFactory
, clientSecrets
, scopes
).setAccessType("offline")
171 .setApprovalPrompt("force").setCredentialStore(getCredentialStore(jsonFactory
)).build();
173 flow
= new GoogleAuthorizationCodeFlow
.Builder(
174 httpTransport
, jsonFactory
, clientSecrets
, scopes
).setAccessType("online")
175 .setApprovalPrompt("auto").build();
181 * Checks if an exception mention JSON in its message. This method is intended to detect a
182 * specific code path and is somewhat fragile.
184 * @param exception the exception to check
185 * @return true if the exception mentions JSON in its message, false otherwise.
187 private static boolean exceptionMentionsJson(Exception exception
) {
188 return exception
.getMessage().toLowerCase().indexOf("json") >= 0;
192 * Calls the method refreshToken if there is no access token or if the token is close to expire.
193 * Returns true if refreshToken was called. If the token was refreshed and credentialStore is not
194 * null, it saves the updated credential.
196 * @param credential the credential to check
197 * @return whether the credential was refreshed or not
199 protected boolean refreshCredentialIfNeeded(Credential credential
) throws IOException
{
200 if (credential
!= null) {
201 Long expiresInSeconds
= credential
.getExpiresInSeconds();
202 if (credential
.getAccessToken() == null || expiresInSeconds
== null ||
203 expiresInSeconds
< MIN_EXPIRES_SECONDS
) {
204 credential
.refreshToken();
206 if (flow
.getCredentialStore() != null) {
207 flow
.getCredentialStore().store(userId
, credential
);
217 * Authorizes the installed application to access user's protected data.
219 * @return a credential with the accesToken or null if no authorization token could be obtained.
221 public Credential
authorize(){
226 String redirectUri
= receiver
.getRedirectUri();
228 final Credential credential
= getCredential();
229 refreshCredentialIfNeeded(credential
);
230 if (credential
!= null && credential
.getAccessToken() != null){
234 browse(flow
.newAuthorizationUrl().setRedirectUri(redirectUri
).build());
235 String code
= receiver
.waitForCode();
236 GoogleTokenResponse response
=
237 flow
.newTokenRequest(code
).setRedirectUri(redirectUri
).execute();
238 return flow
.createAndStoreCredential(response
, userId
);
239 } catch (TokenResponseException e
) {
240 System
.err
.format("Either the access code is invalid or the OAuth token is revoked." +
241 "Details: %s%n.", e
.getDetails().getError());
242 } catch (IOException e
) {
243 System
.err
.println(e
);
244 } catch (VerificationCodeReceiverRedirectUriException e
) {
245 System
.err
.println(e
.getMessage());
249 } catch (VerificationCodeReceiverStopException e
) {
250 System
.err
.println(e
.getMessage());
257 * Gets a new credential either from the configured credential store, or with a specific refresh
260 * @return A newly created and credential
261 * @throws IOException
263 Credential
getCredential() throws IOException
{
264 if (refreshTokenOverride
== null) {
265 return flow
.loadCredential(userId
);
268 final Credential credential
=
269 new Credential
.Builder(flow
.getMethod()).setTransport(flow
.getTransport())
270 .setJsonFactory(flow
.getJsonFactory())
271 .setTokenServerEncodedUrl(flow
.getTokenServerEncodedUrl())
272 .setClientAuthentication(flow
.getClientAuthentication())
273 .setRequestInitializer(flow
.getRequestInitializer())
274 .setClock(flow
.getClock())
276 credential
.setRefreshToken(refreshTokenOverride
);
282 * Open a browser at the given URL.
284 * It attempts to open the browser using {@link Desktop#isDesktopSupported()}.
285 * If that fails, on Windows it tries {@code rundll32}. If that fails, it opens the browser
286 * specified in {@link #BROWSER}.
287 * Note though that currently we've only tested this code with Google Chrome (hence this is the
291 * @param url absolute url to open in the browser
293 protected void browse(String url
) {
294 if (Desktop
.isDesktopSupported()) {
295 Desktop desktop
= Desktop
.getDesktop();
296 if (desktop
.isSupported(Action
.BROWSE
)) {
298 desktop
.browse(URI
.create(url
));
300 } catch (IOException e
) {
305 Runtime
.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + url
);
307 } catch (IOException e
) {
309 if (BROWSER
!= null) {
311 Runtime
.getRuntime().exec(new String
[] {BROWSER
, url
});
313 } catch (IOException e
) {
316 System
.out
.format("Please open the following URL in your browser:%n %s%n", url
);