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 static final String
[] OAUTH2_SCOPE
= {
46 "https://www.googleapis.com/auth/appengine.admin",
47 "https://www.googleapis.com/auth/cloud-platform"
49 protected static final long MIN_EXPIRES_SECONDS
= 300;
51 /** Token store filename. */
52 protected static final String TOKEN_STORE_BASE
= ".appcfg_oauth2_tokens_java";
54 /** Google client secrets. */
55 private static final GoogleClientSecrets DEFAULT_CLIENT_SECRETS
=
56 createClientSecrets(OAUTH2_CLIENT_ID
, OAUTH2_CLIENT_SECRET
);
58 private GoogleClientSecrets clientSecrets
;
59 private String refreshTokenOverride
;
61 private GoogleAuthorizationCodeFlow flow
;
62 private final VerificationCodeReceiver receiver
;
63 private final String userId
;
66 * Creates the client secrets used for authentication
68 * @param clientIdOverride The client id to use
69 * @param clientSecretOverride The client secret to use
70 * @return The client secrets
72 private static GoogleClientSecrets
createClientSecrets(
73 final String clientIdOverride
, final String clientSecretOverride
) {
74 return new GoogleClientSecrets().setInstalled(new GoogleClientSecrets
.Details().setClientId(
75 clientIdOverride
).setClientSecret(clientSecretOverride
));
78 public OAuth2Native(boolean usePersistedCredentials
) {
79 this(usePersistedCredentials
, null, null, null);
83 * Initialize a native OAuth2 flow using the specific client id and client secret provided.
85 * @param usePersistedCredentials {@code true} to use a file to store credentials, {@code false}
87 * @param clientIdOverride A client id to use for authentication requests or {@code null} to use
88 * the default for this application. If provided, {@code clientSecretOverride} must also be
90 * @param clientSecretOverride A client secret to use for authentication requests or {@code null}
91 * to use the default for this application. If provided, {@code clientIdOverride} must also
93 * @param refreshTokenOverride An alternate oauth2 refresh token to use for authorization or
94 * {@code null} to use the default token from the credential store.
96 public OAuth2Native(boolean usePersistedCredentials
, String clientIdOverride
,
97 String clientSecretOverride
, String refreshTokenOverride
) {
98 this(new PromptReceiver(), System
.getProperty("user.name"), null);
100 Preconditions
.checkArgument(!(clientIdOverride
== null ^ clientSecretOverride
== null),
101 "If either is given, both a client id and a client secret must be provided");
103 if (clientIdOverride
!= null) {
104 clientSecrets
= createClientSecrets(clientIdOverride
, clientSecretOverride
);
107 Preconditions
.checkArgument(refreshTokenOverride
== null || !usePersistedCredentials
,
108 "A credential store cannot be used when overriding the refresh token");
110 this.refreshTokenOverride
= refreshTokenOverride
;
113 flow
= getAuthorizationCodeFlow(usePersistedCredentials
);
114 } catch (IOException e
) {
115 System
.err
.println("Error creating the Authorization Flow: " + e
);
116 } catch (IllegalArgumentException e
) {
117 if (exceptionMentionsJson(e
)) {
118 System
.err
.format("The credentials file is malformed. Please delete the file '%s'.%n",
119 getTokenStoreFile());
126 public OAuth2Native(VerificationCodeReceiver receiver
, String userId
,
127 GoogleAuthorizationCodeFlow flow
) {
128 this.receiver
= receiver
;
129 this.userId
= userId
;
131 this.clientSecrets
= DEFAULT_CLIENT_SECRETS
;
132 this.refreshTokenOverride
= null;
136 * @return the Google client secrets.
138 public GoogleClientSecrets
getClientSecrets() {
139 return clientSecrets
;
143 * @return a File representing the token store file name in the user's home directory.
145 protected File
getTokenStoreFile() {
146 String userDir
= System
.getProperty("user.home");
147 return new File(userDir
, TOKEN_STORE_BASE
);
151 * Returns the CredentialStore to be used when calling
152 * {@link OAuth2Native#getAuthorizationCodeFlow(boolean)} with parameter {@code true}.
154 protected CredentialStore
getCredentialStore(JsonFactory jsonFactory
) throws IOException
{
155 return new FileCredentialStore(getTokenStoreFile(), jsonFactory
);
159 * Creates an authorization code flow with the right CredentialStore based on the argument.
161 * @param usePersistedCredentials whether or not to persist the credentials
162 * @return a GoogleAuthorizationCodeFlow
164 protected GoogleAuthorizationCodeFlow
getAuthorizationCodeFlow(boolean usePersistedCredentials
)
166 HttpTransport httpTransport
= new NetHttpTransport();
167 JsonFactory jsonFactory
= new JacksonFactory();
168 Collection
<String
> scopes
= Arrays
.asList(OAUTH2_SCOPE
);
169 GoogleClientSecrets clientSecrets
= getClientSecrets();
170 GoogleAuthorizationCodeFlow flow
;
171 if (usePersistedCredentials
) {
172 flow
= new GoogleAuthorizationCodeFlow
.Builder(
173 httpTransport
, jsonFactory
, clientSecrets
, scopes
).setAccessType("offline")
174 .setApprovalPrompt("force").setCredentialStore(getCredentialStore(jsonFactory
)).build();
176 flow
= new GoogleAuthorizationCodeFlow
.Builder(
177 httpTransport
, jsonFactory
, clientSecrets
, scopes
).setAccessType("online")
178 .setApprovalPrompt("auto").build();
184 * Checks if an exception mention JSON in its message. This method is intended to detect a
185 * specific code path and is somewhat fragile.
187 * @param exception the exception to check
188 * @return true if the exception mentions JSON in its message, false otherwise.
190 private static boolean exceptionMentionsJson(Exception exception
) {
191 return exception
.getMessage().toLowerCase().contains("json");
195 * Calls the method refreshToken if there is no access token or if the token is close to expire.
196 * Returns true if refreshToken was called. If the token was refreshed and credentialStore is not
197 * null, it saves the updated credential.
199 * @param credential the credential to check
200 * @return whether the credential was refreshed or not
202 protected boolean refreshCredentialIfNeeded(Credential credential
) throws IOException
{
203 if (credential
!= null) {
204 Long expiresInSeconds
= credential
.getExpiresInSeconds();
205 if (credential
.getAccessToken() == null || expiresInSeconds
== null ||
206 expiresInSeconds
< MIN_EXPIRES_SECONDS
) {
207 credential
.refreshToken();
209 if (flow
.getCredentialStore() != null) {
210 flow
.getCredentialStore().store(userId
, credential
);
220 * Authorizes the installed application to access user's protected data.
222 * @return a credential with the accesToken or null if no authorization token could be obtained.
224 public Credential
authorize(){
229 String redirectUri
= receiver
.getRedirectUri();
231 final Credential credential
= getCredential();
232 refreshCredentialIfNeeded(credential
);
233 if (credential
!= null && credential
.getAccessToken() != null){
237 browse(flow
.newAuthorizationUrl().setRedirectUri(redirectUri
).build());
238 String code
= receiver
.waitForCode();
239 GoogleTokenResponse response
=
240 flow
.newTokenRequest(code
).setRedirectUri(redirectUri
).execute();
241 return flow
.createAndStoreCredential(response
, userId
);
242 } catch (TokenResponseException e
) {
243 System
.err
.format("Either the access code is invalid or the OAuth token is revoked." +
244 "Details: %s%n.", e
.getDetails().getError());
245 } catch (IOException e
) {
246 System
.err
.println(e
);
247 } catch (VerificationCodeReceiverRedirectUriException e
) {
248 System
.err
.println(e
.getMessage());
252 } catch (VerificationCodeReceiverStopException e
) {
253 System
.err
.println(e
.getMessage());
260 * Gets a new credential either from the configured credential store, or with a specific refresh
263 * @return A newly created and credential
264 * @throws IOException
266 Credential
getCredential() throws IOException
{
267 if (refreshTokenOverride
== null) {
268 return flow
.loadCredential(userId
);
271 final Credential credential
=
272 new Credential
.Builder(flow
.getMethod()).setTransport(flow
.getTransport())
273 .setJsonFactory(flow
.getJsonFactory())
274 .setTokenServerEncodedUrl(flow
.getTokenServerEncodedUrl())
275 .setClientAuthentication(flow
.getClientAuthentication())
276 .setRequestInitializer(flow
.getRequestInitializer())
277 .setClock(flow
.getClock())
279 credential
.setRefreshToken(refreshTokenOverride
);
285 * Open a browser at the given URL.
287 * It attempts to open the browser using {@link Desktop#isDesktopSupported()}.
288 * If that fails, on Windows it tries {@code rundll32}. If that fails, it opens the browser
289 * specified in {@link #BROWSER}.
290 * Note though that currently we've only tested this code with Google Chrome (hence this is the
294 * @param url absolute url to open in the browser
296 protected void browse(String url
) {
297 if (Desktop
.isDesktopSupported()) {
298 Desktop desktop
= Desktop
.getDesktop();
299 if (desktop
.isSupported(Action
.BROWSE
)) {
301 desktop
.browse(URI
.create(url
));
303 } catch (IOException e
) {
308 Runtime
.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + url
);
310 } catch (IOException e
) {
312 if (BROWSER
!= null) {
314 Runtime
.getRuntime().exec(new String
[] {BROWSER
, url
});
316 } catch (IOException e
) {
319 System
.out
.format("Please open the following URL in your browser:%n %s%n", url
);