2015-07-01 15:53:41 +00:00
|
|
|
try:
|
|
|
|
from urllib.parse import unquote, urlencode
|
2015-07-01 19:43:35 +00:00
|
|
|
except ImportError:
|
|
|
|
from urllib import unquote, urlencode
|
2015-03-12 15:42:52 +00:00
|
|
|
import uuid
|
2015-02-27 20:40:17 +00:00
|
|
|
|
2015-02-12 18:04:58 +00:00
|
|
|
from django.contrib.auth import REDIRECT_FIELD_NAME
|
|
|
|
from django.contrib.auth.models import AnonymousUser
|
2015-02-11 18:37:51 +00:00
|
|
|
from django.core.urlresolvers import reverse
|
|
|
|
from django.test import RequestFactory
|
|
|
|
from django.test import TestCase
|
2015-02-27 20:40:17 +00:00
|
|
|
|
2015-02-18 18:07:22 +00:00
|
|
|
from oidc_provider import settings
|
2015-02-27 20:40:17 +00:00
|
|
|
from oidc_provider.models import *
|
2015-07-14 16:27:46 +00:00
|
|
|
from oidc_provider.tests.app.utils import *
|
2015-02-18 18:07:22 +00:00
|
|
|
from oidc_provider.views import *
|
2015-02-11 18:37:51 +00:00
|
|
|
|
|
|
|
|
2015-02-19 18:45:51 +00:00
|
|
|
class AuthorizationCodeFlowTestCase(TestCase):
|
2015-03-11 17:36:52 +00:00
|
|
|
"""
|
2015-03-30 18:37:48 +00:00
|
|
|
Test cases for Authorize Endpoint using Authorization Code Flow.
|
2015-03-11 17:36:52 +00:00
|
|
|
"""
|
2015-02-11 18:37:51 +00:00
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
self.factory = RequestFactory()
|
|
|
|
self.user = create_fake_user()
|
|
|
|
self.client = create_fake_client(response_type='code')
|
2015-02-27 20:40:17 +00:00
|
|
|
self.state = uuid.uuid4().hex
|
2015-02-11 18:37:51 +00:00
|
|
|
|
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
|
|
|
|
"""
|
2015-02-18 18:07:22 +00:00
|
|
|
url = reverse('oidc_provider:authorize')
|
2015-02-27 20:40:17 +00:00
|
|
|
|
2015-02-11 18:37:51 +00:00
|
|
|
request = self.factory.get(url)
|
|
|
|
|
|
|
|
response = AuthorizeView.as_view()(request)
|
|
|
|
|
|
|
|
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.
|
2015-07-01 15:53:41 +00:00
|
|
|
query_str = urlencode({
|
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,
|
|
|
|
}).replace('+', '%20')
|
|
|
|
|
|
|
|
url = reverse('oidc_provider:authorize') + '?' + query_str
|
2015-02-12 18:04:58 +00:00
|
|
|
|
2015-02-11 20:38:37 +00:00
|
|
|
request = self.factory.get(url)
|
|
|
|
|
|
|
|
response = AuthorizeView.as_view()(request)
|
|
|
|
|
|
|
|
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.
|
|
|
|
query_exists = 'error=' in response['Location']
|
|
|
|
self.assertEqual(query_exists, True)
|
|
|
|
|
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
|
|
|
|
"""
|
2015-07-01 15:53:41 +00:00
|
|
|
query_str = urlencode({
|
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,
|
|
|
|
}).replace('+', '%20')
|
|
|
|
|
|
|
|
url = reverse('oidc_provider:authorize') + '?' + query_str
|
2015-02-12 18:04:58 +00:00
|
|
|
|
|
|
|
request = self.factory.get(url)
|
|
|
|
request.user = AnonymousUser()
|
|
|
|
|
|
|
|
response = AuthorizeView.as_view()(request)
|
|
|
|
|
|
|
|
# Check if user was redirected to the login view.
|
|
|
|
login_url_exists = settings.get('LOGIN_URL') in response['Location']
|
|
|
|
self.assertEqual(login_url_exists, True)
|
|
|
|
|
|
|
|
# Check if the login will redirect to a valid url.
|
|
|
|
try:
|
|
|
|
next_value = response['Location'].split(REDIRECT_FIELD_NAME + '=')[1]
|
2015-07-01 15:53:41 +00:00
|
|
|
next_url = unquote(next_value)
|
2015-02-12 18:04:58 +00:00
|
|
|
is_next_ok = next_url == url
|
|
|
|
except:
|
|
|
|
is_next_ok = False
|
2015-02-13 13:44:09 +00:00
|
|
|
self.assertEqual(is_next_ok, True)
|
|
|
|
|
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
|
|
|
|
"""
|
2015-07-01 15:53:41 +00:00
|
|
|
query_str = urlencode({
|
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',
|
2015-02-27 20:40:17 +00:00
|
|
|
}).replace('+', '%20')
|
2015-02-19 18:45:51 +00:00
|
|
|
|
2015-02-27 20:40:17 +00:00
|
|
|
url = reverse('oidc_provider:authorize') + '?' + query_str
|
2015-02-13 13:44:09 +00:00
|
|
|
|
|
|
|
request = self.factory.get(url)
|
|
|
|
# Simulate that the user is logged.
|
|
|
|
request.user = self.user
|
|
|
|
|
2015-07-22 19:25:17 +00:00
|
|
|
response = AuthorizeView.as_view()(request)
|
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')
|
2015-02-19 18:45:51 +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.
|
|
|
|
"""
|
|
|
|
response_type = 'code'
|
|
|
|
|
|
|
|
url = reverse('oidc_provider:authorize')
|
|
|
|
|
|
|
|
post_data = {
|
|
|
|
'client_id': self.client.client_id,
|
|
|
|
'redirect_uri': self.client.default_redirect_uri,
|
|
|
|
'response_type': response_type,
|
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
|
|
|
}
|
|
|
|
|
|
|
|
request = self.factory.post(url, data=post_data)
|
|
|
|
# Simulate that the user is logged.
|
|
|
|
request.user = self.user
|
|
|
|
|
|
|
|
response = AuthorizeView.as_view()(request)
|
|
|
|
|
|
|
|
# Because user doesn't allow app, SHOULD exists an error parameter
|
|
|
|
# in the query.
|
2015-03-06 15:56:35 +00:00
|
|
|
self.assertEqual('error=' in response['Location'], True,
|
2015-04-30 15:42:00 +00:00
|
|
|
msg='error param is missing in query.')
|
2015-03-06 15:56:35 +00:00
|
|
|
self.assertEqual('access_denied' in response['Location'], True,
|
2015-04-30 15:42:00 +00:00
|
|
|
msg='"access_denied" code is missing in query.')
|
2015-02-27 20:40:17 +00:00
|
|
|
|
|
|
|
# Simulate user authorization.
|
|
|
|
post_data['allow'] = 'Accept' # Should be the value of the button.
|
|
|
|
|
|
|
|
request = self.factory.post(url, data=post_data)
|
|
|
|
# Simulate that the user is logged.
|
|
|
|
request.user = self.user
|
|
|
|
|
|
|
|
response = AuthorizeView.as_view()(request)
|
|
|
|
|
2015-06-23 19:32:12 +00:00
|
|
|
is_code_ok = is_code_valid(url=response['Location'],
|
|
|
|
user=self.user,
|
|
|
|
client=self.client)
|
2015-03-06 15:56:35 +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]
|
2015-03-06 15:56:35 +00:00
|
|
|
self.assertEqual(state == self.state, True,
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
post_data = {
|
|
|
|
'client_id': self.client.client_id,
|
|
|
|
'redirect_uri': self.client.default_redirect_uri,
|
|
|
|
'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
|
|
|
|
|
2016-02-01 17:34:39 +00:00
|
|
|
with self.settings(OIDC_SKIP_CONSENT_ALWAYS=True):
|
|
|
|
response = AuthorizeView.as_view()(request)
|
|
|
|
|
|
|
|
self.assertEqual('code' in response['Location'], True,
|
|
|
|
msg='Code is missing in the returned url.')
|
|
|
|
|
2015-06-23 19:32:12 +00:00
|
|
|
response = AuthorizeView.as_view()(request)
|
|
|
|
|
|
|
|
is_code_ok = is_code_valid(url=response['Location'],
|
|
|
|
user=self.user,
|
|
|
|
client=self.client)
|
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
|
|
|
|
|
|
|
del post_data['allow']
|
2015-07-01 15:53:41 +00:00
|
|
|
query_str = urlencode(post_data).replace('+', '%20')
|
2015-06-23 19:32:12 +00:00
|
|
|
|
|
|
|
url = reverse('oidc_provider:authorize') + '?' + query_str
|
|
|
|
|
|
|
|
request = self.factory.get(url)
|
|
|
|
# Simulate that the user is logged.
|
|
|
|
request.user = self.user
|
|
|
|
|
|
|
|
# Ensure user consent skip is enabled.
|
2015-07-22 19:25:17 +00:00
|
|
|
response = AuthorizeView.as_view()(request)
|
2015-06-23 19:32:12 +00:00
|
|
|
|
|
|
|
is_code_ok = is_code_valid(url=response['Location'],
|
|
|
|
user=self.user,
|
|
|
|
client=self.client)
|
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):
|
2016-02-01 17:34:39 +00:00
|
|
|
"""
|
|
|
|
TODO
|
|
|
|
"""
|
2015-07-10 10:22:25 +00:00
|
|
|
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.')
|
2015-07-28 18:54:52 +00:00
|
|
|
|
|
|
|
def test_scope_with_plus(self):
|
|
|
|
"""
|
|
|
|
In query string, scope use `+` instead of the space url-encoded.
|
|
|
|
"""
|
|
|
|
scope_test = 'openid email profile'
|
|
|
|
|
|
|
|
query_str = urlencode({
|
|
|
|
'client_id': self.client.client_id,
|
|
|
|
'response_type': 'code',
|
|
|
|
'redirect_uri': self.client.default_redirect_uri,
|
|
|
|
'scope': scope_test,
|
|
|
|
'state': self.state,
|
|
|
|
})
|
|
|
|
|
|
|
|
url = reverse('oidc_provider:authorize') + '?' + query_str
|
|
|
|
|
|
|
|
request = self.factory.get(url)
|
|
|
|
# Simulate that the user is logged.
|
|
|
|
request.user = self.user
|
|
|
|
|
|
|
|
response = AuthorizeView.as_view()(request)
|
|
|
|
|
|
|
|
self.assertEqual(scope_test in response.content.decode('utf-8'), True)
|
2016-01-19 19:08:13 +00:00
|
|
|
|
|
|
|
|
|
|
|
class ImplicitFlowTestCase(TestCase):
|
|
|
|
"""
|
|
|
|
Test cases for Authorize Endpoint using Implicit Grant Flow.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
self.factory = RequestFactory()
|
|
|
|
self.user = create_fake_user()
|
|
|
|
self.client = create_fake_client(response_type='id_token token')
|
|
|
|
self.state = uuid.uuid4().hex
|
|
|
|
self.nonce = uuid.uuid4().hex
|
2016-01-25 20:52:24 +00:00
|
|
|
create_rsakey()
|
2016-01-19 19:08:13 +00:00
|
|
|
|
|
|
|
def test_missing_nonce(self):
|
|
|
|
"""
|
|
|
|
The `nonce` parameter is REQUIRED if you use the Implicit Flow.
|
|
|
|
"""
|
|
|
|
query_str = urlencode({
|
|
|
|
'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,
|
|
|
|
}).replace('+', '%20')
|
|
|
|
|
|
|
|
url = reverse('oidc_provider:authorize') + '?' + query_str
|
|
|
|
|
|
|
|
request = self.factory.get(url)
|
|
|
|
# Simulate that the user is logged.
|
|
|
|
request.user = self.user
|
|
|
|
|
|
|
|
response = AuthorizeView.as_view()(request)
|
|
|
|
|
|
|
|
self.assertEqual('#error=invalid_request' in response['Location'], True)
|
|
|
|
|
|
|
|
def test_access_token_response(self):
|
|
|
|
"""
|
|
|
|
Unlike the Authorization Code flow, in which the client makes
|
|
|
|
separate requests for authorization and for an access token, the client
|
|
|
|
receives the access token as the result of the authorization request.
|
|
|
|
"""
|
|
|
|
post_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',
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
self.assertEqual('access_token' in response['Location'], True)
|