diff options
Diffstat (limited to 'remoting/android')
4 files changed, 286 insertions, 8 deletions
diff --git a/remoting/android/java/AndroidManifest.xml.jinja2 b/remoting/android/java/AndroidManifest.xml.jinja2 index 7521f52..5f2af0a 100644 --- a/remoting/android/java/AndroidManifest.xml.jinja2 +++ b/remoting/android/java/AndroidManifest.xml.jinja2 @@ -12,12 +12,25 @@ android:theme="@android:style/Theme.Holo"> <activity android:name="org.chromium.chromoting.Chromoting" android:configChanges="orientation|screenSize" - android:theme="@style/MainTheme"> + android:theme="@style/MainTheme" + android:launchMode="singleTask"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> + <activity + android:name="org.chromium.chromoting.ThirdPartyTokenFetcher$OAuthRedirectActivity" + android:enabled="false" + android:noHistory="true"> + <intent-filter> + <action android:name="android.intent.action.VIEW"/> + <category android:name="android.intent.category.DEFAULT"/> + <category android:name="android.intent.category.BROWSABLE"/> + <data android:scheme="{{ APK_PACKAGE_NAME }}"/> + <data android:host="oauthredirect"/> + </intent-filter> + </activity> <activity android:name="org.chromium.chromoting.Desktop" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize"/> diff --git a/remoting/android/java/src/org/chromium/chromoting/Chromoting.java b/remoting/android/java/src/org/chromium/chromoting/Chromoting.java index 29aec73..42d6603 100644 --- a/remoting/android/java/src/org/chromium/chromoting/Chromoting.java +++ b/remoting/android/java/src/org/chromium/chromoting/Chromoting.java @@ -86,6 +86,9 @@ public class Chromoting extends Activity implements JniInterface.ConnectionListe /** Dialog for reporting connection progress. */ private ProgressDialog mProgressIndicator; + /** Object for fetching OAuth2 access tokens from third party authorization servers. */ + private ThirdPartyTokenFetcher mTokenFetcher; + /** * This is set when receiving an authentication error from the HostListLoader. If that occurs, * this flag is set and a fresh authentication token is fetched from the AccountsService, and @@ -162,6 +165,15 @@ public class Chromoting extends Activity implements JniInterface.ConnectionListe JniInterface.loadLibrary(this); } + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + if (mTokenFetcher != null) { + if (mTokenFetcher.handleTokenFetched(intent)) { + mTokenFetcher = null; + } + } + } /** * Called when the activity becomes visible. This happens on initial launch and whenever the * user switches to the activity, for example, by using the window-switcher or when coming from @@ -446,4 +458,28 @@ public class Chromoting extends Activity implements JniInterface.ConnectionListe mProgressIndicator = null; } } + + public void fetchThirdPartyToken(String tokenUrl, String clientId, String scope) { + assert mTokenFetcher == null; + + ThirdPartyTokenFetcher.Callback callback = new ThirdPartyTokenFetcher.Callback() { + public void onTokenFetched(String code, String accessToken) { + // The native client sends the OAuth authorization code to the host as the token so + // that the host can obtain the shared secret from the third party authorization + // server. + String token = code; + + // The native client uses the OAuth access token as the shared secret to + // authenticate itself with the host using spake. + String sharedSecret = accessToken; + + JniInterface.nativeOnThirdPartyTokenFetched(token, sharedSecret); + } + }; + + mTokenFetcher = new ThirdPartyTokenFetcher(this, tokenUrl, clientId, scope, callback); + mTokenFetcher.fetchToken(); + } + + } 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 0000000..a00038b --- /dev/null +++ b/remoting/android/java/src/org/chromium/chromoting/ThirdPartyTokenFetcher.java @@ -0,0 +1,225 @@ +// 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.ActivityNotFoundException; +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_HOST = "oauthredirect"; + + /** + * 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 of 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; + + private final String mRedirectUriScheme; + + private final String mRedirectUri; + + 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; + + this.mRedirectUriScheme = context.getApplicationContext().getPackageName(); + this.mRedirectUri = mRedirectUriScheme + "://" + REDIRECT_URI_HOST; + } + + public void fetchToken() { + Uri.Builder uriBuilder = Uri.parse(mTokenUrl).buildUpon(); + uriBuilder.appendQueryParameter("redirect_uri", this.mRedirectUri); + 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); + Log.i("ThirdPartyAuth", "fetchToken() url:" + uri); + OAuthRedirectActivity.setEnabled(mContext, true); + + try { + mContext.startActivity(intent); + } catch (ActivityNotFoundException e) { + failFetchToken("No browser is installed to open the third party authentication page."); + } + } + + private boolean isValidIntent(Intent intent) { + assert intent != null; + + String action = intent.getAction(); + + Uri data = intent.getData(); + if (data != null) { + return Intent.ACTION_VIEW.equals(action) && + this.mRedirectUriScheme.equals(data.getScheme()) && + REDIRECT_URI_HOST.equals(data.getHost()); + } + 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); + + String accessToken = params.get("access_token"); + String code = params.get("code"); + String state = params.get("state"); + + if (!mState.equals(state)) { + failFetchToken("Ignoring redirect with invalid state."); + return false; + } + + if (code == null || accessToken == null) { + failFetchToken("Ignoring redirect with missing code or token."); + return false; + } + + Log.i("ThirdPartyAuth", "handleTokenFetched()."); + mCallback.onTokenFetched(code, accessToken); + OAuthRedirectActivity.setEnabled(mContext, false); + return true; + } + + private void failFetchToken(String errorMessage) { + Log.e("ThirdPartyAuth", errorMessage); + mCallback.onTokenFetched("", ""); + OAuthRedirectActivity.setEnabled(mContext, false); + } + + /** 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>(); + + 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, reload 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 enabledState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED + : PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; + ComponentName component = new ComponentName( + context.getApplicationContext(), + ThirdPartyTokenFetcher.OAuthRedirectActivity.class); + context.getPackageManager().setComponentEnabledSetting( + component, + enabledState, + PackageManager.DONT_KILL_APP); + } + } +} diff --git a/remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java b/remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java index 81e94cc..6691e4f 100644 --- a/remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java +++ b/remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java @@ -21,6 +21,7 @@ import android.widget.TextView; import org.chromium.base.CalledByNative; import org.chromium.base.JNINamespace; +import org.chromium.chromoting.Chromoting; import org.chromium.chromoting.R; import java.nio.ByteBuffer; @@ -434,16 +435,19 @@ public class JniInterface { /** Returns the current cursor shape. Called on the graphics thread. */ public static Bitmap getCursorBitmap() { return sCursorBitmap; } - /** - * Third Party Authentication - */ - /** Pops up a third party login page to fetch the token required for authentication.*/ + // + // Third Party Authentication + // + + /** Pops up a third party login page to fetch the token required for authentication. */ @CalledByNative public static void fetchThirdPartyToken(String tokenUrl, String clientId, String scope) { - // TODO(kelvinp): Create a intent to fetch the token from the browser - // (Android Third Party Auth - Part III) + Chromoting app = (Chromoting) sContext; + app.fetchThirdPartyToken(tokenUrl, clientId, scope); } - /* Notify the native code to continue authentication with the |token| and the |sharedSecret| */ + /** + * Notify the native code to continue authentication with the |token| and the |sharedSecret|. + */ public static native void nativeOnThirdPartyTokenFetched(String token, String sharedSecret); } |