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