diff --git a/oidc_provider/lib/endpoints/authorize.py b/oidc_provider/lib/endpoints/authorize.py index 967e0bb..49e8fbc 100644 --- a/oidc_provider/lib/endpoints/authorize.py +++ b/oidc_provider/lib/endpoints/authorize.py @@ -2,6 +2,11 @@ from datetime import timedelta import logging from django.utils import timezone +try: + from urllib import urlencode + from urlparse import urlsplit, parse_qs, urlunsplit +except ImportError: + from urllib.parse import urlsplit, parse_qs, urlunsplit, urlencode from oidc_provider.lib.errors import * from oidc_provider.lib.utils.params import * @@ -72,7 +77,9 @@ class AuthorizeEndpoint(object): try: self.client = Client.objects.get(client_id=self.params.client_id) - if not (self.params.redirect_uri in self.client.redirect_uris): + clean_redirect_uri = urlsplit(self.params.redirect_uri) + clean_redirect_uri = urlunsplit(clean_redirect_uri._replace(query='')) + if not (clean_redirect_uri in self.client.redirect_uris): logger.error('[Authorize] Invalid redirect uri: %s', self.params.redirect_uri) raise RedirectUriError() @@ -88,6 +95,10 @@ class AuthorizeEndpoint(object): raise ClientIdError() def create_response_uri(self): + uri = urlsplit(self.params.redirect_uri) + query_params = parse_qs(uri.query) + query_fragment = parse_qs(uri.fragment) + try: if self.grant_type == 'authorization_code': code = create_code( @@ -97,8 +108,8 @@ class AuthorizeEndpoint(object): code.save() - # Create the response uri. - uri = self.params.redirect_uri + '?code={0}'.format(code.code) + query_params['code'] = code.code + query_params['state'] = self.params.state if self.params.state else '' elif self.grant_type == 'implicit': id_token_dic = create_id_token( @@ -114,18 +125,17 @@ class AuthorizeEndpoint(object): # Store the token. token.save() - # Create the response uri. - uri = self.params.redirect_uri + \ - '#token_type={0}&id_token={1}&expires_in={2}'.format( - 'bearer', - encode_id_token(id_token_dic), - 60 * 10, - ) + query_fragment['token_type'] = 'bearer' + query_fragment['id_token'] = encode_id_token(id_token_dic) + query_fragment['expires_in'] = 60 * 10 # Check if response_type is 'id_token token' then # add access_token to the fragment. if self.params.response_type == 'id_token token': - uri += '&access_token={0}'.format(token.access_token) + query_fragment['access_token'] = token.access_token + + query_fragment['state'] = self.params.state if self.params.state else '' + except Exception as error: logger.error('[Authorize] Error when trying to create response uri: %s', error) raise AuthorizeError( @@ -133,10 +143,10 @@ class AuthorizeEndpoint(object): 'server_error', self.grant_type) - # Add state if present. - uri += ('&state={0}'.format(self.params.state) if self.params.state else '') + uri = uri._replace(query=urlencode(query_params, doseq=True)) + uri = uri._replace(fragment=urlencode(query_fragment, doseq=True)) - return uri + return urlunsplit(uri) def set_client_user_consent(self): """ diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index 72a211d..e68c6c2 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -33,6 +33,7 @@ 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'): @@ -70,7 +71,9 @@ class TokenEndpoint(object): def create_response_dic(self): id_token_dic = create_id_token( user=self.code.user, - aud=self.client.client_id) + aud=self.client.client_id, + nonce=self.params.nonce, + ) token = create_token( user=self.code.user, diff --git a/oidc_provider/lib/utils/token.py b/oidc_provider/lib/utils/token.py index bbfb7c9..4874a79 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): +def create_id_token(user, aud, nonce=None): """ Receives a user object and aud (audience). Then creates the id_token dictionary. @@ -39,6 +39,9 @@ def create_id_token(user, aud): 'auth_time': auth_time, } + if nonce: + dic['nonce'] = nonce + return dic @@ -86,4 +89,4 @@ def create_code(user, client, scope): seconds=settings.get('OIDC_CODE_EXPIRE')) code.scope = scope - return code \ No newline at end of file + return code diff --git a/oidc_provider/tests/test_authorize_endpoint.py b/oidc_provider/tests/test_authorize_endpoint.py index a1a6310..3d1ec3e 100644 --- a/oidc_provider/tests/test_authorize_endpoint.py +++ b/oidc_provider/tests/test_authorize_endpoint.py @@ -258,3 +258,26 @@ class AuthorizationCodeFlowTestCase(TestCase): client=self.client) self.assertEqual(is_code_ok, True, msg='Code returned is invalid or missing.') + + def test_response_uri_is_properly_constructed(self): + post_data = { + 'client_id': self.client.client_id, + 'redirect_uri': self.client.default_redirect_uri + "?redirect_state=xyz", + 'response_type': 'code', + 'scope': 'openid email', + 'state': self.state, + 'allow': 'Accept', + } + + request = self.factory.post(reverse('oidc_provider:authorize'), + data=post_data) + # Simulate that the user is logged. + request.user = self.user + + response = AuthorizeView.as_view()(request) + + is_code_ok = is_code_valid(url=response['Location'], + user=self.user, + client=self.client) + self.assertEqual(is_code_ok, True, + msg='Code returned is invalid.') diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index 1bd6568..19d2983 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -9,6 +9,8 @@ from django.core.urlresolvers import reverse from django.test import RequestFactory from django.test import TestCase +import jwt + from oidc_provider.lib.utils.token import * from oidc_provider.tests.utils import * from oidc_provider.views import * @@ -123,3 +125,32 @@ class TokenTestCase(TestCase): msg='"error" key should exists in response.') 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): + """ + If present in the Authentication Request, Authorization Servers MUST + include a nonce Claim in the ID Token with the Claim Value being the + nonce value sent in the Authentication Request. + + 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' + } + + 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['nonce'], 'thisisanonce') diff --git a/oidc_provider/tests/utils.py b/oidc_provider/tests/utils.py index 713a818..306239a 100644 --- a/oidc_provider/tests/utils.py +++ b/oidc_provider/tests/utils.py @@ -1,4 +1,8 @@ from django.contrib.auth.models import User +try: + from urlparse import parse_qs, urlsplit +except ImportError: + from urllib.parse import parse_qs, urlsplit from oidc_provider.models import * @@ -40,7 +44,9 @@ def is_code_valid(url, user, client): Check if the code inside the url is valid. """ try: - code = (url.split('code='))[1].split('&')[0] + parsed = urlsplit(url) + params = parse_qs(parsed.query) + code = params['code'][0] code = Code.objects.get(code=code) is_code_ok = (code.client == client) and \ (code.user == user)