blob: faccccf0b6e01f76f9d42bd6c740a38b14588c75 [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):
102 http = HttpMockSequence([
103 ({'status': '401'}, ''),
104 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
105 ({'status': '200'}, 'echo_request_headers'),
106 ])
107 http = self.credentials.authorize(http)
108 resp, content = http.request("http://example.com")
Joe Gregorio654f4a22012-02-09 14:15:44 -0500109 self.assertEqual('Bearer 1/3w', content['Authorization'])
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400110 self.assertFalse(self.credentials.access_token_expired)
Joe Gregorioccc79542011-02-19 00:05:26 -0500111
112 def test_token_refresh_failure(self):
113 http = HttpMockSequence([
114 ({'status': '401'}, ''),
115 ({'status': '400'}, '{"error":"access_denied"}'),
116 ])
117 http = self.credentials.authorize(http)
118 try:
119 http.request("http://example.com")
120 self.fail("should raise AccessTokenRefreshError exception")
121 except AccessTokenRefreshError:
122 pass
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400123 self.assertTrue(self.credentials.access_token_expired)
Joe Gregorioccc79542011-02-19 00:05:26 -0500124
125 def test_non_401_error_response(self):
126 http = HttpMockSequence([
127 ({'status': '400'}, ''),
128 ])
129 http = self.credentials.authorize(http)
130 resp, content = http.request("http://example.com")
131 self.assertEqual(400, resp.status)
132
Joe Gregorio562b7312011-09-15 09:06:38 -0400133 def test_to_from_json(self):
134 json = self.credentials.to_json()
135 instance = OAuth2Credentials.from_json(json)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500136 self.assertEqual(OAuth2Credentials, type(instance))
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400137 instance.token_expiry = None
138 self.credentials.token_expiry = None
139
Joe Gregorio654f4a22012-02-09 14:15:44 -0500140 self.assertEqual(instance.__dict__, self.credentials.__dict__)
Joe Gregorio562b7312011-09-15 09:06:38 -0400141
Joe Gregorioccc79542011-02-19 00:05:26 -0500142
143class AccessTokenCredentialsTests(unittest.TestCase):
144
145 def setUp(self):
146 access_token = "foo"
147 user_agent = "refresh_checker/1.0"
148 self.credentials = AccessTokenCredentials(access_token, user_agent)
149
150 def test_token_refresh_success(self):
151 http = HttpMockSequence([
152 ({'status': '401'}, ''),
153 ])
154 http = self.credentials.authorize(http)
155 try:
156 resp, content = http.request("http://example.com")
157 self.fail("should throw exception if token expires")
158 except AccessTokenCredentialsError:
159 pass
160 except Exception:
161 self.fail("should only throw AccessTokenCredentialsError")
162
163 def test_non_401_error_response(self):
164 http = HttpMockSequence([
165 ({'status': '400'}, ''),
166 ])
167 http = self.credentials.authorize(http)
Joe Gregorio83cd4392011-06-20 10:11:35 -0400168 resp, content = http.request('http://example.com')
Joe Gregorioccc79542011-02-19 00:05:26 -0500169 self.assertEqual(400, resp.status)
170
Joe Gregorio83cd4392011-06-20 10:11:35 -0400171 def test_auth_header_sent(self):
172 http = HttpMockSequence([
173 ({'status': '200'}, 'echo_request_headers'),
174 ])
175 http = self.credentials.authorize(http)
176 resp, content = http.request('http://example.com')
Joe Gregorio654f4a22012-02-09 14:15:44 -0500177 self.assertEqual('Bearer foo', content['Authorization'])
Joe Gregorioccc79542011-02-19 00:05:26 -0500178
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500179
JacobMoshenko8e905102011-06-20 09:53:10 -0400180class TestAssertionCredentials(unittest.TestCase):
181 assertion_text = "This is the assertion"
182 assertion_type = "http://www.google.com/assertionType"
183
184 class AssertionCredentialsTestImpl(AssertionCredentials):
185
186 def _generate_assertion(self):
187 return TestAssertionCredentials.assertion_text
188
189 def setUp(self):
190 user_agent = "fun/2.0"
191 self.credentials = self.AssertionCredentialsTestImpl(self.assertion_type,
192 user_agent)
193
194 def test_assertion_body(self):
195 body = urlparse.parse_qs(self.credentials._generate_refresh_request_body())
Joe Gregorio654f4a22012-02-09 14:15:44 -0500196 self.assertEqual(self.assertion_text, body['assertion'][0])
197 self.assertEqual(self.assertion_type, body['assertion_type'][0])
JacobMoshenko8e905102011-06-20 09:53:10 -0400198
199 def test_assertion_refresh(self):
200 http = HttpMockSequence([
201 ({'status': '200'}, '{"access_token":"1/3w"}'),
202 ({'status': '200'}, 'echo_request_headers'),
203 ])
204 http = self.credentials.authorize(http)
205 resp, content = http.request("http://example.com")
Joe Gregorio654f4a22012-02-09 14:15:44 -0500206 self.assertEqual('Bearer 1/3w', content['Authorization'])
JacobMoshenko8e905102011-06-20 09:53:10 -0400207
208
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500209class ExtractIdTokenText(unittest.TestCase):
210 """Tests _extract_id_token()."""
211
212 def test_extract_success(self):
213 body = {'foo': 'bar'}
214 payload = base64.urlsafe_b64encode(simplejson.dumps(body)).strip('=')
215 jwt = 'stuff.' + payload + '.signature'
216
217 extracted = _extract_id_token(jwt)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500218 self.assertEqual(extracted, body)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500219
220 def test_extract_failure(self):
221 body = {'foo': 'bar'}
222 payload = base64.urlsafe_b64encode(simplejson.dumps(body)).strip('=')
223 jwt = 'stuff.' + payload
224
225 self.assertRaises(VerifyJwtTokenError, _extract_id_token, jwt)
226
Joe Gregorioccc79542011-02-19 00:05:26 -0500227class OAuth2WebServerFlowTest(unittest.TestCase):
228
229 def setUp(self):
230 self.flow = OAuth2WebServerFlow(
231 client_id='client_id+1',
232 client_secret='secret+1',
233 scope='foo',
234 user_agent='unittest-sample/1.0',
235 )
236
237 def test_construct_authorize_url(self):
Joe Gregoriof2326c02012-02-09 12:18:44 -0500238 authorize_url = self.flow.step1_get_authorize_url('OOB_CALLBACK_URN')
Joe Gregorioccc79542011-02-19 00:05:26 -0500239
240 parsed = urlparse.urlparse(authorize_url)
241 q = parse_qs(parsed[4])
Joe Gregorio654f4a22012-02-09 14:15:44 -0500242 self.assertEqual('client_id+1', q['client_id'][0])
243 self.assertEqual('code', q['response_type'][0])
244 self.assertEqual('foo', q['scope'][0])
245 self.assertEqual('OOB_CALLBACK_URN', q['redirect_uri'][0])
246 self.assertEqual('offline', q['access_type'][0])
Joe Gregorio69a0aca2011-11-03 10:47:32 -0400247
248 def test_override_flow_access_type(self):
249 """Passing access_type overrides the default."""
250 flow = OAuth2WebServerFlow(
251 client_id='client_id+1',
252 client_secret='secret+1',
253 scope='foo',
254 user_agent='unittest-sample/1.0',
255 access_type='online'
256 )
Joe Gregoriof2326c02012-02-09 12:18:44 -0500257 authorize_url = flow.step1_get_authorize_url('OOB_CALLBACK_URN')
Joe Gregorio69a0aca2011-11-03 10:47:32 -0400258
259 parsed = urlparse.urlparse(authorize_url)
260 q = parse_qs(parsed[4])
Joe Gregorio654f4a22012-02-09 14:15:44 -0500261 self.assertEqual('client_id+1', q['client_id'][0])
262 self.assertEqual('code', q['response_type'][0])
263 self.assertEqual('foo', q['scope'][0])
264 self.assertEqual('OOB_CALLBACK_URN', q['redirect_uri'][0])
265 self.assertEqual('online', q['access_type'][0])
Joe Gregorioccc79542011-02-19 00:05:26 -0500266
267 def test_exchange_failure(self):
268 http = HttpMockSequence([
JacobMoshenko8e905102011-06-20 09:53:10 -0400269 ({'status': '400'}, '{"error":"invalid_request"}'),
Joe Gregorioccc79542011-02-19 00:05:26 -0500270 ])
271
272 try:
273 credentials = self.flow.step2_exchange('some random code', http)
274 self.fail("should raise exception if exchange doesn't get 200")
275 except FlowExchangeError:
276 pass
277
Joe Gregorioddb969a2012-07-11 11:04:12 -0400278 def test_urlencoded_exchange_failure(self):
279 http = HttpMockSequence([
280 ({'status': '400'}, "error=invalid_request"),
281 ])
282
283 try:
284 credentials = self.flow.step2_exchange('some random code', http)
285 self.fail("should raise exception if exchange doesn't get 200")
286 except FlowExchangeError, e:
287 self.assertEquals('invalid_request', str(e))
288
289 def test_exchange_failure_with_json_error(self):
290 # Some providers have "error" attribute as a JSON object
291 # in place of regular string.
292 # This test makes sure no strange object-to-string coversion
293 # exceptions are being raised instead of FlowExchangeError.
294 http = HttpMockSequence([
295 ({'status': '400'},
296 """ {"error": {
297 "type": "OAuthException",
298 "message": "Error validating verification code."} }"""),
299 ])
300
301 try:
302 credentials = self.flow.step2_exchange('some random code', http)
303 self.fail("should raise exception if exchange doesn't get 200")
304 except FlowExchangeError, e:
305 pass
306
Joe Gregorioccc79542011-02-19 00:05:26 -0500307 def test_exchange_success(self):
308 http = HttpMockSequence([
309 ({'status': '200'},
310 """{ "access_token":"SlAV32hkKG",
311 "expires_in":3600,
312 "refresh_token":"8xLOxBtZp8" }"""),
313 ])
314
315 credentials = self.flow.step2_exchange('some random code', http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500316 self.assertEqual('SlAV32hkKG', credentials.access_token)
317 self.assertNotEqual(None, credentials.token_expiry)
318 self.assertEqual('8xLOxBtZp8', credentials.refresh_token)
Joe Gregorioccc79542011-02-19 00:05:26 -0500319
Joe Gregorioddb969a2012-07-11 11:04:12 -0400320 def test_urlencoded_exchange_success(self):
321 http = HttpMockSequence([
322 ({'status': '200'}, "access_token=SlAV32hkKG&expires_in=3600"),
323 ])
324
325 credentials = self.flow.step2_exchange('some random code', http)
326 self.assertEqual('SlAV32hkKG', credentials.access_token)
327 self.assertNotEqual(None, credentials.token_expiry)
328
329 def test_urlencoded_expires_param(self):
330 http = HttpMockSequence([
331 # Note the "expires=3600" where you'd normally
332 # have if named "expires_in"
333 ({'status': '200'}, "access_token=SlAV32hkKG&expires=3600"),
334 ])
335
336 credentials = self.flow.step2_exchange('some random code', http)
337 self.assertNotEqual(None, credentials.token_expiry)
338
Joe Gregorioccc79542011-02-19 00:05:26 -0500339 def test_exchange_no_expires_in(self):
340 http = HttpMockSequence([
341 ({'status': '200'}, """{ "access_token":"SlAV32hkKG",
342 "refresh_token":"8xLOxBtZp8" }"""),
343 ])
344
345 credentials = self.flow.step2_exchange('some random code', http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500346 self.assertEqual(None, credentials.token_expiry)
Joe Gregorioccc79542011-02-19 00:05:26 -0500347
Joe Gregorioddb969a2012-07-11 11:04:12 -0400348 def test_urlencoded_exchange_no_expires_in(self):
349 http = HttpMockSequence([
350 # This might be redundant but just to make sure
351 # urlencoded access_token gets parsed correctly
352 ({'status': '200'}, "access_token=SlAV32hkKG"),
353 ])
354
355 credentials = self.flow.step2_exchange('some random code', http)
356 self.assertEqual(None, credentials.token_expiry)
357
Joe Gregorio4b4002f2012-06-14 15:41:01 -0400358 def test_exchange_fails_if_no_code(self):
359 http = HttpMockSequence([
360 ({'status': '200'}, """{ "access_token":"SlAV32hkKG",
361 "refresh_token":"8xLOxBtZp8" }"""),
362 ])
363
364 code = {'error': 'thou shall not pass'}
365 try:
366 credentials = self.flow.step2_exchange(code, http)
367 self.fail('should raise exception if no code in dictionary.')
368 except FlowExchangeError, e:
369 self.assertTrue('shall not pass' in str(e))
370
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500371 def test_exchange_id_token_fail(self):
372 http = HttpMockSequence([
373 ({'status': '200'}, """{ "access_token":"SlAV32hkKG",
374 "refresh_token":"8xLOxBtZp8",
375 "id_token": "stuff.payload"}"""),
376 ])
377
378 self.assertRaises(VerifyJwtTokenError, self.flow.step2_exchange,
379 'some random code', http)
380
381 def test_exchange_id_token_fail(self):
382 body = {'foo': 'bar'}
383 payload = base64.urlsafe_b64encode(simplejson.dumps(body)).strip('=')
Joe Gregoriobd512b52011-12-06 15:39:26 -0500384 jwt = (base64.urlsafe_b64encode('stuff')+ '.' + payload + '.' +
385 base64.urlsafe_b64encode('signature'))
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500386
387 http = HttpMockSequence([
388 ({'status': '200'}, """{ "access_token":"SlAV32hkKG",
389 "refresh_token":"8xLOxBtZp8",
390 "id_token": "%s"}""" % jwt),
391 ])
392
393 credentials = self.flow.step2_exchange('some random code', http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500394 self.assertEqual(credentials.id_token, body)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500395
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400396class FlowFromCachedClientsecrets(unittest.TestCase):
397
398 def test_flow_from_clientsecrets_cached(self):
399 cache_mock = CacheMock()
400 load_and_cache('client_secrets.json', 'some_secrets', cache_mock)
401
402 # flow_from_clientsecrets(filename, scope, message=None, cache=None)
403 flow = flow_from_clientsecrets('some_secrets', '', cache=cache_mock)
404 self.assertEquals('foo_client_secret', flow.client_secret)
405
Joe Gregorio32d852d2012-06-14 09:08:18 -0400406class CredentialsFromCodeTests(unittest.TestCase):
407 def setUp(self):
408 self.client_id = 'client_id_abc'
409 self.client_secret = 'secret_use_code'
410 self.scope = 'foo'
411 self.code = '12345abcde'
412 self.redirect_uri = 'postmessage'
413
414 def test_exchange_code_for_token(self):
415 http = HttpMockSequence([
416 ({'status': '200'},
417 """{ "access_token":"asdfghjkl",
418 "expires_in":3600 }"""),
419 ])
420 credentials = credentials_from_code(self.client_id, self.client_secret,
421 self.scope, self.code, self.redirect_uri,
422 http)
423 self.assertEquals(credentials.access_token, 'asdfghjkl')
424 self.assertNotEqual(None, credentials.token_expiry)
425
426 def test_exchange_code_for_token_fail(self):
427 http = HttpMockSequence([
428 ({'status': '400'}, '{"error":"invalid_request"}'),
429 ])
430
431 try:
432 credentials = credentials_from_code(self.client_id, self.client_secret,
433 self.scope, self.code, self.redirect_uri,
434 http)
435 self.fail("should raise exception if exchange doesn't get 200")
436 except FlowExchangeError:
437 pass
438
439
440 def test_exchange_code_and_file_for_token(self):
441 http = HttpMockSequence([
442 ({'status': '200'},
443 """{ "access_token":"asdfghjkl",
444 "expires_in":3600 }"""),
445 ])
446 credentials = credentials_from_clientsecrets_and_code(
447 datafile('client_secrets.json'), self.scope,
448 self.code, http=http)
449 self.assertEquals(credentials.access_token, 'asdfghjkl')
450 self.assertNotEqual(None, credentials.token_expiry)
451
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400452 def test_exchange_code_and_cached_file_for_token(self):
453 http = HttpMockSequence([
454 ({'status': '200'}, '{ "access_token":"asdfghjkl"}'),
455 ])
456 cache_mock = CacheMock()
457 load_and_cache('client_secrets.json', 'some_secrets', cache_mock)
458
459 credentials = credentials_from_clientsecrets_and_code(
460 'some_secrets', self.scope,
461 self.code, http=http, cache=cache_mock)
462 self.assertEquals(credentials.access_token, 'asdfghjkl')
463
Joe Gregorio32d852d2012-06-14 09:08:18 -0400464 def test_exchange_code_and_file_for_token_fail(self):
465 http = HttpMockSequence([
466 ({'status': '400'}, '{"error":"invalid_request"}'),
467 ])
468
469 try:
470 credentials = credentials_from_clientsecrets_and_code(
471 datafile('client_secrets.json'), self.scope,
472 self.code, http=http)
473 self.fail("should raise exception if exchange doesn't get 200")
474 except FlowExchangeError:
475 pass
476
477
Joe Gregorioccc79542011-02-19 00:05:26 -0500478
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400479class MemoryCacheTests(unittest.TestCase):
480
481 def test_get_set_delete(self):
482 m = MemoryCache()
483 self.assertEqual(None, m.get('foo'))
484 self.assertEqual(None, m.delete('foo'))
485 m.set('foo', 'bar')
486 self.assertEqual('bar', m.get('foo'))
487 m.delete('foo')
488 self.assertEqual(None, m.get('foo'))
489
490
Joe Gregorioccc79542011-02-19 00:05:26 -0500491if __name__ == '__main__':
492 unittest.main()