1.9.30 sync.
[gae.git] / java / src / main / com / google / appengine / tools / admin / OAuth2Native.java
blobcdee84948961dbdf015595f4fd4cd73d278c79af
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 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;
65 /**
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);
82 /**
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}
86 * otherwise
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
89 * provided.
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
92 * be provided.
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;
112 try {
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());
120 } else {
121 throw e;
126 public OAuth2Native(VerificationCodeReceiver receiver, String userId,
127 GoogleAuthorizationCodeFlow flow) {
128 this.receiver = receiver;
129 this.userId = userId;
130 this.flow = flow;
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)
165 throws IOException {
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();
175 } else {
176 flow = new GoogleAuthorizationCodeFlow.Builder(
177 httpTransport, jsonFactory, clientSecrets, scopes).setAccessType("online")
178 .setApprovalPrompt("auto").build();
180 return flow;
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);
213 return true;
216 return false;
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(){
225 if (flow == null) {
226 return null;
228 try {
229 String redirectUri = receiver.getRedirectUri();
231 final Credential credential = getCredential();
232 refreshCredentialIfNeeded(credential);
233 if (credential != null && credential.getAccessToken() != null){
234 return credential;
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());
249 } finally {
250 try {
251 receiver.stop();
252 } catch (VerificationCodeReceiverStopException e) {
253 System.err.println(e.getMessage());
256 return null;
260 * Gets a new credential either from the configured credential store, or with a specific refresh
261 * token.
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())
278 .build();
279 credential.setRefreshToken(refreshTokenOverride);
281 return credential;
285 * Open a browser at the given URL.
286 * <p>
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
291 * default value).
292 * </p>
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)) {
300 try {
301 desktop.browse(URI.create(url));
302 return;
303 } catch (IOException e) {
307 try {
308 Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + url);
309 return;
310 } catch (IOException e) {
312 if (BROWSER != null) {
313 try {
314 Runtime.getRuntime().exec(new String[] {BROWSER, url});
315 return;
316 } catch (IOException e) {
319 System.out.format("Please open the following URL in your browser:%n %s%n", url);