blob: a618674cd9a14e756d051acece06c502a89d4366 [file] [log] [blame]
Joe Gregorioccc79542011-02-19 00:05:26 -05001#!/usr/bin/python2.4
2#
3# Copyright 2010 Google Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17
Joe Gregorio0bc70912011-05-24 15:30:49 -040018"""Oauth2client tests
Joe Gregorioccc79542011-02-19 00:05:26 -050019
Joe Gregorio0bc70912011-05-24 15:30:49 -040020Unit tests for oauth2client.
Joe Gregorioccc79542011-02-19 00:05:26 -050021"""
22
23__author__ = 'jcgregorio@google.com (Joe Gregorio)'
24
Joe Gregorio8b4c1732011-12-06 11:28:29 -050025import base64
Joe Gregorio562b7312011-09-15 09:06:38 -040026import datetime
Joe Gregorioe1de4162011-02-23 11:30:29 -050027import httplib2
Joe Gregorio32d852d2012-06-14 09:08:18 -040028import os
Joe Gregorioccc79542011-02-19 00:05:26 -050029import unittest
30import urlparse
Joe Gregorioe1de4162011-02-23 11:30:29 -050031
Joe Gregorioccc79542011-02-19 00:05:26 -050032try:
33 from urlparse import parse_qs
34except ImportError:
35 from cgi import parse_qs
36
37from apiclient.http import HttpMockSequence
Joe Gregorio549230c2012-01-11 10:38:05 -050038from oauth2client.anyjson import simplejson
Joe Gregorioc29aaa92012-07-16 16:16:31 -040039from oauth2client.clientsecrets import _loadfile
Joe Gregorioccc79542011-02-19 00:05:26 -050040from oauth2client.client import AccessTokenCredentials
41from oauth2client.client import AccessTokenCredentialsError
42from oauth2client.client import AccessTokenRefreshError
JacobMoshenko8e905102011-06-20 09:53:10 -040043from oauth2client.client import AssertionCredentials
Joe Gregorio08cdcb82012-03-14 00:09:33 -040044from oauth2client.client import Credentials
Joe Gregorioccc79542011-02-19 00:05:26 -050045from oauth2client.client import FlowExchangeError
Joe Gregorio08cdcb82012-03-14 00:09:33 -040046from oauth2client.client import MemoryCache
Joe Gregorioccc79542011-02-19 00:05:26 -050047from oauth2client.client import OAuth2Credentials
48from oauth2client.client import OAuth2WebServerFlow
Joe Gregoriof2326c02012-02-09 12:18:44 -050049from oauth2client.client import OOB_CALLBACK_URN
Joe Gregorio8b4c1732011-12-06 11:28:29 -050050from oauth2client.client import VerifyJwtTokenError
51from oauth2client.client import _extract_id_token
Joe Gregorio32d852d2012-06-14 09:08:18 -040052from oauth2client.client import credentials_from_code
53from oauth2client.client import credentials_from_clientsecrets_and_code
Joe Gregorioc29aaa92012-07-16 16:16:31 -040054from oauth2client.client import flow_from_clientsecrets
Joe Gregorio32d852d2012-06-14 09:08:18 -040055
56DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
57
58def datafile(filename):
59 return os.path.join(DATA_DIR, filename)
Joe Gregorioccc79542011-02-19 00:05:26 -050060
Joe Gregorioc29aaa92012-07-16 16:16:31 -040061def load_and_cache(existing_file, fakename, cache_mock):
62 client_type, client_info = _loadfile(datafile(existing_file))
63 cache_mock.cache[fakename] = {client_type: client_info}
64
65class CacheMock(object):
66 def __init__(self):
67 self.cache = {}
68
69 def get(self, key, namespace=''):
70 # ignoring namespace for easier testing
71 return self.cache.get(key, None)
72
73 def set(self, key, value, namespace=''):
74 # ignoring namespace for easier testing
75 self.cache[key] = value
76
Joe Gregorioccc79542011-02-19 00:05:26 -050077
Joe Gregorio08cdcb82012-03-14 00:09:33 -040078class CredentialsTests(unittest.TestCase):
79
80 def test_to_from_json(self):
81 credentials = Credentials()
82 json = credentials.to_json()
83 restored = Credentials.new_from_json(json)
84
85
Joe Gregorioccc79542011-02-19 00:05:26 -050086class OAuth2CredentialsTests(unittest.TestCase):
87
88 def setUp(self):
89 access_token = "foo"
90 client_id = "some_client_id"
91 client_secret = "cOuDdkfjxxnv+"
92 refresh_token = "1/0/a.df219fjls0"
Joe Gregorio562b7312011-09-15 09:06:38 -040093 token_expiry = datetime.datetime.utcnow()
Joe Gregorioccc79542011-02-19 00:05:26 -050094 token_uri = "https://www.google.com/accounts/o8/oauth2/token"
95 user_agent = "refresh_checker/1.0"
96 self.credentials = OAuth2Credentials(
97 access_token, client_id, client_secret,
98 refresh_token, token_expiry, token_uri,
99 user_agent)
100
101 def test_token_refresh_success(self):
Joe Gregorio7c7c6b12012-07-16 16:31:01 -0400102 # Older API (GData) respond with 403
103 for status_code in ['401', '403']:
104 http = HttpMockSequence([
105 ({'status': status_code}, ''),
106 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
107 ({'status': '200'}, 'echo_request_headers'),
108 ])
109 http = self.credentials.authorize(http)
110 resp, content = http.request("http://example.com")
111 self.assertEqual('Bearer 1/3w', content['Authorization'])
112 self.assertFalse(self.credentials.access_token_expired)
Joe Gregorioccc79542011-02-19 00:05:26 -0500113
114 def test_token_refresh_failure(self):
Joe Gregorio7c7c6b12012-07-16 16:31:01 -0400115 # Older API (GData) respond with 403
116 for status_code in ['401', '403']:
117 http = HttpMockSequence([
118 ({'status': status_code}, ''),
119 ({'status': '400'}, '{"error":"access_denied"}'),
120 ])
121 http = self.credentials.authorize(http)
122 try:
123 http.request("http://example.com")
124 self.fail("should raise AccessTokenRefreshError exception")
125 except AccessTokenRefreshError:
126 pass
127 self.assertTrue(self.credentials.access_token_expired)
Joe Gregorioccc79542011-02-19 00:05:26 -0500128
129 def test_non_401_error_response(self):
130 http = HttpMockSequence([
131 ({'status': '400'}, ''),
132 ])
133 http = self.credentials.authorize(http)
134 resp, content = http.request("http://example.com")
135 self.assertEqual(400, resp.status)
136
Joe Gregorio562b7312011-09-15 09:06:38 -0400137 def test_to_from_json(self):
138 json = self.credentials.to_json()
139 instance = OAuth2Credentials.from_json(json)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500140 self.assertEqual(OAuth2Credentials, type(instance))
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400141 instance.token_expiry = None
142 self.credentials.token_expiry = None
143
Joe Gregorio654f4a22012-02-09 14:15:44 -0500144 self.assertEqual(instance.__dict__, self.credentials.__dict__)
Joe Gregorio562b7312011-09-15 09:06:38 -0400145
Joe Gregorioccc79542011-02-19 00:05:26 -0500146
147class AccessTokenCredentialsTests(unittest.TestCase):
148
149 def setUp(self):
150 access_token = "foo"
151 user_agent = "refresh_checker/1.0"
152 self.credentials = AccessTokenCredentials(access_token, user_agent)
153
154 def test_token_refresh_success(self):
Joe Gregorio7c7c6b12012-07-16 16:31:01 -0400155 # Older API (GData) respond with 403
156 for status_code in ['401', '403']:
157 http = HttpMockSequence([
158 ({'status': status_code}, ''),
159 ])
160 http = self.credentials.authorize(http)
161 try:
162 resp, content = http.request("http://example.com")
163 self.fail("should throw exception if token expires")
164 except AccessTokenCredentialsError:
165 pass
166 except Exception:
167 self.fail("should only throw AccessTokenCredentialsError")
Joe Gregorioccc79542011-02-19 00:05:26 -0500168
169 def test_non_401_error_response(self):
170 http = HttpMockSequence([
171 ({'status': '400'}, ''),
172 ])
173 http = self.credentials.authorize(http)
Joe Gregorio83cd4392011-06-20 10:11:35 -0400174 resp, content = http.request('http://example.com')
Joe Gregorioccc79542011-02-19 00:05:26 -0500175 self.assertEqual(400, resp.status)
176
Joe Gregorio83cd4392011-06-20 10:11:35 -0400177 def test_auth_header_sent(self):
178 http = HttpMockSequence([
179 ({'status': '200'}, 'echo_request_headers'),
180 ])
181 http = self.credentials.authorize(http)
182 resp, content = http.request('http://example.com')
Joe Gregorio654f4a22012-02-09 14:15:44 -0500183 self.assertEqual('Bearer foo', content['Authorization'])
Joe Gregorioccc79542011-02-19 00:05:26 -0500184
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500185
JacobMoshenko8e905102011-06-20 09:53:10 -0400186class TestAssertionCredentials(unittest.TestCase):
187 assertion_text = "This is the assertion"
188 assertion_type = "http://www.google.com/assertionType"
189
190 class AssertionCredentialsTestImpl(AssertionCredentials):
191
192 def _generate_assertion(self):
193 return TestAssertionCredentials.assertion_text
194
195 def setUp(self):
196 user_agent = "fun/2.0"
197 self.credentials = self.AssertionCredentialsTestImpl(self.assertion_type,
198 user_agent)
199
200 def test_assertion_body(self):
201 body = urlparse.parse_qs(self.credentials._generate_refresh_request_body())
Joe Gregorio654f4a22012-02-09 14:15:44 -0500202 self.assertEqual(self.assertion_text, body['assertion'][0])
203 self.assertEqual(self.assertion_type, body['assertion_type'][0])
JacobMoshenko8e905102011-06-20 09:53:10 -0400204
205 def test_assertion_refresh(self):
206 http = HttpMockSequence([
207 ({'status': '200'}, '{"access_token":"1/3w"}'),
208 ({'status': '200'}, 'echo_request_headers'),
209 ])
210 http = self.credentials.authorize(http)
211 resp, content = http.request("http://example.com")
Joe Gregorio654f4a22012-02-09 14:15:44 -0500212 self.assertEqual('Bearer 1/3w', content['Authorization'])
JacobMoshenko8e905102011-06-20 09:53:10 -0400213
214
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500215class ExtractIdTokenText(unittest.TestCase):
216 """Tests _extract_id_token()."""
217
218 def test_extract_success(self):
219 body = {'foo': 'bar'}
220 payload = base64.urlsafe_b64encode(simplejson.dumps(body)).strip('=')
221 jwt = 'stuff.' + payload + '.signature'
222
223 extracted = _extract_id_token(jwt)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500224 self.assertEqual(extracted, body)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500225
226 def test_extract_failure(self):
227 body = {'foo': 'bar'}
228 payload = base64.urlsafe_b64encode(simplejson.dumps(body)).strip('=')
229 jwt = 'stuff.' + payload
230
231 self.assertRaises(VerifyJwtTokenError, _extract_id_token, jwt)
232
Joe Gregorioccc79542011-02-19 00:05:26 -0500233class OAuth2WebServerFlowTest(unittest.TestCase):
234
235 def setUp(self):
236 self.flow = OAuth2WebServerFlow(
237 client_id='client_id+1',
238 client_secret='secret+1',
239 scope='foo',
240 user_agent='unittest-sample/1.0',
241 )
242
243 def test_construct_authorize_url(self):
Joe Gregoriof2326c02012-02-09 12:18:44 -0500244 authorize_url = self.flow.step1_get_authorize_url('OOB_CALLBACK_URN')
Joe Gregorioccc79542011-02-19 00:05:26 -0500245
246 parsed = urlparse.urlparse(authorize_url)
247 q = parse_qs(parsed[4])
Joe Gregorio654f4a22012-02-09 14:15:44 -0500248 self.assertEqual('client_id+1', q['client_id'][0])
249 self.assertEqual('code', q['response_type'][0])
250 self.assertEqual('foo', q['scope'][0])
251 self.assertEqual('OOB_CALLBACK_URN', q['redirect_uri'][0])
252 self.assertEqual('offline', q['access_type'][0])
Joe Gregorio69a0aca2011-11-03 10:47:32 -0400253
254 def test_override_flow_access_type(self):
255 """Passing access_type overrides the default."""
256 flow = OAuth2WebServerFlow(
257 client_id='client_id+1',
258 client_secret='secret+1',
259 scope='foo',
260 user_agent='unittest-sample/1.0',
261 access_type='online'
262 )
Joe Gregoriof2326c02012-02-09 12:18:44 -0500263 authorize_url = flow.step1_get_authorize_url('OOB_CALLBACK_URN')
Joe Gregorio69a0aca2011-11-03 10:47:32 -0400264
265 parsed = urlparse.urlparse(authorize_url)
266 q = parse_qs(parsed[4])
Joe Gregorio654f4a22012-02-09 14:15:44 -0500267 self.assertEqual('client_id+1', q['client_id'][0])
268 self.assertEqual('code', q['response_type'][0])
269 self.assertEqual('foo', q['scope'][0])
270 self.assertEqual('OOB_CALLBACK_URN', q['redirect_uri'][0])
271 self.assertEqual('online', q['access_type'][0])
Joe Gregorioccc79542011-02-19 00:05:26 -0500272
273 def test_exchange_failure(self):
274 http = HttpMockSequence([
JacobMoshenko8e905102011-06-20 09:53:10 -0400275 ({'status': '400'}, '{"error":"invalid_request"}'),
Joe Gregorioccc79542011-02-19 00:05:26 -0500276 ])
277
278 try:
279 credentials = self.flow.step2_exchange('some random code', http)
280 self.fail("should raise exception if exchange doesn't get 200")
281 except FlowExchangeError:
282 pass
283
Joe Gregorioddb969a2012-07-11 11:04:12 -0400284 def test_urlencoded_exchange_failure(self):
285 http = HttpMockSequence([
286 ({'status': '400'}, "error=invalid_request"),
287 ])
288
289 try:
290 credentials = self.flow.step2_exchange('some random code', http)
291 self.fail("should raise exception if exchange doesn't get 200")
292 except FlowExchangeError, e:
293 self.assertEquals('invalid_request', str(e))
294
295 def test_exchange_failure_with_json_error(self):
296 # Some providers have "error" attribute as a JSON object
297 # in place of regular string.
298 # This test makes sure no strange object-to-string coversion
299 # exceptions are being raised instead of FlowExchangeError.
300 http = HttpMockSequence([
301 ({'status': '400'},
302 """ {"error": {
303 "type": "OAuthException",
304 "message": "Error validating verification code."} }"""),
305 ])
306
307 try:
308 credentials = self.flow.step2_exchange('some random code', http)
309 self.fail("should raise exception if exchange doesn't get 200")
310 except FlowExchangeError, e:
311 pass
312
Joe Gregorioccc79542011-02-19 00:05:26 -0500313 def test_exchange_success(self):
314 http = HttpMockSequence([
315 ({'status': '200'},
316 """{ "access_token":"SlAV32hkKG",
317 "expires_in":3600,
318 "refresh_token":"8xLOxBtZp8" }"""),
319 ])
320
321 credentials = self.flow.step2_exchange('some random code', http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500322 self.assertEqual('SlAV32hkKG', credentials.access_token)
323 self.assertNotEqual(None, credentials.token_expiry)
324 self.assertEqual('8xLOxBtZp8', credentials.refresh_token)
Joe Gregorioccc79542011-02-19 00:05:26 -0500325
Joe Gregorioddb969a2012-07-11 11:04:12 -0400326 def test_urlencoded_exchange_success(self):
327 http = HttpMockSequence([
328 ({'status': '200'}, "access_token=SlAV32hkKG&expires_in=3600"),
329 ])
330
331 credentials = self.flow.step2_exchange('some random code', http)
332 self.assertEqual('SlAV32hkKG', credentials.access_token)
333 self.assertNotEqual(None, credentials.token_expiry)
334
335 def test_urlencoded_expires_param(self):
336 http = HttpMockSequence([
337 # Note the "expires=3600" where you'd normally
338 # have if named "expires_in"
339 ({'status': '200'}, "access_token=SlAV32hkKG&expires=3600"),
340 ])
341
342 credentials = self.flow.step2_exchange('some random code', http)
343 self.assertNotEqual(None, credentials.token_expiry)
344
Joe Gregorioccc79542011-02-19 00:05:26 -0500345 def test_exchange_no_expires_in(self):
346 http = HttpMockSequence([
347 ({'status': '200'}, """{ "access_token":"SlAV32hkKG",
348 "refresh_token":"8xLOxBtZp8" }"""),
349 ])
350
351 credentials = self.flow.step2_exchange('some random code', http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500352 self.assertEqual(None, credentials.token_expiry)
Joe Gregorioccc79542011-02-19 00:05:26 -0500353
Joe Gregorioddb969a2012-07-11 11:04:12 -0400354 def test_urlencoded_exchange_no_expires_in(self):
355 http = HttpMockSequence([
356 # This might be redundant but just to make sure
357 # urlencoded access_token gets parsed correctly
358 ({'status': '200'}, "access_token=SlAV32hkKG"),
359 ])
360
361 credentials = self.flow.step2_exchange('some random code', http)
362 self.assertEqual(None, credentials.token_expiry)
363
Joe Gregorio4b4002f2012-06-14 15:41:01 -0400364 def test_exchange_fails_if_no_code(self):
365 http = HttpMockSequence([
366 ({'status': '200'}, """{ "access_token":"SlAV32hkKG",
367 "refresh_token":"8xLOxBtZp8" }"""),
368 ])
369
370 code = {'error': 'thou shall not pass'}
371 try:
372 credentials = self.flow.step2_exchange(code, http)
373 self.fail('should raise exception if no code in dictionary.')
374 except FlowExchangeError, e:
375 self.assertTrue('shall not pass' in str(e))
376
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500377 def test_exchange_id_token_fail(self):
378 http = HttpMockSequence([
379 ({'status': '200'}, """{ "access_token":"SlAV32hkKG",
380 "refresh_token":"8xLOxBtZp8",
381 "id_token": "stuff.payload"}"""),
382 ])
383
384 self.assertRaises(VerifyJwtTokenError, self.flow.step2_exchange,
385 'some random code', http)
386
387 def test_exchange_id_token_fail(self):
388 body = {'foo': 'bar'}
389 payload = base64.urlsafe_b64encode(simplejson.dumps(body)).strip('=')
Joe Gregoriobd512b52011-12-06 15:39:26 -0500390 jwt = (base64.urlsafe_b64encode('stuff')+ '.' + payload + '.' +
391 base64.urlsafe_b64encode('signature'))
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500392
393 http = HttpMockSequence([
394 ({'status': '200'}, """{ "access_token":"SlAV32hkKG",
395 "refresh_token":"8xLOxBtZp8",
396 "id_token": "%s"}""" % jwt),
397 ])
398
399 credentials = self.flow.step2_exchange('some random code', http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500400 self.assertEqual(credentials.id_token, body)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500401
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400402class FlowFromCachedClientsecrets(unittest.TestCase):
403
404 def test_flow_from_clientsecrets_cached(self):
405 cache_mock = CacheMock()
406 load_and_cache('client_secrets.json', 'some_secrets', cache_mock)
407
408 # flow_from_clientsecrets(filename, scope, message=None, cache=None)
409 flow = flow_from_clientsecrets('some_secrets', '', cache=cache_mock)
410 self.assertEquals('foo_client_secret', flow.client_secret)
411
Joe Gregorio32d852d2012-06-14 09:08:18 -0400412class CredentialsFromCodeTests(unittest.TestCase):
413 def setUp(self):
414 self.client_id = 'client_id_abc'
415 self.client_secret = 'secret_use_code'
416 self.scope = 'foo'
417 self.code = '12345abcde'
418 self.redirect_uri = 'postmessage'
419
420 def test_exchange_code_for_token(self):
421 http = HttpMockSequence([
422 ({'status': '200'},
423 """{ "access_token":"asdfghjkl",
424 "expires_in":3600 }"""),
425 ])
426 credentials = credentials_from_code(self.client_id, self.client_secret,
427 self.scope, self.code, self.redirect_uri,
428 http)
429 self.assertEquals(credentials.access_token, 'asdfghjkl')
430 self.assertNotEqual(None, credentials.token_expiry)
431
432 def test_exchange_code_for_token_fail(self):
433 http = HttpMockSequence([
434 ({'status': '400'}, '{"error":"invalid_request"}'),
435 ])
436
437 try:
438 credentials = credentials_from_code(self.client_id, self.client_secret,
439 self.scope, self.code, self.redirect_uri,
440 http)
441 self.fail("should raise exception if exchange doesn't get 200")
442 except FlowExchangeError:
443 pass
444
445
446 def test_exchange_code_and_file_for_token(self):
447 http = HttpMockSequence([
448 ({'status': '200'},
449 """{ "access_token":"asdfghjkl",
450 "expires_in":3600 }"""),
451 ])
452 credentials = credentials_from_clientsecrets_and_code(
453 datafile('client_secrets.json'), self.scope,
454 self.code, http=http)
455 self.assertEquals(credentials.access_token, 'asdfghjkl')
456 self.assertNotEqual(None, credentials.token_expiry)
457
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400458 def test_exchange_code_and_cached_file_for_token(self):
459 http = HttpMockSequence([
460 ({'status': '200'}, '{ "access_token":"asdfghjkl"}'),
461 ])
462 cache_mock = CacheMock()
463 load_and_cache('client_secrets.json', 'some_secrets', cache_mock)
464
465 credentials = credentials_from_clientsecrets_and_code(
466 'some_secrets', self.scope,
467 self.code, http=http, cache=cache_mock)
468 self.assertEquals(credentials.access_token, 'asdfghjkl')
469
Joe Gregorio32d852d2012-06-14 09:08:18 -0400470 def test_exchange_code_and_file_for_token_fail(self):
471 http = HttpMockSequence([
472 ({'status': '400'}, '{"error":"invalid_request"}'),
473 ])
474
475 try:
476 credentials = credentials_from_clientsecrets_and_code(
477 datafile('client_secrets.json'), self.scope,
478 self.code, http=http)
479 self.fail("should raise exception if exchange doesn't get 200")
480 except FlowExchangeError:
481 pass
482
483
Joe Gregorioccc79542011-02-19 00:05:26 -0500484
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400485class MemoryCacheTests(unittest.TestCase):
486
487 def test_get_set_delete(self):
488 m = MemoryCache()
489 self.assertEqual(None, m.get('foo'))
490 self.assertEqual(None, m.delete('foo'))
491 m.set('foo', 'bar')
492 self.assertEqual('bar', m.get('foo'))
493 m.delete('foo')
494 self.assertEqual(None, m.get('foo'))
495
496
Joe Gregorioccc79542011-02-19 00:05:26 -0500497if __name__ == '__main__':
498 unittest.main()