blob: 49433df1de2b04b9206fc6b165bf6fdfe4a56b94 [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 Gregorioccc79542011-02-19 00:05:26 -050039from oauth2client.client import AccessTokenCredentials
40from oauth2client.client import AccessTokenCredentialsError
41from oauth2client.client import AccessTokenRefreshError
JacobMoshenko8e905102011-06-20 09:53:10 -040042from oauth2client.client import AssertionCredentials
Joe Gregorio08cdcb82012-03-14 00:09:33 -040043from oauth2client.client import Credentials
Joe Gregorioccc79542011-02-19 00:05:26 -050044from oauth2client.client import FlowExchangeError
Joe Gregorio08cdcb82012-03-14 00:09:33 -040045from oauth2client.client import MemoryCache
Joe Gregorioccc79542011-02-19 00:05:26 -050046from oauth2client.client import OAuth2Credentials
47from oauth2client.client import OAuth2WebServerFlow
Joe Gregoriof2326c02012-02-09 12:18:44 -050048from oauth2client.client import OOB_CALLBACK_URN
Joe Gregorio8b4c1732011-12-06 11:28:29 -050049from oauth2client.client import VerifyJwtTokenError
50from oauth2client.client import _extract_id_token
Joe Gregorio32d852d2012-06-14 09:08:18 -040051from oauth2client.client import credentials_from_code
52from oauth2client.client import credentials_from_clientsecrets_and_code
53
54DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
55
56def datafile(filename):
57 return os.path.join(DATA_DIR, filename)
Joe Gregorioccc79542011-02-19 00:05:26 -050058
59
Joe Gregorio08cdcb82012-03-14 00:09:33 -040060class CredentialsTests(unittest.TestCase):
61
62 def test_to_from_json(self):
63 credentials = Credentials()
64 json = credentials.to_json()
65 restored = Credentials.new_from_json(json)
66
67
Joe Gregorioccc79542011-02-19 00:05:26 -050068class OAuth2CredentialsTests(unittest.TestCase):
69
70 def setUp(self):
71 access_token = "foo"
72 client_id = "some_client_id"
73 client_secret = "cOuDdkfjxxnv+"
74 refresh_token = "1/0/a.df219fjls0"
Joe Gregorio562b7312011-09-15 09:06:38 -040075 token_expiry = datetime.datetime.utcnow()
Joe Gregorioccc79542011-02-19 00:05:26 -050076 token_uri = "https://www.google.com/accounts/o8/oauth2/token"
77 user_agent = "refresh_checker/1.0"
78 self.credentials = OAuth2Credentials(
79 access_token, client_id, client_secret,
80 refresh_token, token_expiry, token_uri,
81 user_agent)
82
83 def test_token_refresh_success(self):
84 http = HttpMockSequence([
85 ({'status': '401'}, ''),
86 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
87 ({'status': '200'}, 'echo_request_headers'),
88 ])
89 http = self.credentials.authorize(http)
90 resp, content = http.request("http://example.com")
Joe Gregorio654f4a22012-02-09 14:15:44 -050091 self.assertEqual('Bearer 1/3w', content['Authorization'])
Joe Gregorio08cdcb82012-03-14 00:09:33 -040092 self.assertFalse(self.credentials.access_token_expired)
Joe Gregorioccc79542011-02-19 00:05:26 -050093
94 def test_token_refresh_failure(self):
95 http = HttpMockSequence([
96 ({'status': '401'}, ''),
97 ({'status': '400'}, '{"error":"access_denied"}'),
98 ])
99 http = self.credentials.authorize(http)
100 try:
101 http.request("http://example.com")
102 self.fail("should raise AccessTokenRefreshError exception")
103 except AccessTokenRefreshError:
104 pass
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400105 self.assertTrue(self.credentials.access_token_expired)
Joe Gregorioccc79542011-02-19 00:05:26 -0500106
107 def test_non_401_error_response(self):
108 http = HttpMockSequence([
109 ({'status': '400'}, ''),
110 ])
111 http = self.credentials.authorize(http)
112 resp, content = http.request("http://example.com")
113 self.assertEqual(400, resp.status)
114
Joe Gregorio562b7312011-09-15 09:06:38 -0400115 def test_to_from_json(self):
116 json = self.credentials.to_json()
117 instance = OAuth2Credentials.from_json(json)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500118 self.assertEqual(OAuth2Credentials, type(instance))
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400119 instance.token_expiry = None
120 self.credentials.token_expiry = None
121
Joe Gregorio654f4a22012-02-09 14:15:44 -0500122 self.assertEqual(instance.__dict__, self.credentials.__dict__)
Joe Gregorio562b7312011-09-15 09:06:38 -0400123
Joe Gregorioccc79542011-02-19 00:05:26 -0500124
125class AccessTokenCredentialsTests(unittest.TestCase):
126
127 def setUp(self):
128 access_token = "foo"
129 user_agent = "refresh_checker/1.0"
130 self.credentials = AccessTokenCredentials(access_token, user_agent)
131
132 def test_token_refresh_success(self):
133 http = HttpMockSequence([
134 ({'status': '401'}, ''),
135 ])
136 http = self.credentials.authorize(http)
137 try:
138 resp, content = http.request("http://example.com")
139 self.fail("should throw exception if token expires")
140 except AccessTokenCredentialsError:
141 pass
142 except Exception:
143 self.fail("should only throw AccessTokenCredentialsError")
144
145 def test_non_401_error_response(self):
146 http = HttpMockSequence([
147 ({'status': '400'}, ''),
148 ])
149 http = self.credentials.authorize(http)
Joe Gregorio83cd4392011-06-20 10:11:35 -0400150 resp, content = http.request('http://example.com')
Joe Gregorioccc79542011-02-19 00:05:26 -0500151 self.assertEqual(400, resp.status)
152
Joe Gregorio83cd4392011-06-20 10:11:35 -0400153 def test_auth_header_sent(self):
154 http = HttpMockSequence([
155 ({'status': '200'}, 'echo_request_headers'),
156 ])
157 http = self.credentials.authorize(http)
158 resp, content = http.request('http://example.com')
Joe Gregorio654f4a22012-02-09 14:15:44 -0500159 self.assertEqual('Bearer foo', content['Authorization'])
Joe Gregorioccc79542011-02-19 00:05:26 -0500160
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500161
JacobMoshenko8e905102011-06-20 09:53:10 -0400162class TestAssertionCredentials(unittest.TestCase):
163 assertion_text = "This is the assertion"
164 assertion_type = "http://www.google.com/assertionType"
165
166 class AssertionCredentialsTestImpl(AssertionCredentials):
167
168 def _generate_assertion(self):
169 return TestAssertionCredentials.assertion_text
170
171 def setUp(self):
172 user_agent = "fun/2.0"
173 self.credentials = self.AssertionCredentialsTestImpl(self.assertion_type,
174 user_agent)
175
176 def test_assertion_body(self):
177 body = urlparse.parse_qs(self.credentials._generate_refresh_request_body())
Joe Gregorio654f4a22012-02-09 14:15:44 -0500178 self.assertEqual(self.assertion_text, body['assertion'][0])
179 self.assertEqual(self.assertion_type, body['assertion_type'][0])
JacobMoshenko8e905102011-06-20 09:53:10 -0400180
181 def test_assertion_refresh(self):
182 http = HttpMockSequence([
183 ({'status': '200'}, '{"access_token":"1/3w"}'),
184 ({'status': '200'}, 'echo_request_headers'),
185 ])
186 http = self.credentials.authorize(http)
187 resp, content = http.request("http://example.com")
Joe Gregorio654f4a22012-02-09 14:15:44 -0500188 self.assertEqual('Bearer 1/3w', content['Authorization'])
JacobMoshenko8e905102011-06-20 09:53:10 -0400189
190
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500191class ExtractIdTokenText(unittest.TestCase):
192 """Tests _extract_id_token()."""
193
194 def test_extract_success(self):
195 body = {'foo': 'bar'}
196 payload = base64.urlsafe_b64encode(simplejson.dumps(body)).strip('=')
197 jwt = 'stuff.' + payload + '.signature'
198
199 extracted = _extract_id_token(jwt)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500200 self.assertEqual(extracted, body)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500201
202 def test_extract_failure(self):
203 body = {'foo': 'bar'}
204 payload = base64.urlsafe_b64encode(simplejson.dumps(body)).strip('=')
205 jwt = 'stuff.' + payload
206
207 self.assertRaises(VerifyJwtTokenError, _extract_id_token, jwt)
208
Joe Gregorioccc79542011-02-19 00:05:26 -0500209class OAuth2WebServerFlowTest(unittest.TestCase):
210
211 def setUp(self):
212 self.flow = OAuth2WebServerFlow(
213 client_id='client_id+1',
214 client_secret='secret+1',
215 scope='foo',
216 user_agent='unittest-sample/1.0',
217 )
218
219 def test_construct_authorize_url(self):
Joe Gregoriof2326c02012-02-09 12:18:44 -0500220 authorize_url = self.flow.step1_get_authorize_url('OOB_CALLBACK_URN')
Joe Gregorioccc79542011-02-19 00:05:26 -0500221
222 parsed = urlparse.urlparse(authorize_url)
223 q = parse_qs(parsed[4])
Joe Gregorio654f4a22012-02-09 14:15:44 -0500224 self.assertEqual('client_id+1', q['client_id'][0])
225 self.assertEqual('code', q['response_type'][0])
226 self.assertEqual('foo', q['scope'][0])
227 self.assertEqual('OOB_CALLBACK_URN', q['redirect_uri'][0])
228 self.assertEqual('offline', q['access_type'][0])
Joe Gregorio69a0aca2011-11-03 10:47:32 -0400229
230 def test_override_flow_access_type(self):
231 """Passing access_type overrides the default."""
232 flow = OAuth2WebServerFlow(
233 client_id='client_id+1',
234 client_secret='secret+1',
235 scope='foo',
236 user_agent='unittest-sample/1.0',
237 access_type='online'
238 )
Joe Gregoriof2326c02012-02-09 12:18:44 -0500239 authorize_url = flow.step1_get_authorize_url('OOB_CALLBACK_URN')
Joe Gregorio69a0aca2011-11-03 10:47:32 -0400240
241 parsed = urlparse.urlparse(authorize_url)
242 q = parse_qs(parsed[4])
Joe Gregorio654f4a22012-02-09 14:15:44 -0500243 self.assertEqual('client_id+1', q['client_id'][0])
244 self.assertEqual('code', q['response_type'][0])
245 self.assertEqual('foo', q['scope'][0])
246 self.assertEqual('OOB_CALLBACK_URN', q['redirect_uri'][0])
247 self.assertEqual('online', q['access_type'][0])
Joe Gregorioccc79542011-02-19 00:05:26 -0500248
249 def test_exchange_failure(self):
250 http = HttpMockSequence([
JacobMoshenko8e905102011-06-20 09:53:10 -0400251 ({'status': '400'}, '{"error":"invalid_request"}'),
Joe Gregorioccc79542011-02-19 00:05:26 -0500252 ])
253
254 try:
255 credentials = self.flow.step2_exchange('some random code', http)
256 self.fail("should raise exception if exchange doesn't get 200")
257 except FlowExchangeError:
258 pass
259
Joe Gregorioddb969a2012-07-11 11:04:12 -0400260 def test_urlencoded_exchange_failure(self):
261 http = HttpMockSequence([
262 ({'status': '400'}, "error=invalid_request"),
263 ])
264
265 try:
266 credentials = self.flow.step2_exchange('some random code', http)
267 self.fail("should raise exception if exchange doesn't get 200")
268 except FlowExchangeError, e:
269 self.assertEquals('invalid_request', str(e))
270
271 def test_exchange_failure_with_json_error(self):
272 # Some providers have "error" attribute as a JSON object
273 # in place of regular string.
274 # This test makes sure no strange object-to-string coversion
275 # exceptions are being raised instead of FlowExchangeError.
276 http = HttpMockSequence([
277 ({'status': '400'},
278 """ {"error": {
279 "type": "OAuthException",
280 "message": "Error validating verification code."} }"""),
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 pass
288
Joe Gregorioccc79542011-02-19 00:05:26 -0500289 def test_exchange_success(self):
290 http = HttpMockSequence([
291 ({'status': '200'},
292 """{ "access_token":"SlAV32hkKG",
293 "expires_in":3600,
294 "refresh_token":"8xLOxBtZp8" }"""),
295 ])
296
297 credentials = self.flow.step2_exchange('some random code', http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500298 self.assertEqual('SlAV32hkKG', credentials.access_token)
299 self.assertNotEqual(None, credentials.token_expiry)
300 self.assertEqual('8xLOxBtZp8', credentials.refresh_token)
Joe Gregorioccc79542011-02-19 00:05:26 -0500301
Joe Gregorioddb969a2012-07-11 11:04:12 -0400302 def test_urlencoded_exchange_success(self):
303 http = HttpMockSequence([
304 ({'status': '200'}, "access_token=SlAV32hkKG&expires_in=3600"),
305 ])
306
307 credentials = self.flow.step2_exchange('some random code', http)
308 self.assertEqual('SlAV32hkKG', credentials.access_token)
309 self.assertNotEqual(None, credentials.token_expiry)
310
311 def test_urlencoded_expires_param(self):
312 http = HttpMockSequence([
313 # Note the "expires=3600" where you'd normally
314 # have if named "expires_in"
315 ({'status': '200'}, "access_token=SlAV32hkKG&expires=3600"),
316 ])
317
318 credentials = self.flow.step2_exchange('some random code', http)
319 self.assertNotEqual(None, credentials.token_expiry)
320
Joe Gregorioccc79542011-02-19 00:05:26 -0500321 def test_exchange_no_expires_in(self):
322 http = HttpMockSequence([
323 ({'status': '200'}, """{ "access_token":"SlAV32hkKG",
324 "refresh_token":"8xLOxBtZp8" }"""),
325 ])
326
327 credentials = self.flow.step2_exchange('some random code', http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500328 self.assertEqual(None, credentials.token_expiry)
Joe Gregorioccc79542011-02-19 00:05:26 -0500329
Joe Gregorioddb969a2012-07-11 11:04:12 -0400330 def test_urlencoded_exchange_no_expires_in(self):
331 http = HttpMockSequence([
332 # This might be redundant but just to make sure
333 # urlencoded access_token gets parsed correctly
334 ({'status': '200'}, "access_token=SlAV32hkKG"),
335 ])
336
337 credentials = self.flow.step2_exchange('some random code', http)
338 self.assertEqual(None, credentials.token_expiry)
339
Joe Gregorio4b4002f2012-06-14 15:41:01 -0400340 def test_exchange_fails_if_no_code(self):
341 http = HttpMockSequence([
342 ({'status': '200'}, """{ "access_token":"SlAV32hkKG",
343 "refresh_token":"8xLOxBtZp8" }"""),
344 ])
345
346 code = {'error': 'thou shall not pass'}
347 try:
348 credentials = self.flow.step2_exchange(code, http)
349 self.fail('should raise exception if no code in dictionary.')
350 except FlowExchangeError, e:
351 self.assertTrue('shall not pass' in str(e))
352
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500353 def test_exchange_id_token_fail(self):
354 http = HttpMockSequence([
355 ({'status': '200'}, """{ "access_token":"SlAV32hkKG",
356 "refresh_token":"8xLOxBtZp8",
357 "id_token": "stuff.payload"}"""),
358 ])
359
360 self.assertRaises(VerifyJwtTokenError, self.flow.step2_exchange,
361 'some random code', http)
362
363 def test_exchange_id_token_fail(self):
364 body = {'foo': 'bar'}
365 payload = base64.urlsafe_b64encode(simplejson.dumps(body)).strip('=')
Joe Gregoriobd512b52011-12-06 15:39:26 -0500366 jwt = (base64.urlsafe_b64encode('stuff')+ '.' + payload + '.' +
367 base64.urlsafe_b64encode('signature'))
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500368
369 http = HttpMockSequence([
370 ({'status': '200'}, """{ "access_token":"SlAV32hkKG",
371 "refresh_token":"8xLOxBtZp8",
372 "id_token": "%s"}""" % jwt),
373 ])
374
375 credentials = self.flow.step2_exchange('some random code', http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500376 self.assertEqual(credentials.id_token, body)
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500377
Joe Gregorio32d852d2012-06-14 09:08:18 -0400378class CredentialsFromCodeTests(unittest.TestCase):
379 def setUp(self):
380 self.client_id = 'client_id_abc'
381 self.client_secret = 'secret_use_code'
382 self.scope = 'foo'
383 self.code = '12345abcde'
384 self.redirect_uri = 'postmessage'
385
386 def test_exchange_code_for_token(self):
387 http = HttpMockSequence([
388 ({'status': '200'},
389 """{ "access_token":"asdfghjkl",
390 "expires_in":3600 }"""),
391 ])
392 credentials = credentials_from_code(self.client_id, self.client_secret,
393 self.scope, self.code, self.redirect_uri,
394 http)
395 self.assertEquals(credentials.access_token, 'asdfghjkl')
396 self.assertNotEqual(None, credentials.token_expiry)
397
398 def test_exchange_code_for_token_fail(self):
399 http = HttpMockSequence([
400 ({'status': '400'}, '{"error":"invalid_request"}'),
401 ])
402
403 try:
404 credentials = credentials_from_code(self.client_id, self.client_secret,
405 self.scope, self.code, self.redirect_uri,
406 http)
407 self.fail("should raise exception if exchange doesn't get 200")
408 except FlowExchangeError:
409 pass
410
411
412 def test_exchange_code_and_file_for_token(self):
413 http = HttpMockSequence([
414 ({'status': '200'},
415 """{ "access_token":"asdfghjkl",
416 "expires_in":3600 }"""),
417 ])
418 credentials = credentials_from_clientsecrets_and_code(
419 datafile('client_secrets.json'), self.scope,
420 self.code, http=http)
421 self.assertEquals(credentials.access_token, 'asdfghjkl')
422 self.assertNotEqual(None, credentials.token_expiry)
423
424 def test_exchange_code_and_file_for_token_fail(self):
425 http = HttpMockSequence([
426 ({'status': '400'}, '{"error":"invalid_request"}'),
427 ])
428
429 try:
430 credentials = credentials_from_clientsecrets_and_code(
431 datafile('client_secrets.json'), self.scope,
432 self.code, http=http)
433 self.fail("should raise exception if exchange doesn't get 200")
434 except FlowExchangeError:
435 pass
436
437
Joe Gregorioccc79542011-02-19 00:05:26 -0500438
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400439class MemoryCacheTests(unittest.TestCase):
440
441 def test_get_set_delete(self):
442 m = MemoryCache()
443 self.assertEqual(None, m.get('foo'))
444 self.assertEqual(None, m.delete('foo'))
445 m.set('foo', 'bar')
446 self.assertEqual('bar', m.get('foo'))
447 m.delete('foo')
448 self.assertEqual(None, m.get('foo'))
449
450
Joe Gregorioccc79542011-02-19 00:05:26 -0500451if __name__ == '__main__':
452 unittest.main()