Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / admin / OAuth2Native.java
blob4f4ad4f725a49f9abf7ace9b95c910a23f607536
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.extensions.java6.auth.oauth2.FileCredentialStore;
8 import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
9 import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
10 import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse;
11 import com.google.api.client.http.HttpTransport;
12 import com.google.api.client.http.javanet.NetHttpTransport;
13 import com.google.api.client.json.JsonFactory;
14 import com.google.api.client.json.jackson.JacksonFactory;
16 import java.awt.Desktop;
17 import java.awt.Desktop.Action;
18 import java.io.File;
19 import java.io.IOException;
20 import java.net.URI;
21 import java.util.Arrays;
23 /**
24 * Implements OAuth authentication "native" flow recommended for installed clients in which the end
25 * user must grant access in a web browser and then copy a code into the application.
27 * <p>
28 * Warning: the client ID and secret are not secured and are plainly visible to users.
29 * It is a hard problem to secure client credentials in installed applications.
30 * </p>
33 public class OAuth2Native {
34 /**
35 * Browser to open in case {@link Desktop#isDesktopSupported()} is {@code false} or {@code null}
36 * to prompt user to open the URL in their favorite browser.
38 private static final String BROWSER = "google-chrome";
40 protected static final String OAUTH2_CLIENT_ID = "550516889912.apps.googleusercontent.com";
41 protected static final String OAUTH2_CLIENT_SECRET = "ykPq-0UYfKNprLRjVx1hBBar";
42 protected static final String OAUTH2_SCOPE = "https://www.googleapis.com/auth/appengine.admin";
43 protected static final long MIN_EXPIRES_SECONDS = 300;
44 /**
45 * Token store filename.
47 protected static final String TOKEN_STORE_BASE = ".appcfg_oauth2_tokens_java";
49 /** Google client secrets. */
50 private static GoogleClientSecrets clientSecrets = new GoogleClientSecrets()
51 .setInstalled(new GoogleClientSecrets.Details()
52 .setClientId(OAUTH2_CLIENT_ID).setClientSecret(OAUTH2_CLIENT_SECRET));
54 private GoogleAuthorizationCodeFlow flow;
55 private final VerificationCodeReceiver receiver;
56 private final String userId;
58 public OAuth2Native(boolean usePersistedCredentials) {
59 this(new PromptReceiver(), System.getProperty("user.name"), null);
61 try {
62 flow = getAuthorizationCodeFlow(usePersistedCredentials);
63 } catch (IOException e) {
67 public OAuth2Native(VerificationCodeReceiver receiver,
68 String userId, GoogleAuthorizationCodeFlow flow) {
69 this.receiver = receiver;
70 this.userId = userId;
71 this.flow = flow;
74 /** Returns the Google client secrets. */
75 public GoogleClientSecrets getClientSecrets() {
76 return clientSecrets;
79 /**
80 * Returns a File representing the token store file name in the user's home directory.
82 protected File getTokenStoreFile() {
83 String userDir = System.getProperty("user.home");
84 return new File(userDir, TOKEN_STORE_BASE);
87 /**
88 * Returns the CredentialStore to be used when calling
89 * {@link OAuth2Native#getAuthorizationCodeFlow()} with parameter {@code true}.
91 protected CredentialStore getCredentialStore(JsonFactory jsonFactory) throws IOException {
92 return new FileCredentialStore(getTokenStoreFile(), jsonFactory);
95 /**
96 * Returns an authorization code flow.
98 * @param usePersistedCredential boolean: wheter or not to persist the credentials
100 protected GoogleAuthorizationCodeFlow getAuthorizationCodeFlow(boolean usePersistedCredentials)
101 throws IOException {
102 HttpTransport httpTransport = new NetHttpTransport();
103 JsonFactory jsonFactory = new JacksonFactory();
104 Iterable<String> scopes = Arrays.asList(OAUTH2_SCOPE);
105 GoogleClientSecrets clientSecrets = getClientSecrets();
106 GoogleAuthorizationCodeFlow flow;
107 if (usePersistedCredentials) {
108 flow = new GoogleAuthorizationCodeFlow.Builder(
109 httpTransport, jsonFactory, clientSecrets, scopes).setAccessType("offline")
110 .setApprovalPrompt("force").setCredentialStore(getCredentialStore(jsonFactory)).build();
111 } else {
112 flow = new GoogleAuthorizationCodeFlow.Builder(
113 httpTransport, jsonFactory, clientSecrets, scopes).setAccessType("online")
114 .setApprovalPrompt("auto").build();
116 return flow;
120 * Calls the method refreshToken if there is no access token or if the token is close to expire.
121 * Returns true if refreshToken was called. If the token was refreshed and credentialStore is not
122 * null, it saves the updated credential.
124 * @param credential Credential the credential to check
126 protected boolean refreshCredentialIfNeeded(Credential credential) throws IOException {
127 if (credential != null) {
128 Long expiresInSeconds = credential.getExpiresInSeconds();
129 if (credential.getAccessToken() == null || expiresInSeconds == null ||
130 expiresInSeconds < MIN_EXPIRES_SECONDS) {
131 credential.refreshToken();
133 if (flow.getCredentialStore() != null) {
134 flow.getCredentialStore().store(userId, credential);
137 return true;
140 return false;
144 * Authorizes the installed application to access user's protected data.
145 * Returns a credential with the accesToken or null if no authorization token
146 * could be obtained.
148 public Credential authorize(){
149 if (flow == null) {
150 return null;
152 try {
153 String redirectUri = receiver.getRedirectUri();
155 Credential credential = flow.loadCredential(userId);
156 refreshCredentialIfNeeded(credential);
157 if (credential != null && credential.getAccessToken() != null){
158 return credential;
161 browse(flow.newAuthorizationUrl().setRedirectUri(redirectUri).build());
162 String code = receiver.waitForCode();
163 GoogleTokenResponse response =
164 flow.newTokenRequest(code).setRedirectUri(redirectUri).execute();
165 return flow.createAndStoreCredential(response, userId);
166 } catch (IOException e) {
167 System.err.println(e.getMessage());
168 } catch (VerificationCodeReceiverRedirectUriException e) {
169 System.err.println(e.getMessage());
170 } finally {
171 try {
172 receiver.stop();
173 } catch (VerificationCodeReceiverStopException e) {
174 System.err.println(e.getMessage());
177 return null;
181 * Open a browser at the given URL.
182 * <p>
183 * It attempts to open the browser using {@link Desktop#isDesktopSupported()}.
184 * If that fails, on Windows it tries {@code rundll32}. If that fails, it opens the browser
185 * specified in {@link #BROWSER}.
186 * Note though that currently we've only tested this code with Google Chrome (hence this is the
187 * default value).
188 * </p>
190 protected void browse(String url) {
191 if (Desktop.isDesktopSupported()) {
192 Desktop desktop = Desktop.getDesktop();
193 if (desktop.isSupported(Action.BROWSE)) {
194 try {
195 desktop.browse(URI.create(url));
196 return;
197 } catch (IOException e) {
201 try {
202 Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + url);
203 return;
204 } catch (IOException e) {
206 if (BROWSER != null) {
207 try {
208 Runtime.getRuntime().exec(new String[] {BROWSER, url});
209 return;
210 } catch (IOException e) {
213 System.out.println("Please open the following URL in your browser:");
214 System.out.println(" " + url);