Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(50)

Side by Side Diff: remoting/android/java/src/org/chromium/chromoting/ThirdPartyTokenFetcher.java

Issue 337013002: Third Party Authentication for Android Part III - Android OAuth2 (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Fix indentation Created 6 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
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.util.Base64;
14 import android.util.Log;
15
16 import java.security.SecureRandom;
17 import java.util.HashMap;
18
19 /**
20 * This class is responsible for fetching a third party token from the user usin g the OAuth2
21 * implicit flow. It pops up a third party login page located at |tokenurl|. I t relies on the
22 * |ThirdPartyTokenFetcher$OAuthRedirectActivity| to intercept the access token from the redirect at
23 * |REDIRECT_URI_SCHEME|://|REDIRECT_URI_HOST| upon successful login.
24 */
25 public class ThirdPartyTokenFetcher {
26 /** Callback for receiving the token. */
27 public interface Callback {
28 void onTokenFetched(String code, String accessToken);
29 }
30
31 /** Redirect URI. See http://tools.ietf.org/html/rfc6749#section-3.1.2. */
32 private static final String REDIRECT_URI_HOST = "oauthredirect";
33
34 /**
35 * Request both the authorization code and access token from the server. Se e
36 * http://tools.ietf.org/html/rfc6749#section-3.1.1.
37 */
38 private static final String RESPONSE_TYPE = "code token";
39
40 /** This is used to securely generate an opaque 128 bit for the |mState| var iable. */
41 private static SecureRandom sSecureRandom = new SecureRandom();
42
43 /** This is used to launch the third party login page in the browser. */
44 private Activity mContext;
45
46 /**
47 * An opaque value used by the client to maintain state between the request and callback. The
48 * authorization server includes this value when redirecting the user-agent back to the client.
49 * The parameter is used for preventing cross-site request forgery. See
50 * http://tools.ietf.org/html/rfc6749#section-10.12.
51 */
52 private final String mState;
53
54 /** URL of the third party login page. */
55 private final String mTokenUrl;
56
57 /** The client identifier. See http://tools.ietf.org/html/rfc6749#section-2. 2. */
58 private final String mClientId;
59
60 /** The scope of access request. See http://tools.ietf.org/html/rfc6749#sect ion-3.3. */
61 private final String mScope;
62
63 private final Callback mCallback;
64
65 private final String mRedirectUriScheme;
66
67 private final String mRedirectUri;
68
69 public ThirdPartyTokenFetcher(Activity context,
70 String tokenUrl,
71 String clientId,
72 String scope,
73 Callback callback) {
74 this.mContext = context;
75 this.mTokenUrl = tokenUrl;
76 this.mClientId = clientId;
77 this.mState = generateXsrfToken();
78 this.mScope = scope;
79 this.mCallback = callback;
80
81 this.mRedirectUriScheme = context.getApplicationContext().getPackageName ();
82 this.mRedirectUri = mRedirectUriScheme + "://" + REDIRECT_URI_HOST;
83 }
84
85 public void fetchToken() {
86 Uri.Builder uriBuilder = Uri.parse(mTokenUrl).buildUpon();
87 uriBuilder.appendQueryParameter("redirect_uri", this.mRedirectUri);
88 uriBuilder.appendQueryParameter("scope", mScope);
89 uriBuilder.appendQueryParameter("client_id", mClientId);
90 uriBuilder.appendQueryParameter("state", mState);
91 uriBuilder.appendQueryParameter("response_type", RESPONSE_TYPE);
92
93 Uri uri = uriBuilder.build();
94 Intent intent = new Intent(Intent.ACTION_VIEW, uri);
95 Log.i("ThirdPartyAuth", "fetchToken() url:" + uri);
96 OAuthRedirectActivity.setEnabled(mContext, true);
97
98 try {
99 mContext.startActivity(intent);
100 } catch (ActivityNotFoundException e) {
101 failFetchToken("No browser is installed to open the third party auth entication page.");
102 }
103 }
104
105 private boolean isValidIntent(Intent intent) {
106 assert intent != null;
107
108 String action = intent.getAction();
109
110 Uri data = intent.getData();
111 if (data != null) {
112 return Intent.ACTION_VIEW.equals(action) &&
113 this.mRedirectUriScheme.equals(data.getScheme()) &&
114 REDIRECT_URI_HOST.equals(data.getHost());
115 }
116 return false;
117 }
118
119 public boolean handleTokenFetched(Intent intent) {
120 assert intent != null;
121
122 if (!isValidIntent(intent)) {
123 Log.w("ThirdPartyAuth", "Ignoring unmatched intent.");
124 return false;
125 }
126
127 Uri data = intent.getData();
128 HashMap<String, String> params = getFragmentParameters(data);
129
130 String accessToken = params.get("access_token");
131 String code = params.get("code");
132 String state = params.get("state");
133
134 if (!mState.equals(state)) {
135 failFetchToken("Ignoring redirect with invalid state.");
136 return false;
137 }
138
139 if (code == null || accessToken == null) {
140 failFetchToken("Ignoring redirect with missing code or token.");
141 return false;
142 }
143
144 Log.i("ThirdPartyAuth", "handleTokenFetched().");
145 mCallback.onTokenFetched(code, accessToken);
146 OAuthRedirectActivity.setEnabled(mContext, false);
147 return true;
148 }
149
150 private void failFetchToken(String errorMessage) {
151 Log.e("ThirdPartyAuth", errorMessage);
152 mCallback.onTokenFetched("", "");
153 OAuthRedirectActivity.setEnabled(mContext, false);
154 }
155
156 /** Generate a 128 bit URL-safe opaque string to prevent cross site request forgery (XSRF).*/
157 private static String generateXsrfToken() {
158 byte[] bytes = new byte[16];
159 sSecureRandom.nextBytes(bytes);
160 // Uses a variant of Base64 to make sure the URL is URL safe:
161 // URL_SAFE replaces - with _ and + with /.
162 // NO_WRAP removes the trailing newline character.
163 // NO_PADDING removes any trailing =.
164 return Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP | B ase64.NO_PADDING);
165 }
166
167 /** Parses the fragment string into a key value pair. */
168 private static HashMap<String, String> getFragmentParameters(Uri uri) {
169 assert uri != null;
170 HashMap<String, String> result = new HashMap<String, String>();
171
172 String fragment = uri.getFragment();
173
174 if (fragment != null) {
175 String[] parts = fragment.split("&");
176
177 for (String part : parts) {
178 String keyValuePair[] = part.split("=", 2);
179 if (keyValuePair.length == 2) {
180 result.put(keyValuePair[0], keyValuePair[1]);
181 }
182 }
183 }
184 return result;
185 };
186
187 /**
188 * In the OAuth2 implicit flow, the browser will be redirected to
189 * |REDIRECT_URI_SCHEME|://|REDIRECT_URI_HOST| upon a successful login. OAut hRedirectActivity
190 * uses an intent filter in the manifest to intercept the URL and launch the chromoting app.
191 *
192 * Unfortunately, most browsers on Android, e.g. chrome, reload the URL when a browser
193 * tab is activated. As a result, chromoting is launched unintentionally wh en the user restarts
194 * chrome or closes other tabs that causes the redirect URL to become the to pmost tab.
195 *
196 * To solve the problem, the redirect intent-filter is declared in a separat e activity,
197 * |OAuthRedirectActivity| instead of the MainActivity. In this way, we can disable it,
198 * together with its intent filter, by default. |OAuthRedirectActivity| is o nly enabled when
199 * there is a pending token fetch request.
200 */
201 public static class OAuthRedirectActivity extends Activity {
202 @Override
203 public void onStart() {
204 super.onStart();
205 // |OAuthRedirectActivity| runs in its own task, it needs to route t he intent back
206 // to Chromoting.java to access the state of the current request.
207 Intent intent = getIntent();
208 intent.setClass(this, Chromoting.class);
209 startActivity(intent);
210 finishActivity(0);
211 }
212
213 public static void setEnabled(Activity context, boolean enabled) {
214 int enabledState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ ENABLED
215 : PackageManager.COMPONENT_ENABLED_STATE_ DEFAULT;
216 ComponentName component = new ComponentName(
217 context.getApplicationContext(),
218 ThirdPartyTokenFetcher.OAuthRedirectActivity.class);
219 context.getPackageManager().setComponentEnabledSetting(
220 component,
221 enabledState,
222 PackageManager.DONT_KILL_APP);
223 }
224 }
225 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698