Index: remoting/android/java/src/org/chromium/chromoting/ThirdPartyTokenFetcher.java |
diff --git a/remoting/android/java/src/org/chromium/chromoting/ThirdPartyTokenFetcher.java b/remoting/android/java/src/org/chromium/chromoting/ThirdPartyTokenFetcher.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..5624a233dc2a04d43f5b07d11c8de88749210796 |
--- /dev/null |
+++ b/remoting/android/java/src/org/chromium/chromoting/ThirdPartyTokenFetcher.java |
@@ -0,0 +1,223 @@ |
+// Copyright 2014 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.chromoting; |
+ |
+import android.app.Activity; |
+import android.content.ComponentName; |
+import android.content.Intent; |
+import android.content.pm.PackageManager; |
+import android.net.Uri; |
+import android.util.Base64; |
+import android.util.Log; |
+ |
+import java.security.SecureRandom; |
+import java.util.HashMap; |
+ |
+/** |
+ * This class is responsible for fetching a third party token from the user using the OAuth2 |
+ * implicit flow. It pops up a third party login page located at |tokenurl|. It relies on the |
+ * |ThirdPartyTokenFetcher$OAuthRedirectActivity| to intercept the access token from the redirect at |
+ * |REDIRECT_URI_SCHEME|://|REDIRECT_URI_HOST| upon successful login. |
+ */ |
+public class ThirdPartyTokenFetcher { |
+ /** Callback for receiving the token. */ |
+ public interface Callback { |
+ void onTokenFetched(String code, String accessToken); |
+ } |
+ |
+ /** Redirect Uri. See http://tools.ietf.org/html/rfc6749#section-3.1.2. */ |
+ private static final String REDIRECT_URI_SCHEME = "chromoting"; |
+ |
+ private static final String REDIRECT_URI_HOST = "oauthredirect"; |
+ |
+ private static final String REDIRECT_URI = REDIRECT_URI_SCHEME + "://" + REDIRECT_URI_HOST; |
+ |
+ /** |
+ * Request both the authorization code and access token from the server. See |
+ * http://tools.ietf.org/html/rfc6749#section-3.1.1. |
+ */ |
+ private static final String RESPONSE_TYPE = "code token"; |
+ |
+ /** This is used to securely generate an opaque 128 bit for the |mState| variable. */ |
+ private static SecureRandom sSecureRandom = new SecureRandom(); |
+ |
+ /** This is used to launch the third party login page in the browser. */ |
+ private Activity mContext; |
+ |
+ /** |
+ * An opaque value used by the client to maintain state between the request and callback. The |
+ * authorization server includes this value when redirecting the user-agent back to the client. |
+ * The parameter is used for preventing cross-site request forgery. See |
+ * http://tools.ietf.org/html/rfc6749#section-10.12. |
+ */ |
+ private final String mState; |
+ |
+ /** Url to pop the third party login page. */ |
+ private final String mTokenUrl; |
+ |
+ /** The client identifier. See http://tools.ietf.org/html/rfc6749#section-2.2. */ |
+ private final String mClientId; |
+ |
+ /** The scope of access request. See http://tools.ietf.org/html/rfc6749#section-3.3. */ |
+ private final String mScope; |
+ |
+ private final Callback mCallback; |
+ |
+ public ThirdPartyTokenFetcher(Activity context, |
+ String tokenUrl, |
+ String clientId, |
+ String scope, |
+ Callback callback) { |
+ this.mContext = context; |
+ this.mTokenUrl = tokenUrl; |
+ this.mClientId = clientId; |
+ this.mState = generateXsrfToken(); |
+ this.mScope = scope; |
+ this.mCallback = callback; |
+ } |
+ |
+ public void fetchToken() { |
+ Uri.Builder uriBuilder = Uri.parse(mTokenUrl).buildUpon(); |
+ uriBuilder.appendQueryParameter("redirect_uri", REDIRECT_URI); |
+ uriBuilder.appendQueryParameter("scope", mScope); |
+ uriBuilder.appendQueryParameter("client_id", mClientId); |
+ uriBuilder.appendQueryParameter("state", mState); |
+ uriBuilder.appendQueryParameter("response_type", RESPONSE_TYPE); |
+ |
+ Uri uri = uriBuilder.build(); |
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri); |
Sergey Ulanov
2014/06/16 22:55:38
Does this pop out external browser?
If yes, then I
kelvinp
2014/06/17 00:02:57
Spoke offline with Sergey and Renato. We are usin
|
+ |
+ // Verify that the device can launch an application for this intent, otherwise |
+ // startActivity() may crash the application. |
Sergey Ulanov
2014/06/16 22:55:38
Can we just catch exception thrown by startActivit
kelvinp
2014/06/17 00:02:56
Good point.
|
+ if (null == intent.resolveActivity(mContext.getPackageManager())) { |
+ Log.e("ThirdPartyAuth", "No browser is installed to open " + uri); |
+ this.failFetchToken(); |
+ return; |
+ } |
+ Log.i("ThirdPartyAuth", "fetchToken() url:" + uri); |
+ OAuthRedirectActivity.setEnabled(mContext, true); |
+ mContext.startActivity(intent); |
+ } |
+ |
+ private boolean isValidIntent(Intent intent) { |
+ assert intent != null; |
+ |
+ final String action = intent.getAction(); |
+ |
+ Uri data = intent.getData(); |
+ if (data != null) { |
+ String scheme = data.getScheme(); |
+ String host = data.getHost(); |
Sergey Ulanov
2014/06/16 22:55:38
I think you want getAuthority here, to also verify
kelvinp
2014/06/17 00:02:56
Done for the extra variables.
I kept the call to g
|
+ return Intent.ACTION_VIEW.equals(action) && |
+ REDIRECT_URI_SCHEME.equals(scheme) && |
+ REDIRECT_URI_HOST.equals(host); |
+ } |
+ return false; |
+ } |
+ |
+ public boolean handleTokenFetched(Intent intent) { |
+ assert intent != null; |
+ |
+ if (!isValidIntent(intent)) { |
+ Log.w("ThirdPartyAuth", "Ignoring unmatched intent."); |
+ return false; |
+ } |
+ |
+ Uri data = intent.getData(); |
+ HashMap<String, String> params = getFragmentParameters(data); |
+ |
+ final String accessToken = params.get("access_token"); |
+ final String code = params.get("code"); |
+ final String state = params.get("state"); |
+ |
+ if (!mState.equals(state)) { |
+ Log.e("ThirdPartyAuth", "Ignoring redirect with invalid state."); |
+ return false; |
+ } |
+ |
+ if (code == null || accessToken == null) { |
+ Log.e("ThirdPartyAuth", "Ignoring redirect with missing code or token."); |
+ return false; |
rmsousa
2014/06/16 22:13:15
This would ignore explicit errors (where the error
kelvinp
2014/06/17 00:02:56
Done.
|
+ } |
+ |
+ Log.i("ThirdPartyAuth", "handleTokenFetched()."); |
+ mCallback.onTokenFetched(code, accessToken); |
+ OAuthRedirectActivity.setEnabled(mContext, false); |
+ return true; |
+ } |
+ |
+ private void failFetchToken() { |
+ mCallback.onTokenFetched("", ""); |
+ } |
+ |
+ /** Generate a 128 bit URL-safe opaque string to prevent cross site request forgery (XSRF).*/ |
+ private static String generateXsrfToken() { |
+ byte[] bytes = new byte[16]; |
+ sSecureRandom.nextBytes(bytes); |
+ // Uses a variant of Base64 to make sure the URL is URL safe: |
+ // URL_SAFE replaces - with _ and + with /. |
+ // NO_WRAP removes the trailing newline character. |
+ // NO_PADDING removes any trailing =. |
+ return Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); |
+ } |
+ |
+ /** Parses the fragment string into a key value pair. */ |
+ private static HashMap<String, String> getFragmentParameters(Uri uri) { |
+ assert uri != null; |
+ HashMap<String, String> result = new HashMap<String, String>(); |
+ |
+ final String fragment = uri.getFragment(); |
+ |
+ if (fragment != null) { |
+ String[] parts = fragment.split("&"); |
+ |
+ for (String part : parts) { |
+ String keyValuePair[] = part.split("=", 2); |
+ if (keyValuePair.length == 2) { |
+ result.put(keyValuePair[0], keyValuePair[1]); |
+ } |
+ } |
+ } |
+ return result; |
+ }; |
+ |
+ /** |
+ * In the OAuth2 implicit flow, the browser will be redirected to |
+ * |REDIRECT_URI_SCHEME|://|REDIRECT_URI_HOST| upon a successful login. OAuthRedirectActivity |
+ * uses an intent filter in the manifest to intercept the URL and launch the chromoting app. |
+ * |
+ * Unfortunately, Most browsers on Android, e.g. chrome, reloads the URL when a browser |
+ * tab is activated. As a result, chromoting is launched unintentionally when the user restarts |
+ * chrome or closes other tabs that causes the |redirect URL| to become the topmost tab. |
+ * |
+ * To solve the problem, the redirect intent-filter is declared in a separate activity, |
+ * |OAuthRedirectActivity| instead of the MainActivity. In this way, we can disable it, |
+ * together with its intent filter, by default. |OAuthRedirectActivity| is only enabled when |
+ * there is a pending token fetch request. |
+ */ |
+ public static class OAuthRedirectActivity extends Activity { |
+ @Override |
+ public void onStart() { |
+ super.onStart(); |
+ // |OAuthRedirectActivity| runs in its own task, it needs to route the intent back |
+ // to Chromoting.java to access the state of the current request. |
+ Intent intent = getIntent(); |
+ intent.setClass(this, Chromoting.class); |
+ startActivity(intent); |
+ finishActivity(0); |
+ } |
+ |
+ public static void setEnabled(Activity context, boolean enabled) { |
+ int state = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED |
+ : PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; |
+ context.getPackageManager().setComponentEnabledSetting( |
+ new ComponentName( |
+ context.getApplicationContext(), |
+ ThirdPartyTokenFetcher.OAuthRedirectActivity.class), |
+ state, |
+ PackageManager.DONT_KILL_APP); |
+ } |
+ } |
+} |