Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / remoting / android / java / src / org / chromium / chromoting / ThirdPartyTokenFetcher.java
blobfd983f79f532952f6623d6d12235aa3df3255d0c
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.chromoting;
7 import android.annotation.SuppressLint;
8 import android.app.Activity;
9 import android.content.ActivityNotFoundException;
10 import android.content.ComponentName;
11 import android.content.Intent;
12 import android.content.pm.PackageManager;
13 import android.net.Uri;
14 import android.support.v7.app.ActionBarActivity;
15 import android.text.TextUtils;
16 import android.util.Base64;
17 import android.util.Log;
19 import org.chromium.base.SecureRandomInitializer;
21 import java.io.IOException;
22 import java.security.SecureRandom;
23 import java.util.ArrayList;
25 /**
26 * This class is responsible for fetching a third party token from the user using the OAuth2
27 * implicit flow. It directs the user to a third party login page located at |tokenUrl|. It relies
28 * on the |ThirdPartyTokenFetcher$OAuthRedirectActivity| to intercept the access token from the
29 * redirect at intent://|REDIRECT_URI_PATH|#Intent;...end; upon successful login.
31 public class ThirdPartyTokenFetcher {
32 /** Callback for receiving the token. */
33 public interface Callback {
34 void onTokenFetched(String code, String accessToken);
37 /** The path of the Redirect URI. */
38 private static final String REDIRECT_URI_PATH = "/oauthredirect/";
40 /**
41 * Request both the authorization code and access token from the server. See
42 * http://tools.ietf.org/html/rfc6749#section-3.1.1.
44 private static final String RESPONSE_TYPE = "code token";
46 /** This is used to securely generate an opaque 128 bit for the |mState| variable. */
47 @SuppressLint("TrulyRandom")
48 private static SecureRandom sSecureRandom;
50 // TODO(lambroslambrou): Refactor this class to only initialize a PRNG when ThirdPartyAuth is
51 // actually used.
52 static {
53 sSecureRandom = new SecureRandom();
54 try {
55 SecureRandomInitializer.initialize(sSecureRandom);
56 } catch (IOException e) {
57 throw new RuntimeException("Failed to initialize PRNG: " + e);
61 /** This is used to launch the third party login page in the browser. */
62 private Activity mContext;
64 /**
65 * An opaque value used by the client to maintain state between the request and callback. The
66 * authorization server includes this value when redirecting the user-agent back to the client.
67 * The parameter is used for preventing cross-site request forgery. See
68 * http://tools.ietf.org/html/rfc6749#section-10.12.
70 private final String mState;
72 private final Callback mCallback;
74 /** The list of TokenUrls allowed by the domain. */
75 private final ArrayList<String> mTokenUrlPatterns;
77 private final String mRedirectUriScheme;
79 private final String mRedirectUri;
81 public ThirdPartyTokenFetcher(Activity context,
82 ArrayList<String> tokenUrlPatterns,
83 Callback callback) {
84 this.mContext = context;
85 this.mState = generateXsrfToken();
86 this.mCallback = callback;
87 this.mTokenUrlPatterns = tokenUrlPatterns;
89 this.mRedirectUriScheme = context.getApplicationContext().getPackageName();
91 // We don't follow the OAuth spec (http://tools.ietf.org/html/rfc6749#section-3.1.2) of the
92 // redirect URI as it is possible for the other applications to intercept the redirect URI.
93 // Instead, we use the intent scheme URI, which can restrict a specific package to handle
94 // the intent. See https://developer.chrome.com/multidevice/android/intents.
95 this.mRedirectUri = "intent://" + REDIRECT_URI_PATH + "#Intent;"
96 + "package=" + mRedirectUriScheme + ";"
97 + "scheme=" + mRedirectUriScheme + ";end;";
101 * @param tokenUrl URL of the third party login page.
102 * @param clientId The client identifier. See http://tools.ietf.org/html/rfc6749#section-2.2.
103 * @param scope The scope of access request. See http://tools.ietf.org/html/rfc6749#section-3.3.
105 public void fetchToken(String tokenUrl, String clientId, String scope) {
106 if (!isValidTokenUrl(tokenUrl)) {
107 failFetchToken("Token URL does not match the domain\'s allowed URL patterns."
108 + " URL: " + tokenUrl
109 + ", patterns: " + TextUtils.join(",", this.mTokenUrlPatterns));
110 return;
113 Uri uri = buildRequestUri(tokenUrl, clientId, scope);
114 Intent intent = new Intent(Intent.ACTION_VIEW, uri);
115 Log.i("ThirdPartyAuth", "fetchToken() url:" + uri);
116 OAuthRedirectActivity.setEnabled(mContext, true);
118 try {
119 mContext.startActivity(intent);
120 } catch (ActivityNotFoundException e) {
121 failFetchToken("No browser is installed to open the third party authentication page.");
125 private Uri buildRequestUri(String tokenUrl, String clientId, String scope) {
126 Uri.Builder uriBuilder = Uri.parse(tokenUrl).buildUpon();
127 uriBuilder.appendQueryParameter("redirect_uri", this.mRedirectUri);
128 uriBuilder.appendQueryParameter("scope", scope);
129 uriBuilder.appendQueryParameter("client_id", clientId);
130 uriBuilder.appendQueryParameter("state", mState);
131 uriBuilder.appendQueryParameter("response_type", RESPONSE_TYPE);
133 return uriBuilder.build();
136 /** Verifies the host-supplied URL matches the domain's allowed URL patterns. */
137 private boolean isValidTokenUrl(String tokenUrl) {
138 for (String pattern : mTokenUrlPatterns) {
139 if (tokenUrl.matches(pattern)) {
140 return true;
143 return false;
146 private boolean isValidIntent(Intent intent) {
147 assert intent != null;
149 String action = intent.getAction();
151 Uri data = intent.getData();
152 if (data != null) {
153 return Intent.ACTION_VIEW.equals(action)
154 && this.mRedirectUriScheme.equals(data.getScheme())
155 && REDIRECT_URI_PATH.equals(data.getPath());
157 return false;
160 public boolean handleTokenFetched(Intent intent) {
161 assert intent != null;
163 if (!isValidIntent(intent)) {
164 Log.w("ThirdPartyAuth", "Ignoring unmatched intent.");
165 return false;
168 String accessToken = intent.getStringExtra("access_token");
169 String code = intent.getStringExtra("code");
170 String state = intent.getStringExtra("state");
172 if (!mState.equals(state)) {
173 failFetchToken("Ignoring redirect with invalid state.");
174 return false;
177 if (code == null || accessToken == null) {
178 failFetchToken("Ignoring redirect with missing code or token.");
179 return false;
182 Log.i("ThirdPartyAuth", "handleTokenFetched().");
183 mCallback.onTokenFetched(code, accessToken);
184 OAuthRedirectActivity.setEnabled(mContext, false);
185 return true;
188 private void failFetchToken(String errorMessage) {
189 Log.e("ThirdPartyAuth", errorMessage);
190 mCallback.onTokenFetched("", "");
191 OAuthRedirectActivity.setEnabled(mContext, false);
194 /** Generate a 128 bit URL-safe opaque string to prevent cross site request forgery (XSRF).*/
195 private static String generateXsrfToken() {
196 byte[] bytes = new byte[16];
197 sSecureRandom.nextBytes(bytes);
198 // Uses a variant of Base64 to make sure the URL is URL safe:
199 // URL_SAFE replaces - with _ and + with /.
200 // NO_WRAP removes the trailing newline character.
201 // NO_PADDING removes any trailing =.
202 return Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
206 * In the OAuth2 implicit flow, the browser will be redirected to
207 * intent://|REDIRECT_URI_PATH|#Intent;...end; upon a successful login. OAuthRedirectActivity
208 * uses an intent filter in the manifest to intercept the URL and launch the chromoting app.
210 * Unfortunately, most browsers on Android, e.g. chrome, reload the URL when a browser
211 * tab is activated. As a result, chromoting is launched unintentionally when the user restarts
212 * chrome or closes other tabs that causes the redirect URL to become the topmost tab.
214 * To solve the problem, the redirect intent-filter is declared in a separate activity,
215 * |OAuthRedirectActivity| instead of the MainActivity. In this way, we can disable it,
216 * together with its intent filter, by default. |OAuthRedirectActivity| is only enabled when
217 * there is a pending token fetch request.
219 public static class OAuthRedirectActivity extends ActionBarActivity {
220 @Override
221 public void onStart() {
222 super.onStart();
223 // |OAuthRedirectActivity| runs in its own task, it needs to route the intent back
224 // to Chromoting.java to access the state of the current request.
225 Intent intent = getIntent();
226 intent.setClass(this, Chromoting.class);
227 startActivity(intent);
228 finishActivity(0);
231 public static void setEnabled(Activity context, boolean enabled) {
232 int enabledState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
233 : PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
234 ComponentName component = new ComponentName(
235 context.getApplicationContext(),
236 ThirdPartyTokenFetcher.OAuthRedirectActivity.class);
237 context.getPackageManager().setComponentEnabledSetting(
238 component,
239 enabledState,
240 PackageManager.DONT_KILL_APP);