Use stored user consent for public clients too (#189)
When using Implicit Flow, it should be OK to use the stored user consent even if the client is public. The redirect uri checks should make sure that the stored consent of another client cannot be misused to get a consent to a site that is not related to the client. It is also important to support this, since public clients using Implicit Flow do not have a refresh token to update their access tokens, so only way to keep their login session open is by issuing authorization requests from an iframe with the "prompt=none" parameter (which does not work without the previously stored consent). See the following links for more info and examples on how to renew the access token with SPAs: https://auth0.com/docs/api-auth/tutorials/silent-authentication#refresh-expired-tokens https://damienbod.com/2017/06/02/ https://github.com/IdentityServer/IdentityServer3/issues/719#issuecomment-230145034
This commit is contained in:
parent
1215c27d7e
commit
5165312d01
2 changed files with 42 additions and 9 deletions
|
@ -314,7 +314,7 @@ class AuthorizationCodeFlowTestCase(TestCase, AuthorizeEndpointMixin):
|
||||||
|
|
||||||
def test_public_client_auto_approval(self):
|
def test_public_client_auto_approval(self):
|
||||||
"""
|
"""
|
||||||
It's recommended not auto-approving requests for non-confidential clients.
|
It's recommended not auto-approving requests for non-confidential clients using Authorization Code.
|
||||||
"""
|
"""
|
||||||
data = {
|
data = {
|
||||||
'client_id': self.client_public_with_no_consent.client_id,
|
'client_id': self.client_public_with_no_consent.client_id,
|
||||||
|
@ -449,6 +449,9 @@ class AuthorizationImplicitFlowTestCase(TestCase, AuthorizeEndpointMixin):
|
||||||
self.user = create_fake_user()
|
self.user = create_fake_user()
|
||||||
self.client = create_fake_client(response_type='id_token token')
|
self.client = create_fake_client(response_type='id_token token')
|
||||||
self.client_public = create_fake_client(response_type='id_token token', is_public=True)
|
self.client_public = create_fake_client(response_type='id_token token', is_public=True)
|
||||||
|
self.client_public_no_consent = create_fake_client(
|
||||||
|
response_type='id_token token', is_public=True,
|
||||||
|
require_consent=False)
|
||||||
self.client_no_access = create_fake_client(response_type='id_token')
|
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.client_public_no_access = create_fake_client(response_type='id_token', is_public=True)
|
||||||
self.state = uuid.uuid4().hex
|
self.state = uuid.uuid4().hex
|
||||||
|
@ -582,6 +585,28 @@ class AuthorizationImplicitFlowTestCase(TestCase, AuthorizeEndpointMixin):
|
||||||
|
|
||||||
self.assertNotIn('at_hash', id_token)
|
self.assertNotIn('at_hash', id_token)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationHybridFlowTestCase(TestCase, AuthorizeEndpointMixin):
|
class AuthorizationHybridFlowTestCase(TestCase, AuthorizeEndpointMixin):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -85,13 +85,21 @@ class AuthorizeView(View):
|
||||||
if {'none', 'consent'}.issubset(authorize.params['prompt']):
|
if {'none', 'consent'}.issubset(authorize.params['prompt']):
|
||||||
raise AuthorizeError(authorize.params['redirect_uri'], 'consent_required', authorize.grant_type)
|
raise AuthorizeError(authorize.params['redirect_uri'], 'consent_required', authorize.grant_type)
|
||||||
|
|
||||||
if 'consent' not in authorize.params['prompt']:
|
implicit_flow_resp_types = set(['id_token', 'id_token token'])
|
||||||
if not authorize.client.require_consent and not (authorize.client.client_type == 'public'):
|
allow_skipping_consent = (
|
||||||
|
authorize.client.client_type != 'public' or
|
||||||
|
authorize.client.response_type in implicit_flow_resp_types)
|
||||||
|
|
||||||
|
if not authorize.client.require_consent and (
|
||||||
|
allow_skipping_consent and
|
||||||
|
'consent' not in authorize.params['prompt']):
|
||||||
return redirect(authorize.create_response_uri())
|
return redirect(authorize.create_response_uri())
|
||||||
|
|
||||||
if authorize.client.reuse_consent:
|
if authorize.client.reuse_consent:
|
||||||
# Check if user previously give consent.
|
# Check if user previously give consent.
|
||||||
if authorize.client_has_user_consent() and not (authorize.client.client_type == 'public'):
|
if authorize.client_has_user_consent() and (
|
||||||
|
allow_skipping_consent and
|
||||||
|
'consent' not in authorize.params['prompt']):
|
||||||
return redirect(authorize.create_response_uri())
|
return redirect(authorize.create_response_uri())
|
||||||
|
|
||||||
if 'none' in authorize.params['prompt']:
|
if 'none' in authorize.params['prompt']:
|
||||||
|
|
Loading…
Reference in a new issue