Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / admin / OAuth2Native.java
blob09386649a31a1f07d05c809ee488b430eb7d881b
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;
20 import java.io.File;
21 import java.io.IOException;
22 import java.net.URI;
23 import java.util.Arrays;
24 import java.util.Collection;
26 /**
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.
30 * <p>
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.
33 * </p>
36 public class OAuth2Native {
37 /**
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;
62 /**
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);
79 /**
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}
83 * otherwise
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
86 * provided.
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
89 * be provided.
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;
109 try {
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());
117 } else {
118 throw e;
123 public OAuth2Native(VerificationCodeReceiver receiver, String userId,
124 GoogleAuthorizationCodeFlow flow) {
125 this.receiver = receiver;
126 this.userId = userId;
127 this.flow = flow;
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)
162 throws IOException {
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();
172 } else {
173 flow = new GoogleAuthorizationCodeFlow.Builder(
174 httpTransport, jsonFactory, clientSecrets, scopes).setAccessType("online")
175 .setApprovalPrompt("auto").build();
177 return flow;
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);
210 return true;
213 return false;
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(){
222 if (flow == null) {
223 return null;
225 try {
226 String redirectUri = receiver.getRedirectUri();
228 final Credential credential = getCredential();
229 refreshCredentialIfNeeded(credential);
230 if (credential != null && credential.getAccessToken() != null){
231 return credential;
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());
246 } finally {
247 try {
248 receiver.stop();
249 } catch (VerificationCodeReceiverStopException e) {
250 System.err.println(e.getMessage());
253 return null;
257 * Gets a new credential either from the configured credential store, or with a specific refresh
258 * token.
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())
275 .build();
276 credential.setRefreshToken(refreshTokenOverride);
278 return credential;
282 * Open a browser at the given URL.
283 * <p>
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
288 * default value).
289 * </p>
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)) {
297 try {
298 desktop.browse(URI.create(url));
299 return;
300 } catch (IOException e) {
304 try {
305 Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + url);
306 return;
307 } catch (IOException e) {
309 if (BROWSER != null) {
310 try {
311 Runtime.getRuntime().exec(new String[] {BROWSER, url});
312 return;
313 } catch (IOException e) {
316 System.out.format("Please open the following URL in your browser:%n %s%n", url);