From 27f95c822ad63aa996ef21ecd0c41b1870999291 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Wed, 22 Jan 2020 23:35:48 +0100 Subject: [PATCH 2/4] ajustements --- src/authentic2_idp_oidc/app_settings.py | 4 + .../migrations/0001_initial.py | 4 +- .../migrations/0012_auto_20200122_2258.py | 25 ++ src/authentic2_idp_oidc/models.py | 16 +- src/authentic2_idp_oidc/views.py | 251 ++++++++++-------- tests/test_idp_oidc.py | 61 ++--- 6 files changed, 210 insertions(+), 151 deletions(-) create mode 100644 src/authentic2_idp_oidc/migrations/0012_auto_20200122_2258.py diff --git a/src/authentic2_idp_oidc/app_settings.py b/src/authentic2_idp_oidc/app_settings.py index 19b61bea..8f814d2c 100644 --- a/src/authentic2_idp_oidc/app_settings.py +++ b/src/authentic2_idp_oidc/app_settings.py @@ -53,6 +53,10 @@ class AppSettings(object): def IDTOKEN_DURATION(self): return self._setting('IDTOKEN_DURATION', 30) + @property + def ACCESS_TOKEN_DURATION(self): + return self._setting('ACCESS_TOKEN_DURATION', 3600 * 8) + @property def PASSWORD_GRANT_RATELIMIT(self): return self._setting('PASSWORD_GRANT_RATELIMIT', '100/m') diff --git a/src/authentic2_idp_oidc/migrations/0001_initial.py b/src/authentic2_idp_oidc/migrations/0001_initial.py index 8da764c3..78c72fbc 100644 --- a/src/authentic2_idp_oidc/migrations/0001_initial.py +++ b/src/authentic2_idp_oidc/migrations/0001_initial.py @@ -20,7 +20,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('uuid', models.CharField(default=authentic2_idp_oidc.models.generate_uuid, max_length=128, verbose_name='uuid')), ('scopes', models.TextField(verbose_name='scopes')), - ('session_key', models.CharField(max_length=128, verbose_name='session key')), + ('session_key', models.CharField(blank=True, max_length=128, verbose_name='session key')), ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')), ('expired', models.DateTimeField(verbose_name='expire')), ], @@ -40,7 +40,7 @@ class Migration(migrations.Migration): ('service_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='authentic2.Service')), ('client_id', models.CharField(default=authentic2_idp_oidc.models.generate_uuid, unique=True, max_length=255, verbose_name='client id')), ('client_secret', models.CharField(default=authentic2_idp_oidc.models.generate_uuid, max_length=255, verbose_name='client secret')), - ('authorization_flow', models.PositiveIntegerField(default=1, verbose_name='authorization flow', choices=[(1, 'authorization code'), (2, 'implicit/native')])), + ('authorization_flow', models.PositiveIntegerField(choices=[(1, 'authorization code'), (2, 'implicit/native'), (3, 'resource owner password credentials')], default=1, verbose_name='authorization flow')), ('redirect_uris', models.TextField(verbose_name='redirect URIs', validators=[authentic2_idp_oidc.models.validate_https_url])), ('sector_identifier_uri', models.URLField(verbose_name='sector identifier URI', blank=True)), ('identifier_policy', models.PositiveIntegerField(default=2, verbose_name='identifier policy', choices=[(1, 'uuid'), (2, 'pairwise'), (3, 'email')])), diff --git a/src/authentic2_idp_oidc/migrations/0012_auto_20200122_2258.py b/src/authentic2_idp_oidc/migrations/0012_auto_20200122_2258.py new file mode 100644 index 00000000..048d1d3f --- /dev/null +++ b/src/authentic2_idp_oidc/migrations/0012_auto_20200122_2258.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2020-01-22 21:58 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentic2_idp_oidc', '0011_auto_20180808_1546'), + ] + + operations = [ + migrations.AddField( + model_name='oidcclient', + name='access_token_duration', + field=models.DurationField(blank=True, default=None, null=True, verbose_name='time during which the access token is valid'), + ), + migrations.AddField( + model_name='oidcclient', + name='scope', + field=models.TextField(blank=True, default=b'', verbose_name='resource owner credentials grant scope'), + ), + ] diff --git a/src/authentic2_idp_oidc/models.py b/src/authentic2_idp_oidc/models.py index c5e0e78a..138f4e97 100644 --- a/src/authentic2_idp_oidc/models.py +++ b/src/authentic2_idp_oidc/models.py @@ -108,6 +108,11 @@ class OIDCClient(Service): blank=True, null=True, default=None) + access_token_duration = models.DurationField( + verbose_name=_('time during which the access token is valid'), + blank=True, + null=True, + default=None) authorization_mode = models.PositiveIntegerField( default=AUTHORIZATION_MODE_BY_SERVICE, choices=AUTHORIZATION_MODES, @@ -131,6 +136,11 @@ class OIDCClient(Service): verbose_name=_('identifier policy'), default=POLICY_PAIRWISE, choices=IDENTIFIER_POLICIES) + scope = models.TextField( + verbose_name=_('resource owner credentials grant scope'), + help_text=_('Permitted or default scopes (for credentials grant)'), + default='', + blank=True) @to_iter def get_idtoken_algorithms(): @@ -200,6 +210,9 @@ class OIDCClient(Service): return True return False + def scope_set(self): + return utils.scope_set(self.scope) + def __repr__(self): return ('' % (self.name, self.client_id, self.get_identifier_policy_display())) @@ -314,7 +327,8 @@ class OIDCAccessToken(models.Model): verbose_name=_('scopes')) session_key = models.CharField( verbose_name=_('session key'), - max_length=128) + max_length=128, + blank=True) # metadata created = models.DateTimeField( diff --git a/src/authentic2_idp_oidc/views.py b/src/authentic2_idp_oidc/views.py index 367fce24..159ff097 100644 --- a/src/authentic2_idp_oidc/views.py +++ b/src/authentic2_idp_oidc/views.py @@ -22,7 +22,7 @@ import base64 import time from django.http import (HttpResponse, HttpResponseBadRequest, - HttpResponseNotAllowed, JsonResponse) + HttpResponseNotAllowed, JsonResponse) from django.utils import six from django.utils.timezone import now, utc from django.utils.http import urlencode @@ -66,7 +66,7 @@ def openid_configuration(request, *args, **kwargs): 'frontchannel_logout_supported': True, 'frontchannel_logout_session_supported': True, } - return HttpResponse(json.dumps(metadata), content_type='application/json') + return JsonResponse(metadata) @setting_enabled('ENABLE', settings=app_settings) @@ -96,9 +96,19 @@ def authorization_error(request, redirect_uri, error, error_description=None, er def idtoken_duration(client): - if client.idtoken_duration: - return client.idtoken_duration - return datetime.timedelta(seconds=app_settings.IDTOKEN_DURATION) + return client.idtoken_duration or datetime.timedelta(seconds=app_settings.IDTOKEN_DURATION) + + +def access_token_duration(client): + return client.access_token_duration or datetime.timedelta(seconds=app_settings.IDTOKEN_DURATION) + + +def allowed_scopes(client): + return client.scope_set() or app_settings.SCOPES or ['openid', 'email', 'profile'] + + +def is_scopes_allowed(scopes, client): + return scopes <= set(allowed_scopes(client)) @setting_enabled('ENABLE', settings=app_settings) @@ -122,11 +132,11 @@ def authorize(request, *args, **kwargs): return redirect(request, 'auth_homepage') if client.authorization_flow == client.FLOW_RESOURCE_OWNER_CRED: + messages.warning(request, _('Client is configured for resource owner password crendetial grant type')) return authorization_error(request, 'auth_homepage', 'unauthorized_client', - error_description='authz endpoint is not ' - 'part of resource owner password credential ' - 'grant type') + error_description='authz endpoint is configured ' + 'for resource owner password credential grant type') if not client.is_valid_redirect_uri(redirect_uri): messages.warning(request, _('Authorization request is invalid')) @@ -184,10 +194,10 @@ def authorize(request, *args, **kwargs): error_description='openid scope is missing', state=state, fragment=fragment) - allowed_scopes = app_settings.SCOPES or ['openid', 'email', 'profile'] - if not (scopes <= set(allowed_scopes)): + + if not is_scopes_allowed(scopes, client): message = 'only "%s" scope(s) are supported, but "%s" requested' % ( - ', '.join(allowed_scopes), ', '.join(scopes)) + ', '.join(allowed_scopes(client)), ', '.join(scopes)) return authorization_error(request, redirect_uri, 'invalid_scope', error_description=message, state=state, @@ -303,14 +313,14 @@ def authorize(request, *args, **kwargs): else: # FIXME: we should probably factorize this part with the token endpoint similar code need_access_token = 'token' in response_type.split() - expires_in = 3600 * 8 + expires_in = access_token_duration(client) if need_access_token: access_token = models.OIDCAccessToken.objects.create( client=client, user=request.user, scopes=u' '.join(scopes), session_key=request.session.session_key, - expired=start + datetime.timedelta(seconds=expires_in)) + expired=start + expires_in) acr = '0' if nonce is not None and last_auth.get('nonce') == nonce: acr = '1' @@ -339,7 +349,7 @@ def authorize(request, *args, **kwargs): params.update({ 'access_token': access_token.uuid, 'token_type': 'Bearer', - 'expires_in': expires_in, + 'expires_in': expires_in.total_seconds(), }) # query is transfered through the hashtag response = redirect(request, redirect_uri + '#%s' % urlencode(params), resolve=False) @@ -378,40 +388,29 @@ def authenticate_client(request, client=None): return client -def invalid_request(desc=None): +def error_response(error, error_description=None, status=400): content = { - 'error': 'invalid_request', + 'error': error, } - if desc: - content['desc'] = desc - return HttpResponseBadRequest(json.dumps(content), content_type='application/json') + if error_description: + content['error_description'] = error_description + return JsonResponse(content, status=status) -def access_denied(desc=None): - content = { - 'error': 'access_denied', - } - if desc: - content['desc'] = desc - return HttpResponseBadRequest(json.dumps(content), content_type='application/json') +def invalid_request_response(error_description=None): + return error_response('invalid_request', error_description=error_description) -def unauthorized_client(desc=None): - content = { - 'error': 'unauthorized_client', - } - if desc: - content['desc'] = desc - return HttpResponseBadRequest(json.dumps(content), content_type='application/json') +def access_denied_response(error_description=None): + return error_response('access_denied', error_description=error_description) -def invalid_client(desc=None): - content = { - 'error': 'invalid_client', - } - if desc: - content['desc'] = desc - return HttpResponseBadRequest(json.dumps(content), content_type='application/json') +def unauthorized_client_response(error_description=None): + return error_response('unauthorized_client', error_description=error_description) + + +def invalid_client_response(error_description=None): + return error_response('invalid_client', error_description=error_description) def credential_grant_ratelimit_key(group, request): @@ -423,85 +422,102 @@ def credential_grant_ratelimit_key(group, request): def idtoken_from_user_credential(request): - if request.META.get('CONTENT_TYPE') != 'application/x-www-form-urlencoded': - return invalid_request( - 'wrong content type \'%s\'. request content type must be ' - '\'application/x-www-form-urlencoded\'') - username = request.POST.get('username') - scope = request.POST.get('scope', '') - - if not all((username, request.POST.get('password'))): - return invalid_request( - 'request must bear both username and password as ' - 'parameters using the "application/x-www-form-urlencoded" ' - 'media type') - - if is_ratelimited( - request, group='ro-cred-grant', increment=True, - key=credential_grant_ratelimit_key, - rate=app_settings.PASSWORD_GRANT_RATELIMIT): - return invalid_request( - 'reached rate limitation, too many erroneous requests') - - client = authenticate_client(request, client=None) - - if not client: - return invalid_client('client authentication failed') - - if client.authorization_flow != models.OIDCClient.FLOW_RESOURCE_OWNER_CRED: - return unauthorized_client( - 'client is not configured for resource owner password ' - 'credential grant') - - exponential_backoff = ExponentialRetryTimeout( - key_prefix='idp-oidc-ro-cred-grant', - duration=a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION, - factor=a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR) - backoff_keys = (username, client.client_id) - - seconds_to_wait = exponential_backoff.seconds_to_wait(*backoff_keys) - if seconds_to_wait > a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MAX_DURATION: - seconds_to_wait = a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MAX_DURATION - if seconds_to_wait: - return invalid_request( - 'too many attempts with erroneous RO password, you must wait ' - '%s seconds to try again.' % int(math.ceil(seconds_to_wait))) - - user = authenticate(request, username=username, password=request.POST.get('password')) - if not user: - exponential_backoff.failure(*backoff_keys) - return access_denied( - 'invalid resource owner credentials') - - exponential_backoff.success(*backoff_keys) - start = now() - id_token = utils.create_user_info( - request, - client, - user, - scope, - id_token=True) - id_token.update({ - 'iss': utils.get_issuer(request), - 'aud': client.client_id, - 'exp': timestamp_from_datetime(start + idtoken_duration(client)), - 'iat': timestamp_from_datetime(start), - 'auth_time': timestamp_from_datetime(start), - 'acr': '0', - }) - return JsonResponse({'id_token': utils.make_idtoken(client, id_token)}) + if request.META.get('CONTENT_TYPE') != 'application/x-www-form-urlencoded': + return invalid_request_response( + 'wrong content type. request content type must be \'application/x-www-form-urlencoded\'') + username = request.POST.get('username') + scope = request.POST.get('scope', '') + + # scope is ignored, we used the configured scope + + if not all((username, request.POST.get('password'))): + return invalid_request_response( + 'request must bear both username and password as ' + 'parameters using the "application/x-www-form-urlencoded" ' + 'media type') + + if is_ratelimited( + request, group='ro-cred-grant', increment=True, + key=credential_grant_ratelimit_key, + rate=app_settings.PASSWORD_GRANT_RATELIMIT): + return invalid_request_response( + 'reached rate limitation, too many erroneous requests') + + client = authenticate_client(request, client=None) + + if not client: + return invalid_client_response('client authentication failed') + + if client.authorization_flow != models.OIDCClient.FLOW_RESOURCE_OWNER_CRED: + return unauthorized_client_response( + 'client is not configured for resource owner password ' + 'credential grant') + + exponential_backoff = ExponentialRetryTimeout( + key_prefix='idp-oidc-ro-cred-grant', + duration=a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION, + factor=a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR) + backoff_keys = (username, client.client_id) + + seconds_to_wait = exponential_backoff.seconds_to_wait(*backoff_keys) + if seconds_to_wait > a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MAX_DURATION: + seconds_to_wait = a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MAX_DURATION + if seconds_to_wait: + return invalid_request_response( + 'too many attempts with erroneous RO password, you must wait ' + '%s seconds to try again.' % int(math.ceil(seconds_to_wait))) + + user = authenticate(request, username=username, password=request.POST.get('password')) + if not user: + exponential_backoff.failure(*backoff_keys) + return access_denied_response('invalid resource owner credentials') + + # limit requested scopes + scopes = utils.scope_set(scope) & client.scope_set() + + exponential_backoff.success(*backoff_keys) + start = now() + # make access_token + expires_in = access_token_duration(client) + access_token = models.OIDCAccessToken.objects.create( + client=client, + user=user, + scopes=' '.join(scopes), + session_key='', + expired=start + expires_in) + # make id_token + id_token = utils.create_user_info( + request, + client, + user, + scopes, + id_token=True) + id_token.update({ + 'iss': utils.get_issuer(request), + 'aud': client.client_id, + 'exp': timestamp_from_datetime(start + idtoken_duration(client)), + 'iat': timestamp_from_datetime(start), + 'auth_time': timestamp_from_datetime(start), + 'acr': '0', + }) + return JsonResponse({ + 'access_token': six.text_type(access_token.uuid), + 'token_type': 'Bearer', + 'expires_in': expires_in.total_seconds(), + 'id_token': utils.make_idtoken(client, id_token), + }) def tokens_from_authz_code(request): code = request.POST.get('code') if code is None: - return invalid_request('missing code') + return invalid_request_response('missing code') try: oidc_code = models.OIDCCode.objects.select_related().get(uuid=code) except models.OIDCCode.DoesNotExist: - return invalid_request('invalid code') + return invalid_request_response('invalid code') if not oidc_code.is_valid(): - return invalid_request('code has expired or user is disconnected') + return invalid_request_response('code has expired or user is disconnected') client = authenticate_client(request, client=oidc_code.client) if client is None: return HttpResponse('unauthenticated', status=401) @@ -509,14 +525,14 @@ def tokens_from_authz_code(request): models.OIDCCode.objects.filter(uuid=code).delete() redirect_uri = request.POST.get('redirect_uri') if oidc_code.redirect_uri != redirect_uri: - return invalid_request('invalid redirect_uri') - expires_in = 3600 * 8 + return invalid_request_response('invalid redirect_uri') + expires_in = access_token_duration(client) access_token = models.OIDCAccessToken.objects.create( client=client, user=oidc_code.user, scopes=oidc_code.scopes, session_key=oidc_code.session_key, - expired=oidc_code.created + datetime.timedelta(seconds=expires_in)) + expired=oidc_code.created + expires_in) start = now() acr = '0' if (oidc_code.nonce is not None @@ -543,7 +559,7 @@ def tokens_from_authz_code(request): return JsonResponse({ 'access_token': six.text_type(access_token.uuid), 'token_type': 'Bearer', - 'expires_in': expires_in, + 'expires_in': expires_in.total_seconds(), 'id_token': utils.make_idtoken(client, id_token), }) @@ -557,10 +573,9 @@ def token(request, *args, **kwargs): if grant_type == 'password': response = idtoken_from_user_credential(request) elif grant_type == 'authorization_code': - response= tokens_from_authz_code(request) + response = tokens_from_authz_code(request) else: - return invalid_request( - 'grant_type must be either authorization_code or password') + return invalid_request_response('grant_type must be either authorization_code or password') response['Cache-Control'] = 'no-store' response['Pragma'] = 'no-cache' return response @@ -591,7 +606,7 @@ def user_info(request, *args, **kwargs): access_token.client, access_token.user, access_token.scope_set()) - return HttpResponse(json.dumps(user_info), content_type='application/json') + return JsonResponse(user_info) @setting_enabled('ENABLE', settings=app_settings) diff --git a/tests/test_idp_oidc.py b/tests/test_idp_oidc.py index 42a244bf..cd4d5133 100644 --- a/tests/test_idp_oidc.py +++ b/tests/test_idp_oidc.py @@ -672,7 +672,7 @@ def test_invalid_request(caplog, oidc_settings, oidc_client, simple_user, app): }, headers=client_authentication_headers(oidc_client), status=400) assert 'error' in response.json assert response.json['error'] == 'invalid_request' - assert response.json['desc'] == 'code has expired or user is disconnected' + assert response.json['error_description'] == 'code has expired or user is disconnected' # invalid logout logout_url = make_url('oidc-logout', params={ @@ -698,7 +698,7 @@ def test_invalid_request(caplog, oidc_settings, oidc_client, simple_user, app): }, headers=client_authentication_headers(oidc_client), status=400) assert 'error' in response.json assert response.json['error'] == 'invalid_request' - assert response.json['desc'] == 'code has expired or user is disconnected' + assert response.json['error_description'] == 'code has expired or user is disconnected' def test_expired_manager(db, simple_user): @@ -1194,16 +1194,13 @@ def test_resource_owner_password_credential_grant(app, oidc_client, admin, simpl jwt.deserialize(token, key=jwk) claims = json.loads(jwt.claims) # xxx already verified by jwcrypto deserialization? - assert all(claims.get(key) for key in ('acr', 'aud', 'auth_time', 'exp', - 'iat', 'iss', 'sub')) + assert all(claims.get(key) for key in ('acr', 'aud', 'auth_time', 'exp', 'iat', 'iss', 'sub')) # 2. test basic authz params.pop('client_id') params.pop('client_secret') - response = app.post( - token_url, params=params, - headers=client_authentication_headers(oidc_client)) + response = app.post(token_url, params=params, headers=client_authentication_headers(oidc_client)) assert 'id_token' in response.json token = response.json['id_token'] header, payload, signature = token.split('.') @@ -1211,11 +1208,11 @@ def test_resource_owner_password_credential_grant(app, oidc_client, admin, simpl jwt.deserialize(token, key=jwk) claims = json.loads(jwt.claims) # xxx already verified by jwcrypto deserialization? - assert all(claims.get(key) for key in ('acr', 'aud', 'auth_time', 'exp', - 'iat', 'iss', 'sub')) + assert all(claims.get(key) for key in ('acr', 'aud', 'auth_time', 'exp', 'iat', 'iss', 'sub')) -def test_resource_owner_password_credential_grant_ratelimitation_invalid_client(app, oidc_client, admin, simple_user, oidc_settings): +def test_resource_owner_password_credential_grant_ratelimitation_invalid_client( + app, oidc_client, admin, simple_user, oidc_settings): cache.clear() oidc_client.authorization_flow = OIDCClient.FLOW_RESOURCE_OWNER_CRED oidc_client.save() @@ -1230,26 +1227,26 @@ def test_resource_owner_password_credential_grant_ratelimitation_invalid_client( attempts = 0 dummy_post = RequestFactory().post('/dummy') while attempts < 1000: - before = now() attempts += 1 ratelimited = is_ratelimited( - request=dummy_post, group='test-ro-cred-grant', increment=True, - key=lambda x, y: '127.0.0.1', - rate=oidc_settings.A2_IDP_OIDC_PASSWORD_GRANT_RATELIMIT) + request=dummy_post, group='test-ro-cred-grant', increment=True, + key=lambda x, y: '127.0.0.1', + rate=oidc_settings.A2_IDP_OIDC_PASSWORD_GRANT_RATELIMIT) response = app.post(token_url, params=params, status=400) if not ratelimited: assert response.json['error'] == 'invalid_client' - assert 'client authentication failed' in response.json['desc'] + assert 'client authentication failed' in response.json['error_description'] continue else: assert response.json['error'] == 'invalid_request' - assert 'reached rate limitation' in response.json['desc'] + assert 'reached rate limitation' in response.json['error_description'] break if not ratelimited: assert 0 -def test_resource_owner_password_credential_grant_ratelimitation_valid_client(app, oidc_client, admin, simple_user, oidc_settings): +def test_credentials_grant_ratelimitation_valid_client( + app, oidc_client, admin, simple_user, oidc_settings): cache.clear() oidc_client.authorization_flow = OIDCClient.FLOW_RESOURCE_OWNER_CRED oidc_client.save() @@ -1273,7 +1270,7 @@ def test_resource_owner_password_credential_grant_ratelimitation_valid_client(ap if ratelimited: response = app.post(token_url, params=params, status=400) assert response.json['error'] == 'invalid_request' - assert 'reached rate limitation' in response.json['desc'] + assert 'reached rate limitation' in response.json['error_description'] break else: response = app.post(token_url, params=params) @@ -1281,7 +1278,8 @@ def test_resource_owner_password_credential_grant_ratelimitation_valid_client(ap assert 0 -def test_resource_owner_password_credential_grant_retrytimout(app, oidc_client, admin, simple_user, settings, freezer): +def test_credentials_grant_retrytimout( + app, oidc_client, admin, simple_user, settings, freezer): cache.clear() settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION = 2 oidc_client.authorization_flow = OIDCClient.FLOW_RESOURCE_OWNER_CRED @@ -1296,12 +1294,11 @@ def test_resource_owner_password_credential_grant_retrytimout(app, oidc_client, } attempts = 0 while attempts < 100: - before = now() response = app.post(token_url, params=params, status=400) attempts += 1 if attempts >= 10: assert response.json['error'] == 'invalid_request' - assert 'too many attempts with erroneous RO password' in response.json['desc'] + assert 'too many attempts with erroneous RO password' in response.json['error_description'] # freeze some time after backoff delay expiration today = datetime.date.today() @@ -1314,13 +1311,14 @@ def test_resource_owner_password_credential_grant_retrytimout(app, oidc_client, assert 'id_token' in response.json -def test_resource_owner_password_credential_grant_invalid_client(app, oidc_client, admin, simple_user, settings): +def test_credentials_grant_invalid_client( + app, oidc_client, admin, simple_user, settings): cache.clear() oidc_client.authorization_flow = OIDCClient.FLOW_RESOURCE_OWNER_CRED oidc_client.save() params = { 'client_id': oidc_client.client_id, - 'client_secret': 'tryingthis', # Nope, wrong secret + 'client_secret': 'tryingthis', # Nope, wrong secret 'grant_type': 'password', 'username': simple_user.username, 'password': simple_user.username, @@ -1328,10 +1326,11 @@ def test_resource_owner_password_credential_grant_invalid_client(app, oidc_clien token_url = make_url('oidc-token') response = app.post(token_url, params=params, status=400) assert response.json['error'] == 'invalid_client' - assert response.json['desc'] == 'client authentication failed' + assert response.json['error_description'] == 'client authentication failed' -def test_resource_owner_password_credential_grant_unauthz_client(app, oidc_client, admin, simple_user, settings): +def test_credentials_grant_unauthz_client( + app, oidc_client, admin, simple_user, settings): cache.clear() params = { 'client_id': oidc_client.client_id, @@ -1343,10 +1342,11 @@ def test_resource_owner_password_credential_grant_unauthz_client(app, oidc_clien token_url = make_url('oidc-token') response = app.post(token_url, params=params, status=400) assert response.json['error'] == 'unauthorized_client' - assert 'client is not configured for resource owner'in response.json['desc'] + assert 'client is not configured for resource owner'in response.json['error_description'] -def test_resource_owner_password_credential_grant_invalid_content_type(app, oidc_client, admin, simple_user, settings): +def test_credentials_grant_invalid_content_type( + app, oidc_client, admin, simple_user, settings): cache.clear() oidc_client.authorization_flow = OIDCClient.FLOW_RESOURCE_OWNER_CRED oidc_client.save() @@ -1359,7 +1359,8 @@ def test_resource_owner_password_credential_grant_invalid_content_type(app, oidc } token_url = make_url('oidc-token') response = app.post( - token_url, params=params, content_type='multipart/form-data', - status=400) + token_url, params=params, + content_type='multipart/form-data', + status=400) assert response.json['error'] == 'invalid_request' - assert 'wrong content type' in response.json['desc'] + assert 'wrong content type' in response.json['error_description'] -- 2.24.0