From 2e0072cad7e5c7de2cc7d1f5ec6be398f0f35d19 Mon Sep 17 00:00:00 2001 From: Niels van Huijstee Date: Wed, 7 Dec 2016 10:05:33 +0100 Subject: [PATCH 1/2] Add tests for Resource Owner Password Credentials Flow --- oidc_provider/tests/test_token_endpoint.py | 86 ++++++++++++++++++++-- 1 file changed, 80 insertions(+), 6 deletions(-) diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index de7972e..50f556e 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -51,6 +51,14 @@ class TokenTestCase(TestCase): self.user = create_fake_user() self.client = create_fake_client(response_type='code') + def _password_grant_post_data(self): + return { + 'username': 'johndoe', + 'password': '1234', + 'grant_type': 'password', + 'scope': 'openid email', + } + def _auth_code_post_data(self, code): """ All the data that will be POSTed to the Token Endpoint. @@ -128,6 +136,77 @@ class TokenTestCase(TestCase): return userinfo(request) + def _auth_header(self): + user_pass = self.client.client_id + ':' + self.client.client_secret + auth = b'Basic ' + b64encode(user_pass.encode('utf-8')) + auth_header = {'HTTP_AUTHORIZATION': auth.decode('utf-8')} + return auth_header + + # Resource Owner Password Credentials Grant + # requirements to satisfy in all test_password_grant methods + # https://tools.ietf.org/html/rfc6749#section-4.3.2 + # + # grant_type + # REQUIRED. Value MUST be set to "password". + # username + # REQUIRED. The resource owner username. + # password + # REQUIRED. The resource owner password. + # scope + # OPTIONAL. The scope of the access request as described by + # Section 3.3. + # + # The authorization server MUST: + # o require client authentication for confidential clients or for any + # client that was issued client credentials (or with other + # authentication requirements), + # o authenticate the client if client authentication is included, and + # o validate the resource owner password credentials using its + # existing password validation algorithm. + + def test_password_grant_get_access_token_without_scope(self): + post_data = self._password_grant_post_data() + del (post_data['scope']) + + response = self._post_request( + post_data=post_data, + extras=self._auth_header() + ) + + response_dict = json.loads(response.content.decode('utf-8')) + self.assertIn('access_token', response_dict) + + def test_password_grant_get_access_token_with_scope(self): + response = self._post_request( + post_data=self._password_grant_post_data(), + extras=self._auth_header() + ) + + response_dict = json.loads(response.content.decode('utf-8')) + self.assertIn('access_token', response_dict) + + def test_password_grant_get_access_token_invalid_user_credentials(self): + invalid_post = self._password_grant_post_data() + invalid_post['password'] = 'wrong!' + + response = self._post_request( + post_data=invalid_post, + extras=self._auth_header() + ) + + self.assertEqual(400, response.status_code) + + def test_password_grant_get_access_token_invalid_client_credentials(self): + self.client.client_id = 'foo' + self.client.client_secret = 'bar' + + response = self._post_request( + post_data=self._password_grant_post_data(), + extras=self._auth_header() + ) + + self.assertEqual(400, response.status_code) + @override_settings(OIDC_TOKEN_EXPIRE=720) def test_authorization_code(self): """ @@ -307,12 +386,7 @@ class TokenTestCase(TestCase): del basicauth_data['client_id'] del basicauth_data['client_secret'] - # Generate HTTP Basic Auth header with id and secret. - user_pass = self.client.client_id + ':' + self.client.client_secret - auth_header = b'Basic ' + b64encode(user_pass.encode('utf-8')) - response = self._post_request(basicauth_data, { - 'HTTP_AUTHORIZATION': auth_header.decode('utf-8'), - }) + response = self._post_request(basicauth_data, self._auth_header()) response.content.decode('utf-8') self.assertEqual('invalid_client' in response.content.decode('utf-8'), From 0633b664a017a00815ee06933e0bb17314bb2316 Mon Sep 17 00:00:00 2001 From: Niels van Huijstee Date: Wed, 7 Dec 2016 11:51:24 +0100 Subject: [PATCH 2/2] Password Grant -Response according to specification --- oidc_provider/lib/endpoints/token.py | 48 ++++++++++++++-------- oidc_provider/tests/test_token_endpoint.py | 25 ++++++++++- 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index cf1e10c..327ed81 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -2,7 +2,7 @@ from base64 import b64decode, urlsafe_b64encode import hashlib import logging import re - +from django.contrib.auth import authenticate from oidc_provider.lib.utils.common import cleanup_url_from_query_string try: @@ -34,6 +34,7 @@ class TokenEndpoint(object): def __init__(self, request): self.request = request self.params = {} + self.user = None self._extract_params() def _extract_params(self): @@ -122,23 +123,15 @@ class TokenEndpoint(object): raise TokenError('invalid_grant') elif self.params['grant_type'] == 'password': - from django.contrib.auth import authenticate - user = authenticate(username=self.params['username'], password=self.params['password']) + user = authenticate( + username=self.params['username'], + password=self.params['password'] + ) + if not user: raise TokenError('Invalid user credentials') - self.token = create_token(user, self.client, self.params['scope'].split(' ')) - - self.token.id_token = create_id_token( - user=user, - aud=self.client.client_id, - nonce='self.code.nonce', - at_hash=self.token.at_hash, - request=self.request, - scope=self.params['scope'], - ) - - self.token.save() + self.user = user elif self.params['grant_type'] == 'refresh_token': if not self.params['refresh_token']: @@ -163,7 +156,30 @@ class TokenEndpoint(object): elif self.params['grant_type'] == 'refresh_token': return self.create_refresh_response_dic() elif self.params['grant_type'] == 'password': - return {'access_token': self.token.access_token} + return self.create_access_token_response_dic() + + def create_access_token_response_dic(self): + token = create_token( + self.user, + self.client, + self.params['scope'].split(' ')) + + token.id_token = create_id_token( + user=self.user, + aud=self.client.client_id, + nonce='self.code.nonce', + at_hash=token.at_hash, + request=self.request, + scope=self.params['scope'], + ) + + token.save() + return { + 'access_token': token.access_token, + 'refresh_token': token.refresh_token, + 'expires_in': settings.get('OIDC_TOKEN_EXPIRE'), + 'token_type': 'bearer' + } def create_code_response_dic(self): token = create_token( diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index 50f556e..1da2c07 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -19,7 +19,7 @@ from django.utils import timezone from jwkest.jwk import KEYS from jwkest.jws import JWS from jwkest.jwt import JWT -from mock import patch +from mock import patch, Mock from oidc_provider.lib.utils.token import create_code from oidc_provider.models import Token @@ -207,6 +207,29 @@ class TokenTestCase(TestCase): self.assertEqual(400, response.status_code) + @patch('oidc_provider.lib.utils.token.uuid') + @override_settings(OIDC_TOKEN_EXPIRE=120) + def test_password_grant_full_response(self, mock_uuid): + test_hex = 'fake_token' + mock_uuid4 = Mock(spec=uuid.uuid4) + mock_uuid4.hex = test_hex + mock_uuid.uuid4.return_value = mock_uuid4 + + response = self._post_request( + post_data=self._password_grant_post_data(), + extras=self._auth_header() + ) + + response_dict = json.loads(response.content.decode('utf-8')) + expected_response_dic = { + "access_token": 'fake_token', + "refresh_token": 'fake_token', + "expires_in": 120, + "token_type": "bearer", + } + + self.assertDictEqual(expected_response_dic, response_dict) + @override_settings(OIDC_TOKEN_EXPIRE=720) def test_authorization_code(self): """