Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / remoting / android / java / src / org / chromium / chromoting / ThirdPartyTokenFetcher.java
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.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;
17
18 import org.chromium.base.SecureRandomInitializer;
19
20 import java.io.IOException;
21 import java.security.SecureRandom;
22 import java.util.ArrayList;
23
24 /**
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.
29  */
30 public class ThirdPartyTokenFetcher {
31     /** Callback for receiving the token. */
32     public interface Callback {
33         void onTokenFetched(String code, String accessToken);
34     }
35
36     /** The path of the Redirect URI. */
37     private static final String REDIRECT_URI_PATH = "/oauthredirect/";
38
39     /**
40      * Request both the authorization code and access token from the server.  See
41      * http://tools.ietf.org/html/rfc6749#section-3.1.1.
42      */
43     private static final String RESPONSE_TYPE = "code token";
44
45     /** This is used to securely generate an opaque 128 bit for the |mState| variable. */
46     @SuppressLint("TrulyRandom")
47     private static SecureRandom sSecureRandom;
48
49     // TODO(lambroslambrou): Refactor this class to only initialize a PRNG when ThirdPartyAuth is
50     // actually used.
51     static {
52         sSecureRandom = new SecureRandom();
53         try {
54             SecureRandomInitializer.initialize(sSecureRandom);
55         } catch (IOException e) {
56             throw new RuntimeException("Failed to initialize PRNG: " + e);
57         }
58     }
59
60     /** This is used to launch the third party login page in the browser. */
61     private Activity mContext;
62
63     /**
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.
68      */
69     private final String mState;
70
71     private final Callback mCallback;
72
73     /** The list of TokenUrls allowed by the domain. */
74     private final ArrayList<String> mTokenUrlPatterns;
75
76     private final String mRedirectUriScheme;
77
78     private final String mRedirectUri;
79
80     public ThirdPartyTokenFetcher(Activity context,
81                                   ArrayList<String> tokenUrlPatterns,
82                                   Callback callback) {
83         this.mContext = context;
84         this.mState = generateXsrfToken();
85         this.mCallback = callback;
86         this.mTokenUrlPatterns = tokenUrlPatterns;
87
88         this.mRedirectUriScheme = context.getApplicationContext().getPackageName();
89
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;";
97     }
98
99     /**
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.
103      */
104     public void fetchToken(String tokenUrl, String clientId, String scope) {
105         if (!isValidTokenUrl(tokenUrl)) {
106             failFetchToken(
107                     "Token URL does not match the domain\'s allowed URL patterns." +
108                     " URL: " + tokenUrl +
109                     ", patterns: " + TextUtils.join(",", this.mTokenUrlPatterns));
110             return;
111         }
112
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);
117
118         try {
119             mContext.startActivity(intent);
120         } catch (ActivityNotFoundException e) {
121             failFetchToken("No browser is installed to open the third party authentication page.");
122         }
123     }
124
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);
132
133         return uriBuilder.build();
134     }
135
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;
141             }
142         }
143         return false;
144     }
145
146     private boolean isValidIntent(Intent intent) {
147         assert intent != null;
148
149         String action = intent.getAction();
150
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());
156         }
157         return false;
158     }
159
160     public boolean handleTokenFetched(Intent intent) {
161         assert intent != null;
162
163         if (!isValidIntent(intent)) {
164             Log.w("ThirdPartyAuth", "Ignoring unmatched intent.");
165             return false;
166         }
167
168         String accessToken = intent.getStringExtra("access_token");
169         String code = intent.getStringExtra("code");
170         String state = intent.getStringExtra("state");
171
172         if (!mState.equals(state)) {
173             failFetchToken("Ignoring redirect with invalid state.");
174             return false;
175         }
176
177         if (code == null || accessToken == null) {
178             failFetchToken("Ignoring redirect with missing code or token.");
179             return false;
180         }
181
182         Log.i("ThirdPartyAuth", "handleTokenFetched().");
183         mCallback.onTokenFetched(code, accessToken);
184         OAuthRedirectActivity.setEnabled(mContext, false);
185         return true;
186     }
187
188     private void failFetchToken(String errorMessage) {
189         Log.e("ThirdPartyAuth", errorMessage);
190         mCallback.onTokenFetched("", "");
191         OAuthRedirectActivity.setEnabled(mContext, false);
192     }
193
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);
203     }
204
205     /**
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.
209      *
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.
213      *
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.
218      */
219     public static class OAuthRedirectActivity extends Activity {
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);
229         }
230
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);
241         }
242     }
243 }