diff --git a/CHANGELOG.md b/CHANGELOG.md index 3861832..cfeedb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. ### [Unreleased] +##### Fixed +- Nonce support for both Code and Implicit flow. + ### [0.0.7] - 2015-07-06 ##### Added diff --git a/example_project/.gitignore b/example_project/.gitignore index afe839c..0d9de6a 100644 --- a/example_project/.gitignore +++ b/example_project/.gitignore @@ -1,2 +1,2 @@ *.sqlite3 -*.pem \ No newline at end of file +*.pem diff --git a/oidc_provider/lib/endpoints/authorize.py b/oidc_provider/lib/endpoints/authorize.py index 4057984..1a92564 100644 --- a/oidc_provider/lib/endpoints/authorize.py +++ b/oidc_provider/lib/endpoints/authorize.py @@ -36,7 +36,6 @@ class AuthorizeEndpoint(object): self.grant_type = 'authorization_code' elif self.params.response_type in ['id_token', 'id_token token']: self.grant_type = 'implicit' - self._extract_implicit_params() else: self.grant_type = None @@ -52,13 +51,6 @@ class AuthorizeEndpoint(object): self.params.response_type = self.query_dict.get('response_type', '') self.params.scope = self.query_dict.get('scope', '').split() self.params.state = self.query_dict.get('state', '') - - def _extract_implicit_params(self): - """ - Get specific params used by the Implicit Flow. - - See: http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthRequest - """ self.params.nonce = self.query_dict.get('nonce', '') def validate_params(self): @@ -104,7 +96,8 @@ class AuthorizeEndpoint(object): code = create_code( user=self.request.user, client=self.client, - scope=self.params.scope) + scope=self.params.scope, + nonce=self.params.nonce) code.save() @@ -114,7 +107,8 @@ class AuthorizeEndpoint(object): elif self.grant_type == 'implicit': id_token_dic = create_id_token( user=self.request.user, - aud=self.client.client_id) + aud=self.client.client_id, + nonce=self.params.nonce) token = create_token( user=self.request.user, diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index e68c6c2..2c62f33 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -33,7 +33,6 @@ class TokenEndpoint(object): self.params.grant_type = query_dict.get('grant_type', '') self.params.code = query_dict.get('code', '') self.params.state = query_dict.get('state', '') - self.params.nonce = query_dict.get('nonce', '') def validate_params(self): if not (self.params.grant_type == 'authorization_code'): @@ -72,7 +71,7 @@ class TokenEndpoint(object): id_token_dic = create_id_token( user=self.code.user, aud=self.client.client_id, - nonce=self.params.nonce, + nonce=self.code.nonce, ) token = create_token( diff --git a/oidc_provider/lib/utils/token.py b/oidc_provider/lib/utils/token.py index 4874a79..1e7320b 100644 --- a/oidc_provider/lib/utils/token.py +++ b/oidc_provider/lib/utils/token.py @@ -10,7 +10,7 @@ from oidc_provider.models import * from oidc_provider import settings -def create_id_token(user, aud, nonce=None): +def create_id_token(user, aud, nonce): """ Receives a user object and aud (audience). Then creates the id_token dictionary. @@ -22,13 +22,12 @@ def create_id_token(user, aud, nonce=None): expires_in = settings.get('OIDC_IDTOKEN_EXPIRE') - now = timezone.now() # Convert datetimes into timestamps. - iat_time = time.mktime(now.timetuple()) - exp_time = time.mktime((now + timedelta(seconds=expires_in)).timetuple()) - + now = timezone.now() + iat_time = int(time.mktime(now.timetuple())) + exp_time = int(time.mktime((now + timedelta(seconds=expires_in)).timetuple())) user_auth_time = user.last_login or user.date_joined - auth_time = time.mktime(user_auth_time.timetuple()) + auth_time = int(time.mktime(user_auth_time.timetuple())) dic = { 'iss': get_issuer(), @@ -75,7 +74,7 @@ def create_token(user, client, id_token_dic, scope): return token -def create_code(user, client, scope): +def create_code(user, client, scope, nonce): """ Create and populate a Code object. @@ -88,5 +87,6 @@ def create_code(user, client, scope): code.expires_at = timezone.now() + timedelta( seconds=settings.get('OIDC_CODE_EXPIRE')) code.scope = scope + code.nonce = nonce return code diff --git a/oidc_provider/migrations/0003_code_nonce.py b/oidc_provider/migrations/0003_code_nonce.py new file mode 100644 index 0000000..0d49615 --- /dev/null +++ b/oidc_provider/migrations/0003_code_nonce.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('oidc_provider', '0002_userconsent'), + ] + + operations = [ + migrations.AddField( + model_name='code', + name='nonce', + field=models.CharField(default=b'', max_length=255, blank=True), + ), + ] diff --git a/oidc_provider/models.py b/oidc_provider/models.py index 8ec7e95..709f334 100644 --- a/oidc_provider/models.py +++ b/oidc_provider/models.py @@ -71,6 +71,7 @@ class BaseCodeTokenModel(models.Model): class Code(BaseCodeTokenModel): code = models.CharField(max_length=255, unique=True) + nonce = models.CharField(max_length=255, blank=True, default='') class Token(BaseCodeTokenModel): diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index e48ce27..9edc99f 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -60,7 +60,7 @@ class DefaultSettings(object): which is intended to be consumed by the Client. """ def default_sub_generator(user): - return user.id + return str(user.id) return default_sub_generator diff --git a/oidc_provider/tests/app/utils.py b/oidc_provider/tests/app/utils.py index 306239a..0155787 100644 --- a/oidc_provider/tests/app/utils.py +++ b/oidc_provider/tests/app/utils.py @@ -6,6 +6,8 @@ except ImportError: from oidc_provider.models import * +FAKE_NONCE = 'cb584e44c43ed6bd0bc2d9c7e242837d' + def create_fake_user(): """ Create a test user. diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index 4926730..d98d013 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -27,7 +27,21 @@ class TokenTestCase(TestCase): self.factory = RequestFactory() self.user = create_fake_user() self.client = create_fake_client(response_type='code') - self.state = uuid.uuid4().hex + + def _post_data(self, code): + """ + All the data that will be POSTed to the Token Endpoint. + """ + post_data = { + 'client_id': self.client.client_id, + 'client_secret': self.client.client_secret, + 'redirect_uri': self.client.default_redirect_uri, + 'grant_type': 'authorization_code', + 'code': code, + 'state': uuid.uuid4().hex, + } + + return post_data def _post_request(self, post_data): """ @@ -52,7 +66,8 @@ class TokenTestCase(TestCase): code = create_code( user=self.user, client=self.client, - scope=['openid', 'email']) + scope=['openid', 'email'], + nonce=FAKE_NONCE) code.save() return code @@ -94,14 +109,8 @@ class TokenTestCase(TestCase): code = self._create_code() # Test a valid request to the token endpoint. - post_data = { - 'client_id': self.client.client_id, - 'client_secret': self.client.client_secret, - 'redirect_uri': self.client.default_redirect_uri, - 'grant_type': 'authorization_code', - 'code': code.code, - 'state': self.state, - } + post_data = self._post_data(code=code.code) + response = self._post_request(post_data) response_dic = json.loads(response.content.decode('utf-8')) @@ -126,7 +135,7 @@ class TokenTestCase(TestCase): self.assertEqual(response_dic.get('error') == 'invalid_client', True, msg='"error" key value should be "invalid_client".') - def test_token_contains_nonce_if_provided(self): + def test_access_token_contains_nonce(self): """ If present in the Authentication Request, Authorization Servers MUST include a nonce Claim in the ID Token with the Claim Value being the @@ -134,18 +143,9 @@ class TokenTestCase(TestCase): See http://openid.net/specs/openid-connect-core-1_0.html#IDToken """ - code = self._create_code() - post_data = { - 'client_id': self.client.client_id, - 'client_secret': self.client.client_secret, - 'redirect_uri': self.client.default_redirect_uri, - 'grant_type': 'authorization_code', - 'code': code.code, - 'state': self.state, - 'nonce': 'thisisanonce' - } + post_data = self._post_data(code=code.code) response = self._post_request(post_data) @@ -153,4 +153,23 @@ class TokenTestCase(TestCase): id_token = jwt.decode(response_dic['id_token'], options={'verify_signature': False, 'verify_aud': False}) - self.assertEqual(id_token['nonce'], 'thisisanonce') + self.assertEqual(id_token['nonce'], FAKE_NONCE) + + def test_access_token_not_contains_nonce(self): + """ + If the client does not supply a nonce parameter, it SHOULD not be + included in the `id_token`. + """ + code = self._create_code() + code.nonce = '' + code.save() + + post_data = self._post_data(code=code.code) + + response = self._post_request(post_data) + + response_dic = json.loads(response.content.decode('utf-8')) + id_token = jwt.decode(response_dic['id_token'], + options={'verify_signature': False, 'verify_aud': False}) + + self.assertEqual(id_token.get('nonce'), None) diff --git a/oidc_provider/tests/test_userinfo_endpoint.py b/oidc_provider/tests/test_userinfo_endpoint.py index 290da67..67bdf95 100644 --- a/oidc_provider/tests/test_userinfo_endpoint.py +++ b/oidc_provider/tests/test_userinfo_endpoint.py @@ -22,7 +22,8 @@ class UserInfoTestCase(TestCase): """ Generate a valid token. """ - id_token_dic = create_id_token(self.user, self.client.client_id) + id_token_dic = create_id_token(self.user, + self.client.client_id, FAKE_NONCE) token = create_token( user=self.user,