2017-07-07 07:07:21 +00:00
|
|
|
from oidc_provider.lib.errors import RedirectUriError
|
|
|
|
|
2015-07-01 15:53:41 +00:00
|
|
|
try:
|
2017-07-19 08:52:10 +00:00
|
|
|
from urllib.parse import urlencode, quote
|
2015-07-01 19:43:35 +00:00
|
|
|
except ImportError:
|
2017-07-19 08:52:10 +00:00
|
|
|
from urllib import urlencode, quote
|
2016-08-08 18:20:47 +00:00
|
|
|
try:
|
|
|
|
from urllib.parse import parse_qs, urlsplit
|
|
|
|
except ImportError:
|
|
|
|
from urlparse import parse_qs, urlsplit
|
2015-03-12 15:42:52 +00:00
|
|
|
import uuid
|
2017-05-05 03:19:57 +00:00
|
|
|
from mock import patch, mock
|
2015-02-27 20:40:17 +00:00
|
|
|
|
2015-02-12 18:04:58 +00:00
|
|
|
from django.contrib.auth.models import AnonymousUser
|
2016-04-14 19:22:38 +00:00
|
|
|
from django.core.management import call_command
|
2015-02-11 18:37:51 +00:00
|
|
|
from django.core.urlresolvers import reverse
|
2016-09-09 16:10:12 +00:00
|
|
|
from django.test import (
|
|
|
|
RequestFactory,
|
|
|
|
override_settings,
|
|
|
|
)
|
2015-02-11 18:37:51 +00:00
|
|
|
from django.test import TestCase
|
2016-08-08 18:20:47 +00:00
|
|
|
from jwkest.jwt import JWT
|
2015-02-27 20:40:17 +00:00
|
|
|
|
2015-02-18 18:07:22 +00:00
|
|
|
from oidc_provider import settings
|
2016-08-11 22:05:13 +00:00
|
|
|
from oidc_provider.tests.app.utils import (
|
|
|
|
create_fake_user,
|
|
|
|
create_fake_client,
|
|
|
|
FAKE_CODE_CHALLENGE,
|
|
|
|
is_code_valid,
|
|
|
|
)
|
|
|
|
from oidc_provider.views import AuthorizeView
|
2017-05-05 03:19:57 +00:00
|
|
|
from oidc_provider.lib.endpoints.authorize import AuthorizeEndpoint
|
2015-02-11 18:37:51 +00:00
|
|
|
|
|
|
|
|
2016-09-08 20:15:25 +00:00
|
|
|
class AuthorizeEndpointMixin(object):
|
2016-04-14 19:22:38 +00:00
|
|
|
|
2017-08-08 22:41:42 +00:00
|
|
|
def _auth_request(self, method, data=None, is_user_authenticated=False):
|
|
|
|
if data is None:
|
|
|
|
data = {}
|
2016-04-14 19:22:38 +00:00
|
|
|
url = reverse('oidc_provider:authorize')
|
|
|
|
|
|
|
|
if method.lower() == 'get':
|
2016-04-14 20:45:30 +00:00
|
|
|
query_str = urlencode(data).replace('+', '%20')
|
2016-04-14 19:22:38 +00:00
|
|
|
if query_str:
|
|
|
|
url += '?' + query_str
|
|
|
|
request = self.factory.get(url)
|
|
|
|
elif method.lower() == 'post':
|
2016-04-14 20:45:30 +00:00
|
|
|
request = self.factory.post(url, data=data)
|
2016-04-14 19:22:38 +00:00
|
|
|
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
|
|
|
|
|
2016-09-08 20:15:25 +00:00
|
|
|
|
|
|
|
class AuthorizationCodeFlowTestCase(TestCase, AuthorizeEndpointMixin):
|
|
|
|
"""
|
|
|
|
Test cases for Authorize Endpoint using Code Flow.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
call_command('creatersakey')
|
|
|
|
self.factory = RequestFactory()
|
|
|
|
self.user = create_fake_user()
|
|
|
|
self.client = create_fake_client(response_type='code')
|
2017-03-31 16:34:03 +00:00
|
|
|
self.client_with_no_consent = create_fake_client(response_type='code', require_consent=False)
|
2016-09-08 20:15:25 +00:00
|
|
|
self.client_public = create_fake_client(response_type='code', is_public=True)
|
2017-08-08 22:41:42 +00:00
|
|
|
self.client_public_with_no_consent = create_fake_client(
|
|
|
|
response_type='code', is_public=True, require_consent=False)
|
2016-09-08 20:15:25 +00:00
|
|
|
self.state = uuid.uuid4().hex
|
|
|
|
self.nonce = uuid.uuid4().hex
|
|
|
|
|
2015-02-27 20:40:17 +00:00
|
|
|
def test_missing_parameters(self):
|
2015-02-11 18:37:51 +00:00
|
|
|
"""
|
|
|
|
If the request fails due to a missing, invalid, or mismatching
|
|
|
|
redirection URI, or if the client identifier is missing or invalid,
|
|
|
|
the authorization server SHOULD inform the resource owner of the error.
|
|
|
|
|
|
|
|
See: https://tools.ietf.org/html/rfc6749#section-4.1.2.1
|
|
|
|
"""
|
2016-04-14 19:22:38 +00:00
|
|
|
response = self._auth_request('get')
|
2015-02-11 18:37:51 +00:00
|
|
|
|
|
|
|
self.assertEqual(response.status_code, 200)
|
2015-02-11 20:38:37 +00:00
|
|
|
self.assertEqual(bool(response.content), True)
|
|
|
|
|
2015-02-27 20:40:17 +00:00
|
|
|
def test_invalid_response_type(self):
|
2015-02-11 20:38:37 +00:00
|
|
|
"""
|
|
|
|
The OP informs the RP by using the Error Response parameters defined
|
|
|
|
in Section 4.1.2.1 of OAuth 2.0.
|
|
|
|
|
|
|
|
See: http://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
|
|
|
"""
|
|
|
|
# Create an authorize request with an unsupported response_type.
|
2016-04-14 20:45:30 +00:00
|
|
|
data = {
|
2015-02-27 20:40:17 +00:00
|
|
|
'client_id': self.client.client_id,
|
|
|
|
'response_type': 'something_wrong',
|
|
|
|
'redirect_uri': self.client.default_redirect_uri,
|
|
|
|
'scope': 'openid email',
|
|
|
|
'state': self.state,
|
2016-04-14 19:22:38 +00:00
|
|
|
}
|
2015-02-12 18:04:58 +00:00
|
|
|
|
2016-04-14 20:45:30 +00:00
|
|
|
response = self._auth_request('get', data)
|
2015-02-11 20:38:37 +00:00
|
|
|
|
|
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
self.assertEqual(response.has_header('Location'), True)
|
|
|
|
|
2015-02-12 18:04:58 +00:00
|
|
|
# Should be an 'error' component in query.
|
2016-09-08 20:15:25 +00:00
|
|
|
self.assertIn('error=', response['Location'])
|
2015-02-12 18:04:58 +00:00
|
|
|
|
2015-03-19 17:19:27 +00:00
|
|
|
def test_user_not_logged(self):
|
2015-02-12 18:04:58 +00:00
|
|
|
"""
|
|
|
|
The Authorization Server attempts to Authenticate the End-User by
|
|
|
|
redirecting to the login view.
|
|
|
|
|
|
|
|
See: http://openid.net/specs/openid-connect-core-1_0.html#Authenticates
|
|
|
|
"""
|
2016-04-14 20:45:30 +00:00
|
|
|
data = {
|
2015-02-27 20:40:17 +00:00
|
|
|
'client_id': self.client.client_id,
|
|
|
|
'response_type': 'code',
|
|
|
|
'redirect_uri': self.client.default_redirect_uri,
|
|
|
|
'scope': 'openid email',
|
|
|
|
'state': self.state,
|
2016-04-14 19:22:38 +00:00
|
|
|
}
|
2015-02-12 18:04:58 +00:00
|
|
|
|
2016-04-14 20:45:30 +00:00
|
|
|
response = self._auth_request('get', data)
|
2015-02-12 18:04:58 +00:00
|
|
|
|
|
|
|
# Check if user was redirected to the login view.
|
2017-05-05 03:19:57 +00:00
|
|
|
self.assertIn(settings.get('OIDC_LOGIN_URL'), response['Location'])
|
2015-02-12 18:04:58 +00:00
|
|
|
|
2015-03-19 17:19:27 +00:00
|
|
|
def test_user_consent_inputs(self):
|
2015-02-20 17:33:18 +00:00
|
|
|
"""
|
|
|
|
Once the End-User is authenticated, the Authorization Server MUST
|
|
|
|
obtain an authorization decision before releasing information to
|
|
|
|
the Client.
|
|
|
|
|
|
|
|
See: http://openid.net/specs/openid-connect-core-1_0.html#Consent
|
|
|
|
"""
|
2016-04-14 20:45:30 +00:00
|
|
|
data = {
|
2015-02-27 20:40:17 +00:00
|
|
|
'client_id': self.client.client_id,
|
2015-03-30 18:37:48 +00:00
|
|
|
'response_type': 'code',
|
2015-02-27 20:40:17 +00:00
|
|
|
'redirect_uri': self.client.default_redirect_uri,
|
2015-03-30 18:37:48 +00:00
|
|
|
'scope': 'openid email',
|
2015-02-27 20:40:17 +00:00
|
|
|
'state': self.state,
|
2016-04-07 14:45:35 +00:00
|
|
|
# PKCE parameters.
|
|
|
|
'code_challenge': FAKE_CODE_CHALLENGE,
|
|
|
|
'code_challenge_method': 'S256',
|
2016-04-14 19:22:38 +00:00
|
|
|
}
|
2015-02-13 13:44:09 +00:00
|
|
|
|
2016-04-14 20:45:30 +00:00
|
|
|
response = self._auth_request('get', data, is_user_authenticated=True)
|
2015-02-13 13:44:09 +00:00
|
|
|
|
2015-02-27 20:40:17 +00:00
|
|
|
# Check if hidden inputs exists in the form,
|
|
|
|
# also if their values are valid.
|
2015-02-19 18:45:51 +00:00
|
|
|
input_html = '<input name="{0}" type="hidden" value="{1}" />'
|
|
|
|
|
|
|
|
to_check = {
|
|
|
|
'client_id': self.client.client_id,
|
|
|
|
'redirect_uri': self.client.default_redirect_uri,
|
2015-03-30 18:37:48 +00:00
|
|
|
'response_type': 'code',
|
2016-04-07 14:45:35 +00:00
|
|
|
'code_challenge': FAKE_CODE_CHALLENGE,
|
|
|
|
'code_challenge_method': 'S256',
|
2015-02-19 18:45:51 +00:00
|
|
|
}
|
|
|
|
|
2015-07-01 19:43:35 +00:00
|
|
|
for key, value in iter(to_check.items()):
|
2015-07-01 20:20:16 +00:00
|
|
|
is_input_ok = input_html.format(key, value) in response.content.decode('utf-8')
|
2017-08-08 22:41:42 +00:00
|
|
|
self.assertEqual(is_input_ok, True, msg='Hidden input for "' + key + '" fails.')
|
2015-02-27 20:40:17 +00:00
|
|
|
|
2015-03-19 17:19:27 +00:00
|
|
|
def test_user_consent_response(self):
|
2015-02-27 20:40:17 +00:00
|
|
|
"""
|
|
|
|
First,
|
|
|
|
if the user denied the consent we must ensure that
|
|
|
|
the error response parameters are added to the query component
|
|
|
|
of the Redirection URI.
|
|
|
|
|
|
|
|
Second,
|
|
|
|
if the user allow the RP then the server MUST return
|
|
|
|
the parameters defined in Section 4.1.2 of OAuth 2.0 [RFC6749]
|
|
|
|
by adding them as query parameters to the redirect_uri.
|
|
|
|
"""
|
2016-04-14 19:22:38 +00:00
|
|
|
data = {
|
2015-02-27 20:40:17 +00:00
|
|
|
'client_id': self.client.client_id,
|
|
|
|
'redirect_uri': self.client.default_redirect_uri,
|
2016-04-14 19:22:38 +00:00
|
|
|
'response_type': 'code',
|
2015-03-12 15:42:52 +00:00
|
|
|
'scope': 'openid email',
|
2015-02-27 20:40:17 +00:00
|
|
|
'state': self.state,
|
2016-04-07 14:45:35 +00:00
|
|
|
# PKCE parameters.
|
|
|
|
'code_challenge': FAKE_CODE_CHALLENGE,
|
|
|
|
'code_challenge_method': 'S256',
|
2015-02-27 20:40:17 +00:00
|
|
|
}
|
|
|
|
|
2016-04-14 19:22:38 +00:00
|
|
|
response = self._auth_request('post', data, is_user_authenticated=True)
|
2015-02-27 20:40:17 +00:00
|
|
|
|
|
|
|
# Because user doesn't allow app, SHOULD exists an error parameter
|
|
|
|
# in the query.
|
2016-09-08 20:15:25 +00:00
|
|
|
self.assertIn('error=', response['Location'], msg='error param is missing in query.')
|
|
|
|
self.assertIn('access_denied', response['Location'], msg='"access_denied" code is missing in query.')
|
2015-02-27 20:40:17 +00:00
|
|
|
|
|
|
|
# Simulate user authorization.
|
2016-08-08 17:24:07 +00:00
|
|
|
data['allow'] = 'Accept' # Will be the value of the button.
|
2015-02-27 20:40:17 +00:00
|
|
|
|
2016-04-14 19:22:38 +00:00
|
|
|
response = self._auth_request('post', data, is_user_authenticated=True)
|
2015-02-27 20:40:17 +00:00
|
|
|
|
2015-06-23 19:32:12 +00:00
|
|
|
is_code_ok = is_code_valid(url=response['Location'],
|
|
|
|
user=self.user,
|
|
|
|
client=self.client)
|
2017-08-08 22:41:42 +00:00
|
|
|
self.assertEqual(is_code_ok, True, msg='Code returned is invalid.')
|
2015-02-27 20:40:17 +00:00
|
|
|
|
|
|
|
# Check if the state is returned.
|
|
|
|
state = (response['Location'].split('state='))[1].split('&')[0]
|
2016-09-08 20:15:25 +00:00
|
|
|
self.assertEqual(state, self.state, msg='State change or is missing.')
|
2015-03-11 17:36:52 +00:00
|
|
|
|
2015-06-23 19:32:12 +00:00
|
|
|
def test_user_consent_skipped(self):
|
|
|
|
"""
|
|
|
|
If users previously gave consent to some client (for a specific
|
|
|
|
list of scopes) and because they might be prompted for the same
|
|
|
|
authorization multiple times, the server skip it.
|
|
|
|
"""
|
2016-04-14 19:22:38 +00:00
|
|
|
data = {
|
2017-03-31 16:34:03 +00:00
|
|
|
'client_id': self.client_with_no_consent.client_id,
|
|
|
|
'redirect_uri': self.client_with_no_consent.default_redirect_uri,
|
2015-06-23 19:32:12 +00:00
|
|
|
'response_type': 'code',
|
|
|
|
'scope': 'openid email',
|
|
|
|
'state': self.state,
|
|
|
|
'allow': 'Accept',
|
|
|
|
}
|
|
|
|
|
|
|
|
request = self.factory.post(reverse('oidc_provider:authorize'),
|
2016-04-14 19:22:38 +00:00
|
|
|
data=data)
|
2015-06-23 19:32:12 +00:00
|
|
|
# Simulate that the user is logged.
|
|
|
|
request.user = self.user
|
|
|
|
|
2017-03-31 16:34:03 +00:00
|
|
|
response = self._auth_request('post', data, is_user_authenticated=True)
|
2016-02-01 17:34:39 +00:00
|
|
|
|
2017-03-31 16:34:03 +00:00
|
|
|
self.assertIn('code', response['Location'], msg='Code is missing in the returned url.')
|
2016-02-01 17:34:39 +00:00
|
|
|
|
2016-04-14 19:22:38 +00:00
|
|
|
response = self._auth_request('post', data, is_user_authenticated=True)
|
2015-06-23 19:32:12 +00:00
|
|
|
|
|
|
|
is_code_ok = is_code_valid(url=response['Location'],
|
|
|
|
user=self.user,
|
2017-03-31 16:34:03 +00:00
|
|
|
client=self.client_with_no_consent)
|
2016-02-01 17:34:39 +00:00
|
|
|
self.assertEqual(is_code_ok, True, msg='Code returned is invalid.')
|
2015-06-23 19:32:12 +00:00
|
|
|
|
2016-04-14 19:22:38 +00:00
|
|
|
del data['allow']
|
|
|
|
response = self._auth_request('get', data, is_user_authenticated=True)
|
2015-06-23 19:32:12 +00:00
|
|
|
|
|
|
|
is_code_ok = is_code_valid(url=response['Location'],
|
|
|
|
user=self.user,
|
2017-03-31 16:34:03 +00:00
|
|
|
client=self.client_with_no_consent)
|
2016-02-01 17:34:39 +00:00
|
|
|
self.assertEqual(is_code_ok, True, msg='Code returned is invalid or missing.')
|
2015-07-10 10:22:25 +00:00
|
|
|
|
|
|
|
def test_response_uri_is_properly_constructed(self):
|
2017-07-07 07:07:21 +00:00
|
|
|
"""
|
|
|
|
Check that the redirect_uri matches the one configured for the client.
|
|
|
|
Only 'state' and 'code' should be appended.
|
|
|
|
"""
|
2016-04-14 19:22:38 +00:00
|
|
|
data = {
|
2015-07-10 10:22:25 +00:00
|
|
|
'client_id': self.client.client_id,
|
2017-07-07 07:07:21 +00:00
|
|
|
'redirect_uri': self.client.default_redirect_uri,
|
2015-07-10 10:22:25 +00:00
|
|
|
'response_type': 'code',
|
|
|
|
'scope': 'openid email',
|
|
|
|
'state': self.state,
|
|
|
|
'allow': 'Accept',
|
|
|
|
}
|
|
|
|
|
2016-04-14 19:22:38 +00:00
|
|
|
response = self._auth_request('post', data, is_user_authenticated=True)
|
2015-07-28 18:54:52 +00:00
|
|
|
|
2017-07-07 07:07:21 +00:00
|
|
|
parsed = urlsplit(response['Location'])
|
|
|
|
params = parse_qs(parsed.query or parsed.fragment)
|
|
|
|
state = params['state'][0]
|
|
|
|
self.assertEquals(self.state, state, msg="State returned is invalid or missing")
|
|
|
|
|
|
|
|
is_code_ok = is_code_valid(url=response['Location'],
|
|
|
|
user=self.user,
|
|
|
|
client=self.client)
|
|
|
|
self.assertTrue(is_code_ok, msg='Code returned is invalid or missing')
|
|
|
|
|
2017-08-08 22:41:42 +00:00
|
|
|
self.assertEquals(set(params.keys()), {'state', 'code'}, msg='More than state or code appended as query params')
|
2017-07-07 07:07:21 +00:00
|
|
|
|
2017-08-08 22:41:42 +00:00
|
|
|
self.assertTrue(
|
|
|
|
response['Location'].startswith(self.client.default_redirect_uri), msg='Different redirect_uri returned')
|
2017-07-07 07:07:21 +00:00
|
|
|
|
|
|
|
def test_unknown_redirect_uris_are_rejected(self):
|
|
|
|
"""
|
|
|
|
If a redirect_uri is not registered with the client the request must be rejected.
|
|
|
|
See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest.
|
|
|
|
"""
|
|
|
|
data = {
|
|
|
|
'client_id': self.client.client_id,
|
|
|
|
'response_type': 'code',
|
|
|
|
'redirect_uri': 'http://neverseenthis.com',
|
|
|
|
'scope': 'openid email',
|
|
|
|
'state': self.state,
|
|
|
|
}
|
|
|
|
|
|
|
|
response = self._auth_request('get', data)
|
|
|
|
self.assertIn(RedirectUriError.error, response.content.decode('utf-8'), msg='No redirect_uri error')
|
|
|
|
|
|
|
|
def test_manipulated_redirect_uris_are_rejected(self):
|
|
|
|
"""
|
|
|
|
If a redirect_uri does not exactly match the registered uri it must be rejected.
|
|
|
|
See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest.
|
|
|
|
"""
|
|
|
|
data = {
|
|
|
|
'client_id': self.client.client_id,
|
|
|
|
'response_type': 'code',
|
|
|
|
'redirect_uri': self.client.default_redirect_uri + "?some=query",
|
|
|
|
'scope': 'openid email',
|
|
|
|
'state': self.state,
|
|
|
|
}
|
|
|
|
|
|
|
|
response = self._auth_request('get', data)
|
|
|
|
self.assertIn(RedirectUriError.error, response.content.decode('utf-8'), msg='No redirect_uri error')
|
2016-01-19 19:08:13 +00:00
|
|
|
|
2016-04-08 21:09:24 +00:00
|
|
|
def test_public_client_auto_approval(self):
|
|
|
|
"""
|
2017-07-07 11:18:36 +00:00
|
|
|
It's recommended not auto-approving requests for non-confidential clients using Authorization Code.
|
2016-04-08 21:09:24 +00:00
|
|
|
"""
|
2016-04-14 20:45:30 +00:00
|
|
|
data = {
|
2017-03-31 16:34:03 +00:00
|
|
|
'client_id': self.client_public_with_no_consent.client_id,
|
2016-04-08 21:09:24 +00:00
|
|
|
'response_type': 'code',
|
2017-03-31 16:34:03 +00:00
|
|
|
'redirect_uri': self.client_public_with_no_consent.default_redirect_uri,
|
2016-04-08 21:09:24 +00:00
|
|
|
'scope': 'openid email',
|
|
|
|
'state': self.state,
|
2016-04-14 19:22:38 +00:00
|
|
|
}
|
2016-04-08 21:09:24 +00:00
|
|
|
|
2017-03-31 16:34:03 +00:00
|
|
|
response = self._auth_request('get', data, is_user_authenticated=True)
|
2016-04-08 21:09:24 +00:00
|
|
|
|
2016-09-08 20:15:25 +00:00
|
|
|
self.assertIn('Request for Permission', response.content.decode('utf-8'))
|
2016-01-19 19:08:13 +00:00
|
|
|
|
2017-06-06 09:12:37 +00:00
|
|
|
def test_prompt_none_parameter(self):
|
2016-04-14 20:45:30 +00:00
|
|
|
"""
|
|
|
|
Specifies whether the Authorization Server prompts the End-User for reauthentication and consent.
|
|
|
|
See: http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
|
|
|
"""
|
|
|
|
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,
|
2017-06-06 09:12:37 +00:00
|
|
|
'prompt': 'none'
|
2016-04-14 20:45:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
response = self._auth_request('get', data)
|
|
|
|
|
|
|
|
# An error is returned if an End-User is not already authenticated.
|
2016-09-08 20:15:25 +00:00
|
|
|
self.assertIn('login_required', response['Location'])
|
2016-04-14 20:45:30 +00:00
|
|
|
|
|
|
|
response = self._auth_request('get', data, is_user_authenticated=True)
|
|
|
|
|
|
|
|
# An error is returned if the Client does not have pre-configured consent for the requested Claims.
|
2017-06-06 09:12:37 +00:00
|
|
|
self.assertIn('consent_required', response['Location'])
|
|
|
|
|
|
|
|
@patch('oidc_provider.views.django_user_logout')
|
|
|
|
def test_prompt_login_parameter(self, logout_function):
|
|
|
|
"""
|
|
|
|
Specifies whether the Authorization Server prompts the End-User for reauthentication and consent.
|
|
|
|
See: http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
|
|
|
"""
|
|
|
|
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,
|
|
|
|
'prompt': 'login'
|
|
|
|
}
|
|
|
|
|
|
|
|
response = self._auth_request('get', data)
|
|
|
|
self.assertIn(settings.get('OIDC_LOGIN_URL'), response['Location'])
|
2017-07-19 08:52:10 +00:00
|
|
|
self.assertNotIn(
|
|
|
|
quote('prompt=login'),
|
|
|
|
response['Location'],
|
2017-08-08 22:41:42 +00:00
|
|
|
"Found prompt=login, this leads to infinite login loop. See "
|
|
|
|
"https://github.com/juanifioren/django-oidc-provider/issues/197."
|
2017-07-19 08:52:10 +00:00
|
|
|
)
|
2017-06-06 09:12:37 +00:00
|
|
|
|
|
|
|
response = self._auth_request('get', data, is_user_authenticated=True)
|
|
|
|
self.assertIn(settings.get('OIDC_LOGIN_URL'), response['Location'])
|
|
|
|
self.assertTrue(logout_function.called_once())
|
2017-07-19 08:52:10 +00:00
|
|
|
self.assertNotIn(
|
|
|
|
quote('prompt=login'),
|
|
|
|
response['Location'],
|
2017-08-08 22:41:42 +00:00
|
|
|
"Found prompt=login, this leads to infinite login loop. See "
|
|
|
|
"https://github.com/juanifioren/django-oidc-provider/issues/197."
|
2017-07-19 08:52:10 +00:00
|
|
|
)
|
2017-06-06 09:12:37 +00:00
|
|
|
|
|
|
|
def test_prompt_login_none_parameter(self):
|
|
|
|
"""
|
|
|
|
Specifies whether the Authorization Server prompts the End-User for reauthentication and consent.
|
|
|
|
See: http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
|
|
|
"""
|
|
|
|
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,
|
|
|
|
'prompt': 'login none'
|
|
|
|
}
|
|
|
|
|
|
|
|
response = self._auth_request('get', data)
|
|
|
|
self.assertIn('login_required', response['Location'])
|
|
|
|
|
|
|
|
response = self._auth_request('get', data, is_user_authenticated=True)
|
|
|
|
self.assertIn('login_required', response['Location'])
|
|
|
|
|
|
|
|
@patch('oidc_provider.views.render')
|
|
|
|
def test_prompt_consent_parameter(self, render_patched):
|
|
|
|
"""
|
|
|
|
Specifies whether the Authorization Server prompts the End-User for reauthentication and consent.
|
|
|
|
See: http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
|
|
|
"""
|
|
|
|
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,
|
|
|
|
'prompt': 'consent'
|
|
|
|
}
|
|
|
|
|
|
|
|
response = self._auth_request('get', data)
|
|
|
|
self.assertIn(settings.get('OIDC_LOGIN_URL'), response['Location'])
|
|
|
|
|
|
|
|
response = self._auth_request('get', data, is_user_authenticated=True)
|
|
|
|
render_patched.assert_called_once()
|
|
|
|
self.assertTrue(render_patched.call_args[0][1], settings.get('OIDC_TEMPLATES')['authorize'])
|
|
|
|
|
|
|
|
def test_prompt_consent_none_parameter(self):
|
|
|
|
"""
|
|
|
|
Specifies whether the Authorization Server prompts the End-User for reauthentication and consent.
|
|
|
|
See: http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
|
|
|
"""
|
|
|
|
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,
|
|
|
|
'prompt': 'consent none'
|
|
|
|
}
|
|
|
|
|
|
|
|
response = self._auth_request('get', data)
|
|
|
|
self.assertIn('login_required', response['Location'])
|
|
|
|
|
|
|
|
response = self._auth_request('get', data, is_user_authenticated=True)
|
|
|
|
self.assertIn('consent_required', response['Location'])
|
|
|
|
|
2016-08-08 18:20:47 +00:00
|
|
|
|
2016-09-08 20:15:25 +00:00
|
|
|
class AuthorizationImplicitFlowTestCase(TestCase, AuthorizeEndpointMixin):
|
2016-08-08 18:20:47 +00:00
|
|
|
"""
|
|
|
|
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)
|
2017-07-07 11:18:36 +00:00
|
|
|
self.client_public_no_consent = create_fake_client(
|
|
|
|
response_type='id_token token', is_public=True,
|
|
|
|
require_consent=False)
|
2016-08-08 18:20:47 +00:00
|
|
|
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 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)
|
|
|
|
|
2016-09-08 20:15:25 +00:00
|
|
|
self.assertIn('#error=invalid_request', response['Location'])
|
2016-08-08 18:20:47 +00:00
|
|
|
|
2016-09-08 20:15:25 +00:00
|
|
|
def test_idtoken_token_response(self):
|
2016-08-08 18:20:47 +00:00
|
|
|
"""
|
|
|
|
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'])
|
|
|
|
|
2016-09-08 20:15:25 +00:00
|
|
|
def test_idtoken_response(self):
|
2016-08-08 18:20:47 +00:00
|
|
|
"""
|
|
|
|
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'])
|
|
|
|
|
2016-09-08 20:15:25 +00:00
|
|
|
def test_idtoken_token_at_hash(self):
|
2016-08-08 18:20:47 +00:00
|
|
|
"""
|
|
|
|
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)
|
|
|
|
|
2016-09-08 20:15:25 +00:00
|
|
|
def test_idtoken_at_hash(self):
|
2016-08-08 18:20:47 +00:00
|
|
|
"""
|
|
|
|
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)
|
2016-09-08 20:15:25 +00:00
|
|
|
|
2017-07-07 11:18:36 +00:00
|
|
|
def test_public_client_implicit_auto_approval(self):
|
|
|
|
"""
|
|
|
|
Public clients using Implicit Flow should be able to reuse consent.
|
|
|
|
"""
|
|
|
|
data = {
|
|
|
|
'client_id': self.client_public_no_consent.client_id,
|
|
|
|
'response_type': self.client_public_no_consent.response_type,
|
|
|
|
'redirect_uri': self.client_public_no_consent.default_redirect_uri,
|
|
|
|
'scope': 'openid email',
|
|
|
|
'state': self.state,
|
|
|
|
'nonce': self.nonce,
|
|
|
|
}
|
|
|
|
|
|
|
|
response = self._auth_request('get', data, is_user_authenticated=True)
|
|
|
|
response_text = response.content.decode('utf-8')
|
|
|
|
self.assertEquals(response_text, '')
|
|
|
|
components = urlsplit(response['Location'])
|
|
|
|
fragment = parse_qs(components[4])
|
|
|
|
self.assertIn('access_token', fragment)
|
|
|
|
self.assertIn('id_token', fragment)
|
|
|
|
self.assertIn('expires_in', fragment)
|
|
|
|
|
2016-09-08 20:15:25 +00:00
|
|
|
|
|
|
|
class AuthorizationHybridFlowTestCase(TestCase, AuthorizeEndpointMixin):
|
|
|
|
"""
|
|
|
|
Test cases for Authorization Endpoint using Hybrid Flow.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
call_command('creatersakey')
|
|
|
|
self.factory = RequestFactory()
|
|
|
|
self.user = create_fake_user()
|
|
|
|
self.client_code_idtoken_token = create_fake_client(response_type='code id_token token', is_public=True)
|
|
|
|
self.state = uuid.uuid4().hex
|
|
|
|
self.nonce = uuid.uuid4().hex
|
|
|
|
|
2016-09-09 16:10:12 +00:00
|
|
|
# Base data for the auth request.
|
|
|
|
self.data = {
|
2016-09-08 20:15:25 +00:00
|
|
|
'client_id': self.client_code_idtoken_token.client_id,
|
|
|
|
'redirect_uri': self.client_code_idtoken_token.default_redirect_uri,
|
|
|
|
'response_type': self.client_code_idtoken_token.response_type,
|
|
|
|
'scope': 'openid email',
|
|
|
|
'state': self.state,
|
|
|
|
'nonce': self.nonce,
|
|
|
|
'allow': 'Accept',
|
|
|
|
}
|
|
|
|
|
2016-09-09 16:10:12 +00:00
|
|
|
def test_code_idtoken_token_response(self):
|
|
|
|
"""
|
|
|
|
Implicit client requesting `id_token token` receives both id token
|
|
|
|
and access token as the result of the authorization request.
|
|
|
|
"""
|
|
|
|
response = self._auth_request('post', self.data, is_user_authenticated=True)
|
2016-09-08 20:15:25 +00:00
|
|
|
|
|
|
|
self.assertIn('#', response['Location'])
|
|
|
|
self.assertIn('access_token', response['Location'])
|
|
|
|
self.assertIn('id_token', response['Location'])
|
|
|
|
self.assertIn('state', response['Location'])
|
|
|
|
self.assertIn('code', response['Location'])
|
|
|
|
|
|
|
|
# Validate code.
|
|
|
|
is_code_ok = is_code_valid(url=response['Location'],
|
|
|
|
user=self.user,
|
|
|
|
client=self.client_code_idtoken_token)
|
|
|
|
self.assertEqual(is_code_ok, True, msg='Code returned is invalid.')
|
2016-09-09 16:10:12 +00:00
|
|
|
|
|
|
|
@override_settings(OIDC_TOKEN_EXPIRE=36000)
|
|
|
|
def test_access_token_expiration(self):
|
|
|
|
"""
|
|
|
|
Add ten hours of expiration to access_token. Check for the expires_in query in fragment.
|
|
|
|
"""
|
|
|
|
response = self._auth_request('post', self.data, is_user_authenticated=True)
|
|
|
|
|
|
|
|
self.assertIn('expires_in=36000', response['Location'])
|
2017-05-05 03:19:57 +00:00
|
|
|
|
|
|
|
|
|
|
|
class TestCreateResponseURI(TestCase):
|
|
|
|
def setUp(self):
|
|
|
|
url = reverse('oidc_provider:authorize')
|
|
|
|
user = create_fake_user()
|
|
|
|
client = create_fake_client(response_type='code', is_public=True)
|
|
|
|
|
|
|
|
# Base data to create a uri response
|
|
|
|
data = {
|
|
|
|
'client_id': client.client_id,
|
|
|
|
'redirect_uri': client.default_redirect_uri,
|
|
|
|
'response_type': client.response_type,
|
|
|
|
}
|
|
|
|
|
|
|
|
factory = RequestFactory()
|
|
|
|
self.request = factory.post(url, data=data)
|
|
|
|
self.request.user = user
|
|
|
|
|
|
|
|
@patch('oidc_provider.lib.endpoints.authorize.create_code')
|
|
|
|
@patch('oidc_provider.lib.endpoints.authorize.logger.exception')
|
|
|
|
def test_create_response_uri_logs_to_error(self, log_exception, create_code):
|
|
|
|
"""
|
|
|
|
A lot can go wrong when creating a response uri and this is caught with a general Exception error. The
|
|
|
|
information contained within this error should show up in the error log so production servers have something
|
|
|
|
to work with when things don't work as expected.
|
|
|
|
"""
|
|
|
|
exception = Exception("Something went wrong!")
|
|
|
|
create_code.side_effect = exception
|
|
|
|
|
|
|
|
authorization_endpoint = AuthorizeEndpoint(self.request)
|
|
|
|
authorization_endpoint.validate_params()
|
|
|
|
|
|
|
|
with self.assertRaises(Exception):
|
|
|
|
authorization_endpoint.create_response_uri()
|
|
|
|
|
|
|
|
log_exception.assert_called_once_with('[Authorize] Error when trying to create response uri: %s', exception)
|
|
|
|
|
|
|
|
@override_settings(OIDC_SESSION_MANAGEMENT_ENABLE=True)
|
|
|
|
def test_create_response_uri_generates_session_state_if_session_management_enabled(self):
|
|
|
|
# RequestFactory doesn't support sessions, so we mock it
|
|
|
|
self.request.session = mock.Mock(session_key=None)
|
|
|
|
|
|
|
|
authorization_endpoint = AuthorizeEndpoint(self.request)
|
|
|
|
authorization_endpoint.validate_params()
|
|
|
|
|
|
|
|
uri = authorization_endpoint.create_response_uri()
|
|
|
|
self.assertIn('session_state=', uri)
|