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
;
19 import java
.io
.IOException
;
21 import java
.util
.Arrays
;
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.
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.
33 public class OAuth2Native
{
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;
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);
62 flow
= getAuthorizationCodeFlow(usePersistedCredentials
);
63 } catch (IOException e
) {
67 public OAuth2Native(VerificationCodeReceiver receiver
,
68 String userId
, GoogleAuthorizationCodeFlow flow
) {
69 this.receiver
= receiver
;
74 /** Returns the Google client secrets. */
75 public GoogleClientSecrets
getClientSecrets() {
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
);
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
);
96 * Returns an authorization code flow.
98 * @param usePersistedCredential boolean: wheter or not to persist the credentials
100 protected GoogleAuthorizationCodeFlow
getAuthorizationCodeFlow(boolean usePersistedCredentials
)
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();
112 flow
= new GoogleAuthorizationCodeFlow
.Builder(
113 httpTransport
, jsonFactory
, clientSecrets
, scopes
).setAccessType("online")
114 .setApprovalPrompt("auto").build();
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
);
144 * Authorizes the installed application to access user's protected data.
145 * Returns a credential with the accesToken or null if no authorization token
148 public Credential
authorize(){
153 String redirectUri
= receiver
.getRedirectUri();
155 Credential credential
= flow
.loadCredential(userId
);
156 refreshCredentialIfNeeded(credential
);
157 if (credential
!= null && credential
.getAccessToken() != null){
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());
173 } catch (VerificationCodeReceiverStopException e
) {
174 System
.err
.println(e
.getMessage());
181 * Open a browser at the given URL.
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
190 protected void browse(String url
) {
191 if (Desktop
.isDesktopSupported()) {
192 Desktop desktop
= Desktop
.getDesktop();
193 if (desktop
.isSupported(Action
.BROWSE
)) {
195 desktop
.browse(URI
.create(url
));
197 } catch (IOException e
) {
202 Runtime
.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + url
);
204 } catch (IOException e
) {
206 if (BROWSER
!= null) {
208 Runtime
.getRuntime().exec(new String
[] {BROWSER
, url
});
210 } catch (IOException e
) {
213 System
.out
.println("Please open the following URL in your browser:");
214 System
.out
.println(" " + url
);