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
.text
.TextUtils
;
15 import android
.util
.Base64
;
16 import android
.util
.Log
;
18 import org
.chromium
.base
.SecureRandomInitializer
;
20 import java
.io
.IOException
;
21 import java
.security
.SecureRandom
;
22 import java
.util
.ArrayList
;
25 * This class is responsible for fetching a third party token from the user using the OAuth2
26 * implicit flow. It directs the user to a third party login page located at |tokenUrl|. It relies
27 * on the |ThirdPartyTokenFetcher$OAuthRedirectActivity| to intercept the access token from the
28 * redirect at intent://|REDIRECT_URI_PATH|#Intent;...end; upon successful login.
30 public class ThirdPartyTokenFetcher
{
31 /** Callback for receiving the token. */
32 public interface Callback
{
33 void onTokenFetched(String code
, String accessToken
);
36 /** The path of the Redirect URI. */
37 private static final String REDIRECT_URI_PATH
= "/oauthredirect/";
40 * Request both the authorization code and access token from the server. See
41 * http://tools.ietf.org/html/rfc6749#section-3.1.1.
43 private static final String RESPONSE_TYPE
= "code token";
45 /** This is used to securely generate an opaque 128 bit for the |mState| variable. */
46 @SuppressLint("TrulyRandom")
47 private static SecureRandom sSecureRandom
;
49 // TODO(lambroslambrou): Refactor this class to only initialize a PRNG when ThirdPartyAuth is
52 sSecureRandom
= new SecureRandom();
54 SecureRandomInitializer
.initialize(sSecureRandom
);
55 } catch (IOException e
) {
56 throw new RuntimeException("Failed to initialize PRNG: " + e
);
60 /** This is used to launch the third party login page in the browser. */
61 private Activity mContext
;
64 * An opaque value used by the client to maintain state between the request and callback. The
65 * authorization server includes this value when redirecting the user-agent back to the client.
66 * The parameter is used for preventing cross-site request forgery. See
67 * http://tools.ietf.org/html/rfc6749#section-10.12.
69 private final String mState
;
71 private final Callback mCallback
;
73 /** The list of TokenUrls allowed by the domain. */
74 private final ArrayList
<String
> mTokenUrlPatterns
;
76 private final String mRedirectUriScheme
;
78 private final String mRedirectUri
;
80 public ThirdPartyTokenFetcher(Activity context
,
81 ArrayList
<String
> tokenUrlPatterns
,
83 this.mContext
= context
;
84 this.mState
= generateXsrfToken();
85 this.mCallback
= callback
;
86 this.mTokenUrlPatterns
= tokenUrlPatterns
;
88 this.mRedirectUriScheme
= context
.getApplicationContext().getPackageName();
90 // We don't follow the OAuth spec (http://tools.ietf.org/html/rfc6749#section-3.1.2) of the
91 // redirect URI as it is possible for the other applications to intercept the redirect URI.
92 // Instead, we use the intent scheme URI, which can restrict a specific package to handle
93 // the intent. See https://developer.chrome.com/multidevice/android/intents.
94 this.mRedirectUri
= "intent://" + REDIRECT_URI_PATH
+ "#Intent;"
95 + "package=" + mRedirectUriScheme
+ ";"
96 + "scheme=" + mRedirectUriScheme
+ ";end;";
100 * @param tokenUrl URL of the third party login page.
101 * @param clientId The client identifier. See http://tools.ietf.org/html/rfc6749#section-2.2.
102 * @param scope The scope of access request. See http://tools.ietf.org/html/rfc6749#section-3.3.
104 public void fetchToken(String tokenUrl
, String clientId
, String scope
) {
105 if (!isValidTokenUrl(tokenUrl
)) {
106 failFetchToken("Token URL does not match the domain\'s allowed URL patterns."
107 + " URL: " + tokenUrl
108 + ", patterns: " + TextUtils
.join(",", this.mTokenUrlPatterns
));
112 Uri uri
= buildRequestUri(tokenUrl
, clientId
, scope
);
113 Intent intent
= new Intent(Intent
.ACTION_VIEW
, uri
);
114 Log
.i("ThirdPartyAuth", "fetchToken() url:" + uri
);
115 OAuthRedirectActivity
.setEnabled(mContext
, true);
118 mContext
.startActivity(intent
);
119 } catch (ActivityNotFoundException e
) {
120 failFetchToken("No browser is installed to open the third party authentication page.");
124 private Uri
buildRequestUri(String tokenUrl
, String clientId
, String scope
) {
125 Uri
.Builder uriBuilder
= Uri
.parse(tokenUrl
).buildUpon();
126 uriBuilder
.appendQueryParameter("redirect_uri", this.mRedirectUri
);
127 uriBuilder
.appendQueryParameter("scope", scope
);
128 uriBuilder
.appendQueryParameter("client_id", clientId
);
129 uriBuilder
.appendQueryParameter("state", mState
);
130 uriBuilder
.appendQueryParameter("response_type", RESPONSE_TYPE
);
132 return uriBuilder
.build();
135 /** Verifies the host-supplied URL matches the domain's allowed URL patterns. */
136 private boolean isValidTokenUrl(String tokenUrl
) {
137 for (String pattern
: mTokenUrlPatterns
) {
138 if (tokenUrl
.matches(pattern
)) {
145 private boolean isValidIntent(Intent intent
) {
146 assert intent
!= null;
148 String action
= intent
.getAction();
150 Uri data
= intent
.getData();
152 return Intent
.ACTION_VIEW
.equals(action
)
153 && this.mRedirectUriScheme
.equals(data
.getScheme())
154 && REDIRECT_URI_PATH
.equals(data
.getPath());
159 public boolean handleTokenFetched(Intent intent
) {
160 assert intent
!= null;
162 if (!isValidIntent(intent
)) {
163 Log
.w("ThirdPartyAuth", "Ignoring unmatched intent.");
167 String accessToken
= intent
.getStringExtra("access_token");
168 String code
= intent
.getStringExtra("code");
169 String state
= intent
.getStringExtra("state");
171 if (!mState
.equals(state
)) {
172 failFetchToken("Ignoring redirect with invalid state.");
176 if (code
== null || accessToken
== null) {
177 failFetchToken("Ignoring redirect with missing code or token.");
181 Log
.i("ThirdPartyAuth", "handleTokenFetched().");
182 mCallback
.onTokenFetched(code
, accessToken
);
183 OAuthRedirectActivity
.setEnabled(mContext
, false);
187 private void failFetchToken(String errorMessage
) {
188 Log
.e("ThirdPartyAuth", errorMessage
);
189 mCallback
.onTokenFetched("", "");
190 OAuthRedirectActivity
.setEnabled(mContext
, false);
193 /** Generate a 128 bit URL-safe opaque string to prevent cross site request forgery (XSRF).*/
194 private static String
generateXsrfToken() {
195 byte[] bytes
= new byte[16];
196 sSecureRandom
.nextBytes(bytes
);
197 // Uses a variant of Base64 to make sure the URL is URL safe:
198 // URL_SAFE replaces - with _ and + with /.
199 // NO_WRAP removes the trailing newline character.
200 // NO_PADDING removes any trailing =.
201 return Base64
.encodeToString(bytes
, Base64
.URL_SAFE
| Base64
.NO_WRAP
| Base64
.NO_PADDING
);
205 * In the OAuth2 implicit flow, the browser will be redirected to
206 * intent://|REDIRECT_URI_PATH|#Intent;...end; upon a successful login. OAuthRedirectActivity
207 * uses an intent filter in the manifest to intercept the URL and launch the chromoting app.
209 * Unfortunately, most browsers on Android, e.g. chrome, reload the URL when a browser
210 * tab is activated. As a result, chromoting is launched unintentionally when the user restarts
211 * chrome or closes other tabs that causes the redirect URL to become the topmost tab.
213 * To solve the problem, the redirect intent-filter is declared in a separate activity,
214 * |OAuthRedirectActivity| instead of the MainActivity. In this way, we can disable it,
215 * together with its intent filter, by default. |OAuthRedirectActivity| is only enabled when
216 * there is a pending token fetch request.
218 public static class OAuthRedirectActivity
extends Activity
{
220 public void onStart() {
222 // |OAuthRedirectActivity| runs in its own task, it needs to route the intent back
223 // to Chromoting.java to access the state of the current request.
224 Intent intent
= getIntent();
225 intent
.setClass(this, Chromoting
.class);
226 startActivity(intent
);
230 public static void setEnabled(Activity context
, boolean enabled
) {
231 int enabledState
= enabled ? PackageManager
.COMPONENT_ENABLED_STATE_ENABLED
232 : PackageManager
.COMPONENT_ENABLED_STATE_DEFAULT
;
233 ComponentName component
= new ComponentName(
234 context
.getApplicationContext(),
235 ThirdPartyTokenFetcher
.OAuthRedirectActivity
.class);
236 context
.getPackageManager().setComponentEnabledSetting(
239 PackageManager
.DONT_KILL_APP
);