Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion oidc_provider/lib/endpoints/authorize.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def __init__(self, request):
elif self.params['response_type'] in ['id_token', 'id_token token', 'token']:
self.grant_type = 'implicit'
elif self.params['response_type'] in [
'code token', 'code id_token', 'code id_token token']:
'code token', 'code id_token', 'code id_token token']:
self.grant_type = 'hybrid'
else:
self.grant_type = None
Expand All @@ -77,6 +77,7 @@ def _extract_params(self):
self.params['scope'] = query_dict.get('scope', '').split()
self.params['state'] = query_dict.get('state', '')
self.params['nonce'] = query_dict.get('nonce', '')
self.params['response_mode'] = query_dict.get('response_mode', '')

self.params['prompt'] = self._allowed_prompt_params.intersection(
set(query_dict.get('prompt', '').split()))
Expand Down Expand Up @@ -227,6 +228,23 @@ def create_response_uri(self):

return urlunsplit(uri)

def get_form_post_context(self, uri):
"""
Return dict of context for `form_post.html`

Returns dict:
- params: dict of key: value for hidden form_post fields
- redirect_url: url for form action
"""
split_uri = urlsplit(uri)
frag = parse_qs(split_uri.fragment)
query = parse_qs(split_uri.query)
params = frag if frag else query
params = {key: value[0] for key, value in params.items()}
dic = {'redirect_url': self.params['redirect_uri']}
dic['params'] = params
return dic

def set_client_user_consent(self):
"""
Save the user consent given to a specific client.
Expand Down
3 changes: 2 additions & 1 deletion oidc_provider/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ def OIDC_GRANT_TYPE_PASSWORD_ENABLE(self):
def OIDC_TEMPLATES(self):
return {
'authorize': 'oidc_provider/authorize.html',
'error': 'oidc_provider/error.html'
'error': 'oidc_provider/error.html',
'form_post': 'oidc_provider/form_post.html',
}

@property
Expand Down
12 changes: 12 additions & 0 deletions oidc_provider/templates/oidc_provider/form_post.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<html>
<body onload="document.form.submit();">
form post
<form method="post" action="{{ redirect_url }}" name="form">
{% csrf_token %}
{% for field, value in params.items %}
<input type="hidden" name="{{ field }}" value="{{ value }}" />
{% endfor %}
</form>

</body>
</html>
211 changes: 211 additions & 0 deletions oidc_provider/tests/cases/test_authorize_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,217 @@ def test_strip_prompt_login(self):
self.assertNotIn('login', strip_prompt_login(path3))


class TestAuthorizeResponseModeFormPost(TestCase, AuthorizeEndpointMixin):
"""Test cases authorization endpoint when response_mode=form_post

If response_mode=form_post, then the response from the server
should be a post to the redirect_uri instead of a GET

This will render a template with variables as input fields and then
autosubmit the form via javascript

https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html#FormPostResponseMode
"""

def setUp(self):
call_command('creatersakey')
self.factory = RequestFactory()
self.user = create_fake_user()
self.client = create_fake_client(response_type='code')
self.client_with_no_consent = create_fake_client(
response_type='code', require_consent=False)
self.client_public = create_fake_client(response_type='code', is_public=True)
self.client_public_with_no_consent = create_fake_client(
response_type='code', is_public=True, require_consent=False)
self.state = uuid.uuid4().hex
self.nonce = uuid.uuid4().hex

def test_success_via_get(self):
"""
Test with response_mode=form_post via GET to authorize
"""

data = {
'client_id': self.client_with_no_consent.client_id,
'response_type': 'code',
'response_mode': 'form_post',
'redirect_uri': self.client_with_no_consent.default_redirect_uri,
'scope': 'openid ',
'state': self.state,
'prompt': 'none',
}

response = self._auth_request('get', data, is_user_authenticated=True)

self.assertIn('action="{}"'.format(self.client.redirect_uris[0]),
response.content.decode('utf-8'))
self.assertIn('<input type="hidden" name="state" value="{}" />'.format(self.state),
response.content.decode('utf-8'))
self.assertIn('<input type="hidden" name="code" value='.format(self.state),
response.content.decode('utf-8'))

def test_success_via_post(self):
"""
Test with response_mode=form_post via POST to authorize
"""

data = {
'client_id': self.client.client_id,
'response_type': 'code',
'response_mode': 'form_post',
'redirect_uri': self.client.default_redirect_uri,
'scope': 'openid ',
'state': self.state,
'prompt': 'none',
'allow': 'Accept',
}

response = self._auth_request('post', data, is_user_authenticated=True)

self.assertIn('action="{}"'.format(self.client.redirect_uris[0]),
response.content.decode('utf-8'))
self.assertIn('<input type="hidden" name="state" value="{}" />'.format(self.state),
response.content.decode('utf-8'))
self.assertIn('<input type="hidden" name="code" value='.format(self.state),
response.content.decode('utf-8'))

def test_error_via_get(self):
"""
If error occurs when response_mode=form_post via GET at authorize,
the response_mode should be honored

https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#MultiValueResponseTypes

Error is that user requires consent
"""

data = {
'client_id': self.client.client_id,
'response_type': 'code',
'response_mode': 'form_post',
'redirect_uri': self.client.default_redirect_uri,
'scope': 'openid ',
'state': self.state,
'prompt': 'none',
}

response = self._auth_request('get', data, is_user_authenticated=True)

self.assertIn('action="{}"'.format(self.client.redirect_uris[0]),
response.content.decode('utf-8'))
self.assertIn('<input type="hidden" name="state" value="{}" />'.format(self.state),
response.content.decode('utf-8'))
self.assertIn('<input type="hidden" name="error" value=',
response.content.decode('utf-8'))
self.assertIn('<input type="hidden" name="error_description" value=',
response.content.decode('utf-8'))
self.assertNotIn('<input type="hidden" name="code"',
response.content.decode('utf-8'))

def test_user_declines_via_post(self):
"""
If error user does not consent when response_mode=form_post via POST
at authorize, the response_mode should be honored

https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#MultiValueResponseTypes
"""

data = {
'client_id': self.client.client_id,
'response_type': 'code',
'response_mode': 'form_post',
'redirect_uri': self.client.default_redirect_uri,
'scope': 'openid ',
'state': self.state,
'prompt': 'none',
}

response = self._auth_request('post', data, is_user_authenticated=True)

self.assertIn('action="{}"'.format(self.client.redirect_uris[0]),
response.content.decode('utf-8'))
self.assertIn('<input type="hidden" name="state" value="{}" />'.format(self.state),
response.content.decode('utf-8'))
self.assertIn('<input type="hidden" name="error" value=',
response.content.decode('utf-8'))
self.assertIn('<input type="hidden" name="error_description" value=',
response.content.decode('utf-8'))
self.assertNotIn('<input type="hidden" name="code"',
response.content.decode('utf-8'))

def test_error_via_post(self):
"""
If error occurs when response_mode=form_post via POST at authorize,
the response_mode should be honored

https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#MultiValueResponseTypes
"""

data = {
'client_id': self.client.client_id,
'response_type': 'bad fishy!',
'response_mode': 'form_post',
'redirect_uri': self.client.default_redirect_uri,
'scope': 'openid ',
'state': self.state,
'prompt': 'none',
'allow': 'Accept',
}

response = self._auth_request('post', data, is_user_authenticated=True)

self.assertIn('action="{}"'.format(self.client.redirect_uris[0]),
response.content.decode('utf-8'))
self.assertIn('<input type="hidden" name="state" value="{}" />'.format(self.state),
response.content.decode('utf-8'))
self.assertIn('<input type="hidden" name="error" value=',
response.content.decode('utf-8'))
self.assertIn('<input type="hidden" name="error_description" value=',
response.content.decode('utf-8'))
self.assertNotIn('<input type="hidden" name="code"',
response.content.decode('utf-8'))

def test_success_get_reuse_consent(self):
"""
Test with response_mode=form_post via GET to authorize when reusing
consent
"""

self.client.reuse_consent = True
self.client.save()

data = {
'client_id': self.client.client_id,
'response_type': 'code',
'response_mode': 'form_post',
'redirect_uri': self.client.default_redirect_uri,
'scope': 'openid ',
'state': self.state,
'prompt': 'none',
'allow': 'Accept',
}

response = self._auth_request('post', data, is_user_authenticated=True)

self.assertIn('action="{}"'.format(self.client.redirect_uris[0]),
response.content.decode('utf-8'))
self.assertIn('<input type="hidden" name="state" value="{}" />'.format(self.state),
response.content.decode('utf-8'))
self.assertIn('<input type="hidden" name="code" value='.format(self.state),
response.content.decode('utf-8'))

del data['allow']

response = self._auth_request('get', data, is_user_authenticated=True)

self.assertIn('action="{}"'.format(self.client.redirect_uris[0]),
response.content.decode('utf-8'))
self.assertIn('<input type="hidden" name="state" value="{}" />'.format(self.state),
response.content.decode('utf-8'))
self.assertIn('<input type="hidden" name="code" value='.format(self.state),
response.content.decode('utf-8'))


class AuthorizationImplicitFlowTestCase(TestCase, AuthorizeEndpointMixin):
"""
Test cases for Authorization Endpoint using Implicit Flow.
Expand Down
3 changes: 2 additions & 1 deletion oidc_provider/tests/cases/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

CUSTOM_TEMPLATES = {
'authorize': 'custom/authorize.html',
'error': 'custom/error.html'
'error': 'custom/error.html',
'form_post': 'custom/form_post.html',
}


Expand Down
33 changes: 31 additions & 2 deletions oidc_provider/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,26 @@ def get(self, request, *args, **kwargs):
if not authorize.client.require_consent and (
allow_skipping_consent and
'consent' not in authorize.params['prompt']):
return redirect(authorize.create_response_uri())
response_uri = authorize.create_response_uri()
if authorize.params['response_mode'] == 'form_post':
return render(
request,
OIDC_TEMPLATES['form_post'],
authorize.get_form_post_context(response_uri))
return redirect(response_uri)

if authorize.client.reuse_consent:
# Check if user previously give consent.
if authorize.client_has_user_consent() and (
allow_skipping_consent and
'consent' not in authorize.params['prompt']):
return redirect(authorize.create_response_uri())
response_uri = authorize.create_response_uri()
if authorize.params['response_mode'] == 'form_post':
return render(
request,
OIDC_TEMPLATES['form_post'],
authorize.get_form_post_context(response_uri))
return redirect(response_uri)

if 'none' in authorize.params['prompt']:
raise AuthorizeError(
Expand Down Expand Up @@ -166,6 +178,12 @@ def get(self, request, *args, **kwargs):
authorize.params['redirect_uri'],
authorize.params['state'])

if authorize.params['response_mode'] == 'form_post':
return render(
request,
OIDC_TEMPLATES['form_post'],
authorize.get_form_post_context(uri))

return redirect(uri)

def post(self, request, *args, **kwargs):
Expand All @@ -191,6 +209,11 @@ def post(self, request, *args, **kwargs):
authorize.set_client_user_consent()

uri = authorize.create_response_uri()
if authorize.params['response_mode'] == 'form_post':
return render(
request,
OIDC_TEMPLATES['form_post'],
authorize.get_form_post_context(uri))

return redirect(uri)

Expand All @@ -199,6 +222,12 @@ def post(self, request, *args, **kwargs):
authorize.params['redirect_uri'],
authorize.params['state'])

if authorize.params['response_mode'] == 'form_post':
return render(
request,
OIDC_TEMPLATES['form_post'],
authorize.get_form_post_context(uri))

return redirect(uri)


Expand Down