• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4 
5 package org.chromium.chromoting;
6 
7 import android.app.Activity;
8 import android.content.ActivityNotFoundException;
9 import android.content.ComponentName;
10 import android.content.Intent;
11 import android.content.pm.PackageManager;
12 import android.net.Uri;
13 import android.text.TextUtils;
14 import android.util.Base64;
15 import android.util.Log;
16 
17 import java.security.SecureRandom;
18 import java.util.ArrayList;
19 
20 /**
21  * This class is responsible for fetching a third party token from the user using the OAuth2
22  * implicit flow.  It directs the user to a third party login page located at |tokenUrl|.  It relies
23  * on the |ThirdPartyTokenFetcher$OAuthRedirectActivity| to intercept the access token from the
24  * redirect at intent://|REDIRECT_URI_PATH|#Intent;...end; upon successful login.
25  */
26 public class ThirdPartyTokenFetcher {
27     /** Callback for receiving the token. */
28     public interface Callback {
onTokenFetched(String code, String accessToken)29         void onTokenFetched(String code, String accessToken);
30     }
31 
32     /** The path of the Redirect URI. */
33     private static final String REDIRECT_URI_PATH = "/oauthredirect/";
34 
35     /**
36      * Request both the authorization code and access token from the server.  See
37      * http://tools.ietf.org/html/rfc6749#section-3.1.1.
38      */
39     private static final String RESPONSE_TYPE = "code token";
40 
41     /** This is used to securely generate an opaque 128 bit for the |mState| variable. */
42     private static SecureRandom sSecureRandom = new SecureRandom();
43 
44     /** This is used to launch the third party login page in the browser. */
45     private Activity mContext;
46 
47     /**
48      * An opaque value used by the client to maintain state between the request and callback.  The
49      * authorization server includes this value when redirecting the user-agent back to the client.
50      * The parameter is used for preventing cross-site request forgery. See
51      * http://tools.ietf.org/html/rfc6749#section-10.12.
52      */
53     private final String mState;
54 
55     private final Callback mCallback;
56 
57     /** The list of TokenUrls allowed by the domain. */
58     private final ArrayList<String> mTokenUrlPatterns;
59 
60     private final String mRedirectUriScheme;
61 
62     private final String mRedirectUri;
63 
ThirdPartyTokenFetcher(Activity context, ArrayList<String> tokenUrlPatterns, Callback callback)64     public ThirdPartyTokenFetcher(Activity context,
65                                   ArrayList<String> tokenUrlPatterns,
66                                   Callback callback) {
67         this.mContext = context;
68         this.mState = generateXsrfToken();
69         this.mCallback = callback;
70         this.mTokenUrlPatterns = tokenUrlPatterns;
71 
72         this.mRedirectUriScheme = context.getApplicationContext().getPackageName();
73 
74         // We don't follow the OAuth spec (http://tools.ietf.org/html/rfc6749#section-3.1.2) of the
75         // redirect URI as it is possible for the other applications to intercept the redirect URI.
76         // Instead, we use the intent scheme URI, which can restrict a specific package to handle
77         // the intent.  See https://developer.chrome.com/multidevice/android/intents.
78         this.mRedirectUri = "intent://" + REDIRECT_URI_PATH + "#Intent;" +
79             "package=" + mRedirectUriScheme + ";" +
80             "scheme=" + mRedirectUriScheme + ";end;";
81     }
82 
83     /**
84      * @param tokenUrl URL of the third party login page.
85      * @param clientId The client identifier. See http://tools.ietf.org/html/rfc6749#section-2.2.
86      * @param scope The scope of access request. See http://tools.ietf.org/html/rfc6749#section-3.3.
87      */
fetchToken(String tokenUrl, String clientId, String scope)88     public void fetchToken(String tokenUrl, String clientId, String scope) {
89         if (!isValidTokenUrl(tokenUrl)) {
90             failFetchToken(
91                     "Token URL does not match the domain\'s allowed URL patterns." +
92                     " URL: " + tokenUrl +
93                     ", patterns: " + TextUtils.join(",", this.mTokenUrlPatterns));
94             return;
95         }
96 
97         Uri uri = buildRequestUri(tokenUrl, clientId, scope);
98         Intent intent = new Intent(Intent.ACTION_VIEW, uri);
99         Log.i("ThirdPartyAuth", "fetchToken() url:" + uri);
100         OAuthRedirectActivity.setEnabled(mContext, true);
101 
102         try {
103             mContext.startActivity(intent);
104         } catch (ActivityNotFoundException e) {
105             failFetchToken("No browser is installed to open the third party authentication page.");
106         }
107     }
108 
buildRequestUri(String tokenUrl, String clientId, String scope)109     private Uri buildRequestUri(String tokenUrl, String clientId, String scope) {
110         Uri.Builder uriBuilder = Uri.parse(tokenUrl).buildUpon();
111         uriBuilder.appendQueryParameter("redirect_uri", this.mRedirectUri);
112         uriBuilder.appendQueryParameter("scope", scope);
113         uriBuilder.appendQueryParameter("client_id", clientId);
114         uriBuilder.appendQueryParameter("state", mState);
115         uriBuilder.appendQueryParameter("response_type", RESPONSE_TYPE);
116 
117         return uriBuilder.build();
118     }
119 
120     /** Verifies the host-supplied URL matches the domain's allowed URL patterns. */
isValidTokenUrl(String tokenUrl)121     private boolean isValidTokenUrl(String tokenUrl) {
122         for (String pattern : mTokenUrlPatterns) {
123             if (tokenUrl.matches(pattern)) {
124                 return true;
125             }
126         }
127         return false;
128     }
129 
isValidIntent(Intent intent)130     private boolean isValidIntent(Intent intent) {
131         assert intent != null;
132 
133         String action = intent.getAction();
134 
135         Uri data = intent.getData();
136         if (data != null) {
137             return Intent.ACTION_VIEW.equals(action) &&
138                    this.mRedirectUriScheme.equals(data.getScheme()) &&
139                    REDIRECT_URI_PATH.equals(data.getPath());
140         }
141         return false;
142     }
143 
handleTokenFetched(Intent intent)144     public boolean handleTokenFetched(Intent intent) {
145         assert intent != null;
146 
147         if (!isValidIntent(intent)) {
148             Log.w("ThirdPartyAuth", "Ignoring unmatched intent.");
149             return false;
150         }
151 
152         String accessToken = intent.getStringExtra("access_token");
153         String code = intent.getStringExtra("code");
154         String state = intent.getStringExtra("state");
155 
156         if (!mState.equals(state)) {
157             failFetchToken("Ignoring redirect with invalid state.");
158             return false;
159         }
160 
161         if (code == null || accessToken == null) {
162             failFetchToken("Ignoring redirect with missing code or token.");
163             return false;
164         }
165 
166         Log.i("ThirdPartyAuth", "handleTokenFetched().");
167         mCallback.onTokenFetched(code, accessToken);
168         OAuthRedirectActivity.setEnabled(mContext, false);
169         return true;
170     }
171 
failFetchToken(String errorMessage)172     private void failFetchToken(String errorMessage) {
173         Log.e("ThirdPartyAuth", errorMessage);
174         mCallback.onTokenFetched("", "");
175         OAuthRedirectActivity.setEnabled(mContext, false);
176     }
177 
178     /** Generate a 128 bit URL-safe opaque string to prevent cross site request forgery (XSRF).*/
generateXsrfToken()179     private static String generateXsrfToken() {
180         byte[] bytes = new byte[16];
181         sSecureRandom.nextBytes(bytes);
182         // Uses a variant of Base64 to make sure the URL is URL safe:
183         // URL_SAFE replaces - with _ and + with /.
184         // NO_WRAP removes the trailing newline character.
185         // NO_PADDING removes any trailing =.
186         return Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
187     }
188 
189     /**
190      * In the OAuth2 implicit flow, the browser will be redirected to
191      * intent://|REDIRECT_URI_PATH|#Intent;...end; upon a successful login. OAuthRedirectActivity
192      * uses an intent filter in the manifest to intercept the URL and launch the chromoting app.
193      *
194      * Unfortunately, most browsers on Android, e.g. chrome, reload the URL when a browser
195      * tab is activated.  As a result, chromoting is launched unintentionally when the user restarts
196      * chrome or closes other tabs that causes the redirect URL to become the topmost tab.
197      *
198      * To solve the problem, the redirect intent-filter is declared in a separate activity,
199      * |OAuthRedirectActivity| instead of the MainActivity.  In this way, we can disable it,
200      * together with its intent filter, by default. |OAuthRedirectActivity| is only enabled when
201      * there is a pending token fetch request.
202      */
203     public static class OAuthRedirectActivity extends Activity {
204         @Override
onStart()205         public void onStart() {
206             super.onStart();
207             // |OAuthRedirectActivity| runs in its own task, it needs to route the intent back
208             // to Chromoting.java to access the state of the current request.
209             Intent intent = getIntent();
210             intent.setClass(this, Chromoting.class);
211             startActivity(intent);
212             finishActivity(0);
213         }
214 
setEnabled(Activity context, boolean enabled)215         public static void setEnabled(Activity context, boolean enabled) {
216             int enabledState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
217                                        : PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
218             ComponentName component = new ComponentName(
219                     context.getApplicationContext(),
220                     ThirdPartyTokenFetcher.OAuthRedirectActivity.class);
221             context.getPackageManager().setComponentEnabledSetting(
222                     component,
223                     enabledState,
224                     PackageManager.DONT_KILL_APP);
225         }
226     }
227 }
228