0001-idp_oidc-improve-error-reporting-in-token-endpoint-4.patch
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 |
- |