Projet

Général

Profil

0001-idp_oidc-improve-error-reporting-in-token-endpoint-4.patch

Benjamin Dauvergne, 03 décembre 2020 09:53

Télécharger (68,6 ko)

Voir les différences:

Subject: [PATCH 1/5] idp_oidc: improve error reporting in token endpoint
 (#47900)

 src/authentic2_idp_oidc/views.py | 582 ++++++++++++++++++-------------
 tests/test_idp_oidc.py           | 449 +++++++++++++-----------
 2 files changed, 584 insertions(+), 447 deletions(-)
src/authentic2_idp_oidc/views.py
18 18
import math
19 19
import datetime
20 20
import base64
21
import secrets
21 22
import time
22 23

  
23 24
from django.http import (HttpResponse, HttpResponseNotAllowed, JsonResponse)
......
49 50
logger = logging.getLogger(__name__)
50 51

  
51 52

  
53
class OIDCException(Exception):
54
    error_code = None
55
    error_description = None
56
    show_message = True
57

  
58
    def __init__(self, error_description=None, status=400, client=None, show_message=None):
59
        if error_description:
60
            self.error_description = error_description
61
        self.status = status
62
        self.client = client
63
        if show_message is not None:
64
            self.show_message = show_message
65

  
66
    def json_response(self, request):
67
        content = {
68
            'error': self.error_code,
69
        }
70

  
71
        if self.error_description:
72
            content['error_description'] = self.error_description
73

  
74
        if self.client:
75
            logger.warning('idp_oidc: error "%s" in token endpoint "%s" for client %s',
76
                           self.error_code, self.error_description, self.client)
77
        else:
78
            logger.warning('idp_oidc: error "%s" in token endpoint "%s"',
79
                           self.error_code, self.error_description)
80
        return JsonResponse(content, status=self.status)
81

  
82
    def redirect_response(self, request, redirect_uri=None, use_fragment=None, state=None, client=None):
83
        params = {
84
            'error': self.error_code,
85
            'error_description': self.error_description,
86
        }
87
        if state is not None:
88
            params['state'] = state
89

  
90
        log_method = logger.warning
91
        if not self.show_message:
92
            # errors not shown as Django messages are regular events, no need to log as warning
93
            log_method = logger.info
94

  
95
        client = client or self.client
96
        if client:
97
            log_method('idp_oidc: error "%s" in authorize endpoint for client %s": %s',
98
                       self.error_code, client, self.error_description)
99
        else:
100
            log_method('idp_oidc: error "%s" in authorize endpoint: %s',
101
                       self.error_code, self.error_description)
102

  
103
        if self.show_message:
104
            messages.error(request, _('OpenIDConnect Error "%s": %s') % (self.error_code, self.error_description))
105

  
106
        if redirect_uri:
107
            if use_fragment:
108
                return redirect(request, redirect_uri + '#%s' % urlencode(params), resolve=False)
109
            else:
110
                return redirect(request, redirect_uri, params=params, resolve=False)
111
        else:
112
            return redirect(request, 'continue', resolve=True)
113

  
114

  
115
class InvalidRequest(OIDCException):
116
    error_code = 'invalid_request'
117

  
118

  
119
class MissingParameter(InvalidRequest):
120
    def __init__(self, parameter):
121
        super().__init__(error_description=_('Missing parameter "%s"') % parameter)
122

  
123

  
124
class UnsupportedResponseType(OIDCException):
125
    error_code = 'unsupported_response_type'
126

  
127

  
128
class InvalidScope(OIDCException):
129
    error_code = 'invalid_scope'
130

  
131

  
132
class LoginRequired(OIDCException):
133
    error_code = 'login_required'
134
    show_message = False
135

  
136

  
137
class ConsentRequired(OIDCException):
138
    error_code = 'consent_required'
139
    show_message = False
140

  
141

  
142
class AccessDenied(OIDCException):
143
    error_code = 'access_denied'
144
    show_message = False
145

  
146

  
147
class UnauthorizedClient(OIDCException):
148
    error_code = 'unauthorized_client'
149

  
150

  
151
class InvalidClient(OIDCException):
152
    error_code = 'invalid_client'
153

  
154

  
155
class WrongClientId(InvalidClient):
156
    error_description = _('Wrong client\'s identifier')
157

  
158

  
159
class WrongClientSecret(InvalidClient):
160
    error_description = _('Wrong client\'s secret')
161

  
162

  
163
def idtoken_duration(client):
164
    return client.idtoken_duration or datetime.timedelta(seconds=app_settings.IDTOKEN_DURATION)
165

  
166

  
167
def access_token_duration(client):
168
    return client.access_token_duration or datetime.timedelta(seconds=app_settings.IDTOKEN_DURATION)
169

  
170

  
171
def allowed_scopes(client):
172
    return client.scope_set() or app_settings.SCOPES or ['openid', 'email', 'profile']
173

  
174

  
175
def is_scopes_allowed(scopes, client):
176
    return scopes <= set(allowed_scopes(client))
177

  
178

  
52 179
@setting_enabled('ENABLE', settings=app_settings)
53 180
def openid_configuration(request, *args, **kwargs):
54 181
    metadata = {
......
78 205
                        content_type='application/json')
79 206

  
80 207

  
81
def authorization_error(request, redirect_uri, error, error_description=None, error_uri=None,
82
                        state=None, fragment=False):
83
    params = {
84
        'error': error,
85
    }
86
    if error_description:
87
        params['error_description'] = error_description
88
    if error_uri:
89
        params['error_uri'] = error_uri
90
    if state is not None:
91
        params['state'] = state
92
    logger.warning(u'idp_oidc: authorization request error redirect_uri=%r error=%r error_description=%r',
93
                   redirect_uri, error, error_description, extra={'redirect_uri': redirect_uri})
94
    if fragment:
95
        return redirect(request, redirect_uri + '#%s' % urlencode(params), resolve=False)
96
    else:
97
        return redirect(request, redirect_uri, params=params, resolve=False)
98

  
99

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

  
103

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

  
107

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

  
111

  
112
def is_scopes_allowed(scopes, client):
113
    return scopes <= set(allowed_scopes(client))
114

  
115

  
116
def log_invalid_request(request, debug_info):
117
    logger.warning('idp_oidc: authorization request error, %s', debug_info)
118
    error_message = _('Authorization request is invalid')
119
    if settings.DEBUG:
120
        error_message += ' (%s)' % debug_info
121
    messages.warning(request, error_message)
122

  
123

  
124 208
@setting_enabled('ENABLE', settings=app_settings)
125 209
def authorize(request, *args, **kwargs):
126
    start = now()
127

  
128
    try:
129
        client_id = request.GET['client_id']
130
        redirect_uri = request.GET['redirect_uri']
131
    except KeyError as k:
132
        log_invalid_request(request, 'missing %s' % k.args[0])
133
        return redirect(request, 'auth_homepage')
134
    try:
135
        client = models.OIDCClient.objects.get(client_id=client_id)
136
    except models.OIDCClient.DoesNotExist:
137
        log_invalid_request(request, 'unknown client_id redirect_uri=%r client_id=%r' % (redirect_uri, client_id))
138
        return redirect(request, 'auth_homepage')
139

  
140
    if client.authorization_flow == client.FLOW_RESOURCE_OWNER_CRED:
141
        messages.warning(request, _('Client is configured for resource owner password credentials grant type'))
142
        return authorization_error(request, 'auth_homepage',
143
                                   'unauthorized_client',
144
                                   error_description='authz endpoint is configured '
145
                                   'for resource owner password credential grant type')
146

  
210
    validated_redirect_uri = None
211
    client_id = None
212
    client = None
147 213
    try:
148
        client.validate_redirect_uri(redirect_uri)
149
    except ValueError as e:
150
        log_invalid_request(request, 'invalid redirect_uri redirect_uri=%r client_id=%r (%s)' % (redirect_uri, client_id, e))
151
        return redirect(request, 'auth_homepage')
152

  
153
    fragment = client.authorization_flow == client.FLOW_IMPLICIT
214
        client_id = request.GET.get('client_id', '')
215
        if not client_id:
216
            raise MissingParameter('client_id')
217
        redirect_uri = request.GET.get('redirect_uri', '')
218
        if not redirect_uri:
219
            raise MissingParameter('redirect_uri')
220
        client = get_client(client_id=client_id)
221
        if not client:
222
            raise InvalidRequest(_('Unknown client identifier: "%s"') % client_id)
223
        try:
224
            client.validate_redirect_uri(redirect_uri)
225
        except ValueError:
226
            error_description = _(
227
                'Redirect URI "%s" is unknown.'
228
            ) % redirect_uri
229
            if settings.DEBUG:
230
                error_description += _(
231
                    ' Known redirect URIs are: %s'
232
                ) % ', '.join(client.redirect_uris.split())
233
            raise InvalidRequest(error_description)
234
        state = request.GET.get('state')
235
        use_fragment = client.authorization_flow == client.FLOW_IMPLICIT
236
        validated_redirect_uri = redirect_uri
237
        return authorize_for_client(request, client, validated_redirect_uri)
238
    except OIDCException as e:
239
        return e.redirect_response(
240
            request,
241
            redirect_uri=validated_redirect_uri,
242
            state=validated_redirect_uri and state,
243
            use_fragment=validated_redirect_uri and use_fragment,
244
            client=client)
245

  
246

  
247
def authorize_for_client(request, client, redirect_uri):
248
    hooks.call_hooks('event', name='sso-request', idp='oidc', service=client)
154 249

  
155 250
    state = request.GET.get('state')
251
    nonce = request.GET.get('nonce')
156 252
    login_hint = set(request.GET.get('login_hint', u'').split())
157

  
158
    try:
159
        response_type = request.GET['response_type']
160
        scope = request.GET['scope']
161
    except KeyError as k:
162
        return authorization_error(request, redirect_uri, 'invalid_request',
163
                                   state=state,
164
                                   error_description='missing parameter %s' % k.args[0],
165
                                   fragment=fragment)
166

  
167 253
    prompt = set(filter(None, request.GET.get('prompt', '').split()))
168
    nonce = request.GET.get('nonce')
169
    scopes = utils.scope_set(scope)
170

  
171
    max_age = request.GET.get('max_age')
172
    if max_age:
173
        try:
174
            max_age = int(max_age)
175
            if max_age < 0:
176
                raise ValueError
177
        except ValueError:
178
            return authorization_error(request, redirect_uri, 'invalid_request',
179
                                       error_description='max_age is not a positive integer',
180
                                       state=state,
181
                                       fragment=fragment)
182 254

  
255
    # check response_type
256
    response_type = request.GET.get('response_type', '')
257
    if not response_type:
258
        raise MissingParameter('response_type')
259
    if client.authorization_flow == client.FLOW_RESOURCE_OWNER_CRED:
260
        raise InvalidRequest(_(
261
            'Client is configured for resource owner password credentials grant, '
262
            'authorize endpoint is not usable'
263
        ))
183 264
    if client.authorization_flow == client.FLOW_AUTHORIZATION_CODE:
184 265
        if response_type != 'code':
185
            return authorization_error(request, redirect_uri, 'unsupported_response_type',
186
                                       error_description='only code is supported',
187
                                       state=state,
188
                                       fragment=fragment)
266
            raise UnsupportedResponseType(_('Response type must be "code"'))
189 267
    elif client.authorization_flow == client.FLOW_IMPLICIT:
190 268
        if not set(filter(None, response_type.split())) in (set(['id_token', 'token']),
191 269
                                                            set(['id_token'])):
192
            return authorization_error(request, redirect_uri, 'unsupported_response_type',
193
                                       error_description='only "id_token token" or "id_token" '
194
                                       'are supported',
195
                                       state=state,
196
                                       fragment=fragment)
270
            raise UnsupportedResponseType(_('Response type must be "id_token token" or "id_token"'))
197 271
    else:
198 272
        raise NotImplementedError
199
    if 'openid' not in scopes:
200
        return authorization_error(request, redirect_uri, 'invalid_request',
201
                                   error_description='openid scope is missing',
202
                                   state=state,
203
                                   fragment=fragment)
204 273

  
274
    # check scope
275
    scope = request.GET.get('scope', '')
276
    if not scope:
277
        raise MissingParameter('scope')
278
    scopes = utils.scope_set(scope)
279
    if 'openid' not in scopes:
280
        raise InvalidScope(
281
            _('Scope must contain "openid", received "%s"')
282
            % ', '.join(sorted(scopes)))
205 283
    if not is_scopes_allowed(scopes, client):
206
        message = 'only "%s" scope(s) are supported, but "%s" requested' % (
207
            ', '.join(allowed_scopes(client)), ', '.join(scopes))
208
        return authorization_error(request, redirect_uri, 'invalid_scope',
209
                                   error_description=message,
210
                                   state=state,
211
                                   fragment=fragment)
284
        raise InvalidScope(
285
            _('Scope may contain "%s" scope(s), received "%s"') % (
286
                ', '.join(sorted(allowed_scopes(client))),
287
                ', '.join(sorted(scopes))))
288

  
289
    # check max_age
290
    max_age = request.GET.get('max_age')
291
    if max_age:
292
        try:
293
            max_age = int(max_age)
294
            if max_age < 0:
295
                raise ValueError
296
        except ValueError:
297
            raise InvalidRequest(_('Parameter "max_age" must be a positive integer'))
212 298

  
213
    hooks.call_hooks('event', name='sso-request', idp='oidc', service=client)
214 299
    # authentication canceled by user
215 300
    if 'cancel' in request.GET:
216
        logger.info(u'authentication canceled for service %s', client.name)
217
        return authorization_error(request, redirect_uri, 'access_denied',
218
                                   error_description='user did not authenticate',
219
                                   state=state,
220
                                   fragment=fragment)
301
        raise AccessDenied(_('Authentication cancelled by user'))
221 302

  
222 303
    if not request.user.is_authenticated or 'login' in prompt:
223 304
        if 'none' in prompt:
224
            return authorization_error(request, redirect_uri, 'login_required',
225
                                       error_description='login is required but prompt is none',
226
                                       state=state,
227
                                       fragment=fragment)
305
            raise LoginRequired(_('Login is required but prompt parameter is "none"'))
228 306
        params = {}
229 307
        if nonce is not None:
230 308
            params['nonce'] = nonce
......
237 315
    last_auth = last_authentication_event(request=request)
238 316
    if max_age is not None and time.time() - last_auth['when'] >= max_age:
239 317
        if 'none' in prompt:
240
            return authorization_error(request, redirect_uri, 'login_required',
241
                                       error_description='login is required but prompt is none',
242
                                       state=state,
243
                                       fragment=fragment)
318
            raise LoginRequired(_('Login is required because of max_age, but prompt parameter is "none"'))
244 319
        params = {}
245 320
        if nonce is not None:
246 321
            params['nonce'] = nonce
247 322
        return login_require(request, params=params, service=client, login_hint=login_hint)
248 323

  
324
    iat = now()  # iat = issued at
325

  
249 326
    if client.authorization_mode != client.AUTHORIZATION_MODE_NONE or 'consent' in prompt:
250 327
        # authorization by user is mandatory, as per local configuration or per explicit request by
251 328
        # the RP
......
256 333
            auth_manager = client.ou.oidc_authorizations
257 334

  
258 335
        qs = auth_manager.filter(user=request.user)
259

  
260 336
        if 'consent' in prompt:
261 337
            # if consent is asked we delete existing authorizations
262 338
            # it seems to be the safer option
263 339
            qs.delete()
264 340
            qs = auth_manager.none()
265 341
        else:
266
            qs = qs.filter(expired__gte=start)
342
            qs = qs.filter(expired__gte=iat)
267 343
        authorized_scopes = set()
268 344
        for authorization in qs:
269 345
            authorized_scopes |= authorization.scope_set()
270 346
        if (authorized_scopes & scopes) < scopes:
271 347
            if 'none' in prompt:
272
                return authorization_error(
273
                    request, redirect_uri, 'consent_required',
274
                    error_description='consent is required but prompt is none',
275
                    state=state,
276
                    fragment=fragment)
348
                raise ConsentRequired(_('Consent is required but prompt parameter is "none"'))
277 349
            if request.method == 'POST':
278 350
                if 'accept' in request.POST:
279 351
                    if 'do_not_ask_again' in request.POST:
......
284 356
                                pk_to_deletes.append(authorization.pk)
285 357
                        auth_manager.create(
286 358
                            user=request.user, scopes=u' '.join(sorted(scopes)),
287
                            expired=start + datetime.timedelta(days=365))
359
                            expired=iat + datetime.timedelta(days=365))
288 360
                        if pk_to_deletes:
289 361
                            auth_manager.filter(pk__in=pk_to_deletes).delete()
290 362
                        request.journal.record(
291 363
                            'user.service.sso.authorization',
292 364
                            service=client,
293 365
                            scopes=list(sorted(scopes)))
294
                        logger.info(u'authorized scopes %s saved for service %s', ' '.join(scopes),
295
                                    client.name)
366
                        logger.info(
367
                            'idp_oidc: authorized scopes %s saved for service %s',
368
                            ' '.join(scopes), client)
296 369
                    else:
297
                        logger.info(u'authorized scopes %s for service %s', ' '.join(scopes),
298
                                    client.name)
370
                        logger.info(
371
                            'idp_oidc: authorized scopes %s for service %s',
372
                            ' '.join(scopes),
373
                            client)
299 374
                else:
300
                    logger.info(u'refused scopes %s for service %s', ' '.join(scopes),
301
                                client.name)
302
                    return authorization_error(request, redirect_uri, 'access_denied',
303
                                               error_description='user denied access',
304
                                               state=state,
305
                                               fragment=fragment)
375
                    raise AccessDenied(_('User consent refused'))
306 376
            else:
307 377
                return render(request, 'authentic2_idp_oidc/authorization.html',
308 378
                              {
......
313 383
        code = models.OIDCCode.objects.create(
314 384
            client=client, user=request.user, scopes=u' '.join(scopes),
315 385
            state=state, nonce=nonce, redirect_uri=redirect_uri,
316
            expired=start + datetime.timedelta(seconds=30),
386
            expired=iat + datetime.timedelta(seconds=30),
317 387
            auth_time=datetime.datetime.fromtimestamp(last_auth['when'], utc),
318 388
            session_key=request.session.session_key)
319
        logger.info(u'sending code %s for scopes %s for service %s',
320
                    code.uuid, ' '.join(scopes),
321
                    client.name)
389
        logger.info(
390
            'idp_oidc: sending code %s for scopes %s for service %s',
391
            code.uuid, ' '.join(scopes), client)
322 392
        params = {
323 393
            'code': six.text_type(code.uuid),
324 394
        }
......
326 396
            params['state'] = state
327 397
        response = redirect(request, redirect_uri, params=params, resolve=False)
328 398
    else:
329
        # FIXME: we should probably factorize this part with the token endpoint similar code
330 399
        need_access_token = 'token' in response_type.split()
331 400
        expires_in = access_token_duration(client)
332 401
        if need_access_token:
......
335 404
                user=request.user,
336 405
                scopes=u' '.join(scopes),
337 406
                session_key=request.session.session_key,
338
                expired=start + expires_in)
407
                expired=iat + expires_in)
339 408
        acr = '0'
340 409
        if nonce is not None and last_auth.get('nonce') == nonce:
341 410
            acr = '1'
......
344 413
                                          request.user,
345 414
                                          scopes,
346 415
                                          id_token=True)
347
        exp = start + idtoken_duration(client)
416
        exp = iat + idtoken_duration(client)
348 417
        id_token.update({
349 418
            'iss': utils.get_issuer(request),
350 419
            'aud': client.client_id,
351 420
            'exp': int(exp.timestamp()),
352
            'iat': int(start.timestamp()),
421
            'iat': int(iat.timestamp()),
353 422
            'auth_time': last_auth['when'],
354 423
            'acr': acr,
355 424
            'sid': utils.get_session_id(request, client),
......
378 447
    return response
379 448

  
380 449

  
381
def authenticate_client(request, client=None):
382
    '''Authenticate client on the token endpoint'''
450
def parse_http_basic(request):
451
    authorization = request.META['HTTP_AUTHORIZATION'].split()
452
    if authorization[0] != 'Basic' or len(authorization) != 2:
453
        return None, None
454
    try:
455
        decoded = force_text(base64.b64decode(authorization[1]))
456
    except Base64Error:
457
        return None, None
458
    parts = decoded.split(':')
459
    if len(parts) != 2:
460
        return None, None
461
    return parts
383 462

  
384
    if 'HTTP_AUTHORIZATION' in request.META:
385
        authorization = request.META['HTTP_AUTHORIZATION'].split()
386
        if authorization[0] != 'Basic' or len(authorization) != 2:
387
            return None
388
        try:
389
            decoded = force_text(base64.b64decode(authorization[1]))
390
        except Base64Error:
391
            return None
392
        parts = decoded.split(':')
393
        if len(parts) != 2:
394
            return None
395
        client_id, client_secret = parts
396
    elif 'client_id' in request.POST:
397
        client_id = request.POST['client_id']
398
        client_secret = request.POST.get('client_secret', '')
399
    else:
400
        return None
463

  
464
def get_client(client_id, client=None):
401 465
    if not client:
402 466
        try:
403 467
            client = models.OIDCClient.objects.get(client_id=client_id)
404 468
        except models.OIDCClient.DoesNotExist:
405 469
            return None
406
    if client.client_secret != client_secret:
407
        return None
470
    else:
471
        if client.client_id != client_id:
472
            return None
408 473
    return client
409 474

  
410 475

  
411
def error_response(error, error_description=None, status=400):
412
    content = {
413
        'error': error,
414
    }
415
    if error_description:
416
        content['error_description'] = error_description
417
    return JsonResponse(content, status=status)
418

  
476
def authenticate_client_secret(client, client_secret):
477
    raw_client_client_secret = client.client_secret.encode('utf-8')
478
    raw_provided_client_secret = client_secret.encode('utf-8')
479
    if len(raw_client_client_secret) != len(raw_provided_client_secret):
480
        raise WrongClientSecret(client=client)
481
    if not secrets.compare_digest(
482
            raw_client_client_secret,
483
            raw_provided_client_secret):
484
        raise WrongClientSecret(client=client)
485
    return client
419 486

  
420
def invalid_request_response(error_description=None):
421
    return error_response('invalid_request', error_description=error_description)
422 487

  
488
def check_ratelimited(request, key='ip', increment=True):
489
    return is_ratelimited(
490
        request, group='ro-cred-grant', increment=increment,
491
        key=key, rate=app_settings.PASSWORD_GRANT_RATELIMIT)
423 492

  
424
def access_denied_response(error_description=None):
425
    return error_response('access_denied', error_description=error_description)
426 493

  
494
def authenticate_client(request, ratelimit=False, client=None):
495
    '''Authenticate client on the token endpoint'''
427 496

  
428
def unauthorized_client_response(error_description=None):
429
    return error_response('unauthorized_client', error_description=error_description)
497
    if 'HTTP_AUTHORIZATION' in request.META:
498
        client_id, client_secret = parse_http_basic(request)
499
    elif 'client_id' in request.POST:
500
        client_id = request.POST.get('client_id', '')
501
        client_secret = request.POST.get('client_secret', '')
502
    else:
503
        return None
430 504

  
505
    if not client_id:
506
        raise WrongClientId
431 507

  
432
def invalid_client_response(error_description=None):
433
    return error_response('invalid_client', error_description=error_description)
508
    if not client_secret:
509
        raise InvalidRequest('missing client_secret', client=client_id)
434 510

  
511
    client = get_client(client_id)
512
    if not client:
513
        raise WrongClientId
435 514

  
436
def credential_grant_ratelimit_key(group, request):
437
    client = authenticate_client(request, client=None)
438
    if client:
439
        return client.client_id
440
    # return remote address when no valid client credentials have been provided
441
    return request.META['REMOTE_ADDR']
515
    return authenticate_client_secret(client, client_secret)
442 516

  
443 517

  
444 518
def idtoken_from_user_credential(request):
519
    # if rate limit by ip is exceeded, do not even try client authentication
520
    if check_ratelimited(request, increment=False):
521
        raise InvalidRequest('Rate limit exceeded for IP address "%s"' % request.META.get('REMOTE_ADDR', ''))
522

  
523
    try:
524
        client = authenticate_client(request, ratelimit=True, client=None)
525
    except InvalidClient:
526
        # increment rate limit by IP
527
        if check_ratelimited(request):
528
            raise InvalidRequest(
529
                _('Rate limit exceeded for IP address "%s"') % request.META.get('REMOTE_ADDR', ''))
530
        raise
531

  
532
    # check rate limit by client id
533
    if check_ratelimited(request, key=lambda group, request: client.client_id):
534
        raise InvalidClient(
535
            _('Rate limit of %s exceeded for client "%s"') % (
536
                app_settings.PASSWORD_GRANT_RATELIMIT, client),
537
            client=client)
538

  
445 539
    if request.META.get('CONTENT_TYPE') != 'application/x-www-form-urlencoded':
446
        return invalid_request_response(
447
            'wrong content type. request content type must be \'application/x-www-form-urlencoded\'')
540
        raise InvalidRequest(
541
            _('Wrong content type. request content type must be '
542
              '\'application/x-www-form-urlencoded\''), client=client)
448 543
    username = request.POST.get('username')
449 544
    scope = request.POST.get('scope')
450 545
    OrganizationalUnit = get_ou_model()
......
452 547
    # scope is ignored, we used the configured scope
453 548

  
454 549
    if not all((username, request.POST.get('password'))):
455
        return invalid_request_response(
456
            'request must bear both username and password as '
457
            'parameters using the "application/x-www-form-urlencoded" '
458
            'media type')
459

  
460
    if is_ratelimited(
461
            request, group='ro-cred-grant', increment=True,
462
            key=credential_grant_ratelimit_key,
463
            rate=app_settings.PASSWORD_GRANT_RATELIMIT):
464
        return invalid_request_response(
465
            'reached rate limitation, too many erroneous requests')
466

  
467
    client = authenticate_client(request, client=None)
468

  
469
    if not client:
470
        return invalid_client_response('client authentication failed')
550
        raise InvalidRequest(
551
            _('Request must bear both username and password as '
552
              'parameters using the "application/x-www-form-urlencoded" '
553
              'media type'), client=client)
471 554

  
472 555
    if client.authorization_flow != models.OIDCClient.FLOW_RESOURCE_OWNER_CRED:
473
        return unauthorized_client_response(
474
            'client is not configured for resource owner password '
475
            'credential grant')
556
        raise UnauthorizedClient(
557
            _('Client is not configured for resource owner password '
558
              'credential grant'), client=client)
476 559

  
477 560
    exponential_backoff = ExponentialRetryTimeout(
478 561
        key_prefix='idp-oidc-ro-cred-grant',
......
484 567
    if seconds_to_wait > a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MAX_DURATION:
485 568
        seconds_to_wait = a2_app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MAX_DURATION
486 569
    if seconds_to_wait:
487
        return invalid_request_response(
488
            'too many attempts with erroneous RO password, you must wait '
489
            '%s seconds to try again.' % int(math.ceil(seconds_to_wait)))
570
        raise InvalidRequest(
571
            _('Too many attempts with erroneous RO password, you must wait '
572
              '%s seconds to try again.') % int(math.ceil(seconds_to_wait)), client=client)
490 573

  
491 574
    ou = None
492 575
    if 'ou_slug' in request.POST:
493 576
        try:
494 577
            ou = OrganizationalUnit.objects.get(slug=request.POST.get('ou_slug'))
495 578
        except OrganizationalUnit.DoesNotExist:
496
            return invalid_request_response(
497
                'ou_slug parameter does not match a valid organization unit')
579
            raise InvalidRequest(
580
                _('Parameter "ou_slug" does not match an existing organizational unit'), client=client)
498 581

  
499 582
    user = authenticate(request, username=username, password=request.POST.get('password'), ou=ou)
500 583
    if not user:
501 584
        exponential_backoff.failure(*backoff_keys)
502
        return access_denied_response('invalid resource owner credentials')
585
        raise AccessDenied(_('Invalid user credentials'), client=client)
503 586

  
504 587
    # limit requested scopes
505 588
    if scope is not None:
......
508 591
        scopes = client.scope_set()
509 592

  
510 593
    exponential_backoff.success(*backoff_keys)
511
    start = now()
594
    iat = now()  # iat = issued at
512 595
    # make access_token
513 596
    expires_in = access_token_duration(client)
514 597
    access_token = models.OIDCAccessToken.objects.create(
......
516 599
        user=user,
517 600
        scopes=' '.join(scopes),
518 601
        session_key='',
519
        expired=start + expires_in)
602
        expired=iat + expires_in)
520 603
    # make id_token
521 604
    id_token = utils.create_user_info(
522 605
        request,
......
524 607
        user,
525 608
        scopes,
526 609
        id_token=True)
527
    exp = start + idtoken_duration(client)
610
    exp = iat + idtoken_duration(client)
528 611
    id_token.update({
529 612
        'iss': utils.get_issuer(request),
530 613
        'aud': client.client_id,
531 614
        'exp': int(exp.timestamp()),
532
        'iat': int(start.timestamp()),
533
        'auth_time': int(start.timestamp()),
615
        'iat': int(iat.timestamp()),
616
        'auth_time': int(iat.timestamp()),
534 617
        'acr': '0',
535 618
    })
536 619
    return JsonResponse({
......
542 625

  
543 626

  
544 627
def tokens_from_authz_code(request):
628
    client = authenticate_client(request)
629

  
545 630
    code = request.POST.get('code')
546
    if code is None:
547
        return invalid_request_response('missing code')
631
    if not code:
632
        raise MissingParameter('code', client=client)
548 633
    try:
549 634
        oidc_code = models.OIDCCode.objects.select_related().get(uuid=code)
550 635
    except models.OIDCCode.DoesNotExist:
551
        return invalid_request_response('invalid code')
636
        raise InvalidRequest(_('Parameter "code" is invalid'), client=client)
552 637
    if not oidc_code.is_valid():
553
        return invalid_request_response('code has expired or user is disconnected')
554
    client = authenticate_client(request, client=oidc_code.client)
555
    if client is None:
556
        return HttpResponse('unauthenticated', status=401)
557
    # delete immediately
638
        raise InvalidRequest(_('Parameter "code" has expired or user is disconnected'), client=client)
558 639
    models.OIDCCode.objects.filter(uuid=code).delete()
559 640
    redirect_uri = request.POST.get('redirect_uri')
560 641
    if oidc_code.redirect_uri != redirect_uri:
561
        return invalid_request_response('invalid redirect_uri')
642
        raise InvalidRequest(_('Parameter "redirect_uri" does not match the code.'), client=client)
562 643
    expires_in = access_token_duration(client)
563 644
    access_token = models.OIDCAccessToken.objects.create(
564 645
        client=client,
......
604 685
    if request.method != 'POST':
605 686
        return HttpResponseNotAllowed(['POST'])
606 687
    grant_type = request.POST.get('grant_type')
607
    if grant_type == 'password':
608
        response = idtoken_from_user_credential(request)
609
    elif grant_type == 'authorization_code':
610
        response = tokens_from_authz_code(request)
611
    else:
612
        return invalid_request_response('grant_type must be either authorization_code or password')
613
    response['Cache-Control'] = 'no-store'
614
    response['Pragma'] = 'no-cache'
615
    return response
688
    try:
689
        if grant_type == 'password':
690
            response = idtoken_from_user_credential(request)
691
        elif grant_type == 'authorization_code':
692
            response = tokens_from_authz_code(request)
693
        else:
694
            raise InvalidRequest('grant_type must be either authorization_code or password')
695
        response['Cache-Control'] = 'no-store'
696
        response['Pragma'] = 'no-cache'
697
        return response
698
    except OIDCException as e:
699
        response = e.json_response(request)
700
        # special case of client authentication error with HTTP Basic
701
        if 'HTTP_AUTHORIZATION' in request and e.error_code == 'invalid_client':
702
            response['WWW-Authenticate'] = 'Basic'
703
        return response
616 704

  
617 705

  
618 706
def authenticate_access_token(request):
tests/test_idp_oidc.py
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17 17
import base64
18
import json
19 18
import datetime
19
import functools
20
import json
20 21

  
21 22
import pytest
22 23

  
......
25 26

  
26 27
from . import utils
27 28

  
28
from django import VERSION as DJ_VERSION
29 29
from django.core.exceptions import ValidationError
30 30
from django.core.files import File
31
from django.http import QueryDict
31 32
from django.test.utils import override_settings
32 33
from django.urls import reverse
33 34
from django.utils.encoding import force_text
......
191 192

  
192 193
@pytest.mark.parametrize('do_not_ask_again', [(True,), (False,)])
193 194
@pytest.mark.parametrize('login_first', [(True,), (False,)])
194
def test_authorization_code_sso(login_first, do_not_ask_again, oidc_settings, oidc_client, simple_user, app):
195
def test_authorization_code_sso(login_first, do_not_ask_again, oidc_settings, oidc_client, simple_user, app, caplog):
195 196
    redirect_uri = oidc_client.redirect_uris.split()[0]
196 197
    params = {
197 198
        'client_id': oidc_client.client_id,
......
221 222
        response = response.follow()
222 223
        assert response.request.path == reverse('oidc-authorize')
223 224
    if oidc_client.authorization_mode != OIDCClient.AUTHORIZATION_MODE_NONE:
225
        response = response.maybe_follow()
224 226
        assert 'a2-oidc-authorization-form' in response.text
225 227
        assert OIDCAuthorization.objects.count() == 0
226 228
        assert OIDCCode.objects.count() == 0
......
391 393
                assert iframes.attr('onload').endswith(', 300)')
392 394

  
393 395

  
394
def assert_oidc_error(response, error, error_description=None, fragment=False):
395
    location = urlparse.urlparse(response['Location'])
396
    query = location.fragment if fragment else location.query
397
    query = urlparse.parse_qs(query)
398
    assert query['error'] == [error]
399
    if error_description:
400
        assert len(query['error_description']) == 1
401
        assert error_description in query['error_description'][0]
396
def check_authorize_error(response, error, error_description, fragment, caplog,
397
                          check_next=True, redirect_uri=None, message=True):
398
    # check next_url qs
399
    if message:
400
        location = urlparse.urlparse(response.location)
401
        assert location.path == '/continue/'
402
        if check_next:
403
            location_qs = QueryDict(location.query or '')
404
            assert 'next' in location_qs
405
            assert location_qs['next'].startswith(redirect_uri)
406
            next_url = urlparse.urlparse(location_qs['next'])
407
            next_url_qs = QueryDict(next_url.fragment if fragment else next_url.query)
408
            assert next_url_qs['error'] == error
409
            assert next_url_qs['error_description'] == error_description
410
        # check continue page
411
        continue_response = response.follow()
412
        assert error_description in continue_response.pyquery('.error').text()
413
    elif check_next:
414
        assert response.location.startswith(redirect_uri)
415
        location = urlparse.urlparse(response.location)
416
        location_qs = QueryDict(location.fragment if fragment else location.query)
417
        assert location_qs['error'] == error
418
        assert location_qs['error_description'] == error_description
419
    # check logs
420
    last_record = caplog.records[-1]
421
    if message:
422
        assert last_record.levelname == 'WARNING'
423
    else:
424
        assert last_record.levelname == 'INFO'
425
    assert 'error "%s" in authorize endpoint' % error in last_record.message
426
    assert error_description in last_record.message
427
    if message:
428
        return continue_response
402 429

  
403 430

  
404 431
def assert_authorization_response(response, fragment=False, **kwargs):
405
    location = urlparse.urlparse(response['Location'])
406
    query = location.fragment if fragment else location.query
407
    query = urlparse.parse_qs(query)
432
    location = urlparse.urlparse(response.location)
433
    location_qs = QueryDict(location.fragment if fragment else location.query)
434
    assert set(location_qs) == set(kwargs)
408 435
    for key, value in kwargs.items():
409 436
        if value is None:
410
            assert key in query
437
            assert key in location_qs
411 438
        elif isinstance(value, list):
412
            assert query[key] == value
439
            assert set(location_qs.getlist(key)) == set(value)
413 440
        else:
414
            assert value in query[key][0]
441
            assert value in location_qs[key]
415 442

  
416 443

  
417 444
def test_invalid_request(caplog, oidc_settings, oidc_client, simple_user, app):
......
425 452
    else:
426 453
        raise NotImplementedError
427 454

  
428
    # missing client_id
429
    authorize_url = make_url('oidc-authorize', params={})
455
    assert_authorize_error = functools.partial(
456
        check_authorize_error,
457
        caplog=caplog,
458
        fragment=fragment,
459
        redirect_uri=redirect_uri)
430 460

  
431
    response = app.get(authorize_url, status=302)
432
    assert urlparse.urlparse(response['Location']).path  == '/'
433
    response = response.maybe_follow()
434
    assert 'Authorization request is invalid' in response
461
    # missing client_id
462
    response = app.get(make_url('oidc-authorize', params={}))
463
    assert_authorize_error(response, 'invalid_request', 'Missing parameter "client_id"', check_next=False)
435 464

  
436 465
    # missing redirect_uri
437
    authorize_url = make_url('oidc-authorize', params={
466
    response = app.get(make_url('oidc-authorize', params={
438 467
        'client_id': oidc_client.client_id,
439
    })
440

  
441
    response = app.get(authorize_url, status=302)
442
    assert urlparse.urlparse(response['Location']).path == '/'
443
    response = response.maybe_follow()
444
    assert 'Authorization request is invalid' in response
468
    }))
469
    assert_authorize_error(response, 'invalid_request', 'Missing parameter "redirect_uri"', check_next=False)
445 470

  
446 471
    # invalid client_id
447
    authorize_url = make_url('oidc-authorize', params={
472
    authorize_url = app.get(make_url('oidc-authorize', params={
448 473
        'client_id': 'xxx',
449 474
        'redirect_uri': redirect_uri,
450
    })
451

  
452
    response = app.get(authorize_url, status=302)
453
    assert urlparse.urlparse(response['Location']).path == '/'
454
    response = response.maybe_follow()
455
    assert 'Authorization request is invalid' in response
475
    }))
476
    assert_authorize_error(response, 'invalid_request', 'Unknown client identifier: "xxx"', check_next=False)
456 477

  
457 478
    # invalid redirect_uri
458
    authorize_url = make_url('oidc-authorize', params={
479
    response = app.get(make_url('oidc-authorize', params={
459 480
        'client_id': oidc_client.client_id,
460 481
        'redirect_uri': 'xxx',
461 482
        'response_type': 'code',
462 483
        'scope': 'openid',
463
    })
464

  
465
    response = app.get(authorize_url, status=302)
466
    assert urlparse.urlparse(response['Location']).path == '/'
467
    response = response.maybe_follow()
468
    assert 'Authorization request is invalid' in response
469
    assert not 'invalid redirect_uri' in response
484
    }), status=302)
485
    continue_response = assert_authorize_error(
486
        response, 'invalid_request', 'Redirect URI "xxx" is unknown.', check_next=False)
487
    assert 'Known' not in continue_response.pyquery('.error').text()
470 488

  
489
    # invalid redirect_uri with DEBUG=True, list of redirect_uris is shown
471 490
    with override_settings(DEBUG=True):
472
        response = app.get(authorize_url, status=302)
473
    assert urlparse.urlparse(response['Location']).path  == '/'
474
    response = response.maybe_follow()
475
    assert 'invalid redirect_uri' in response
491
        response = app.get(make_url('oidc-authorize', params={
492
            'client_id': oidc_client.client_id,
493
            'redirect_uri': 'xxx',
494
            'response_type': 'code',
495
            'scope': 'openid',
496
        }), status=302)
497
        continue_response = assert_authorize_error(
498
            response, 'invalid_request', 'Redirect URI "xxx" is unknown.', check_next=False)
499
        assert (
500
            'Known redirect URIs are: https://example.com/callbac%C3%A9'
501
            in continue_response.pyquery('.error').text()
502
        )
476 503

  
477 504
    # missing response_type
478
    authorize_url = make_url('oidc-authorize', params={
505
    response = app.get(make_url('oidc-authorize', params={
479 506
        'client_id': oidc_client.client_id,
480 507
        'redirect_uri': redirect_uri,
481
    })
482

  
483
    response = app.get(authorize_url)
484
    if DJ_VERSION < (2, 0):
485
        errmsg1 = 'missing parameter \'response_type\''
486
        errmsg2 = 'missing parameter \'scope\''
487
    else:
488
        errmsg1 = 'missing parameter response_type'
489
        errmsg2 = 'missing parameter scope'
490
    assert_oidc_error(response, 'invalid_request', errmsg1,
491
                      fragment=fragment)
492
    logrecord = [rec for rec in caplog.records if rec.funcName == 'authorization_error'][0]
493
    assert logrecord.levelname == 'WARNING'
494
    assert logrecord.redirect_uri == 'https://example.com/callbac%C3%A9'
495
    assert errmsg1 in logrecord.message
508
    }))
509
    assert_authorize_error(response, 'invalid_request', 'Missing parameter "response_type"')
496 510

  
497
    # missing scope
498
    authorize_url = make_url('oidc-authorize', params={
511
    # unsupported response_type
512
    response = app.get(make_url('oidc-authorize', params={
499 513
        'client_id': oidc_client.client_id,
500 514
        'redirect_uri': redirect_uri,
501
        'response_type': 'code',
502
    })
515
        'response_type': 'xxx',
516
    }))
503 517

  
504
    response = app.get(authorize_url)
505
    assert_oidc_error(response, 'invalid_request', errmsg2, fragment=fragment)
518
    if oidc_client.authorization_flow == oidc_client.FLOW_AUTHORIZATION_CODE:
519
        assert_authorize_error(response, 'unsupported_response_type', 'Response type must be "code"')
520
    elif oidc_client.authorization_flow == oidc_client.FLOW_IMPLICIT:
521
        assert_authorize_error(
522
            response, 'unsupported_response_type', 'Response type must be "id_token token" or "id_token"')
506 523

  
507
    # invalid max_age
508
    authorize_url = make_url('oidc-authorize', params={
524
    # missing scope
525
    response = app.get(make_url('oidc-authorize', params={
509 526
        'client_id': oidc_client.client_id,
510 527
        'redirect_uri': redirect_uri,
511
        'response_type': 'code',
512
        'scope': 'openid',
513
        'max_age': 'xxx',
514
    })
515
    response = app.get(authorize_url)
516
    assert_oidc_error(response, 'invalid_request', 'max_age is not', fragment=fragment)
517
    authorize_url = make_url('oidc-authorize', params={
528
        'response_type': response_type,
529
    }))
530
    assert_authorize_error(response, 'invalid_request', 'Missing parameter "scope"')
531

  
532
    # invalid max_age : not an integer
533
    response = app.get(make_url('oidc-authorize', params={
518 534
        'client_id': oidc_client.client_id,
519 535
        'redirect_uri': redirect_uri,
520
        'response_type': 'code',
536
        'response_type': response_type,
521 537
        'scope': 'openid',
522
        'max_age': '-1',
523
    })
524
    response = app.get(authorize_url)
525
    assert_oidc_error(response, 'invalid_request', 'max_age is not', fragment=fragment)
538
        'max_age': 'xxx',
539
    }))
540
    assert_authorize_error(response, 'invalid_request', 'Parameter "max_age" must be a positive integer')
526 541

  
527
    # unsupported response_type
528
    authorize_url = make_url('oidc-authorize', params={
542
    # invalid max_age : not positive
543
    response = app.get(make_url('oidc-authorize', params={
529 544
        'client_id': oidc_client.client_id,
530 545
        'redirect_uri': redirect_uri,
531
        'response_type': 'xxx',
546
        'response_type': response_type,
532 547
        'scope': 'openid',
533
    })
534

  
535
    response = app.get(authorize_url)
536
    if oidc_client.authorization_flow == oidc_client.FLOW_AUTHORIZATION_CODE:
537
        assert_oidc_error(response, 'unsupported_response_type', 'only code is supported')
538
    elif oidc_client.authorization_flow == oidc_client.FLOW_IMPLICIT:
539
        assert_oidc_error(response, 'unsupported_response_type',
540
                          'only "id_token token" or "id_token" are supported', fragment=fragment)
548
        'max_age': '-1',
549
    }))
550
    assert_authorize_error(response, 'invalid_request', 'Parameter "max_age" must be a positive integer')
541 551

  
542 552
    # openid scope is missing
543 553
    authorize_url = make_url('oidc-authorize', params={
......
548 558
    })
549 559

  
550 560
    response = app.get(authorize_url)
551
    assert_oidc_error(response, 'invalid_request', 'openid scope is missing', fragment=fragment)
561
    assert_authorize_error(response, 'invalid_scope', 'Scope must contain "openid", received "profile"')
552 562

  
553 563
    # use of an unknown scope
554 564
    authorize_url = make_url('oidc-authorize', params={
......
559 569
    })
560 570

  
561 571
    response = app.get(authorize_url)
562
    assert_oidc_error(response, 'invalid_scope', fragment=fragment)
572
    assert_authorize_error(
573
        response,
574
        'invalid_scope',
575
        'Scope may contain "email, openid, profile" scope(s), received "email, openid, profile, zob"')
563 576

  
564 577
    # restriction on scopes
565
    oidc_settings.A2_IDP_OIDC_SCOPES = ['openid']
566
    authorize_url = make_url('oidc-authorize', params={
567
        'client_id': oidc_client.client_id,
568
        'redirect_uri': redirect_uri,
569
        'response_type': response_type,
570
        'scope': 'openid email',
571
    })
572

  
573
    response = app.get(authorize_url)
574
    assert_oidc_error(response, 'invalid_scope', fragment=fragment)
575
    del oidc_settings.A2_IDP_OIDC_SCOPES
578
    with override_settings(A2_IDP_OIDC_SCOPES=['openid']):
579
        response = app.get(make_url('oidc-authorize', params={
580
            'client_id': oidc_client.client_id,
581
            'redirect_uri': redirect_uri,
582
            'response_type': response_type,
583
            'scope': 'openid email',
584
        }))
585
        assert_authorize_error(
586
            response,
587
            'invalid_scope',
588
            'Scope may contain "openid" scope(s), received "email, openid"')
576 589

  
577 590
    # cancel
578
    authorize_url = make_url('oidc-authorize', params={
591
    response = app.get(make_url('oidc-authorize', params={
579 592
        'client_id': oidc_client.client_id,
580 593
        'redirect_uri': redirect_uri,
581 594
        'response_type': response_type,
582 595
        'scope': 'openid email profile',
583 596
        'cancel': '1',
584
    })
585

  
586
    response = app.get(authorize_url)
587
    assert_oidc_error(response, 'access_denied', error_description='user did not authenticate',
588
                      fragment=fragment)
597
    }))
598
    assert_authorize_error(response, 'access_denied', 'Authentication cancelled by user', message=False)
589 599

  
590 600
    # prompt=none
591
    authorize_url = make_url('oidc-authorize', params={
601
    response = app.get(make_url('oidc-authorize', params={
592 602
        'client_id': oidc_client.client_id,
593 603
        'redirect_uri': redirect_uri,
594 604
        'response_type': response_type,
595 605
        'scope': 'openid email profile',
596 606
        'prompt': 'none',
597
    })
598

  
599
    response = app.get(authorize_url)
600
    assert_oidc_error(response, 'login_required', error_description='prompt is none',
601
                      fragment=fragment)
607
    }))
608
    assert_authorize_error(response,
609
                           'login_required',
610
                           error_description='Login is required but prompt parameter is "none"',
611
                           message=False)
602 612

  
603 613
    utils.login(app, simple_user)
604 614

  
605 615
    # prompt=none max_age=0
606
    authorize_url = make_url('oidc-authorize', params={
616
    response = app.get(make_url('oidc-authorize', params={
607 617
        'client_id': oidc_client.client_id,
608 618
        'redirect_uri': redirect_uri,
609 619
        'response_type': response_type,
610 620
        'scope': 'openid email profile',
611 621
        'max_age': '0',
612 622
        'prompt': 'none',
613
    })
614

  
615
    response = app.get(authorize_url)
616
    assert_oidc_error(response, 'login_required', error_description='prompt is none',
617
                      fragment=fragment)
623
    }))
624
    assert_authorize_error(response, 'login_required',
625
                           error_description='Login is required because of max_age, but prompt parameter is "none"',
626
                           message=False)
618 627

  
619 628
    # max_age=0
620
    authorize_url = make_url('oidc-authorize', params={
629
    response = app.get(make_url('oidc-authorize', params={
621 630
        'client_id': oidc_client.client_id,
622 631
        'redirect_uri': redirect_uri,
623 632
        'response_type': response_type,
624 633
        'scope': 'openid email profile',
625 634
        'max_age': '0',
626
    })
627
    response = app.get(authorize_url)
628
    assert urlparse.urlparse(response['Location']).path == reverse('auth_login')
635
    }))
636
    assert response.location.startswith(reverse('auth_login') + '?')
629 637

  
630 638
    # prompt=login
631 639
    authorize_url = make_url('oidc-authorize', params={
......
638 646
    response = app.get(authorize_url)
639 647
    assert urlparse.urlparse(response['Location']).path == reverse('auth_login')
640 648

  
641
    # user refuse authorization
642
    authorize_url = make_url('oidc-authorize', params={
643
        'client_id': oidc_client.client_id,
644
        'redirect_uri': redirect_uri,
645
        'response_type': response_type,
646
        'scope': 'openid email profile',
647
        'prompt': 'none',
648
    })
649
    response = app.get(authorize_url)
650
    if oidc_client.authorization_mode != oidc_client.AUTHORIZATION_MODE_NONE:
651
        assert_oidc_error(response, 'consent_required', error_description='prompt is none',
652
                          fragment=fragment)
653

  
654
    # user refuse authorization
655
    authorize_url = make_url('oidc-authorize', params={
656
        'client_id': oidc_client.client_id,
657
        'redirect_uri': redirect_uri,
658
        'response_type': response_type,
659
        'scope': 'openid email profile',
660
    })
661
    response = app.get(authorize_url)
662 649
    if oidc_client.authorization_mode != oidc_client.AUTHORIZATION_MODE_NONE:
650
        # prompt is none, but consent is required
651
        response = app.get(make_url('oidc-authorize', params={
652
            'client_id': oidc_client.client_id,
653
            'redirect_uri': redirect_uri,
654
            'response_type': response_type,
655
            'scope': 'openid email profile',
656
            'prompt': 'none',
657
        }))
658
        assert_authorize_error(
659
            response,
660
            'consent_required',
661
            'Consent is required but prompt parameter is "none"',
662
            message=False)
663

  
664
        # user do not consent
665
        response = app.get(make_url('oidc-authorize', params={
666
            'client_id': oidc_client.client_id,
667
            'redirect_uri': redirect_uri,
668
            'response_type': response_type,
669
            'scope': 'openid email profile',
670
        }))
663 671
        response = response.form.submit('refuse')
664
        assert_oidc_error(response, 'access_denied', error_description='user denied access',
665
                          fragment=fragment)
672
        assert_authorize_error(
673
            response,
674
            'access_denied',
675
            'User consent refused',
676
            message=False)
666 677

  
667 678
    # authorization exists
668 679
    authorize = OIDCAuthorization.objects.create(
669 680
        client=oidc_client, user=simple_user, scopes='openid profile email',
670 681
        expired=now() + datetime.timedelta(days=2))
671
    response = app.get(authorize_url)
682
    response = app.get(make_url('oidc-authorize', params={
683
        'client_id': oidc_client.client_id,
684
        'redirect_uri': redirect_uri,
685
        'response_type': response_type,
686
        'scope': 'openid email profile',
687
    }))
672 688
    if oidc_client.authorization_flow == oidc_client.FLOW_AUTHORIZATION_CODE:
673
        assert_authorization_response(response, code=None, fragment=fragment)
689
        assert_authorization_response(response, code=None)
674 690
    elif oidc_client.authorization_flow == oidc_client.FLOW_IMPLICIT:
675 691
        assert_authorization_response(response, access_token=None, id_token=None, expires_in=None,
676
                                      token_type=None, fragment=fragment)
692
                                      token_type=None, fragment=True)
677 693

  
678 694
    # client ask for explicit authorization
679 695
    authorize_url = make_url('oidc-authorize', params={
......
729 745
        }, headers=client_authentication_headers(oidc_client), status=400)
730 746
        assert 'error' in response.json
731 747
        assert response.json['error'] == 'invalid_request'
732
        assert response.json['error_description'] == 'code has expired or user is disconnected'
748
        assert response.json['error_description'] == 'Parameter "code" has expired or user is disconnected'
733 749

  
734 750
    # invalid logout
735 751
    logout_url = make_url('oidc-logout', params={
......
755 771
        }, headers=client_authentication_headers(oidc_client), status=400)
756 772
        assert 'error' in response.json
757 773
        assert response.json['error'] == 'invalid_request'
758
        assert response.json['error_description'] == 'code has expired or user is disconnected'
774
        assert response.json['error_description'] == 'Parameter "code" has expired or user is disconnected'
759 775

  
760 776

  
761 777
def test_expired_manager(db, simple_user):
......
1029 1045
        if client.name == 'test1':
1030 1046
            continue
1031 1047
        if client.name == 'test3':
1032
            OIDCClaim.objects.create(client=client, name='preferred_username', value='django_user_full_name', scopes='profile')
1048
            OIDCClaim.objects.create(client=client, name='preferred_username',
1049
                                     value='django_user_full_name',
1050
                                     scopes='profile')
1033 1051
        else:
1034
            OIDCClaim.objects.create(client=client, name='preferred_username', value='django_user_username', scopes='profile')
1052
            OIDCClaim.objects.create(client=client, name='preferred_username',
1053
                                     value='django_user_username',
1054
                                     scopes='profile')
1035 1055
        OIDCClaim.objects.create(client=client, name='given_name', value='django_user_first_name', scopes='profile')
1036 1056
        OIDCClaim.objects.create(client=client, name='family_name', value='django_user_last_name', scopes='profile')
1037 1057
        if client.name == 'test2':
1038 1058
            continue
1039
        OIDCClaim.objects.create(client=client, name='email', value='django_user_email', scopes='email')
1040
        OIDCClaim.objects.create(client=client, name='email_verified', value='django_user_email_verified', scopes='email')
1059
        OIDCClaim.objects.create(client=client, name='email',
1060
                                 value='django_user_email', scopes='email')
1061
        OIDCClaim.objects.create(client=client, name='email_verified',
1062
                                 value='django_user_email_verified',
1063
                                 scopes='email')
1041 1064

  
1042 1065
    new_apps = migration.apply(migrate_to)
1043 1066
    OIDCClient = new_apps.get_model('authentic2_idp_oidc', 'OIDCClient')
......
1047 1070
        claims = client.oidcclaim_set.all()
1048 1071
        if client.name == 'test':
1049 1072
            assert claims.count() == 5
1050
            assert sorted(claims.values_list('name', flat=True)) == [u'email', u'email_verified', u'family_name', u'given_name', u'preferred_username']
1051
            assert sorted(claims.values_list('value', flat=True)) == [u'django_user_email', u'django_user_email_verified', u'django_user_first_name', u'django_user_identifier', u'django_user_last_name']
1073
            assert (
1074
                sorted(claims.values_list('name', flat=True))
1075
                == ['email', 'email_verified', 'family_name', 'given_name', 'preferred_username']
1076
            )
1077
            assert (
1078
                sorted(claims.values_list('value', flat=True))
1079
                == ['django_user_email', 'django_user_email_verified',
1080
                    'django_user_first_name', 'django_user_identifier',
1081
                    'django_user_last_name']
1082
            )
1052 1083
        elif client.name == 'test2':
1053 1084
            assert claims.count() == 3
1054
            assert sorted(claims.values_list('name', flat=True)) == [u'family_name', u'given_name', u'preferred_username']
1055
            assert sorted(claims.values_list('value', flat=True)) == [u'django_user_first_name', u'django_user_last_name', u'django_user_username']
1085
            assert (
1086
                sorted(claims.values_list('name', flat=True))
1087
                == ['family_name', 'given_name', 'preferred_username']
1088
            )
1089
            assert (
1090
                sorted(claims.values_list('value', flat=True))
1091
                == ['django_user_first_name', 'django_user_last_name', 'django_user_username']
1092
            )
1056 1093
        elif client.name == 'test3':
1057 1094
            assert claims.count() == 5
1058
            assert sorted(claims.values_list('name', flat=True)) == [u'email', u'email_verified', u'family_name', u'given_name', u'preferred_username']
1059
            assert sorted(claims.values_list('value', flat=True)) == [u'django_user_email', u'django_user_email_verified', u'django_user_first_name', u'django_user_full_name', u'django_user_last_name']
1095
            assert (
1096
                sorted(claims.values_list('name', flat=True))
1097
                == ['email', 'email_verified', 'family_name', 'given_name', 'preferred_username']
1098
            )
1099
            assert (
1100
                sorted(claims.values_list('value', flat=True))
1101
                == ['django_user_email', 'django_user_email_verified',
1102
                    'django_user_first_name', 'django_user_full_name',
1103
                    'django_user_last_name']
1104
            )
1060 1105
        else:
1061 1106
            assert claims.count() == 0
1062 1107

  
......
1131 1176
        access_token = response.json['access_token']
1132 1177
        id_token = response.json['id_token']
1133 1178

  
1134
        k=base64.b64encode(oidc_client.client_secret.encode('utf-8'))
1179
        k = base64.b64encode(oidc_client.client_secret.encode('utf-8'))
1135 1180
        key = JWK(kty='oct', k=force_text(k))
1136 1181
        jwt = JWT(jwt=id_token, key=key)
1137 1182
        claims = json.loads(jwt.claims)
......
1181 1226

  
1182 1227
def test_claim_templated(oidc_settings, normal_oidc_client, simple_user, app):
1183 1228
    oidc_settings.A2_IDP_OIDC_SCOPES = ['openid', 'profile', 'email']
1184
    OIDCClaim.objects.filter(
1185
            client=normal_oidc_client, name='given_name').delete()
1186
    OIDCClaim.objects.filter(
1187
            client=normal_oidc_client, name='family_name').delete()
1188
    claim1 = OIDCClaim.objects.create(
1189
            client=normal_oidc_client,
1190
            name='given_name',
1191
            value='{{ django_user_first_name|add:"ounet" }}',
1192
            scopes='profile')
1193
    claim2 = OIDCClaim.objects.create(
1194
            client=normal_oidc_client,
1195
            name='family_name',
1196
            value='{{ "Von der "|add:django_user_last_name }}',
1197
            scopes='profile')
1229
    OIDCClaim.objects.filter(client=normal_oidc_client, name='given_name').delete()
1230
    OIDCClaim.objects.filter(client=normal_oidc_client, name='family_name').delete()
1231
    OIDCClaim.objects.create(
1232
        client=normal_oidc_client,
1233
        name='given_name',
1234
        value='{{ django_user_first_name|add:"ounet" }}',
1235
        scopes='profile')
1236
    OIDCClaim.objects.create(
1237
        client=normal_oidc_client,
1238
        name='family_name',
1239
        value='{{ "Von der "|add:django_user_last_name }}',
1240
        scopes='profile')
1198 1241
    normal_oidc_client.authorization_flow = normal_oidc_client.FLOW_AUTHORIZATION_CODE
1199 1242
    normal_oidc_client.authorization_mode = normal_oidc_client.AUTHORIZATION_MODE_NONE
1200 1243
    normal_oidc_client.save()
......
1337 1380
    oidc_client.save()
1338 1381
    token_url = make_url('oidc-token')
1339 1382
    if oidc_client.idtoken_algo == OIDCClient.ALGO_HMAC:
1340
        k=base64url(oidc_client.client_secret.encode('utf-8'))
1383
        k = base64url(oidc_client.client_secret.encode('utf-8'))
1341 1384
        jwk = JWK(kty='oct', k=force_text(k))
1342 1385
    elif oidc_client.idtoken_algo == OIDCClient.ALGO_RSA:
1343 1386
        jwk = get_first_rsa_sig_key()
......
1396 1439
    for i in range(int(oidc_settings.A2_IDP_OIDC_PASSWORD_GRANT_RATELIMIT.split('/')[0])):
1397 1440
        response = app.post(token_url, params=params, status=400)
1398 1441
        assert response.json['error'] == 'invalid_client'
1399
        assert 'client authentication failed' in response.json['error_description']
1442
        assert 'Wrong client\'s secret' in response.json['error_description']
1400 1443
    response = app.post(token_url, params=params, status=400)
1401 1444
    assert response.json['error'] == 'invalid_request'
1402
    assert 'reached rate limitation' in response.json['error_description']
1445
    assert response.json['error_description'] == 'Rate limit exceeded for IP address "127.0.0.1"'
1403 1446

  
1404 1447

  
1405 1448
def test_credentials_grant_ratelimitation_valid_client(
......
1419 1462
    for i in range(int(oidc_settings.A2_IDP_OIDC_PASSWORD_GRANT_RATELIMIT.split('/')[0])):
1420 1463
        app.post(token_url, params=params)
1421 1464
    response = app.post(token_url, params=params, status=400)
1422
    assert response.json['error'] == 'invalid_request'
1423
    assert 'reached rate limitation' in response.json['error_description']
1465
    assert response.json['error'] == 'invalid_client'
1466
    assert response.json['error_description'] == 'Rate limit of 100/m exceeded for client "oidcclient"'
1424 1467

  
1425 1468

  
1426 1469
def test_credentials_grant_retrytimout(
......
1436 1479
        'client_secret': oidc_client.client_secret,
1437 1480
        'grant_type': 'password',
1438 1481
        'username': simple_user.username,
1439
        'password': u'SurelyNotTheRightPassword',
1482
        'password': 'SurelyNotTheRightPassword',
1440 1483
    }
1441 1484
    attempts = 0
1442 1485
    while attempts < 100:
......
1444 1487
        attempts += 1
1445 1488
        if attempts >= 10:
1446 1489
            assert response.json['error'] == 'invalid_request'
1447
            assert 'too many attempts with erroneous RO password' in response.json['error_description']
1490
            assert 'Too many attempts with erroneous RO password' in response.json['error_description']
1448 1491

  
1449 1492
    # freeze some time after backoff delay expiration
1450 1493
    freezer.move_to(datetime.timedelta(days=2))
......
1462 1505
        'client_secret': oidc_client.client_secret,
1463 1506
        'grant_type': 'password',
1464 1507
        'username': simple_user.username,
1465
        'password': u'SurelyNotTheRightPassword',
1508
        'password': 'SurelyNotTheRightPassword',
1466 1509
    }
1467 1510
    token_url = make_url('oidc-token')
1468 1511
    response = app.post(token_url, params=params, status=400)
......
1484 1527
    token_url = make_url('oidc-token')
1485 1528
    response = app.post(token_url, params=params, status=400)
1486 1529
    assert response.json['error'] == 'invalid_client'
1487
    assert response.json['error_description'] == 'client authentication failed'
1530
    assert response.json['error_description'] == 'Wrong client\'s secret'
1488 1531

  
1489 1532

  
1490 1533
def test_credentials_grant_unauthz_client(
......
1499 1542
    token_url = make_url('oidc-token')
1500 1543
    response = app.post(token_url, params=params, status=400)
1501 1544
    assert response.json['error'] == 'unauthorized_client'
1502
    assert 'client is not configured for resource owner'in response.json['error_description']
1545
    assert 'Client is not configured for resource owner' in response.json['error_description']
1503 1546

  
1504 1547

  
1505 1548
def test_credentials_grant_invalid_content_type(
......
1519 1562
        content_type='multipart/form-data',
1520 1563
        status=400)
1521 1564
    assert response.json['error'] == 'invalid_request'
1522
    assert 'wrong content type' in response.json['error_description']
1565
    assert 'Wrong content type' in response.json['error_description']
1523 1566

  
1524 1567

  
1525 1568
def test_credentials_grant_ou_selection_simple(
......
1535 1578
        'password': user_ou1.username,
1536 1579
    }
1537 1580
    token_url = make_url('oidc-token')
1538
    response = app.post(token_url, params=params)
1581
    response = app.post(token_url, params=params, status=200)
1539 1582

  
1540 1583
    params['username'] = user_ou2.username
1541 1584
    params['password'] = user_ou2.password
1542 1585
    response = app.post(token_url, params=params, status=400)
1586
    assert response.json['error'] == 'access_denied'
1587
    assert response.json['error_description'] == 'Invalid user credentials'
1543 1588

  
1544 1589

  
1545 1590
def test_credentials_grant_ou_selection_username_not_unique(
......
1560 1605
    }
1561 1606
    token_url = make_url('oidc-token')
1562 1607
    response = app.post(token_url, params=params)
1563
    assert OIDCAccessToken.objects.get(
1564
            uuid=response.json['access_token']).user == user_ou1
1608
    assert OIDCAccessToken.objects.get(uuid=response.json['access_token']).user == user_ou1
1565 1609

  
1566 1610
    params['ou_slug'] = ou2.slug
1567 1611
    response = app.post(token_url, params=params)
1568
    assert OIDCAccessToken.objects.get(
1569
            uuid=response.json['access_token']).user == admin_ou2
1612
    assert OIDCAccessToken.objects.get(uuid=response.json['access_token']).user == admin_ou2
1570 1613

  
1571 1614

  
1572 1615
def test_credentials_grant_ou_selection_username_not_unique_wrong_ou(
......
1589 1632
    params['username'] = admin_ou2.username
1590 1633
    params['password'] = admin_ou2.password
1591 1634
    response = app.post(token_url, params=params, status=400)
1635
    assert response.json['error'] == 'access_denied'
1636
    assert response.json['error_description'] == 'Invalid user credentials'
1592 1637

  
1593 1638

  
1594 1639
def test_credentials_grant_ou_selection_invalid_ou(
1595 1640
        app, oidc_client, admin, user_ou1, settings):
1641
    oidc_client.authorization_flow = OIDCClient.FLOW_RESOURCE_OWNER_CRED
1642
    oidc_client.save()
1596 1643
    params = {
1597 1644
        'client_id': oidc_client.client_id,
1598 1645
        'client_secret': oidc_client.client_secret,
......
1603 1650
    }
1604 1651
    token_url = make_url('oidc-token')
1605 1652
    response = app.post(token_url, params=params, status=400)
1653
    assert response.json['error'] == 'invalid_request'
1654
    assert response.json['error_description'] == 'Parameter "ou_slug" does not match an existing organizational unit'
1606 1655

  
1607 1656

  
1608 1657
def test_oidc_client_clean():
1609
-