summaryrefslogtreecommitdiffstats
path: root/remoting/android
diff options
context:
space:
mode:
Diffstat (limited to 'remoting/android')
-rw-r--r--remoting/android/java/AndroidManifest.xml.jinja215
-rw-r--r--remoting/android/java/src/org/chromium/chromoting/Chromoting.java36
-rw-r--r--remoting/android/java/src/org/chromium/chromoting/ThirdPartyTokenFetcher.java225
-rw-r--r--remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java18
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);
}