Commit f9bc5994 authored by kelvinp@chromium.org's avatar kelvinp@chromium.org

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
parent 8b5eae46
...@@ -12,12 +12,25 @@ ...@@ -12,12 +12,25 @@
android:theme="@android:style/Theme.Holo"> android:theme="@android:style/Theme.Holo">
<activity android:name="org.chromium.chromoting.Chromoting" <activity android:name="org.chromium.chromoting.Chromoting"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
android:theme="@style/MainTheme"> android:theme="@style/MainTheme"
android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </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" <activity android:name="org.chromium.chromoting.Desktop"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
android:windowSoftInputMode="adjustResize"/> android:windowSoftInputMode="adjustResize"/>
......
...@@ -86,6 +86,9 @@ public class Chromoting extends Activity implements JniInterface.ConnectionListe ...@@ -86,6 +86,9 @@ public class Chromoting extends Activity implements JniInterface.ConnectionListe
/** Dialog for reporting connection progress. */ /** Dialog for reporting connection progress. */
private ProgressDialog mProgressIndicator; 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 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 * 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 ...@@ -162,6 +165,15 @@ public class Chromoting extends Activity implements JniInterface.ConnectionListe
JniInterface.loadLibrary(this); 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 * 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 * 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 ...@@ -446,4 +458,28 @@ public class Chromoting extends Activity implements JniInterface.ConnectionListe
mProgressIndicator = null; 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();
}
} }
// 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);
}
}
}
...@@ -21,6 +21,7 @@ import android.widget.TextView; ...@@ -21,6 +21,7 @@ import android.widget.TextView;
import org.chromium.base.CalledByNative; import org.chromium.base.CalledByNative;
import org.chromium.base.JNINamespace; import org.chromium.base.JNINamespace;
import org.chromium.chromoting.Chromoting;
import org.chromium.chromoting.R; import org.chromium.chromoting.R;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
...@@ -434,16 +435,19 @@ public class JniInterface { ...@@ -434,16 +435,19 @@ public class JniInterface {
/** Returns the current cursor shape. Called on the graphics thread. */ /** Returns the current cursor shape. Called on the graphics thread. */
public static Bitmap getCursorBitmap() { return sCursorBitmap; } public static Bitmap getCursorBitmap() { return sCursorBitmap; }
/** //
* Third Party Authentication // Third Party Authentication
*/ //
/** Pops up a third party login page to fetch the token required for authentication.*/
/** Pops up a third party login page to fetch the token required for authentication. */
@CalledByNative @CalledByNative
public static void fetchThirdPartyToken(String tokenUrl, String clientId, String scope) { public static void fetchThirdPartyToken(String tokenUrl, String clientId, String scope) {
// TODO(kelvinp): Create a intent to fetch the token from the browser Chromoting app = (Chromoting) sContext;
// (Android Third Party Auth - Part III) 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); public static native void nativeOnThirdPartyTokenFetched(String token, String sharedSecret);
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment