Projet

Général

Profil

0002-ajustements.patch

Benjamin Dauvergne, 23 janvier 2020 00:23

Télécharger (31,1 ko)

Voir les différences:

Subject: [PATCH 2/3] 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
src/authentic2_idp_oidc/app_settings.py
53 53
    def IDTOKEN_DURATION(self):
54 54
        return self._setting('IDTOKEN_DURATION', 30)
55 55

  
56
    @property
57
    def ACCESS_TOKEN_DURATION(self):
58
        return self._setting('ACCESS_TOKEN_DURATION', 3600 * 8)
59

  
56 60
    @property
57 61
    def PASSWORD_GRANT_RATELIMIT(self):
58 62
        return self._setting('PASSWORD_GRANT_RATELIMIT', '100/m')
src/authentic2_idp_oidc/migrations/0001_initial.py
20 20
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
21 21
                ('uuid', models.CharField(default=authentic2_idp_oidc.models.generate_uuid, max_length=128, verbose_name='uuid')),
22 22
                ('scopes', models.TextField(verbose_name='scopes')),
23
                ('session_key', models.CharField(max_length=128, verbose_name='session key')),
23
                ('session_key', models.CharField(blank=True, max_length=128, verbose_name='session key')),
24 24
                ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
25 25
                ('expired', models.DateTimeField(verbose_name='expire')),
26 26
            ],
......
40 40
                ('service_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='authentic2.Service')),
41 41
                ('client_id', models.CharField(default=authentic2_idp_oidc.models.generate_uuid, unique=True, max_length=255, verbose_name='client id')),
42 42
                ('client_secret', models.CharField(default=authentic2_idp_oidc.models.generate_uuid, max_length=255, verbose_name='client secret')),
43
                ('authorization_flow', models.PositiveIntegerField(default=1, verbose_name='authorization flow', choices=[(1, 'authorization code'), (2, 'implicit/native')])),
43
                ('authorization_flow', models.PositiveIntegerField(choices=[(1, 'authorization code'), (2, 'implicit/native'), (3, 'resource owner password credentials')], default=1, verbose_name='authorization flow')),
44 44
                ('redirect_uris', models.TextField(verbose_name='redirect URIs', validators=[authentic2_idp_oidc.models.validate_https_url])),
45 45
                ('sector_identifier_uri', models.URLField(verbose_name='sector identifier URI', blank=True)),
46 46
                ('identifier_policy', models.PositiveIntegerField(default=2, verbose_name='identifier policy', choices=[(1, 'uuid'), (2, 'pairwise'), (3, 'email')])),
src/authentic2_idp_oidc/migrations/0012_auto_20200122_2258.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.20 on 2020-01-22 21:58
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('authentic2_idp_oidc', '0011_auto_20180808_1546'),
12
    ]
13

  
14
    operations = [
15
        migrations.AddField(
16
            model_name='oidcclient',
17
            name='access_token_duration',
18
            field=models.DurationField(blank=True, default=None, null=True, verbose_name='time during which the access token is valid'),
19
        ),
20
        migrations.AddField(
21
            model_name='oidcclient',
22
            name='scope',
23
            field=models.TextField(blank=True, default=b'', verbose_name='resource owner credentials grant scope'),
24
        ),
25
    ]
src/authentic2_idp_oidc/models.py
108 108
        blank=True,
109 109
        null=True,
110 110
        default=None)
111
    access_token_duration = models.DurationField(
112
        verbose_name=_('time during which the access token is valid'),
113
        blank=True,
114
        null=True,
115
        default=None)
111 116
    authorization_mode = models.PositiveIntegerField(
112 117
        default=AUTHORIZATION_MODE_BY_SERVICE,
113 118
        choices=AUTHORIZATION_MODES,
......
131 136
        verbose_name=_('identifier policy'),
132 137
        default=POLICY_PAIRWISE,
133 138
        choices=IDENTIFIER_POLICIES)
139
    scope = models.TextField(
140
        verbose_name=_('resource owner credentials grant scope'),
141
        help_text=_('Permitted or default scopes (for credentials grant)'),
142
        default='',
143
        blank=True)
134 144

  
135 145
    @to_iter
136 146
    def get_idtoken_algorithms():
......
200 210
            return True
201 211
        return False
202 212

  
213
    def scope_set(self):
214
        return utils.scope_set(self.scope)
215

  
203 216
    def __repr__(self):
204 217
        return ('<OIDCClient name:%r client_id:%r identifier_policy:%r>' %
205 218
                (self.name, self.client_id, self.get_identifier_policy_display()))
......
314 327
        verbose_name=_('scopes'))
315 328
    session_key = models.CharField(
316 329
        verbose_name=_('session key'),
317
        max_length=128)
330
        max_length=128,
331
        blank=True)
318 332

  
319 333
    # metadata
320 334
    created = models.DateTimeField(
src/authentic2_idp_oidc/views.py
22 22
import time
23 23

  
24 24
from django.http import (HttpResponse, HttpResponseBadRequest,
25
        HttpResponseNotAllowed, JsonResponse)
25
                         HttpResponseNotAllowed, JsonResponse)
26 26
from django.utils import six
27 27
from django.utils.timezone import now, utc
28 28
from django.utils.http import urlencode
......
66 66
        'frontchannel_logout_supported': True,
67 67
        'frontchannel_logout_session_supported': True,
68 68
    }
69
    return HttpResponse(json.dumps(metadata), content_type='application/json')
69
    return JsonResponse(metadata)
70 70

  
71 71

  
72 72
@setting_enabled('ENABLE', settings=app_settings)
......
96 96

  
97 97

  
98 98
def idtoken_duration(client):
99
    if client.idtoken_duration:
100
        return client.idtoken_duration
101
    return datetime.timedelta(seconds=app_settings.IDTOKEN_DURATION)
99
    return client.idtoken_duration or datetime.timedelta(seconds=app_settings.IDTOKEN_DURATION)
100

  
101

  
102
def access_token_duration(client):
103
    return client.access_token_duration or datetime.timedelta(seconds=app_settings.IDTOKEN_DURATION)
104

  
105

  
106
def allowed_scopes(client):
107
    return client.scope_set() or app_settings.SCOPES or ['openid', 'email', 'profile']
108

  
109

  
110
def is_scopes_allowed(scopes, client):
111
    return scopes <= set(allowed_scopes(client))
102 112

  
103 113

  
104 114
@setting_enabled('ENABLE', settings=app_settings)
......
122 132
        return redirect(request, 'auth_homepage')
123 133

  
124 134
    if client.authorization_flow == client.FLOW_RESOURCE_OWNER_CRED:
135
        messages.warning(request, _('Client is configured for resource owner password crendetial grant type'))
125 136
        return authorization_error(request, 'auth_homepage',
126 137
                                   'unauthorized_client',
127
                                   error_description='authz endpoint is not '
128
                                   'part of resource owner password credential '
129
                                   'grant type')
138
                                   error_description='authz endpoint is configured '
139
                                   'for resource owner password credential grant type')
130 140

  
131 141
    if not client.is_valid_redirect_uri(redirect_uri):
132 142
        messages.warning(request, _('Authorization request is invalid'))
......
184 194
                                   error_description='openid scope is missing',
185 195
                                   state=state,
186 196
                                   fragment=fragment)
187
    allowed_scopes = app_settings.SCOPES or ['openid', 'email', 'profile']
188
    if not (scopes <= set(allowed_scopes)):
197

  
198
    if not is_scopes_allowed(scopes, client):
189 199
        message = 'only "%s" scope(s) are supported, but "%s" requested' % (
190
            ', '.join(allowed_scopes), ', '.join(scopes))
200
            ', '.join(allowed_scopes(client)), ', '.join(scopes))
191 201
        return authorization_error(request, redirect_uri, 'invalid_scope',
192 202
                                   error_description=message,
193 203
                                   state=state,
......
303 313
    else:
304 314
        # FIXME: we should probably factorize this part with the token endpoint similar code
305 315
        need_access_token = 'token' in response_type.split()
306
        expires_in = 3600 * 8
316
        expires_in = access_token_duration(client)
307 317
        if need_access_token:
308 318
            access_token = models.OIDCAccessToken.objects.create(
309 319
                client=client,
310 320
                user=request.user,
311 321
                scopes=u' '.join(scopes),
312 322
                session_key=request.session.session_key,
313
                expired=start + datetime.timedelta(seconds=expires_in))
323
                expired=start + expires_in)
314 324
        acr = '0'
315 325
        if nonce is not None and last_auth.get('nonce') == nonce:
316 326
            acr = '1'
......
339 349
            params.update({
340 350
                'access_token': access_token.uuid,
341 351
                'token_type': 'Bearer',
342
                'expires_in': expires_in,
352
                'expires_in': expires_in.total_seconds(),
343 353
            })
344 354
        # query is transfered through the hashtag
345 355
        response = redirect(request, redirect_uri + '#%s' % urlencode(params), resolve=False)
......
378 388
    return client
379 389

  
380 390

  
381
def invalid_request(desc=None):
391
def error_response(error, error_description=None, status=400):
382 392
    content = {
383
        'error': 'invalid_request',
393
        'error': error,
384 394
    }
385
    if desc:
386
        content['desc'] = desc
387
    return HttpResponseBadRequest(json.dumps(content), content_type='application/json')
395
    if error_description:
396
        content['error_description'] = error_description
397
    return JsonResponse(content, status=status)
388 398

  
389 399

  
390
def access_denied(desc=None):
391
    content = {
392
        'error': 'access_denied',
393
    }
394
    if desc:
395
        content['desc'] = desc
396
    return HttpResponseBadRequest(json.dumps(content), content_type='application/json')
400
def invalid_request_response(error_description=None):
401
    return error_response('invalid_request', error_description=error_description)
397 402

  
398 403

  
399
def unauthorized_client(desc=None):
400
    content = {
401
        'error': 'unauthorized_client',
402
    }
403
    if desc:
404
        content['desc'] = desc
405
    return HttpResponseBadRequest(json.dumps(content), content_type='application/json')
404
def access_denied_response(error_description=None):
405
    return error_response('access_denied', error_description=error_description)
406 406

  
407 407

  
408
def invalid_client(desc=None):
409
    content = {
410
        'error': 'invalid_client',
411
    }
412
    if desc:
413
        content['desc'] = desc
414
    return HttpResponseBadRequest(json.dumps(content), content_type='application/json')
408
def unauthorized_client_response(error_description=None):
409
    return error_response('unauthorized_client', error_description=error_description)
410

  
411

  
412
def invalid_client_response(error_description=None):
413
    return error_response('invalid_client', error_description=error_description)
415 414

  
416 415

  
417 416
def credential_grant_ratelimit_key(group, request):
......
423 422

  
424 423

  
425 424
def idtoken_from_user_credential(request):
426
        if request.META.get('CONTENT_TYPE') != 'application/x-www-form-urlencoded':
427
            return invalid_request(
428
                    'wrong content type \'%s\'.  request content type must be '
429
                    '\'application/x-www-form-urlencoded\'')
430
        username = request.POST.get('username')
431
        scope = request.POST.get('scope', '')
432

  
433
        if not all((username, request.POST.get('password'))):
434
            return invalid_request(
435
                    'request must bear both username and password as '
436
                    'parameters using the "application/x-www-form-urlencoded" '
437
                    'media type')
438

  
439
        if is_ratelimited(
440
                request, group='ro-cred-grant', increment=True,
441
                key=credential_grant_ratelimit_key,
442
                rate=app_settings.PASSWORD_GRANT_RATELIMIT):
443
            return invalid_request(
444
                    'reached rate limitation, too many erroneous requests')
445

  
446
        client = authenticate_client(request, client=None)
447

  
448
        if not client:
449
            return invalid_client('client authentication failed')
450

  
451
        if client.authorization_flow != models.OIDCClient.FLOW_RESOURCE_OWNER_CRED:
452
            return unauthorized_client(
453
                'client is not configured for resource owner password '
454
                'credential grant')
455

  
456
        exponential_backoff = ExponentialRetryTimeout(
457
            key_prefix='idp-oidc-ro-cred-grant',
458
            duration=a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION,
459
            factor=a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR)
460
        backoff_keys = (username, client.client_id)
461

  
462
        seconds_to_wait = exponential_backoff.seconds_to_wait(*backoff_keys)
463
        if seconds_to_wait > a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MAX_DURATION:
464
            seconds_to_wait = a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MAX_DURATION
465
        if seconds_to_wait:
466
            return invalid_request(
467
                'too many attempts with erroneous RO password, you must wait '
468
                '%s seconds to try again.' % int(math.ceil(seconds_to_wait)))
469

  
470
        user = authenticate(request, username=username, password=request.POST.get('password'))
471
        if not user:
472
            exponential_backoff.failure(*backoff_keys)
473
            return access_denied(
474
                    'invalid resource owner credentials')
475

  
476
        exponential_backoff.success(*backoff_keys)
477
        start = now()
478
        id_token = utils.create_user_info(
479
            request,
480
            client,
481
            user,
482
            scope,
483
            id_token=True)
484
        id_token.update({
485
            'iss': utils.get_issuer(request),
486
            'aud': client.client_id,
487
            'exp': timestamp_from_datetime(start + idtoken_duration(client)),
488
            'iat': timestamp_from_datetime(start),
489
            'auth_time': timestamp_from_datetime(start),
490
            'acr': '0',
491
        })
492
        return JsonResponse({'id_token': utils.make_idtoken(client, id_token)})
425
    if request.META.get('CONTENT_TYPE') != 'application/x-www-form-urlencoded':
426
        return invalid_request_response(
427
            'wrong content type. request content type must be \'application/x-www-form-urlencoded\'')
428
    username = request.POST.get('username')
429
    scope = request.POST.get('scope', '')
430

  
431
    # scope is ignored, we used the configured scope
432

  
433
    if not all((username, request.POST.get('password'))):
434
        return invalid_request_response(
435
            'request must bear both username and password as '
436
            'parameters using the "application/x-www-form-urlencoded" '
437
            'media type')
438

  
439
    if is_ratelimited(
440
            request, group='ro-cred-grant', increment=True,
441
            key=credential_grant_ratelimit_key,
442
            rate=app_settings.PASSWORD_GRANT_RATELIMIT):
443
        return invalid_request_response(
444
            'reached rate limitation, too many erroneous requests')
445

  
446
    client = authenticate_client(request, client=None)
447

  
448
    if not client:
449
        return invalid_client_response('client authentication failed')
450

  
451
    if client.authorization_flow != models.OIDCClient.FLOW_RESOURCE_OWNER_CRED:
452
        return unauthorized_client_response(
453
            'client is not configured for resource owner password '
454
            'credential grant')
455

  
456
    exponential_backoff = ExponentialRetryTimeout(
457
        key_prefix='idp-oidc-ro-cred-grant',
458
        duration=a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION,
459
        factor=a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR)
460
    backoff_keys = (username, client.client_id)
461

  
462
    seconds_to_wait = exponential_backoff.seconds_to_wait(*backoff_keys)
463
    if seconds_to_wait > a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MAX_DURATION:
464
        seconds_to_wait = a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MAX_DURATION
465
    if seconds_to_wait:
466
        return invalid_request_response(
467
            'too many attempts with erroneous RO password, you must wait '
468
            '%s seconds to try again.' % int(math.ceil(seconds_to_wait)))
469

  
470
    user = authenticate(request, username=username, password=request.POST.get('password'))
471
    if not user:
472
        exponential_backoff.failure(*backoff_keys)
473
        return access_denied_response('invalid resource owner credentials')
474

  
475
    # limit requested scopes
476
    scopes = utils.scope_set(scope) & client.scope_set()
477

  
478
    exponential_backoff.success(*backoff_keys)
479
    start = now()
480
    # make access_token
481
    expires_in = access_token_duration(client)
482
    access_token = models.OIDCAccessToken.objects.create(
483
        client=client,
484
        user=user,
485
        scopes=' '.join(scopes),
486
        session_key='',
487
        expired=start + expires_in)
488
    # make id_token
489
    id_token = utils.create_user_info(
490
        request,
491
        client,
492
        user,
493
        scopes,
494
        id_token=True)
495
    id_token.update({
496
        'iss': utils.get_issuer(request),
497
        'aud': client.client_id,
498
        'exp': timestamp_from_datetime(start + idtoken_duration(client)),
499
        'iat': timestamp_from_datetime(start),
500
        'auth_time': timestamp_from_datetime(start),
501
        'acr': '0',
502
    })
503
    return JsonResponse({
504
        'access_token': six.text_type(access_token.uuid),
505
        'token_type': 'Bearer',
506
        'expires_in': expires_in.total_seconds(),
507
        'id_token': utils.make_idtoken(client, id_token),
508
    })
493 509

  
494 510

  
495 511
def tokens_from_authz_code(request):
496 512
    code = request.POST.get('code')
497 513
    if code is None:
498
        return invalid_request('missing code')
514
        return invalid_request_response('missing code')
499 515
    try:
500 516
        oidc_code = models.OIDCCode.objects.select_related().get(uuid=code)
501 517
    except models.OIDCCode.DoesNotExist:
502
        return invalid_request('invalid code')
518
        return invalid_request_response('invalid code')
503 519
    if not oidc_code.is_valid():
504
        return invalid_request('code has expired or user is disconnected')
520
        return invalid_request_response('code has expired or user is disconnected')
505 521
    client = authenticate_client(request, client=oidc_code.client)
506 522
    if client is None:
507 523
        return HttpResponse('unauthenticated', status=401)
......
509 525
    models.OIDCCode.objects.filter(uuid=code).delete()
510 526
    redirect_uri = request.POST.get('redirect_uri')
511 527
    if oidc_code.redirect_uri != redirect_uri:
512
        return invalid_request('invalid redirect_uri')
513
    expires_in = 3600 * 8
528
        return invalid_request_response('invalid redirect_uri')
529
    expires_in = access_token_duration(client)
514 530
    access_token = models.OIDCAccessToken.objects.create(
515 531
        client=client,
516 532
        user=oidc_code.user,
517 533
        scopes=oidc_code.scopes,
518 534
        session_key=oidc_code.session_key,
519
        expired=oidc_code.created + datetime.timedelta(seconds=expires_in))
535
        expired=oidc_code.created + expires_in)
520 536
    start = now()
521 537
    acr = '0'
522 538
    if (oidc_code.nonce is not None
......
543 559
    return JsonResponse({
544 560
        'access_token': six.text_type(access_token.uuid),
545 561
        'token_type': 'Bearer',
546
        'expires_in': expires_in,
562
        'expires_in': expires_in.total_seconds(),
547 563
        'id_token': utils.make_idtoken(client, id_token),
548 564
    })
549 565

  
......
557 573
    if grant_type == 'password':
558 574
        response = idtoken_from_user_credential(request)
559 575
    elif grant_type == 'authorization_code':
560
        response= tokens_from_authz_code(request)
576
        response = tokens_from_authz_code(request)
561 577
    else:
562
        return invalid_request(
563
                'grant_type must be either authorization_code or password')
578
        return invalid_request_response('grant_type must be either authorization_code or password')
564 579
    response['Cache-Control'] = 'no-store'
565 580
    response['Pragma'] = 'no-cache'
566 581
    return response
......
591 606
                                       access_token.client,
592 607
                                       access_token.user,
593 608
                                       access_token.scope_set())
594
    return HttpResponse(json.dumps(user_info), content_type='application/json')
609
    return JsonResponse(user_info)
595 610

  
596 611

  
597 612
@setting_enabled('ENABLE', settings=app_settings)
tests/test_idp_oidc.py
672 672
        }, headers=client_authentication_headers(oidc_client), status=400)
673 673
        assert 'error' in response.json
674 674
        assert response.json['error'] == 'invalid_request'
675
        assert response.json['desc'] == 'code has expired or user is disconnected'
675
        assert response.json['error_description'] == 'code has expired or user is disconnected'
676 676

  
677 677
    # invalid logout
678 678
    logout_url = make_url('oidc-logout', params={
......
698 698
        }, headers=client_authentication_headers(oidc_client), status=400)
699 699
        assert 'error' in response.json
700 700
        assert response.json['error'] == 'invalid_request'
701
        assert response.json['desc'] == 'code has expired or user is disconnected'
701
        assert response.json['error_description'] == 'code has expired or user is disconnected'
702 702

  
703 703

  
704 704
def test_expired_manager(db, simple_user):
......
1194 1194
    jwt.deserialize(token, key=jwk)
1195 1195
    claims = json.loads(jwt.claims)
1196 1196
    # xxx already verified by jwcrypto deserialization?
1197
    assert all(claims.get(key) for key in ('acr', 'aud', 'auth_time', 'exp',
1198
            'iat', 'iss', 'sub'))
1197
    assert all(claims.get(key) for key in ('acr', 'aud', 'auth_time', 'exp', 'iat', 'iss', 'sub'))
1199 1198

  
1200 1199
    # 2. test basic authz
1201 1200
    params.pop('client_id')
1202 1201
    params.pop('client_secret')
1203 1202

  
1204
    response = app.post(
1205
            token_url, params=params,
1206
            headers=client_authentication_headers(oidc_client))
1203
    response = app.post(token_url, params=params, headers=client_authentication_headers(oidc_client))
1207 1204
    assert 'id_token' in response.json
1208 1205
    token = response.json['id_token']
1209 1206
    header, payload, signature = token.split('.')
......
1211 1208
    jwt.deserialize(token, key=jwk)
1212 1209
    claims = json.loads(jwt.claims)
1213 1210
    # xxx already verified by jwcrypto deserialization?
1214
    assert all(claims.get(key) for key in ('acr', 'aud', 'auth_time', 'exp',
1215
            'iat', 'iss', 'sub'))
1211
    assert all(claims.get(key) for key in ('acr', 'aud', 'auth_time', 'exp', 'iat', 'iss', 'sub'))
1216 1212

  
1217 1213

  
1218
def test_resource_owner_password_credential_grant_ratelimitation_invalid_client(app, oidc_client, admin, simple_user, oidc_settings):
1214
def test_resource_owner_password_credential_grant_ratelimitation_invalid_client(
1215
        app, oidc_client, admin, simple_user, oidc_settings):
1219 1216
    cache.clear()
1220 1217
    oidc_client.authorization_flow = OIDCClient.FLOW_RESOURCE_OWNER_CRED
1221 1218
    oidc_client.save()
......
1230 1227
    attempts = 0
1231 1228
    dummy_post = RequestFactory().post('/dummy')
1232 1229
    while attempts < 1000:
1233
        before = now()
1234 1230
        attempts += 1
1235 1231
        ratelimited = is_ratelimited(
1236
                request=dummy_post, group='test-ro-cred-grant', increment=True,
1237
                key=lambda x, y: '127.0.0.1',
1238
                rate=oidc_settings.A2_IDP_OIDC_PASSWORD_GRANT_RATELIMIT)
1232
            request=dummy_post, group='test-ro-cred-grant', increment=True,
1233
            key=lambda x, y: '127.0.0.1',
1234
            rate=oidc_settings.A2_IDP_OIDC_PASSWORD_GRANT_RATELIMIT)
1239 1235
        response = app.post(token_url, params=params, status=400)
1240 1236
        if not ratelimited:
1241 1237
            assert response.json['error'] == 'invalid_client'
1242
            assert 'client authentication failed' in response.json['desc']
1238
            assert 'client authentication failed' in response.json['error_description']
1243 1239
            continue
1244 1240
        else:
1245 1241
            assert response.json['error'] == 'invalid_request'
1246
            assert 'reached rate limitation' in response.json['desc']
1242
            assert 'reached rate limitation' in response.json['error_description']
1247 1243
            break
1248 1244
    if not ratelimited:
1249 1245
        assert 0
1250 1246

  
1251 1247

  
1252
def test_resource_owner_password_credential_grant_ratelimitation_valid_client(app, oidc_client, admin, simple_user, oidc_settings):
1248
def test_credentials_grant_ratelimitation_valid_client(
1249
        app, oidc_client, admin, simple_user, oidc_settings):
1253 1250
    cache.clear()
1254 1251
    oidc_client.authorization_flow = OIDCClient.FLOW_RESOURCE_OWNER_CRED
1255 1252
    oidc_client.save()
......
1273 1270
        if ratelimited:
1274 1271
            response = app.post(token_url, params=params, status=400)
1275 1272
            assert response.json['error'] == 'invalid_request'
1276
            assert 'reached rate limitation' in response.json['desc']
1273
            assert 'reached rate limitation' in response.json['error_description']
1277 1274
            break
1278 1275
        else:
1279 1276
            response = app.post(token_url, params=params)
......
1281 1278
        assert 0
1282 1279

  
1283 1280

  
1284
def test_resource_owner_password_credential_grant_retrytimout(app, oidc_client, admin, simple_user, settings, freezer):
1281
def test_credentials_grant_retrytimout(
1282
        app, oidc_client, admin, simple_user, settings, freezer):
1285 1283
    cache.clear()
1286 1284
    settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION = 2
1287 1285
    oidc_client.authorization_flow = OIDCClient.FLOW_RESOURCE_OWNER_CRED
......
1296 1294
    }
1297 1295
    attempts = 0
1298 1296
    while attempts < 100:
1299
        before = now()
1300 1297
        response = app.post(token_url, params=params, status=400)
1301 1298
        attempts += 1
1302 1299
        if attempts >= 10:
1303 1300
            assert response.json['error'] == 'invalid_request'
1304
            assert 'too many attempts with erroneous RO password' in response.json['desc']
1301
            assert 'too many attempts with erroneous RO password' in response.json['error_description']
1305 1302

  
1306 1303
    # freeze some time after backoff delay expiration
1307 1304
    today = datetime.date.today()
......
1314 1311
    assert 'id_token' in response.json
1315 1312

  
1316 1313

  
1317
def test_resource_owner_password_credential_grant_invalid_client(app, oidc_client, admin, simple_user, settings):
1314
def test_credentials_grant_invalid_client(
1315
        app, oidc_client, admin, simple_user, settings):
1318 1316
    cache.clear()
1319 1317
    oidc_client.authorization_flow = OIDCClient.FLOW_RESOURCE_OWNER_CRED
1320 1318
    oidc_client.save()
1321 1319
    params = {
1322 1320
        'client_id': oidc_client.client_id,
1323
        'client_secret': 'tryingthis', # Nope, wrong secret
1321
        'client_secret': 'tryingthis',  # Nope, wrong secret
1324 1322
        'grant_type': 'password',
1325 1323
        'username': simple_user.username,
1326 1324
        'password': simple_user.username,
......
1328 1326
    token_url = make_url('oidc-token')
1329 1327
    response = app.post(token_url, params=params, status=400)
1330 1328
    assert response.json['error'] == 'invalid_client'
1331
    assert response.json['desc'] == 'client authentication failed'
1329
    assert response.json['error_description'] == 'client authentication failed'
1332 1330

  
1333 1331

  
1334
def test_resource_owner_password_credential_grant_unauthz_client(app, oidc_client, admin, simple_user, settings):
1332
def test_credentials_grant_unauthz_client(
1333
        app, oidc_client, admin, simple_user, settings):
1335 1334
    cache.clear()
1336 1335
    params = {
1337 1336
        'client_id': oidc_client.client_id,
......
1343 1342
    token_url = make_url('oidc-token')
1344 1343
    response = app.post(token_url, params=params, status=400)
1345 1344
    assert response.json['error'] == 'unauthorized_client'
1346
    assert 'client is not configured for resource owner'in response.json['desc']
1345
    assert 'client is not configured for resource owner'in response.json['error_description']
1347 1346

  
1348 1347

  
1349
def test_resource_owner_password_credential_grant_invalid_content_type(app, oidc_client, admin, simple_user, settings):
1348
def test_credentials_grant_invalid_content_type(
1349
        app, oidc_client, admin, simple_user, settings):
1350 1350
    cache.clear()
1351 1351
    oidc_client.authorization_flow = OIDCClient.FLOW_RESOURCE_OWNER_CRED
1352 1352
    oidc_client.save()
......
1359 1359
    }
1360 1360
    token_url = make_url('oidc-token')
1361 1361
    response = app.post(
1362
            token_url, params=params, content_type='multipart/form-data',
1363
            status=400)
1362
        token_url, params=params,
1363
        content_type='multipart/form-data',
1364
        status=400)
1364 1365
    assert response.json['error'] == 'invalid_request'
1365
    assert 'wrong content type' in response.json['desc']
1366
    assert 'wrong content type' in response.json['error_description']
1366
-