auth_test.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. # These tests do not currently do much to verify the correct implementation
  2. # of the openid/oauth protocols, they just exercise the major code paths
  3. # and ensure that it doesn't blow up (e.g. with unicode/bytes issues in
  4. # python 3)
  5. import unittest
  6. from tornado.auth import (
  7. OpenIdMixin,
  8. OAuthMixin,
  9. OAuth2Mixin,
  10. GoogleOAuth2Mixin,
  11. FacebookGraphMixin,
  12. TwitterMixin,
  13. )
  14. from tornado.escape import json_decode
  15. from tornado import gen
  16. from tornado.httpclient import HTTPClientError
  17. from tornado.httputil import url_concat
  18. from tornado.log import app_log
  19. from tornado.testing import AsyncHTTPTestCase, ExpectLog
  20. from tornado.web import RequestHandler, Application, HTTPError
  21. try:
  22. from unittest import mock
  23. except ImportError:
  24. mock = None # type: ignore
  25. class OpenIdClientLoginHandler(RequestHandler, OpenIdMixin):
  26. def initialize(self, test):
  27. self._OPENID_ENDPOINT = test.get_url("/openid/server/authenticate")
  28. @gen.coroutine
  29. def get(self):
  30. if self.get_argument("openid.mode", None):
  31. user = yield self.get_authenticated_user(
  32. http_client=self.settings["http_client"]
  33. )
  34. if user is None:
  35. raise Exception("user is None")
  36. self.finish(user)
  37. return
  38. res = self.authenticate_redirect()
  39. assert res is None
  40. class OpenIdServerAuthenticateHandler(RequestHandler):
  41. def post(self):
  42. if self.get_argument("openid.mode") != "check_authentication":
  43. raise Exception("incorrect openid.mode %r")
  44. self.write("is_valid:true")
  45. class OAuth1ClientLoginHandler(RequestHandler, OAuthMixin):
  46. def initialize(self, test, version):
  47. self._OAUTH_VERSION = version
  48. self._OAUTH_REQUEST_TOKEN_URL = test.get_url("/oauth1/server/request_token")
  49. self._OAUTH_AUTHORIZE_URL = test.get_url("/oauth1/server/authorize")
  50. self._OAUTH_ACCESS_TOKEN_URL = test.get_url("/oauth1/server/access_token")
  51. def _oauth_consumer_token(self):
  52. return dict(key="asdf", secret="qwer")
  53. @gen.coroutine
  54. def get(self):
  55. if self.get_argument("oauth_token", None):
  56. user = yield self.get_authenticated_user(
  57. http_client=self.settings["http_client"]
  58. )
  59. if user is None:
  60. raise Exception("user is None")
  61. self.finish(user)
  62. return
  63. yield self.authorize_redirect(http_client=self.settings["http_client"])
  64. @gen.coroutine
  65. def _oauth_get_user_future(self, access_token):
  66. if self.get_argument("fail_in_get_user", None):
  67. raise Exception("failing in get_user")
  68. if access_token != dict(key="uiop", secret="5678"):
  69. raise Exception("incorrect access token %r" % access_token)
  70. return dict(email="foo@example.com")
  71. class OAuth1ClientLoginCoroutineHandler(OAuth1ClientLoginHandler):
  72. """Replaces OAuth1ClientLoginCoroutineHandler's get() with a coroutine."""
  73. @gen.coroutine
  74. def get(self):
  75. if self.get_argument("oauth_token", None):
  76. # Ensure that any exceptions are set on the returned Future,
  77. # not simply thrown into the surrounding StackContext.
  78. try:
  79. yield self.get_authenticated_user()
  80. except Exception as e:
  81. self.set_status(503)
  82. self.write("got exception: %s" % e)
  83. else:
  84. yield self.authorize_redirect()
  85. class OAuth1ClientRequestParametersHandler(RequestHandler, OAuthMixin):
  86. def initialize(self, version):
  87. self._OAUTH_VERSION = version
  88. def _oauth_consumer_token(self):
  89. return dict(key="asdf", secret="qwer")
  90. def get(self):
  91. params = self._oauth_request_parameters(
  92. "http://www.example.com/api/asdf",
  93. dict(key="uiop", secret="5678"),
  94. parameters=dict(foo="bar"),
  95. )
  96. self.write(params)
  97. class OAuth1ServerRequestTokenHandler(RequestHandler):
  98. def get(self):
  99. self.write("oauth_token=zxcv&oauth_token_secret=1234")
  100. class OAuth1ServerAccessTokenHandler(RequestHandler):
  101. def get(self):
  102. self.write("oauth_token=uiop&oauth_token_secret=5678")
  103. class OAuth2ClientLoginHandler(RequestHandler, OAuth2Mixin):
  104. def initialize(self, test):
  105. self._OAUTH_AUTHORIZE_URL = test.get_url("/oauth2/server/authorize")
  106. def get(self):
  107. res = self.authorize_redirect()
  108. assert res is None
  109. class FacebookClientLoginHandler(RequestHandler, FacebookGraphMixin):
  110. def initialize(self, test):
  111. self._OAUTH_AUTHORIZE_URL = test.get_url("/facebook/server/authorize")
  112. self._OAUTH_ACCESS_TOKEN_URL = test.get_url("/facebook/server/access_token")
  113. self._FACEBOOK_BASE_URL = test.get_url("/facebook/server")
  114. @gen.coroutine
  115. def get(self):
  116. if self.get_argument("code", None):
  117. user = yield self.get_authenticated_user(
  118. redirect_uri=self.request.full_url(),
  119. client_id=self.settings["facebook_api_key"],
  120. client_secret=self.settings["facebook_secret"],
  121. code=self.get_argument("code"),
  122. )
  123. self.write(user)
  124. else:
  125. yield self.authorize_redirect(
  126. redirect_uri=self.request.full_url(),
  127. client_id=self.settings["facebook_api_key"],
  128. extra_params={"scope": "read_stream,offline_access"},
  129. )
  130. class FacebookServerAccessTokenHandler(RequestHandler):
  131. def get(self):
  132. self.write(dict(access_token="asdf", expires_in=3600))
  133. class FacebookServerMeHandler(RequestHandler):
  134. def get(self):
  135. self.write("{}")
  136. class TwitterClientHandler(RequestHandler, TwitterMixin):
  137. def initialize(self, test):
  138. self._OAUTH_REQUEST_TOKEN_URL = test.get_url("/oauth1/server/request_token")
  139. self._OAUTH_ACCESS_TOKEN_URL = test.get_url("/twitter/server/access_token")
  140. self._OAUTH_AUTHORIZE_URL = test.get_url("/oauth1/server/authorize")
  141. self._OAUTH_AUTHENTICATE_URL = test.get_url("/twitter/server/authenticate")
  142. self._TWITTER_BASE_URL = test.get_url("/twitter/api")
  143. def get_auth_http_client(self):
  144. return self.settings["http_client"]
  145. class TwitterClientLoginHandler(TwitterClientHandler):
  146. @gen.coroutine
  147. def get(self):
  148. if self.get_argument("oauth_token", None):
  149. user = yield self.get_authenticated_user()
  150. if user is None:
  151. raise Exception("user is None")
  152. self.finish(user)
  153. return
  154. yield self.authorize_redirect()
  155. class TwitterClientAuthenticateHandler(TwitterClientHandler):
  156. # Like TwitterClientLoginHandler, but uses authenticate_redirect
  157. # instead of authorize_redirect.
  158. @gen.coroutine
  159. def get(self):
  160. if self.get_argument("oauth_token", None):
  161. user = yield self.get_authenticated_user()
  162. if user is None:
  163. raise Exception("user is None")
  164. self.finish(user)
  165. return
  166. yield self.authenticate_redirect()
  167. class TwitterClientLoginGenCoroutineHandler(TwitterClientHandler):
  168. @gen.coroutine
  169. def get(self):
  170. if self.get_argument("oauth_token", None):
  171. user = yield self.get_authenticated_user()
  172. self.finish(user)
  173. else:
  174. # New style: with @gen.coroutine the result must be yielded
  175. # or else the request will be auto-finished too soon.
  176. yield self.authorize_redirect()
  177. class TwitterClientShowUserHandler(TwitterClientHandler):
  178. @gen.coroutine
  179. def get(self):
  180. # TODO: would be nice to go through the login flow instead of
  181. # cheating with a hard-coded access token.
  182. try:
  183. response = yield self.twitter_request(
  184. "/users/show/%s" % self.get_argument("name"),
  185. access_token=dict(key="hjkl", secret="vbnm"),
  186. )
  187. except HTTPClientError:
  188. # TODO(bdarnell): Should we catch HTTP errors and
  189. # transform some of them (like 403s) into AuthError?
  190. self.set_status(500)
  191. self.finish("error from twitter request")
  192. else:
  193. self.finish(response)
  194. class TwitterServerAccessTokenHandler(RequestHandler):
  195. def get(self):
  196. self.write("oauth_token=hjkl&oauth_token_secret=vbnm&screen_name=foo")
  197. class TwitterServerShowUserHandler(RequestHandler):
  198. def get(self, screen_name):
  199. if screen_name == "error":
  200. raise HTTPError(500)
  201. assert "oauth_nonce" in self.request.arguments
  202. assert "oauth_timestamp" in self.request.arguments
  203. assert "oauth_signature" in self.request.arguments
  204. assert self.get_argument("oauth_consumer_key") == "test_twitter_consumer_key"
  205. assert self.get_argument("oauth_signature_method") == "HMAC-SHA1"
  206. assert self.get_argument("oauth_version") == "1.0"
  207. assert self.get_argument("oauth_token") == "hjkl"
  208. self.write(dict(screen_name=screen_name, name=screen_name.capitalize()))
  209. class TwitterServerVerifyCredentialsHandler(RequestHandler):
  210. def get(self):
  211. assert "oauth_nonce" in self.request.arguments
  212. assert "oauth_timestamp" in self.request.arguments
  213. assert "oauth_signature" in self.request.arguments
  214. assert self.get_argument("oauth_consumer_key") == "test_twitter_consumer_key"
  215. assert self.get_argument("oauth_signature_method") == "HMAC-SHA1"
  216. assert self.get_argument("oauth_version") == "1.0"
  217. assert self.get_argument("oauth_token") == "hjkl"
  218. self.write(dict(screen_name="foo", name="Foo"))
  219. class AuthTest(AsyncHTTPTestCase):
  220. def get_app(self):
  221. return Application(
  222. [
  223. # test endpoints
  224. ("/openid/client/login", OpenIdClientLoginHandler, dict(test=self)),
  225. (
  226. "/oauth10/client/login",
  227. OAuth1ClientLoginHandler,
  228. dict(test=self, version="1.0"),
  229. ),
  230. (
  231. "/oauth10/client/request_params",
  232. OAuth1ClientRequestParametersHandler,
  233. dict(version="1.0"),
  234. ),
  235. (
  236. "/oauth10a/client/login",
  237. OAuth1ClientLoginHandler,
  238. dict(test=self, version="1.0a"),
  239. ),
  240. (
  241. "/oauth10a/client/login_coroutine",
  242. OAuth1ClientLoginCoroutineHandler,
  243. dict(test=self, version="1.0a"),
  244. ),
  245. (
  246. "/oauth10a/client/request_params",
  247. OAuth1ClientRequestParametersHandler,
  248. dict(version="1.0a"),
  249. ),
  250. ("/oauth2/client/login", OAuth2ClientLoginHandler, dict(test=self)),
  251. ("/facebook/client/login", FacebookClientLoginHandler, dict(test=self)),
  252. ("/twitter/client/login", TwitterClientLoginHandler, dict(test=self)),
  253. (
  254. "/twitter/client/authenticate",
  255. TwitterClientAuthenticateHandler,
  256. dict(test=self),
  257. ),
  258. (
  259. "/twitter/client/login_gen_coroutine",
  260. TwitterClientLoginGenCoroutineHandler,
  261. dict(test=self),
  262. ),
  263. (
  264. "/twitter/client/show_user",
  265. TwitterClientShowUserHandler,
  266. dict(test=self),
  267. ),
  268. # simulated servers
  269. ("/openid/server/authenticate", OpenIdServerAuthenticateHandler),
  270. ("/oauth1/server/request_token", OAuth1ServerRequestTokenHandler),
  271. ("/oauth1/server/access_token", OAuth1ServerAccessTokenHandler),
  272. ("/facebook/server/access_token", FacebookServerAccessTokenHandler),
  273. ("/facebook/server/me", FacebookServerMeHandler),
  274. ("/twitter/server/access_token", TwitterServerAccessTokenHandler),
  275. (r"/twitter/api/users/show/(.*)\.json", TwitterServerShowUserHandler),
  276. (
  277. r"/twitter/api/account/verify_credentials\.json",
  278. TwitterServerVerifyCredentialsHandler,
  279. ),
  280. ],
  281. http_client=self.http_client,
  282. twitter_consumer_key="test_twitter_consumer_key",
  283. twitter_consumer_secret="test_twitter_consumer_secret",
  284. facebook_api_key="test_facebook_api_key",
  285. facebook_secret="test_facebook_secret",
  286. )
  287. def test_openid_redirect(self):
  288. response = self.fetch("/openid/client/login", follow_redirects=False)
  289. self.assertEqual(response.code, 302)
  290. self.assertTrue("/openid/server/authenticate?" in response.headers["Location"])
  291. def test_openid_get_user(self):
  292. response = self.fetch(
  293. "/openid/client/login?openid.mode=blah"
  294. "&openid.ns.ax=http://openid.net/srv/ax/1.0"
  295. "&openid.ax.type.email=http://axschema.org/contact/email"
  296. "&openid.ax.value.email=foo@example.com"
  297. )
  298. response.rethrow()
  299. parsed = json_decode(response.body)
  300. self.assertEqual(parsed["email"], "foo@example.com")
  301. def test_oauth10_redirect(self):
  302. response = self.fetch("/oauth10/client/login", follow_redirects=False)
  303. self.assertEqual(response.code, 302)
  304. self.assertTrue(
  305. response.headers["Location"].endswith(
  306. "/oauth1/server/authorize?oauth_token=zxcv"
  307. )
  308. )
  309. # the cookie is base64('zxcv')|base64('1234')
  310. self.assertTrue(
  311. '_oauth_request_token="enhjdg==|MTIzNA=="'
  312. in response.headers["Set-Cookie"],
  313. response.headers["Set-Cookie"],
  314. )
  315. def test_oauth10_get_user(self):
  316. response = self.fetch(
  317. "/oauth10/client/login?oauth_token=zxcv",
  318. headers={"Cookie": "_oauth_request_token=enhjdg==|MTIzNA=="},
  319. )
  320. response.rethrow()
  321. parsed = json_decode(response.body)
  322. self.assertEqual(parsed["email"], "foo@example.com")
  323. self.assertEqual(parsed["access_token"], dict(key="uiop", secret="5678"))
  324. def test_oauth10_request_parameters(self):
  325. response = self.fetch("/oauth10/client/request_params")
  326. response.rethrow()
  327. parsed = json_decode(response.body)
  328. self.assertEqual(parsed["oauth_consumer_key"], "asdf")
  329. self.assertEqual(parsed["oauth_token"], "uiop")
  330. self.assertTrue("oauth_nonce" in parsed)
  331. self.assertTrue("oauth_signature" in parsed)
  332. def test_oauth10a_redirect(self):
  333. response = self.fetch("/oauth10a/client/login", follow_redirects=False)
  334. self.assertEqual(response.code, 302)
  335. self.assertTrue(
  336. response.headers["Location"].endswith(
  337. "/oauth1/server/authorize?oauth_token=zxcv"
  338. )
  339. )
  340. # the cookie is base64('zxcv')|base64('1234')
  341. self.assertTrue(
  342. '_oauth_request_token="enhjdg==|MTIzNA=="'
  343. in response.headers["Set-Cookie"],
  344. response.headers["Set-Cookie"],
  345. )
  346. @unittest.skipIf(mock is None, "mock package not present")
  347. def test_oauth10a_redirect_error(self):
  348. with mock.patch.object(OAuth1ServerRequestTokenHandler, "get") as get:
  349. get.side_effect = Exception("boom")
  350. with ExpectLog(app_log, "Uncaught exception"):
  351. response = self.fetch("/oauth10a/client/login", follow_redirects=False)
  352. self.assertEqual(response.code, 500)
  353. def test_oauth10a_get_user(self):
  354. response = self.fetch(
  355. "/oauth10a/client/login?oauth_token=zxcv",
  356. headers={"Cookie": "_oauth_request_token=enhjdg==|MTIzNA=="},
  357. )
  358. response.rethrow()
  359. parsed = json_decode(response.body)
  360. self.assertEqual(parsed["email"], "foo@example.com")
  361. self.assertEqual(parsed["access_token"], dict(key="uiop", secret="5678"))
  362. def test_oauth10a_request_parameters(self):
  363. response = self.fetch("/oauth10a/client/request_params")
  364. response.rethrow()
  365. parsed = json_decode(response.body)
  366. self.assertEqual(parsed["oauth_consumer_key"], "asdf")
  367. self.assertEqual(parsed["oauth_token"], "uiop")
  368. self.assertTrue("oauth_nonce" in parsed)
  369. self.assertTrue("oauth_signature" in parsed)
  370. def test_oauth10a_get_user_coroutine_exception(self):
  371. response = self.fetch(
  372. "/oauth10a/client/login_coroutine?oauth_token=zxcv&fail_in_get_user=true",
  373. headers={"Cookie": "_oauth_request_token=enhjdg==|MTIzNA=="},
  374. )
  375. self.assertEqual(response.code, 503)
  376. def test_oauth2_redirect(self):
  377. response = self.fetch("/oauth2/client/login", follow_redirects=False)
  378. self.assertEqual(response.code, 302)
  379. self.assertTrue("/oauth2/server/authorize?" in response.headers["Location"])
  380. def test_facebook_login(self):
  381. response = self.fetch("/facebook/client/login", follow_redirects=False)
  382. self.assertEqual(response.code, 302)
  383. self.assertTrue("/facebook/server/authorize?" in response.headers["Location"])
  384. response = self.fetch(
  385. "/facebook/client/login?code=1234", follow_redirects=False
  386. )
  387. self.assertEqual(response.code, 200)
  388. user = json_decode(response.body)
  389. self.assertEqual(user["access_token"], "asdf")
  390. self.assertEqual(user["session_expires"], "3600")
  391. def base_twitter_redirect(self, url):
  392. # Same as test_oauth10a_redirect
  393. response = self.fetch(url, follow_redirects=False)
  394. self.assertEqual(response.code, 302)
  395. self.assertTrue(
  396. response.headers["Location"].endswith(
  397. "/oauth1/server/authorize?oauth_token=zxcv"
  398. )
  399. )
  400. # the cookie is base64('zxcv')|base64('1234')
  401. self.assertTrue(
  402. '_oauth_request_token="enhjdg==|MTIzNA=="'
  403. in response.headers["Set-Cookie"],
  404. response.headers["Set-Cookie"],
  405. )
  406. def test_twitter_redirect(self):
  407. self.base_twitter_redirect("/twitter/client/login")
  408. def test_twitter_redirect_gen_coroutine(self):
  409. self.base_twitter_redirect("/twitter/client/login_gen_coroutine")
  410. def test_twitter_authenticate_redirect(self):
  411. response = self.fetch("/twitter/client/authenticate", follow_redirects=False)
  412. self.assertEqual(response.code, 302)
  413. self.assertTrue(
  414. response.headers["Location"].endswith(
  415. "/twitter/server/authenticate?oauth_token=zxcv"
  416. ),
  417. response.headers["Location"],
  418. )
  419. # the cookie is base64('zxcv')|base64('1234')
  420. self.assertTrue(
  421. '_oauth_request_token="enhjdg==|MTIzNA=="'
  422. in response.headers["Set-Cookie"],
  423. response.headers["Set-Cookie"],
  424. )
  425. def test_twitter_get_user(self):
  426. response = self.fetch(
  427. "/twitter/client/login?oauth_token=zxcv",
  428. headers={"Cookie": "_oauth_request_token=enhjdg==|MTIzNA=="},
  429. )
  430. response.rethrow()
  431. parsed = json_decode(response.body)
  432. self.assertEqual(
  433. parsed,
  434. {
  435. u"access_token": {
  436. u"key": u"hjkl",
  437. u"screen_name": u"foo",
  438. u"secret": u"vbnm",
  439. },
  440. u"name": u"Foo",
  441. u"screen_name": u"foo",
  442. u"username": u"foo",
  443. },
  444. )
  445. def test_twitter_show_user(self):
  446. response = self.fetch("/twitter/client/show_user?name=somebody")
  447. response.rethrow()
  448. self.assertEqual(
  449. json_decode(response.body), {"name": "Somebody", "screen_name": "somebody"}
  450. )
  451. def test_twitter_show_user_error(self):
  452. response = self.fetch("/twitter/client/show_user?name=error")
  453. self.assertEqual(response.code, 500)
  454. self.assertEqual(response.body, b"error from twitter request")
  455. class GoogleLoginHandler(RequestHandler, GoogleOAuth2Mixin):
  456. def initialize(self, test):
  457. self.test = test
  458. self._OAUTH_REDIRECT_URI = test.get_url("/client/login")
  459. self._OAUTH_AUTHORIZE_URL = test.get_url("/google/oauth2/authorize")
  460. self._OAUTH_ACCESS_TOKEN_URL = test.get_url("/google/oauth2/token")
  461. @gen.coroutine
  462. def get(self):
  463. code = self.get_argument("code", None)
  464. if code is not None:
  465. # retrieve authenticate google user
  466. access = yield self.get_authenticated_user(self._OAUTH_REDIRECT_URI, code)
  467. user = yield self.oauth2_request(
  468. self.test.get_url("/google/oauth2/userinfo"),
  469. access_token=access["access_token"],
  470. )
  471. # return the user and access token as json
  472. user["access_token"] = access["access_token"]
  473. self.write(user)
  474. else:
  475. yield self.authorize_redirect(
  476. redirect_uri=self._OAUTH_REDIRECT_URI,
  477. client_id=self.settings["google_oauth"]["key"],
  478. client_secret=self.settings["google_oauth"]["secret"],
  479. scope=["profile", "email"],
  480. response_type="code",
  481. extra_params={"prompt": "select_account"},
  482. )
  483. class GoogleOAuth2AuthorizeHandler(RequestHandler):
  484. def get(self):
  485. # issue a fake auth code and redirect to redirect_uri
  486. code = "fake-authorization-code"
  487. self.redirect(url_concat(self.get_argument("redirect_uri"), dict(code=code)))
  488. class GoogleOAuth2TokenHandler(RequestHandler):
  489. def post(self):
  490. assert self.get_argument("code") == "fake-authorization-code"
  491. # issue a fake token
  492. self.finish(
  493. {"access_token": "fake-access-token", "expires_in": "never-expires"}
  494. )
  495. class GoogleOAuth2UserinfoHandler(RequestHandler):
  496. def get(self):
  497. assert self.get_argument("access_token") == "fake-access-token"
  498. # return a fake user
  499. self.finish({"name": "Foo", "email": "foo@example.com"})
  500. class GoogleOAuth2Test(AsyncHTTPTestCase):
  501. def get_app(self):
  502. return Application(
  503. [
  504. # test endpoints
  505. ("/client/login", GoogleLoginHandler, dict(test=self)),
  506. # simulated google authorization server endpoints
  507. ("/google/oauth2/authorize", GoogleOAuth2AuthorizeHandler),
  508. ("/google/oauth2/token", GoogleOAuth2TokenHandler),
  509. ("/google/oauth2/userinfo", GoogleOAuth2UserinfoHandler),
  510. ],
  511. google_oauth={
  512. "key": "fake_google_client_id",
  513. "secret": "fake_google_client_secret",
  514. },
  515. )
  516. def test_google_login(self):
  517. response = self.fetch("/client/login")
  518. self.assertDictEqual(
  519. {
  520. u"name": u"Foo",
  521. u"email": u"foo@example.com",
  522. u"access_token": u"fake-access-token",
  523. },
  524. json_decode(response.body),
  525. )