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
;
26 * Implements OAuth authentication "native" flow recommended for installed clients in which the end
27 * user must grant access in a web browser and then copy a code into the application.
30 * Warning: the client ID and secret are not secured and are plainly visible to users.
31 * It is a hard problem to secure client credentials in installed applications.
35 public class OAuth2Native
{
37 * Browser to open in case {@link Desktop#isDesktopSupported()} is {@code false} or {@code null}
38 * to prompt user to open the URL in their favorite browser.
40 private static final String BROWSER
= "google-chrome";
42 protected static final String OAUTH2_CLIENT_ID
= "550516889912.apps.googleusercontent.com";
43 protected static final String OAUTH2_CLIENT_SECRET
= "ykPq-0UYfKNprLRjVx1hBBar";
44 protected static final String OAUTH2_SCOPE
= "https://www.googleapis.com/auth/appengine.admin";
45 protected static final long MIN_EXPIRES_SECONDS
= 300;
47 /** Token store filename. */
48 protected static final String TOKEN_STORE_BASE
= ".appcfg_oauth2_tokens_java";
50 /** Google client secrets. */
51 private static final GoogleClientSecrets DEFAULT_CLIENT_SECRETS
=
52 createClientSecrets(OAUTH2_CLIENT_ID
, OAUTH2_CLIENT_SECRET
);
54 private GoogleClientSecrets clientSecrets
;
55 private String refreshTokenOverride
;
57 private GoogleAuthorizationCodeFlow flow
;
58 private final VerificationCodeReceiver receiver
;
59 private final String userId
;
62 * Creates the client secrets used for authentication
64 * @param clientIdOverride The client id to use
65 * @param clientSecretOverride The client secret to use
66 * @return The client secrets
68 private static GoogleClientSecrets
createClientSecrets(
69 final String clientIdOverride
, final String clientSecretOverride
) {
70 return new GoogleClientSecrets().setInstalled(new GoogleClientSecrets
.Details().setClientId(
71 clientIdOverride
).setClientSecret(clientSecretOverride
));
74 public OAuth2Native(boolean usePersistedCredentials
) {
75 this(usePersistedCredentials
, null, null, null);
79 * Initialize a native OAuth2 flow using the specific client id and client secret provided.
81 * @param usePersistedCredentials {@code true} to use a file to store credentials, {@code false}
83 * @param clientIdOverride A client id to use for authentication requests or {@code null} to use
84 * the default for this application. If provided, {@code clientSecretOverride} must also be
86 * @param clientSecretOverride A client secret to use for authentication requests or {@code null}
87 * to use the default for this application. If provided, {@code clientIdOverride} must also
89 * @param refreshTokenOverride An alternate oauth2 refresh token to use for authorization or
90 * {@code null} to use the default token from the credential store.
92 public OAuth2Native(boolean usePersistedCredentials
, String clientIdOverride
,
93 String clientSecretOverride
, String refreshTokenOverride
) {
94 this(new PromptReceiver(), System
.getProperty("user.name"), null);
96 Preconditions
.checkArgument(!(clientIdOverride
== null ^ clientSecretOverride
== null),
97 "If either is given, both a client id and a client secret must be provided");
99 if (clientIdOverride
!= null) {
100 clientSecrets
= createClientSecrets(clientIdOverride
, clientSecretOverride
);
103 Preconditions
.checkArgument(refreshTokenOverride
== null || !usePersistedCredentials
,
104 "A credential store cannot be used when overriding the refresh token");
106 this.refreshTokenOverride
= refreshTokenOverride
;
109 flow
= getAuthorizationCodeFlow(usePersistedCredentials
);
110 } catch (IOException e
) {
111 System
.err
.println("Error creating the Authorization Flow: " + e
);
112 } catch (IllegalArgumentException e
) {
113 if (exceptionMentionsJson(e
)) {
114 System
.err
.format("The credentials file is malformed. Please delete the file '%s'.%n",
115 getTokenStoreFile());
122 public OAuth2Native(VerificationCodeReceiver receiver
, String userId
,
123 GoogleAuthorizationCodeFlow flow
) {
124 this.receiver
= receiver
;
125 this.userId
= userId
;
127 this.clientSecrets
= DEFAULT_CLIENT_SECRETS
;
128 this.refreshTokenOverride
= null;
132 * @return the Google client secrets.
134 public GoogleClientSecrets
getClientSecrets() {
135 return clientSecrets
;
139 * @return a File representing the token store file name in the user's home directory.
141 protected File
getTokenStoreFile() {
142 String userDir
= System
.getProperty("user.home");
143 return new File(userDir
, TOKEN_STORE_BASE
);
147 * Returns the CredentialStore to be used when calling
148 * {@link OAuth2Native#getAuthorizationCodeFlow(boolean)} with parameter {@code true}.
150 protected CredentialStore
getCredentialStore(JsonFactory jsonFactory
) throws IOException
{
151 return new FileCredentialStore(getTokenStoreFile(), jsonFactory
);
155 * Creates an authorization code flow with the right CredentialStore based on the argument.
157 * @param usePersistedCredentials whether or not to persist the credentials
158 * @return a GoogleAuthorizationCodeFlow
160 protected GoogleAuthorizationCodeFlow
getAuthorizationCodeFlow(boolean usePersistedCredentials
)
162 HttpTransport httpTransport
= new NetHttpTransport();
163 JsonFactory jsonFactory
= new JacksonFactory();
164 Iterable
<String
> scopes
= Arrays
.asList(OAUTH2_SCOPE
);
165 GoogleClientSecrets clientSecrets
= getClientSecrets();
166 GoogleAuthorizationCodeFlow flow
;
167 if (usePersistedCredentials
) {
168 flow
= new GoogleAuthorizationCodeFlow
.Builder(
169 httpTransport
, jsonFactory
, clientSecrets
, scopes
).setAccessType("offline")
170 .setApprovalPrompt("force").setCredentialStore(getCredentialStore(jsonFactory
)).build();
172 flow
= new GoogleAuthorizationCodeFlow
.Builder(
173 httpTransport
, jsonFactory
, clientSecrets
, scopes
).setAccessType("online")
174 .setApprovalPrompt("auto").build();
180 * Checks if an exception mention JSON in its message. This method is intended to detect a
181 * specific code path and is somewhat fragile.
183 * @param exception the exception to check
184 * @return true if the exception mentions JSON in its message, false otherwise.
186 private static boolean exceptionMentionsJson(Exception exception
) {
187 return exception
.getMessage().toLowerCase().indexOf("json") >= 0;
191 * Calls the method refreshToken if there is no access token or if the token is close to expire.
192 * Returns true if refreshToken was called. If the token was refreshed and credentialStore is not
193 * null, it saves the updated credential.
195 * @param credential the credential to check
196 * @return whether the credential was refreshed or not
198 protected boolean refreshCredentialIfNeeded(Credential credential
) throws IOException
{
199 if (credential
!= null) {
200 Long expiresInSeconds
= credential
.getExpiresInSeconds();
201 if (credential
.getAccessToken() == null || expiresInSeconds
== null ||
202 expiresInSeconds
< MIN_EXPIRES_SECONDS
) {
203 credential
.refreshToken();
205 if (flow
.getCredentialStore() != null) {
206 flow
.getCredentialStore().store(userId
, credential
);
216 * Authorizes the installed application to access user's protected data.
218 * @return a credential with the accesToken or null if no authorization token could be obtained.
220 public Credential
authorize(){
225 String redirectUri
= receiver
.getRedirectUri();
227 final Credential credential
= getCredential();
228 refreshCredentialIfNeeded(credential
);
229 if (credential
!= null && credential
.getAccessToken() != null){
233 browse(flow
.newAuthorizationUrl().setRedirectUri(redirectUri
).build());
234 String code
= receiver
.waitForCode();
235 GoogleTokenResponse response
=
236 flow
.newTokenRequest(code
).setRedirectUri(redirectUri
).execute();
237 return flow
.createAndStoreCredential(response
, userId
);
238 } catch (TokenResponseException e
) {
239 System
.err
.format("Either the access code is invalid or the OAuth token is revoked." +
240 "Details: %s%n.", e
.getDetails().getError());
241 } catch (IOException e
) {
242 System
.err
.println(e
);
243 } catch (VerificationCodeReceiverRedirectUriException e
) {
244 System
.err
.println(e
.getMessage());
248 } catch (VerificationCodeReceiverStopException e
) {
249 System
.err
.println(e
.getMessage());
256 * Gets a new credential either from the configured credential store, or with a specific refresh
259 * @return A newly created and credential
260 * @throws IOException
262 Credential
getCredential() throws IOException
{
263 if (refreshTokenOverride
== null) {
264 return flow
.loadCredential(userId
);
267 final Credential credential
=
268 new Credential
.Builder(flow
.getMethod()).setTransport(flow
.getTransport())
269 .setJsonFactory(flow
.getJsonFactory())
270 .setTokenServerEncodedUrl(flow
.getTokenServerEncodedUrl())
271 .setClientAuthentication(flow
.getClientAuthentication())
272 .setRequestInitializer(flow
.getRequestInitializer())
273 .setClock(flow
.getClock())
275 credential
.setRefreshToken(refreshTokenOverride
);
281 * Open a browser at the given URL.
283 * It attempts to open the browser using {@link Desktop#isDesktopSupported()}.
284 * If that fails, on Windows it tries {@code rundll32}. If that fails, it opens the browser
285 * specified in {@link #BROWSER}.
286 * Note though that currently we've only tested this code with Google Chrome (hence this is the
290 * @param url absolute url to open in the browser
292 protected void browse(String url
) {
293 if (Desktop
.isDesktopSupported()) {
294 Desktop desktop
= Desktop
.getDesktop();
295 if (desktop
.isSupported(Action
.BROWSE
)) {
297 desktop
.browse(URI
.create(url
));
299 } catch (IOException e
) {
304 Runtime
.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + url
);
306 } catch (IOException e
) {
308 if (BROWSER
!= null) {
310 Runtime
.getRuntime().exec(new String
[] {BROWSER
, url
});
312 } catch (IOException e
) {
315 System
.out
.format("Please open the following URL in your browser:%n %s%n", url
);