Projet

Général

Profil

0001-idp_oidc-make-access_token-validity-depends-on-expir.patch

Benjamin Dauvergne, 27 novembre 2020 08:12

Télécharger (9,61 ko)

Voir les différences:

Subject: [PATCH] idp_oidc: make access_token validity depends on expiration or
 session existence (#48889)

 .../migrations/0014_auto_20201126_1812.py     | 18 ++++++
 src/authentic2_idp_oidc/models.py             | 36 ++++++-----
 src/authentic2_idp_oidc/views.py              | 27 +++++---
 tests/test_idp_oidc.py                        | 64 ++++++++++++++++++-
 4 files changed, 118 insertions(+), 27 deletions(-)
 create mode 100644 src/authentic2_idp_oidc/migrations/0014_auto_20201126_1812.py
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
-