Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / admin / OAuth2Native.java
blob3796d10ec6744580aa13a0d1fd554585572535c8
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;
25 /**
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.
29 * <p>
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.
32 * </p>
35 public class OAuth2Native {
36 /**
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;
61 /**
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);
78 /**
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}
82 * otherwise
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
85 * provided.
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
88 * be provided.
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;
108 try {
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());
116 } else {
117 throw e;
122 public OAuth2Native(VerificationCodeReceiver receiver, String userId,
123 GoogleAuthorizationCodeFlow flow) {
124 this.receiver = receiver;
125 this.userId = userId;
126 this.flow = flow;
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)
161 throws IOException {
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();
171 } else {
172 flow = new GoogleAuthorizationCodeFlow.Builder(
173 httpTransport, jsonFactory, clientSecrets, scopes).setAccessType("online")
174 .setApprovalPrompt("auto").build();
176 return flow;
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);
209 return true;
212 return false;
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(){
221 if (flow == null) {
222 return null;
224 try {
225 String redirectUri = receiver.getRedirectUri();
227 final Credential credential = getCredential();
228 refreshCredentialIfNeeded(credential);
229 if (credential != null && credential.getAccessToken() != null){
230 return credential;
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());
245 } finally {
246 try {
247 receiver.stop();
248 } catch (VerificationCodeReceiverStopException e) {
249 System.err.println(e.getMessage());
252 return null;
256 * Gets a new credential either from the configured credential store, or with a specific refresh
257 * token.
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())
274 .build();
275 credential.setRefreshToken(refreshTokenOverride);
277 return credential;
281 * Open a browser at the given URL.
282 * <p>
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
287 * default value).
288 * </p>
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)) {
296 try {
297 desktop.browse(URI.create(url));
298 return;
299 } catch (IOException e) {
303 try {
304 Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + url);
305 return;
306 } catch (IOException e) {
308 if (BROWSER != null) {
309 try {
310 Runtime.getRuntime().exec(new String[] {BROWSER, url});
311 return;
312 } catch (IOException e) {
315 System.out.format("Please open the following URL in your browser:%n %s%n", url);