diff --git a/oidc_provider/tests/test_authorize_endpoint.py b/oidc_provider/tests/test_authorize_endpoint.py index cb0712c..f892ae2 100644 --- a/oidc_provider/tests/test_authorize_endpoint.py +++ b/oidc_provider/tests/test_authorize_endpoint.py @@ -2,6 +2,10 @@ try: from urllib.parse import urlencode except ImportError: from urllib import urlencode +try: + from urllib.parse import parse_qs, urlsplit +except ImportError: + from urlparse import parse_qs, urlsplit import uuid from django.contrib.auth.models import AnonymousUser @@ -9,6 +13,7 @@ from django.core.management import call_command from django.core.urlresolvers import reverse from django.test import RequestFactory from django.test import TestCase +from jwkest.jwt import JWT from oidc_provider import settings from oidc_provider.models import * @@ -18,7 +23,7 @@ from oidc_provider.views import * class AuthorizationCodeFlowTestCase(TestCase): """ - Test cases for Authorize Endpoint using Authorization Code Flow. + Test cases for Authorize Endpoint using Code Flow. """ def setUp(self): @@ -291,3 +296,168 @@ class AuthorizationCodeFlowTestCase(TestCase): # An error is returned if the Client does not have pre-configured consent for the requested Claims. self.assertEqual('interaction_required' in response['Location'], True) + + +class AuthorizationImplicitFlowTestCase(TestCase): + """ + Test cases for Authorization Endpoint using Implicit Flow. + """ + + def setUp(self): + call_command('creatersakey') + self.factory = RequestFactory() + self.user = create_fake_user() + self.client = create_fake_client(response_type='id_token token') + self.client_public = create_fake_client(response_type='id_token token', is_public=True) + self.client_no_access = create_fake_client(response_type='id_token') + self.client_public_no_access = create_fake_client(response_type='id_token', is_public=True) + self.state = uuid.uuid4().hex + self.nonce = uuid.uuid4().hex + + def _auth_request(self, method, data={}, is_user_authenticated=False): + url = reverse('oidc_provider:authorize') + + if method.lower() == 'get': + query_str = urlencode(data).replace('+', '%20') + if query_str: + url += '?' + query_str + request = self.factory.get(url) + elif method.lower() == 'post': + request = self.factory.post(url, data=data) + else: + raise Exception('Method unsupported for an Authorization Request.') + + # Simulate that the user is logged. + request.user = self.user if is_user_authenticated else AnonymousUser() + + response = AuthorizeView.as_view()(request) + + return response + + def test_missing_nonce(self): + """ + The `nonce` parameter is REQUIRED if you use the Implicit Flow. + """ + data = { + 'client_id': self.client.client_id, + 'response_type': self.client.response_type, + 'redirect_uri': self.client.default_redirect_uri, + 'scope': 'openid email', + 'state': self.state, + } + + response = self._auth_request('get', data, is_user_authenticated=True) + + self.assertEqual('#error=invalid_request' in response['Location'], True) + + def test_id_token_token_response(self): + """ + Implicit client requesting `id_token token` receives both id token + and access token as the result of the authorization request. + """ + data = { + 'client_id': self.client.client_id, + 'redirect_uri': self.client.default_redirect_uri, + 'response_type': self.client.response_type, + 'scope': 'openid email', + 'state': self.state, + 'nonce': self.nonce, + 'allow': 'Accept', + } + + response = self._auth_request('post', data, is_user_authenticated=True) + + self.assertIn('access_token', response['Location']) + self.assertIn('id_token', response['Location']) + + # same for public client + data['client_id'] = self.client_public.client_id, + data['redirect_uri'] = self.client_public.default_redirect_uri, + data['response_type'] = self.client_public.response_type, + + response = self._auth_request('post', data, is_user_authenticated=True) + + self.assertIn('access_token', response['Location']) + self.assertIn('id_token', response['Location']) + + def test_id_token_response(self): + """ + Implicit client requesting `id_token` receives + only an id token as the result of the authorization request. + """ + data = { + 'client_id': self.client_no_access.client_id, + 'redirect_uri': self.client_no_access.default_redirect_uri, + 'response_type': self.client_no_access.response_type, + 'scope': 'openid email', + 'state': self.state, + 'nonce': self.nonce, + 'allow': 'Accept', + } + + response = self._auth_request('post', data, is_user_authenticated=True) + + self.assertNotIn('access_token', response['Location']) + self.assertIn('id_token', response['Location']) + + # same for public client + data['client_id'] = self.client_public_no_access.client_id, + data['redirect_uri'] = self.client_public_no_access.default_redirect_uri, + data['response_type'] = self.client_public_no_access.response_type, + + response = self._auth_request('post', data, is_user_authenticated=True) + + self.assertNotIn('access_token', response['Location']) + self.assertIn('id_token', response['Location']) + + def test_id_token_token_at_hash(self): + """ + Implicit client requesting `id_token token` receives + `at_hash` in `id_token`. + """ + data = { + 'client_id': self.client.client_id, + 'redirect_uri': self.client.default_redirect_uri, + 'response_type': self.client.response_type, + 'scope': 'openid email', + 'state': self.state, + 'nonce': self.nonce, + 'allow': 'Accept', + } + + response = self._auth_request('post', data, is_user_authenticated=True) + + self.assertIn('id_token', response['Location']) + + # obtain `id_token` portion of Location + components = urlsplit(response['Location']) + fragment = parse_qs(components[4]) + id_token = JWT().unpack(fragment["id_token"][0].encode('utf-8')).payload() + + self.assertIn('at_hash', id_token) + + def test_id_token_at_hash(self): + """ + Implicit client requesting `id_token` should not receive + `at_hash` in `id_token`. + """ + data = { + 'client_id': self.client_no_access.client_id, + 'redirect_uri': self.client_no_access.default_redirect_uri, + 'response_type': self.client_no_access.response_type, + 'scope': 'openid email', + 'state': self.state, + 'nonce': self.nonce, + 'allow': 'Accept', + } + + response = self._auth_request('post', data, is_user_authenticated=True) + + self.assertIn('id_token', response['Location']) + + # obtain `id_token` portion of Location + components = urlsplit(response['Location']) + fragment = parse_qs(components[4]) + id_token = JWT().unpack(fragment["id_token"][0].encode('utf-8')).payload() + + self.assertNotIn('at_hash', id_token) diff --git a/oidc_provider/tests/test_code_flow.py b/oidc_provider/tests/test_code_flow.py deleted file mode 100644 index 349d02e..0000000 --- a/oidc_provider/tests/test_code_flow.py +++ /dev/null @@ -1,50 +0,0 @@ -try: - from urllib.parse import urlencode -except ImportError: - from urllib import urlencode -import uuid - -from django.contrib.auth.models import AnonymousUser -from django.core.management import call_command -from django.core.urlresolvers import reverse -from django.test import RequestFactory -from django.test import TestCase - -from oidc_provider.models import * -from oidc_provider.tests.app.utils import * -from oidc_provider.views import * - - -class CodeFlowTestCase(TestCase): - """ - Test cases for Authorization Code Flow. - """ - - def setUp(self): - call_command('creatersakey') - self.factory = RequestFactory() - self.user = create_fake_user() - self.client = create_fake_client(response_type='code') - self.client_public = create_fake_client(response_type='code', is_public=True) - self.state = uuid.uuid4().hex - self.nonce = uuid.uuid4().hex - - def _auth_request(self, method, data={}, is_user_authenticated=False): - url = reverse('oidc_provider:authorize') - - if method.lower() == 'get': - query_str = urlencode(data).replace('+', '%20') - if query_str: - url += '?' + query_str - request = self.factory.get(url) - elif method.lower() == 'post': - request = self.factory.post(url, data=data) - else: - raise Exception('Method unsupported for an Authorization Request.') - - # Simulate that the user is logged. - request.user = self.user if is_user_authenticated else AnonymousUser() - - response = AuthorizeView.as_view()(request) - - return response diff --git a/oidc_provider/tests/test_implicit_flow.py b/oidc_provider/tests/test_implicit_flow.py deleted file mode 100644 index 121d621..0000000 --- a/oidc_provider/tests/test_implicit_flow.py +++ /dev/null @@ -1,185 +0,0 @@ -try: - from urllib.parse import urlencode -except ImportError: - from urllib import urlencode -try: - from urllib.parse import parse_qs, urlsplit -except ImportError: - from urlparse import parse_qs, urlsplit -import uuid - -from django.contrib.auth.models import AnonymousUser -from django.core.management import call_command -from django.core.urlresolvers import reverse -from django.test import RequestFactory -from django.test import TestCase -from jwkest.jwt import JWT - -from oidc_provider.models import * -from oidc_provider.tests.app.utils import * -from oidc_provider.views import * - - -class ImplicitFlowTestCase(TestCase): - """ - Test cases for Authorization Implicit Flow. - """ - - def setUp(self): - call_command('creatersakey') - self.factory = RequestFactory() - self.user = create_fake_user() - self.client = create_fake_client(response_type='id_token token') - self.client_public = create_fake_client(response_type='id_token token', is_public=True) - self.client_no_access = create_fake_client(response_type='id_token') - self.client_public_no_access = create_fake_client(response_type='id_token', is_public=True) - self.state = uuid.uuid4().hex - self.nonce = uuid.uuid4().hex - - def _auth_request(self, method, data={}, is_user_authenticated=False): - url = reverse('oidc_provider:authorize') - - if method.lower() == 'get': - query_str = urlencode(data).replace('+', '%20') - if query_str: - url += '?' + query_str - request = self.factory.get(url) - elif method.lower() == 'post': - request = self.factory.post(url, data=data) - else: - raise Exception('Method unsupported for an Authorization Request.') - - # Simulate that the user is logged. - request.user = self.user if is_user_authenticated else AnonymousUser() - - response = AuthorizeView.as_view()(request) - - return response - - def test_missing_nonce(self): - """ - The `nonce` parameter is REQUIRED if you use the Implicit Flow. - """ - data = { - 'client_id': self.client.client_id, - 'response_type': self.client.response_type, - 'redirect_uri': self.client.default_redirect_uri, - 'scope': 'openid email', - 'state': self.state, - } - - response = self._auth_request('get', data, is_user_authenticated=True) - - self.assertEqual('#error=invalid_request' in response['Location'], True) - - def test_id_token_token_response(self): - """ - Implicit client requesting `id_token token` receives both id token - and access token as the result of the authorization request. - """ - data = { - 'client_id': self.client.client_id, - 'redirect_uri': self.client.default_redirect_uri, - 'response_type': self.client.response_type, - 'scope': 'openid email', - 'state': self.state, - 'nonce': self.nonce, - 'allow': 'Accept', - } - - response = self._auth_request('post', data, is_user_authenticated=True) - - self.assertIn('access_token', response['Location']) - self.assertIn('id_token', response['Location']) - - # same for public client - data['client_id'] = self.client_public.client_id, - data['redirect_uri'] = self.client_public.default_redirect_uri, - data['response_type'] = self.client_public.response_type, - - response = self._auth_request('post', data, is_user_authenticated=True) - - self.assertIn('access_token', response['Location']) - self.assertIn('id_token', response['Location']) - - def test_id_token_response(self): - """ - Implicit client requesting `id_token` receives - only an id token as the result of the authorization request. - """ - data = { - 'client_id': self.client_no_access.client_id, - 'redirect_uri': self.client_no_access.default_redirect_uri, - 'response_type': self.client_no_access.response_type, - 'scope': 'openid email', - 'state': self.state, - 'nonce': self.nonce, - 'allow': 'Accept', - } - - response = self._auth_request('post', data, is_user_authenticated=True) - - self.assertNotIn('access_token', response['Location']) - self.assertIn('id_token', response['Location']) - - # same for public client - data['client_id'] = self.client_public_no_access.client_id, - data['redirect_uri'] = self.client_public_no_access.default_redirect_uri, - data['response_type'] = self.client_public_no_access.response_type, - - response = self._auth_request('post', data, is_user_authenticated=True) - - self.assertNotIn('access_token', response['Location']) - self.assertIn('id_token', response['Location']) - - def test_id_token_token_at_hash(self): - """ - Implicit client requesting `id_token token` receives - `at_hash` in `id_token`. - """ - data = { - 'client_id': self.client.client_id, - 'redirect_uri': self.client.default_redirect_uri, - 'response_type': self.client.response_type, - 'scope': 'openid email', - 'state': self.state, - 'nonce': self.nonce, - 'allow': 'Accept', - } - - response = self._auth_request('post', data, is_user_authenticated=True) - - self.assertIn('id_token', response['Location']) - - # obtain `id_token` portion of Location - components = urlsplit(response['Location']) - fragment = parse_qs(components[4]) - id_token = JWT().unpack(fragment["id_token"][0].encode('utf-8')).payload() - - self.assertIn('at_hash', id_token) - - def test_id_token_at_hash(self): - """ - Implicit client requesting `id_token` should not receive - `at_hash` in `id_token`. - """ - data = { - 'client_id': self.client_no_access.client_id, - 'redirect_uri': self.client_no_access.default_redirect_uri, - 'response_type': self.client_no_access.response_type, - 'scope': 'openid email', - 'state': self.state, - 'nonce': self.nonce, - 'allow': 'Accept', - } - - response = self._auth_request('post', data, is_user_authenticated=True) - - self.assertIn('id_token', response['Location']) - - # obtain `id_token` portion of Location - components = urlsplit(response['Location']) - fragment = parse_qs(components[4]) - id_token = JWT().unpack(fragment["id_token"][0].encode('utf-8')).payload() - - self.assertNotIn('at_hash', id_token) diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index 0873b23..b993a2a 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -304,6 +304,21 @@ class TokenTestCase(TestCase): self.assertEqual(id_token.get('nonce'), None) + def test_id_token_contains_at_hash(self): + """ + If access_token is included, the id_token SHOULD contain an at_hash. + """ + code = self._create_code() + + post_data = self._auth_code_post_data(code=code.code) + + response = self._post_request(post_data) + + response_dic = json.loads(response.content.decode('utf-8')) + id_token = JWT().unpack(response_dic['id_token'].encode('utf-8')).payload() + + self.assertTrue(id_token.get('at_hash')) + def test_idtoken_sign_validation(self): """ We MUST validate the signature of the ID Token according to JWS