0001-idp_oidc-make-access_token-validity-depends-on-expir.patch
src/authentic2_idp_oidc/migrations/0014_auto_20201126_1812.py | ||
---|---|---|
1 |
# Generated by Django 2.2.17 on 2020-11-26 17:12 |
|
2 | ||
3 |
from django.db import migrations, models |
|
4 | ||
5 | ||
6 |
class Migration(migrations.Migration): |
|
7 | ||
8 |
dependencies = [ |
|
9 |
('authentic2_idp_oidc', '0013_auto_20200630_1007'), |
|
10 |
] |
|
11 | ||
12 |
operations = [ |
|
13 |
migrations.AlterField( |
|
14 |
model_name='oidcaccesstoken', |
|
15 |
name='expired', |
|
16 |
field=models.DateTimeField(null=True, verbose_name='expire'), |
|
17 |
), |
|
18 |
] |
src/authentic2_idp_oidc/models.py | ||
---|---|---|
24 | 24 |
from django.utils.translation import ugettext_lazy as _ |
25 | 25 |
from django.conf import settings |
26 | 26 |
from django.utils import six |
27 |
from django.utils.functional import cached_property |
|
27 | 28 |
from django.utils.timezone import now |
28 | 29 |
from django.utils.six.moves.urllib import parse as urlparse |
29 | 30 |
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation |
... | ... | |
56 | 57 |
return u'\n'.join([url for url in data.split()]) |
57 | 58 | |
58 | 59 | |
60 |
def get_session(session_key): |
|
61 |
engine = import_module(settings.SESSION_ENGINE) |
|
62 |
session = engine.SessionStore(session_key=session_key) |
|
63 |
session.load() |
|
64 |
if session._session_key == session_key: |
|
65 |
return session |
|
66 |
return None |
|
67 | ||
68 | ||
59 | 69 |
class OIDCClient(Service): |
60 | 70 |
POLICY_UUID = 1 |
61 | 71 |
POLICY_PAIRWISE = 2 |
... | ... | |
332 | 342 | |
333 | 343 |
objects = managers.OIDCExpiredManager() |
334 | 344 | |
335 |
@property |
|
345 |
@cached_property
|
|
336 | 346 |
def session(self): |
337 |
if not hasattr(self, '_session'): |
|
338 |
engine = import_module(settings.SESSION_ENGINE) |
|
339 |
session = engine.SessionStore(session_key=self.session_key) |
|
340 |
session.load() |
|
341 |
if session._session_key == self.session_key: |
|
342 |
self._session = session |
|
343 |
return getattr(self, '_session', None) |
|
347 |
return self.session_key and get_session(self.session_key) |
|
344 | 348 | |
345 | 349 |
def scope_set(self): |
346 | 350 |
return utils.scope_set(self.scopes) |
... | ... | |
382 | 386 |
verbose_name=_('created'), |
383 | 387 |
auto_now_add=True) |
384 | 388 |
expired = models.DateTimeField( |
385 |
verbose_name=_('expire')) |
|
389 |
verbose_name=_('expire'), |
|
390 |
null=True) |
|
386 | 391 | |
387 | 392 |
objects = managers.OIDCExpiredManager() |
388 | 393 | |
389 | 394 |
def scope_set(self): |
390 | 395 |
return utils.scope_set(self.scopes) |
391 | 396 | |
392 |
@property |
|
397 |
@cached_property
|
|
393 | 398 |
def session(self): |
394 |
if not hasattr(self, '_session'): |
|
395 |
engine = import_module(settings.SESSION_ENGINE) |
|
396 |
session = engine.SessionStore(session_key=self.session_key) |
|
397 |
if session._session_key == self.session_key: |
|
398 |
self._session = session |
|
399 |
return getattr(self, '_session', None) |
|
399 |
return self.session_key and get_session(self.session_key) |
|
400 | 400 | |
401 | 401 |
def is_valid(self): |
402 |
return self.expired >= now() and self.session is not None |
|
402 |
return ((self.expired is None or self.expired >= now()) |
|
403 |
and (self.session_key == '' |
|
404 |
or get_session(self.session_key) is not None)) |
|
403 | 405 | |
404 | 406 |
def __repr__(self): |
405 | 407 |
return '<OIDCAccessToken uuid:%s client:%s user:%s expired:%s scopes:%s>' % ( |
src/authentic2_idp_oidc/views.py | ||
---|---|---|
101 | 101 |
return client.idtoken_duration or datetime.timedelta(seconds=app_settings.IDTOKEN_DURATION) |
102 | 102 | |
103 | 103 | |
104 |
def access_token_duration(client): |
|
105 |
return client.access_token_duration or datetime.timedelta(seconds=app_settings.IDTOKEN_DURATION) |
|
106 | ||
107 | ||
108 | 104 |
def allowed_scopes(client): |
109 | 105 |
return client.scope_set() or app_settings.SCOPES or ['openid', 'email', 'profile'] |
110 | 106 | |
... | ... | |
328 | 324 |
else: |
329 | 325 |
# FIXME: we should probably factorize this part with the token endpoint similar code |
330 | 326 |
need_access_token = 'token' in response_type.split() |
331 |
expires_in = access_token_duration(client) |
|
332 | 327 |
if need_access_token: |
328 |
if client.access_token_duration is None: |
|
329 |
expires_in = datetime.timedelta(request.session.get_expiry_age()) |
|
330 |
expired = None |
|
331 |
else: |
|
332 |
expires_in = client.access_token_duration |
|
333 |
expired = start + client.access_token_duration |
|
333 | 334 |
access_token = models.OIDCAccessToken.objects.create( |
334 | 335 |
client=client, |
335 | 336 |
user=request.user, |
336 | 337 |
scopes=u' '.join(scopes), |
337 | 338 |
session_key=request.session.session_key, |
338 |
expired=start + expires_in)
|
|
339 |
expired=expired)
|
|
339 | 340 |
acr = '0' |
340 | 341 |
if nonce is not None and last_auth.get('nonce') == nonce: |
341 | 342 |
acr = '1' |
... | ... | |
510 | 511 |
exponential_backoff.success(*backoff_keys) |
511 | 512 |
start = now() |
512 | 513 |
# make access_token |
513 |
expires_in = access_token_duration(client) |
|
514 |
if client.access_token_duration is None: |
|
515 |
expires_in = datetime.timedelta(seconds=app_settings.ACCESS_TOKEN_DURATION) |
|
516 |
else: |
|
517 |
expires_in = client.access_token_duration |
|
514 | 518 |
access_token = models.OIDCAccessToken.objects.create( |
515 | 519 |
client=client, |
516 | 520 |
user=user, |
... | ... | |
559 | 563 |
redirect_uri = request.POST.get('redirect_uri') |
560 | 564 |
if oidc_code.redirect_uri != redirect_uri: |
561 | 565 |
return invalid_request_response('invalid redirect_uri') |
562 |
expires_in = access_token_duration(client) |
|
566 |
if client.access_token_duration is None: |
|
567 |
expires_in = datetime.timedelta(oidc_code.session.get_expiry_age()) |
|
568 |
expired = None |
|
569 |
else: |
|
570 |
expires_in = client.access_token_duration |
|
571 |
expired = oidc_code.created + expires_in |
|
563 | 572 |
access_token = models.OIDCAccessToken.objects.create( |
564 | 573 |
client=client, |
565 | 574 |
user=oidc_code.user, |
566 | 575 |
scopes=oidc_code.scopes, |
567 | 576 |
session_key=oidc_code.session_key, |
568 |
expired=oidc_code.created + expires_in)
|
|
577 |
expired=expired)
|
|
569 | 578 |
start = now() |
570 | 579 |
acr = '0' |
571 | 580 |
if (oidc_code.nonce is not None |
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 |
from importlib import import_module |
|
20 |
import json |
|
20 | 21 | |
21 | 22 |
import pytest |
22 | 23 | |
... | ... | |
1689 | 1690 |
'button', {'class': 'authorized-oauth-services--revoke-button'})) == 1 |
1690 | 1691 |
assert OIDCAuthorization.objects.filter( |
1691 | 1692 |
client_ct=ContentType.objects.get_for_model(OU)).count() == 0 |
1693 | ||
1694 | ||
1695 |
@pytest.fixture |
|
1696 |
def session(settings, db): |
|
1697 |
engine = import_module(settings.SESSION_ENGINE) |
|
1698 |
session = engine.SessionStore() |
|
1699 |
session.create() |
|
1700 |
return session |
|
1701 | ||
1702 | ||
1703 |
def test_access_token_is_valid_session(simple_oidc_client, simple_user, session): |
|
1704 |
token = OIDCAccessToken.objects.create( |
|
1705 |
client=simple_oidc_client, |
|
1706 |
user=simple_user, |
|
1707 |
scopes='openid', |
|
1708 |
session_key=session.session_key) |
|
1709 | ||
1710 |
assert token.is_valid() |
|
1711 |
session.flush() |
|
1712 |
assert not token.is_valid() |
|
1713 | ||
1714 | ||
1715 |
def test_access_token_is_valid_expired(simple_oidc_client, simple_user, freezer): |
|
1716 |
start = now() |
|
1717 |
expired = start + datetime.timedelta(seconds=30) |
|
1718 | ||
1719 |
token = OIDCAccessToken.objects.create( |
|
1720 |
client=simple_oidc_client, |
|
1721 |
user=simple_user, |
|
1722 |
scopes='openid', |
|
1723 |
expired=expired) |
|
1724 | ||
1725 |
assert token.is_valid() |
|
1726 |
freezer.move_to(expired) |
|
1727 |
assert token.is_valid() |
|
1728 |
freezer.move_to(expired + datetime.timedelta(seconds=1)) |
|
1729 |
assert not token.is_valid() |
|
1730 | ||
1731 | ||
1732 |
def test_access_token_is_valid_session_and_expired(simple_oidc_client, |
|
1733 |
simple_user, |
|
1734 |
session, freezer): |
|
1735 |
start = now() |
|
1736 |
expired = start + datetime.timedelta(seconds=30) |
|
1737 | ||
1738 |
token = OIDCAccessToken.objects.create( |
|
1739 |
client=simple_oidc_client, |
|
1740 |
user=simple_user, |
|
1741 |
scopes='openid', |
|
1742 |
session_key=session.session_key, |
|
1743 |
expired=expired) |
|
1744 | ||
1745 |
assert token.is_valid() |
|
1746 |
freezer.move_to(expired) |
|
1747 |
assert token.is_valid() |
|
1748 |
freezer.move_to(expired + datetime.timedelta(seconds=1)) |
|
1749 |
assert not token.is_valid() |
|
1750 |
freezer.move_to(start) |
|
1751 |
assert token.is_valid() |
|
1752 |
session.flush() |
|
1753 |
assert not token.is_valid() |
|
1692 |
- |