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