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

Side by Side Diff: services/authentication/google_authentication_impl.cc

Issue 1466733002: Google OAuth Device Flow support for FNL (Closed) Base URL: https://github.com/domokit/mojo.git@master
Patch Set: Removed data_unittest.py Created 4 years, 9 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
« no previous file with comments | « services/authentication/google_authentication_impl.h ('k') | services/authentication/main.cc » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2016 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 #include "services/authentication/google_authentication_impl.h"
6
7 #include "base/json/json_reader.h"
8 #include "base/json/json_writer.h"
9 #include "base/message_loop/message_loop.h"
10 #include "base/strings/string_piece.h"
11 #include "base/strings/string_split.h"
12 #include "base/strings/string_tokenizer.h"
13 #include "base/strings/string_util.h"
14 #include "base/strings/stringprintf.h"
15 #include "base/synchronization/waitable_event.h"
16 #include "base/threading/platform_thread.h"
17 #include "base/trace_event/trace_event.h"
18 #include "base/values.h"
19 #include "mojo/common/binding_set.h"
20 #include "mojo/data_pipe_utils/data_pipe_drainer.h"
21 #include "mojo/data_pipe_utils/data_pipe_utils.h"
22 #include "mojo/public/c/system/main.h"
23 #include "mojo/public/cpp/bindings/strong_binding.h"
24 #include "mojo/public/cpp/system/macros.h"
25 #include "mojo/services/network/interfaces/url_loader.mojom.h"
26 #include "services/authentication/credentials_impl_db.mojom.h"
27
28 namespace authentication {
29
30 // Mojo Shell OAuth2 Client configuration.
31 // TODO: These should be retrieved from a secure storage or a configuration file
32 // in the future.
33 char kMojoShellOAuth2ClientId[] =
34 "962611923869-3avg0b4vlisgjhin0l98dgp6d8sd634r.apps.googleusercontent.com";
35 char kMojoShellOAuth2ClientSecret[] = "41IxvPPAt1HyRoYw2hO84dRI";
36
37 // Query params used in Google OAuth2 handshake
38 char kOAuth2ClientIdParamName[] = "client_id";
39 char kOAuth2ClientSecretParamName[] = "client_secret";
40 char kOAuth2ScopeParamName[] = "scope";
41 char kOAuth2GrantTypeParamName[] = "grant_type";
42 char kOAuth2CodeParamName[] = "code";
43 char kOAuth2RefreshTokenParamName[] = "refresh_token";
44 char kOAuth2DeviceFlowGrantType[] = "http://oauth.net/grant_type/device/1.0";
45 char kOAuth2RefreshTokenGrantType[] = "refresh_token";
46
47 // TODO(ukode) : Verify the char list
48 char kEscapableUrlParamChars[] = ".$[]/";
49
50 std::string EncodeParam(std::string param) {
51 for (size_t i = 0; i < strlen(kEscapableUrlParamChars); ++i) {
52 base::ReplaceSubstringsAfterOffset(
53 &param, 0, std::string(1, kEscapableUrlParamChars[i]),
54 base::StringPrintf("%%%x", kEscapableUrlParamChars[i]));
55 }
56 return param;
57 }
58
59 mojo::String BuildUrlQuery(mojo::Map<mojo::String, mojo::String> params) {
60 std::string message;
61 for (auto it = params.begin(); it != params.end(); ++it) {
62 message += EncodeParam(it.GetKey()) + "=" + EncodeParam(it.GetValue());
63 message += "&";
64 }
65
66 if (!message.empty()) {
67 message = message.substr(0, message.size() - 1); // Trims extra "&".
68 }
69 return message;
70 }
71
72 static base::DictionaryValue* ParseOAuth2Response(const std::string& response) {
73 if (response.empty()) {
74 return nullptr;
75 }
76
77 scoped_ptr<base::Value> root(base::JSONReader::Read(response));
78 if (!root || !root->IsType(base::Value::TYPE_DICTIONARY)) {
79 LOG(ERROR) << "Unexpected json response:" << std::endl << response;
80 return nullptr;
81 }
82
83 return static_cast<base::DictionaryValue*>(root.release());
84 }
85
86 GoogleAuthenticationServiceImpl::GoogleAuthenticationServiceImpl(
87 mojo::InterfaceRequest<AuthenticationService> request,
88 const mojo::String app_url,
89 mojo::NetworkServicePtr& network_service,
90 mojo::files::DirectoryPtr& directory)
91 : binding_(this, request.Pass()),
92 app_url_(app_url),
93 network_service_(network_service) {
94 accounts_db_manager_ = new AccountsDbManager(directory.Pass());
95 }
96
97 GoogleAuthenticationServiceImpl::~GoogleAuthenticationServiceImpl() {
98 delete accounts_db_manager_;
99 }
100
101 void GoogleAuthenticationServiceImpl::GetOAuth2Token(
102 const mojo::String& username,
103 mojo::Array<mojo::String> scopes,
104 const GetOAuth2TokenCallback& callback) {
105 if (!accounts_db_manager_->isValid()) {
106 callback.Run(nullptr, "Accounts db validation failed.");
107 return;
108 }
109
110 authentication::CredentialsPtr creds =
111 accounts_db_manager_->GetCredentials(username);
112
113 if (!creds->token) {
114 callback.Run(nullptr, "User grant not found");
115 return;
116 }
117
118 // TODO: Scopes are not used with the scoped refresh tokens. When we start
119 // supporting full login scoped tokens, then the scopes here gets used for
120 // Sidescoping.
121 mojo::Map<mojo::String, mojo::String> params;
122 params[kOAuth2ClientIdParamName] = kMojoShellOAuth2ClientId;
123 params[kOAuth2ClientSecretParamName] = kMojoShellOAuth2ClientSecret;
124 params[kOAuth2GrantTypeParamName] = kOAuth2RefreshTokenGrantType;
125 params[kOAuth2RefreshTokenParamName] = creds->token;
126
127 Request("https://www.googleapis.com/oauth2/v3/token", "POST",
128 BuildUrlQuery(params.Pass()),
129 base::Bind(&GoogleAuthenticationServiceImpl::OnGetOAuth2Token,
130 base::Unretained(this), callback));
131 }
132
133 void GoogleAuthenticationServiceImpl::SelectAccount(
134 bool returnLastSelected,
135 const SelectAccountCallback& callback) {
136 if (!accounts_db_manager_->isValid()) {
137 callback.Run(nullptr, "Accounts db validation failed.");
138 return;
139 }
140
141 mojo::String username;
142 if (returnLastSelected) {
143 username = accounts_db_manager_->GetAuthorizedUserForApp(app_url_);
144 if (!username.is_null()) {
145 callback.Run(username, nullptr);
146 return;
147 }
148 }
149
150 // TODO(ukode): Select one among the list of accounts using an AccountPicker
151 // UI instead of the first account always.
152 mojo::Array<mojo::String> users = accounts_db_manager_->GetAllUsers();
153 if (!users.size()) {
154 callback.Run(nullptr, "No user accounts found.");
155 return;
156 }
157
158 username = users[0];
159 accounts_db_manager_->UpdateAuthorization(app_url_, username);
160 callback.Run(username, nullptr);
161 }
162
163 void GoogleAuthenticationServiceImpl::ClearOAuth2Token(
164 const mojo::String& token) {}
165
166 void GoogleAuthenticationServiceImpl::GetOAuth2DeviceCode(
167 mojo::Array<mojo::String> scopes,
168 const GetOAuth2DeviceCodeCallback& callback) {
169 std::string scopes_str("email");
170 for (size_t i = 0; i < scopes.size(); i++) {
171 scopes_str += " ";
172 scopes_str += std::string(scopes[i].data());
173 }
174
175 mojo::Map<mojo::String, mojo::String> params;
176 params[kOAuth2ClientIdParamName] = kMojoShellOAuth2ClientId;
177 params[kOAuth2ScopeParamName] = scopes_str;
178
179 Request("https://accounts.google.com/o/oauth2/device/code", "POST",
180 BuildUrlQuery(params.Pass()),
181 base::Bind(&GoogleAuthenticationServiceImpl::OnGetOAuth2DeviceCode,
182 base::Unretained(this), callback));
183 }
184
185 void GoogleAuthenticationServiceImpl::AddAccount(
186 const mojo::String& device_code,
187 const AddAccountCallback& callback) {
188 // Resets the poll count to "1"
189 AddAccountInternal(device_code, 1, callback);
190 }
191
192 void GoogleAuthenticationServiceImpl::AddAccountInternal(
193 const mojo::String& device_code,
194 const uint32_t num_poll_attempts,
195 const AddAccountCallback& callback) {
196 mojo::Map<mojo::String, mojo::String> params;
197 params[kOAuth2ClientIdParamName] = kMojoShellOAuth2ClientId;
198 params[kOAuth2ClientSecretParamName] = kMojoShellOAuth2ClientSecret;
199 params[kOAuth2GrantTypeParamName] = kOAuth2DeviceFlowGrantType;
200 params[kOAuth2CodeParamName] = device_code;
201
202 Request("https://www.googleapis.com/oauth2/v3/token", "POST",
203 BuildUrlQuery(params.Pass()),
204 base::Bind(&GoogleAuthenticationServiceImpl::OnAddAccount,
205 base::Unretained(this), callback, device_code,
206 num_poll_attempts));
207 }
208
209 void GoogleAuthenticationServiceImpl::OnGetOAuth2Token(
210 const GetOAuth2TokenCallback& callback,
211 const std::string& response,
212 const std::string& error) {
213 if (response.empty()) {
214 callback.Run(nullptr, "Error from server:" + error);
215 return;
216 }
217
218 scoped_ptr<base::DictionaryValue> dict(ParseOAuth2Response(response.c_str()));
219 if (!dict.get() || dict->HasKey("error")) {
220 callback.Run(nullptr, "Error in parsing response:" + response);
221 return;
222 }
223
224 std::string access_token;
225 dict->GetString("access_token", &access_token);
226
227 callback.Run(access_token, nullptr);
228 }
229
230 void GoogleAuthenticationServiceImpl::OnGetOAuth2DeviceCode(
231 const GetOAuth2DeviceCodeCallback& callback,
232 const std::string& response,
233 const std::string& error) {
234 if (response.empty()) {
235 callback.Run(nullptr, nullptr, nullptr, "Error from server:" + error);
236 return;
237 }
238
239 scoped_ptr<base::DictionaryValue> dict(ParseOAuth2Response(response.c_str()));
240 if (!dict.get() || dict->HasKey("error")) {
241 callback.Run(nullptr, nullptr, nullptr,
242 "Error in parsing response:" + response);
243 return;
244 }
245
246 std::string url;
247 std::string device_code;
248 std::string user_code;
249 dict->GetString("verification_url", &url);
250 dict->GetString("device_code", &device_code);
251 dict->GetString("user_code", &user_code);
252
253 callback.Run(url, device_code, user_code, nullptr);
254 }
255
256 void GoogleAuthenticationServiceImpl::GetTokenInfo(
257 const std::string& access_token) {
258 std::string url("https://www.googleapis.com/oauth2/v1/tokeninfo");
259 url += "?access_token=" + EncodeParam(access_token);
260
261 Request(url, "GET", "",
262 base::Bind(&GoogleAuthenticationServiceImpl::OnGetTokenInfo,
263 base::Unretained(this)));
264 }
265
266 void GoogleAuthenticationServiceImpl::OnGetTokenInfo(
267 const std::string& response,
268 const std::string& error) {
269 if (response.empty()) {
270 return;
271 }
272
273 scoped_ptr<base::DictionaryValue> dict(ParseOAuth2Response(response.c_str()));
274 if (!dict.get() || dict->HasKey("error")) {
275 return;
276 }
277
278 // This field is only present if the profile scope was present in the
279 // request. The value of this field is an immutable identifier for the
280 // logged-in user, and may be used when creating and managing user
281 // sessions in your application.
282 dict->GetString("user_id", &user_id_);
283 dict->GetString("email", &email_);
284 // The space-delimited set of scopes that the user consented to.
285 dict->GetString("scope", &scope_);
286 return;
287 }
288
289 void GoogleAuthenticationServiceImpl::GetUserInfo(const std::string& id_token) {
290 std::string url("https://www.googleapis.com/oauth2/v1/tokeninfo");
291 url += "?id_token=" + EncodeParam(id_token);
292
293 Request(url, "GET", "",
294 base::Bind(&GoogleAuthenticationServiceImpl::OnGetUserInfo,
295 base::Unretained(this)));
296 }
297
298 void GoogleAuthenticationServiceImpl::OnGetUserInfo(const std::string& response,
299 const std::string& error) {
300 if (response.empty()) {
301 return;
302 }
303
304 scoped_ptr<base::DictionaryValue> dict(ParseOAuth2Response(response.c_str()));
305 if (!dict.get() || dict->HasKey("error")) {
306 return;
307 }
308
309 // This field is only present if the email scope was requested
310 dict->GetString("email", &email_);
311 }
312
313 void GoogleAuthenticationServiceImpl::OnAddAccount(
314 const AddAccountCallback& callback,
315 const mojo::String& device_code,
316 const uint32_t num_poll_attempts,
317 const std::string& response,
318 const std::string& error) {
319 if (response.empty()) {
320 callback.Run(nullptr, "Error from server:" + error);
321 return;
322 }
323
324 if (!response.empty() && error.empty()) {
325 scoped_ptr<base::Value> root(base::JSONReader::Read(response));
326 if (!root || !root->IsType(base::Value::TYPE_DICTIONARY)) {
327 callback.Run(response, nullptr);
328 return;
329 }
330 }
331
332 // Parse response and fetch refresh, access and idtokens
333 scoped_ptr<base::DictionaryValue> dict(ParseOAuth2Response(response.c_str()));
334 std::string error_code;
335 if (!dict.get()) {
336 callback.Run(nullptr, "Error in parsing response:" + response);
337 return;
338 } else if (dict->HasKey("error") && dict->GetString("error", &error_code)) {
339 if (error_code != "authorization_pending") {
340 callback.Run(nullptr, "Server error:" + response);
341 return;
342 }
343
344 if (num_poll_attempts > 15) {
345 callback.Run(nullptr, "Timed out after max number of polling attempts");
346 return;
347 }
348
349 // Rate limit by waiting 7 seconds before polling for a new grant
350 base::MessageLoop::current()->PostDelayedTask(
351 FROM_HERE,
352 base::Bind(&GoogleAuthenticationServiceImpl::AddAccountInternal,
353 base::Unretained(this), device_code, num_poll_attempts + 1,
354 callback),
355 base::TimeDelta::FromMilliseconds(7000));
356 return;
357 }
358
359 // Poll success, after detecting user grant.
360 std::string access_token;
361 dict->GetString("access_token", &access_token);
362 GetTokenInfo(access_token); // gets scope, email and user_id
363
364 if (email_.empty()) {
365 std::string id_token;
366 dict->GetString("id_token", &id_token);
367 GetUserInfo(id_token); // gets user's email
368 }
369
370 // TODO(ukode): Store access token in cache for the duration set in
371 // response
372 if (!accounts_db_manager_->isValid()) {
373 callback.Run(nullptr, "Accounts db validation failed.");
374 return;
375 }
376
377 std::string refresh_token;
378 dict->GetString("refresh_token", &refresh_token);
379 authentication::CredentialsPtr creds = authentication::Credentials::New();
380 creds->token = refresh_token;
381 creds->scopes = scope_;
382 creds->auth_provider = AuthProvider::GOOGLE;
383 creds->credential_type = CredentialType::DOWNSCOPED_OAUTH_REFRESH_TOKEN;
384 std::string username = email_.empty() ? user_id_ : email_;
385 accounts_db_manager_->UpdateCredentials(username, creds.Pass());
386
387 callback.Run(username, nullptr);
388 }
389
390 void GoogleAuthenticationServiceImpl::Request(
391 const std::string& url,
392 const std::string& method,
393 const std::string& message,
394 const mojo::Callback<void(std::string, std::string)>& callback) {
395 Request(url, method, message, callback, nullptr, 0);
396 }
397
398 void GoogleAuthenticationServiceImpl::Request(
399 const std::string& url,
400 const std::string& method,
401 const std::string& message,
402 const mojo::Callback<void(std::string, std::string)>& callback,
403 const mojo::String& device_code,
404 const uint32_t num_poll_attempts) {
405 mojo::URLRequestPtr request(mojo::URLRequest::New());
406 request->url = url;
407 request->method = method;
408 request->auto_follow_redirects = true;
409
410 // Add headers
411 auto content_type_header = mojo::HttpHeader::New();
412 content_type_header->name = "Content-Type";
413 content_type_header->value = "application/x-www-form-urlencoded";
414 request->headers.push_back(content_type_header.Pass());
415
416 if (!message.empty()) {
417 request->body.push_back(
418 mojo::common::WriteStringToConsumerHandle(message).Pass());
419 }
420
421 mojo::URLLoaderPtr url_loader;
422 network_service_->CreateURLLoader(GetProxy(&url_loader));
423
424 url_loader->Start(
425 request.Pass(),
426 base::Bind(&GoogleAuthenticationServiceImpl::HandleServerResponse,
427 base::Unretained(this), callback, device_code,
428 num_poll_attempts));
429
430 url_loader.WaitForIncomingResponse();
431 }
432
433 void GoogleAuthenticationServiceImpl::HandleServerResponse(
434 const mojo::Callback<void(std::string, std::string)>& callback,
435 const mojo::String& device_code,
436 const uint32_t num_poll_attempts,
437 mojo::URLResponsePtr response) {
438 if (response.is_null()) {
439 LOG(WARNING) << "Something went horribly wrong...exiting!!";
440 callback.Run("", "Empty response");
441 return;
442 }
443
444 if (response->error) {
445 LOG(ERROR) << "Got error (" << response->error->code
446 << "), reason: " << response->error->description.get().c_str();
447 callback.Run("", response->error->description.get().c_str());
448 return;
449 }
450
451 std::string response_body;
452 mojo::common::BlockingCopyToString(response->body.Pass(), &response_body);
453
454 callback.Run(response_body, "");
455 }
456
457 } // authentication namespace
OLDNEW
« no previous file with comments | « services/authentication/google_authentication_impl.h ('k') | services/authentication/main.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698