diff options
author | kelvinp@chromium.org <kelvinp@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-18 14:45:23 +0000 |
---|---|---|
committer | kelvinp@chromium.org <kelvinp@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-18 14:45:23 +0000 |
commit | f9bc59949fda0ed4cb7bd77e9a34fbb8791ce48b (patch) | |
tree | 0ec0b827477dc27deec24c73cde9f7977b6eee8c /remoting | |
parent | 8b5eae469493b2a1141d010c9ae62da0129ced3b (diff) | |
download | chromium_src-f9bc59949fda0ed4cb7bd77e9a34fbb8791ce48b.zip chromium_src-f9bc59949fda0ed4cb7bd77e9a34fbb8791ce48b.tar.gz chromium_src-f9bc59949fda0ed4cb7bd77e9a34fbb8791ce48b.tar.bz2 |
Third Party Authentication for Android Part III - Android OAuth2 implicit flow
This change implements the OAuth2 implicit flow to fetch a third party token from the user.
1. Introduces a class ThirdPartyTokenFetcher to pop up a third party login page located at |tokenurl| in the browser.
2. Introduces a class OAuthRedirectActivity, which has an intent filter declared in the manifest to intercept the redirect URI upon a successful login.
3. It then starts the chromoting activity, which uses the ThirdPartyTokenFetcher to extract the code and token from the URI and pass it into the native component using JNI.
BUG=329109
Review URL: https://codereview.chromium.org/337013002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@278062 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'remoting')
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); } |