Projet

Général

Profil

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

Benjamin Dauvergne, 03 décembre 2020 23:13

Télécharger (8,28 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             | 11 ++--
 src/authentic2_idp_oidc/views.py              | 27 +++++---
 tests/test_idp_oidc.py                        | 63 +++++++++++++++++++
 4 files changed, 106 insertions(+), 13 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
......
407 408
        verbose_name=_('created'),
408 409
        auto_now_add=True)
409 410
    expired = models.DateTimeField(
410
        verbose_name=_('expire'))
411
        verbose_name=_('expire'),
412
        null=True)
411 413

  
412 414
    objects = managers.OIDCExpiredManager()
413 415

  
......
415 417
        return utils.scope_set(self.scopes)
416 418

  
417 419
    def is_valid(self):
418
        if self.expired < now():
420
        if self.expired is not None and self.expired < now():
419 421
            return False
420 422
        if not self.session_key:
421 423
            return True
422
        if not self.session:
424
        session = get_session(self.session_key)
425
        if session is None:
423 426
            return False
424
        if self.session.get('_auth_user_id') != str(self.user_id):
427
        if session.get('_auth_user_id') != str(self.user_id):
425 428
            return False
426 429
        return True
427 430

  
src/authentic2_idp_oidc/views.py
172 172
    return client.idtoken_duration or datetime.timedelta(seconds=app_settings.IDTOKEN_DURATION)
173 173

  
174 174

  
175
def access_token_duration(client):
176
    return client.access_token_duration or datetime.timedelta(seconds=app_settings.IDTOKEN_DURATION)
177

  
178

  
179 175
def allowed_scopes(client):
180 176
    return client.scope_set() or app_settings.SCOPES or ['openid', 'email', 'profile']
181 177

  
......
405 401
        response = redirect(request, redirect_uri, params=params, resolve=False)
406 402
    else:
407 403
        need_access_token = 'token' in response_type.split()
408
        expires_in = access_token_duration(client)
409 404
        if need_access_token:
405
            if client.access_token_duration is None:
406
                expires_in = datetime.timedelta(request.session.get_expiry_age())
407
                expired = None
408
            else:
409
                expires_in = client.access_token_duration
410
                expired = iat + client.access_token_duration
410 411
            access_token = models.OIDCAccessToken.objects.create(
411 412
                client=client,
412 413
                user=request.user,
413 414
                scopes=u' '.join(scopes),
414 415
                session_key=request.session.session_key,
415
                expired=iat + expires_in)
416
                expired=expired)
416 417
        acr = '0'
417 418
        if nonce is not None and last_auth.get('nonce') == nonce:
418 419
            acr = '1'
......
601 602
    exponential_backoff.success(*backoff_keys)
602 603
    iat = now()  # iat = issued at
603 604
    # make access_token
604
    expires_in = access_token_duration(client)
605
    if client.access_token_duration is None:
606
        expires_in = datetime.timedelta(seconds=app_settings.ACCESS_TOKEN_DURATION)
607
    else:
608
        expires_in = client.access_token_duration
605 609
    access_token = models.OIDCAccessToken.objects.create(
606 610
        client=client,
607 611
        user=user,
......
648 652
    redirect_uri = request.POST.get('redirect_uri')
649 653
    if oidc_code.redirect_uri != redirect_uri:
650 654
        raise InvalidRequest(_('Parameter "redirect_uri" does not match the code.'), client=client)
651
    expires_in = access_token_duration(client)
655
    if client.access_token_duration is None:
656
        expires_in = datetime.timedelta(oidc_code.session.get_expiry_age())
657
        expired = None
658
    else:
659
        expires_in = client.access_token_duration
660
        expired = oidc_code.created + expires_in
652 661
    access_token = models.OIDCAccessToken.objects.create(
653 662
        client=client,
654 663
        user=oidc_code.user,
655 664
        scopes=oidc_code.scopes,
656 665
        session_key=oidc_code.session_key,
657
        expired=oidc_code.created + expires_in)
666
        expired=expired)
658 667
    start = now()
659 668
    acr = '0'
660 669
    if (oidc_code.nonce is not None
tests/test_idp_oidc.py
17 17
import base64
18 18
import datetime
19 19
import functools
20
from importlib import import_module
20 21
import json
21 22

  
22 23
import pytest
......
1857 1858
        response['WWW-Authenticate']
1858 1859
        == 'Bearer error="invalid_token", error_description="Token is expired or user is disconnected"'
1859 1860
    )
1861

  
1862

  
1863
@pytest.fixture
1864
def session(settings, db, simple_user):
1865
    engine = import_module(settings.SESSION_ENGINE)
1866
    session = engine.SessionStore()
1867
    session['_auth_user_id'] = str(simple_user.id)
1868
    session.create()
1869
    return session
1870

  
1871

  
1872
def test_access_token_is_valid_session(simple_oidc_client, simple_user, session):
1873
    token = OIDCAccessToken.objects.create(
1874
        client=simple_oidc_client,
1875
        user=simple_user,
1876
        scopes='openid',
1877
        session_key=session.session_key)
1878

  
1879
    assert token.is_valid()
1880
    session.flush()
1881
    assert not token.is_valid()
1882

  
1883

  
1884
def test_access_token_is_valid_expired(simple_oidc_client, simple_user, freezer):
1885
    start = now()
1886
    expired = start + datetime.timedelta(seconds=30)
1887

  
1888
    token = OIDCAccessToken.objects.create(
1889
        client=simple_oidc_client,
1890
        user=simple_user,
1891
        scopes='openid',
1892
        expired=expired)
1893

  
1894
    assert token.is_valid()
1895
    freezer.move_to(expired)
1896
    assert token.is_valid()
1897
    freezer.move_to(expired + datetime.timedelta(seconds=1))
1898
    assert not token.is_valid()
1899

  
1900

  
1901
def test_access_token_is_valid_session_and_expired(simple_oidc_client,
1902
                                                   simple_user,
1903
                                                   session, freezer):
1904
    start = now()
1905
    expired = start + datetime.timedelta(seconds=30)
1906

  
1907
    token = OIDCAccessToken.objects.create(
1908
        client=simple_oidc_client,
1909
        user=simple_user,
1910
        scopes='openid',
1911
        session_key=session.session_key,
1912
        expired=expired)
1913

  
1914
    assert token.is_valid()
1915
    freezer.move_to(expired)
1916
    assert token.is_valid()
1917
    freezer.move_to(expired + datetime.timedelta(seconds=1))
1918
    assert not token.is_valid()
1919
    freezer.move_to(start)
1920
    assert token.is_valid()
1921
    session.flush()
1922
    assert not token.is_valid()
1860
-