From b973dabbdc1f7873337493066be21a7ac5bb98b5 Mon Sep 17 00:00:00 2001
From: rglauco <37829079+rglauco@users.noreply.github.com>
Date: Thu, 20 Jul 2023 16:50:01 +0200
Subject: [PATCH 02/15] feat: refresh tokens renewal based on user consent
timeframe validity with new settingslocal var
OIDCFED_PROVIDER_MAX_CONSENT_TIMEFRAME #seconds
---
.../provider/settingslocal.py.example | 2 +-
.../provider/views/token_endpoint.py | 102 ++++++++++--------
spid_cie_oidc/relying_party/views/__init__.py | 14 ++-
.../relying_party/views/rp_extend_session.py | 4 +-
.../views/rp_initiated_logout.py | 4 +-
.../relying_party/views/rp_introspection.py | 4 +-
6 files changed, 75 insertions(+), 55 deletions(-)
diff --git a/examples/provider/provider/settingslocal.py.example b/examples/provider/provider/settingslocal.py.example
index d7a780d0..e4f446e1 100644
--- a/examples/provider/provider/settingslocal.py.example
+++ b/examples/provider/provider/settingslocal.py.example
@@ -19,7 +19,7 @@ ADMIN_PATH = 'admin/'
OIDCFED_DEFAULT_TRUST_ANCHOR = "http://127.0.0.1:8000"
OIDCFED_TRUST_ANCHORS = [OIDCFED_DEFAULT_TRUST_ANCHOR]
OIDCFED_PROVIDER_PROFILE = "cie"
-
+ OIDCFED_PROVIDER_MAX_CONSENT_TIMEFRAME = 3600 #seconds
OIDCFED_REQUIRED_TRUST_MARKS = []
#OIDCFED_FEDERATION_TRUST_MARKS_PROFILES = {
diff --git a/spid_cie_oidc/provider/views/token_endpoint.py b/spid_cie_oidc/provider/views/token_endpoint.py
index 9fe0a13b..6a878c7d 100644
--- a/spid_cie_oidc/provider/views/token_endpoint.py
+++ b/spid_cie_oidc/provider/views/token_endpoint.py
@@ -1,6 +1,6 @@
-
import base64
import hashlib
+import json
import logging
from djagger.decorators import schema
@@ -23,30 +23,31 @@
OIDCFED_PROVIDER_PROFILES
)
+from spid_cie_oidc.entity.utils import datetime_from_timestamp, exp_from_now, iat_now
from . import OpBase
-logger = logging.getLogger(__name__)
+logger = logging.getLogger(__name__)
schema_profile = OIDCFED_PROVIDER_PROFILES[OIDCFED_DEFAULT_PROVIDER_PROFILE]
@schema(
- methods=['GET','POST'],
- post_request_schema = {
+ methods=['GET', 'POST'],
+ post_request_schema={
"authn_request": schema_profile["authorization_code"],
"refresh_request": schema_profile["refresh_token"],
},
- post_response_schema = {
- "200": schema_profile["authorization_code_response"],
- # TODO
- # "200": schema_profile["refresh_token_response"],
- "400": schema_profile["token_error_response"],
+ post_response_schema={
+ "200": schema_profile["authorization_code_response"],
+ # TODO
+ # "200": schema_profile["refresh_token_response"],
+ "400": schema_profile["token_error_response"],
},
- get_response_schema = {
- "400": BaseModel
+ get_response_schema={
+ "400": BaseModel
},
- tags = ['Provider']
+ tags=['Provider']
)
@method_decorator(csrf_exempt, name="dispatch")
class TokenEndpoint(OpBase, View):
@@ -77,20 +78,20 @@ def grant_auth_code(self, request, *args, **kwargs):
#
issued_token = IssuedToken.objects.filter(
- session= self.authz,
- revoked = False
+ session=self.authz,
+ revoked=False
).first()
jwk_at = unpad_jwt_payload(issued_token.access_token)
expires_in = self.get_expires_in(jwk_at['iat'], jwk_at['exp'])
- iss_token_data = dict( # nosec B106
- access_token = issued_token.access_token,
- id_token = issued_token.id_token,
- token_type = "Bearer", # nosec B106
- expires_in = expires_in,
+ iss_token_data = dict( # nosec B106
+ access_token=issued_token.access_token,
+ id_token=issued_token.id_token,
+ token_type="Bearer", # nosec B106
+ expires_in=expires_in,
# TODO: remove unsupported scope
- scope = self.authz.authz_request["scope"],
+ scope=self.authz.authz_request["scope"],
)
if issued_token.refresh_token:
iss_token_data['refresh_token'] = issued_token.refresh_token
@@ -98,14 +99,25 @@ def grant_auth_code(self, request, *args, **kwargs):
def is_token_renewable(self, session) -> bool:
issuedToken = IssuedToken.objects.filter(
- session = session
- )
- # TODO: check also ACR
- return (
- (issuedToken.count() - 1) < getattr(
- settings, "OIDCFED_PROVIDER_MAX_REFRESH", 1
- )
- )
+ session=session
+ ).first()
+
+ id_token = unpad_jwt_payload(issuedToken.id_token)
+
+ consent_expiration = id_token['iat'] + getattr(settings, "OIDCFED_PROVIDER_MAX_CONSENT_TIMEFRAME")
+
+ delta = consent_expiration - iat_now()
+
+ if delta > 0:
+ return True
+ return False
+
+ # # TODO: check also ACR
+ # return (
+ # (issuedToken.count() - 1) < getattr(
+ # settings, "OIDCFED_PROVIDER_MAX_REFRESH", 1
+ # )
+ # )
def grant_refresh_token(self, request, *args, **kwargs):
"""
@@ -119,8 +131,8 @@ def grant_refresh_token(self, request, *args, **kwargs):
# 2. create a new instance of issuedtoken linked to the same sessions and revoke the older
# 3. response with a new refresh, access and id_token
issued_token = IssuedToken.objects.filter(
- refresh_token = request.POST['refresh_token'],
- revoked = False
+ refresh_token=request.POST['refresh_token'],
+ revoked=False
).first()
if not issued_token:
@@ -130,17 +142,17 @@ def grant_refresh_token(self, request, *args, **kwargs):
"error_description": "Refresh token not found",
},
- status = 400
+ status=400
)
session = issued_token.session
- if not self.is_token_renewable(session): # pragma: no cover
+ if not self.is_token_renewable(session): # pragma: no cover
return JsonResponse(
- {
- "error": "invalid_request",
- "error_description": "Refresh Token can no longer be updated",
+ {
+ "error": "invalid_request",
+ "error_description": "Refresh Token can no longer be updated",
- }, status = 400
+ }, status=400
)
iss_token_data = self.get_iss_token_data(session, self.get_issuer())
IssuedToken.objects.create(**iss_token_data)
@@ -150,12 +162,12 @@ def grant_refresh_token(self, request, *args, **kwargs):
jwk_at = unpad_jwt_payload(iss_token_data['access_token'])
expires_in = self.get_expires_in(jwk_at['iat'], jwk_at['exp'])
- data = dict( # nosec B106
- access_token = iss_token_data['access_token'],
- id_token = iss_token_data['id_token'],
- refresh_token = iss_token_data['refresh_token'],
- token_type = "Bearer", # nosec B106
- expires_in = expires_in,
+ data = dict( # nosec B106
+ access_token=iss_token_data['access_token'],
+ id_token=iss_token_data['id_token'],
+ refresh_token=iss_token_data['refresh_token'],
+ token_type="Bearer", # nosec B106
+ expires_in=expires_in,
)
return JsonResponse(data)
@@ -174,7 +186,7 @@ def post(self, request, *args, **kwargs):
"error": "invalid_request",
"error_description": "Token request object validation failed",
},
- status = 400
+ status=400
)
self.commons = self.get_jwt_common_data()
@@ -186,7 +198,7 @@ def post(self, request, *args, **kwargs):
request.POST['client_id'],
request.POST['client_assertion']
)
- except Exception as e: # pragma: no cover
+ except Exception as e: # pragma: no cover
logger.warning(
"Client authentication failed for "
f"{request.POST.get('client_id', 'unknown')}: {e}"
@@ -197,7 +209,7 @@ def post(self, request, *args, **kwargs):
'error': "unauthorized_client",
'error_description': ""
- }, status = 403
+ }, status=403
)
if request.POST.get("grant_type") == 'authorization_code':
diff --git a/spid_cie_oidc/relying_party/views/__init__.py b/spid_cie_oidc/relying_party/views/__init__.py
index 563b1d7c..c65836c8 100644
--- a/spid_cie_oidc/relying_party/views/__init__.py
+++ b/spid_cie_oidc/relying_party/views/__init__.py
@@ -9,6 +9,8 @@
from ..oidc import *
from ..oauth2 import *
+from enum import Enum
+
from spid_cie_oidc.entity.exceptions import InvalidTrustchain
from spid_cie_oidc.entity.models import TrustChain
from spid_cie_oidc.entity.trust_chain_operations import get_or_create_trust_chain
@@ -21,6 +23,12 @@
logger = logging.getLogger(__name__)
+class TokenRequestType(str, Enum):
+ refresh = "refresh"
+ revocation = "revocation"
+ introspection = "introspection"
+
+
class SpidCieOidcRp:
"""
Baseclass with common methods for RPs
@@ -110,16 +118,16 @@ def get_token_request(self, auth_token, request, token_type):
client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
)
- if token_type == 'refresh': # nosec - B105
+ if token_type == TokenRequestType.refresh: #'refresh': # nosec - B105
token_request_data["grant_type"] = "refresh_token"
token_request_data["refresh_token"] = auth_token.refresh_token
audience = authz.provider_configuration["token_endpoint"]
- elif token_type == 'revocation': # nosec - B105
+ elif token_type == TokenRequestType.revocation: #'revocation': # nosec - B105
token_request_data["token"] = auth_token.access_token
audience = authz.provider_configuration["revocation_endpoint"]
- elif token_type == 'introspection': # nosec - B105
+ elif token_type == TokenRequestType.introspection: #'introspection': # nosec - B105
token_request_data["token"] = auth_token.access_token
audience = authz.provider_configuration["introspection_endpoint"]
diff --git a/spid_cie_oidc/relying_party/views/rp_extend_session.py b/spid_cie_oidc/relying_party/views/rp_extend_session.py
index f7d11ab4..f1e97b96 100644
--- a/spid_cie_oidc/relying_party/views/rp_extend_session.py
+++ b/spid_cie_oidc/relying_party/views/rp_extend_session.py
@@ -8,7 +8,7 @@
from ..oauth2 import *
from ..oidc import *
-from . import SpidCieOidcRp
+from . import SpidCieOidcRp, TokenRequestType
from django.views import View
from spid_cie_oidc.entity.jwtse import (
@@ -49,7 +49,7 @@ def get(self, request, *args, **kwargs):
auth_token = auth_tokens.last()
try:
- token_response = self.get_token_request(auth_token, request, "refresh")
+ token_response = self.get_token_request(auth_token, request, TokenRequestType.refresh) #"refresh")
if token_response.status_code == 400:
return HttpResponseRedirect(reverse("spid_cie_rp_landing"))
diff --git a/spid_cie_oidc/relying_party/views/rp_initiated_logout.py b/spid_cie_oidc/relying_party/views/rp_initiated_logout.py
index 0f51973d..80fe6239 100644
--- a/spid_cie_oidc/relying_party/views/rp_initiated_logout.py
+++ b/spid_cie_oidc/relying_party/views/rp_initiated_logout.py
@@ -9,7 +9,7 @@
from django.utils import timezone
from ..models import OidcAuthenticationToken
-from . import SpidCieOidcRp
+from . import SpidCieOidcRp, TokenRequestType
from django.views import View
logger = logging.getLogger(__name__)
@@ -47,7 +47,7 @@ def get(self, request):
logout(request)
try:
- self.get_token_request(auth_token, request, "revocation")
+ self.get_token_request(auth_token, request, TokenRequestType.revocation) #"revocation")
auth_token.logged_out = timezone.localtime()
auth_token.save()
except Exception as e: # pragma: no cover
diff --git a/spid_cie_oidc/relying_party/views/rp_introspection.py b/spid_cie_oidc/relying_party/views/rp_introspection.py
index b8842eeb..dbc0387a 100644
--- a/spid_cie_oidc/relying_party/views/rp_introspection.py
+++ b/spid_cie_oidc/relying_party/views/rp_introspection.py
@@ -7,7 +7,7 @@
from ..oauth2 import *
from ..oidc import *
-from . import SpidCieOidcRp
+from . import SpidCieOidcRp, TokenRequestType
from django.views import View
@@ -48,7 +48,7 @@ def get(self, request, *args, **kwargs):
auth_token = auth_tokens.last()
try:
- token_response = self.get_token_request(auth_token, request, "introspection")
+ token_response = self.get_token_request(auth_token, request, TokenRequestType.introspection) # "introspection")
introspection_token_response = json.loads(token_response.content.decode())
data = {"introspection": introspection_token_response}
return render(request, self.template, data)
From a8d8b57c203d196220c5723a62f85732f63f90d1 Mon Sep 17 00:00:00 2001
From: rglauco <37829079+rglauco@users.noreply.github.com>
Date: Thu, 20 Jul 2023 18:45:11 +0200
Subject: [PATCH 03/15] fix: refresh token tests with more complete tokenset
and test RT emission outside OIDCFED_PROVIDER_MAX_CONSENT_TIMEFRAME
---
.../provider/tests/test_03_refresh_token.py | 33 +++++++++++++++++--
1 file changed, 31 insertions(+), 2 deletions(-)
diff --git a/spid_cie_oidc/provider/tests/test_03_refresh_token.py b/spid_cie_oidc/provider/tests/test_03_refresh_token.py
index 568cc761..ca9e6d78 100644
--- a/spid_cie_oidc/provider/tests/test_03_refresh_token.py
+++ b/spid_cie_oidc/provider/tests/test_03_refresh_token.py
@@ -1,5 +1,6 @@
from copy import deepcopy
-
+import time
+from cryptojwt.jws.utils import left_hash
from django.test import Client, TestCase, override_settings
from django.urls import reverse
from django.utils import timezone
@@ -66,6 +67,31 @@ def setUp(self):
"scope": "openid",
}
self.rt_jws = create_jws(refresh_token, op_conf_priv_jwk)
+ id_token = {
+ 'sub': '2ed008b45e66ce53e48273dca5a4463bc8ebd036ebaa824f4582627683c2451b',
+ 'nonce': 'ljbvL3rpscgS4ZGda7cgibXHr7vrNREW',
+ 'at_hash': '',
+ 'c_hash': 'tij0h-zL_bSrsVXy-d3qHw',
+ 'aud': [rp_conf["metadata"]["openid_relying_party"]["client_id"],],
+ 'iss': op_conf["sub"],
+ 'jti': '402e61bd-950c-4934-83d4-c09a05468828',
+ 'exp': exp_from_now(),
+ 'iat': iat_now()
+ }
+ access_token = {
+ 'iss': op_conf["sub"],
+ 'sub': '2ed008b45e66ce53e48273dca5a4463bc8ebd036ebaa824f4582627683c2451b',
+ 'aud': [rp_conf["metadata"]["openid_relying_party"]["client_id"],],
+ 'client_id': rp_conf["metadata"]["openid_relying_party"]["client_id"],
+ 'scope': 'openid',
+ 'jti': '402e61bd-950c-4934-83d4-c09a05468828',
+ 'exp': exp_from_now(),
+ 'iat': iat_now()
+ }
+ self.jwt_at = create_jws(access_token, op_conf_priv_jwk, typ="at+jwt")
+ id_token['at_hash'] = left_hash(self.jwt_at, "HS256")
+ self.jwt_id = create_jws(id_token, op_conf_priv_jwk)
+
session = OidcSession.objects.create(
user=User.objects.create(username="username"),
user_uid="",
@@ -77,6 +103,8 @@ def setUp(self):
)
IssuedToken.objects.create(
refresh_token=self.rt_jws,
+ id_token=self.jwt_id,
+ access_token=self.jwt_at,
session=session,
expires=timezone.localtime()
)
@@ -97,7 +125,7 @@ def test_grant_refresh_token(self):
self.assertEqual(refresh_token["aud"], RP_CLIENT_ID)
self.assertEqual(refresh_token["iss"], self.op_local_conf["sub"])
- @override_settings(OIDCFED_PROVIDER_MAX_REFRESH=1)
+ @override_settings(OIDCFED_PROVIDER_MAX_CONSENT_TIMEFRAME=1)
def test_grant_refresh_token_two_times(self):
client = Client()
url = reverse("oidc_provider_token_endpoint")
@@ -117,5 +145,6 @@ def test_grant_refresh_token_two_times(self):
refresh_token=res.json()["refresh_token"],
grant_type="refresh_token"
)
+ time.sleep(1)
res = client.post(url, request)
self.assertTrue(res.status_code == 400)
From 9dfc4dd4c22fb5a96e6f54cd5775d8787dec032e Mon Sep 17 00:00:00 2001
From: rglauco <37829079+rglauco@users.noreply.github.com>
Date: Fri, 21 Jul 2023 07:26:55 +0200
Subject: [PATCH 04/15] chore: changed RT renewal method description in webpage
---
spid_cie_oidc/relying_party/templates/echo_attributes.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/spid_cie_oidc/relying_party/templates/echo_attributes.html b/spid_cie_oidc/relying_party/templates/echo_attributes.html
index e6e296dd..0a1aa693 100644
--- a/spid_cie_oidc/relying_party/templates/echo_attributes.html
+++ b/spid_cie_oidc/relying_party/templates/echo_attributes.html
@@ -62,7 +62,7 @@
{% endif %}
From cb71dbf924c5343760412022ac6174ef4a85cec6 Mon Sep 17 00:00:00 2001
From: rglauco <37829079+rglauco@users.noreply.github.com>
Date: Fri, 21 Jul 2023 07:26:55 +0200
Subject: [PATCH 05/15] chore: changed RT renewal method description in webpage
---
spid_cie_oidc/relying_party/templates/echo_attributes.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/spid_cie_oidc/relying_party/templates/echo_attributes.html b/spid_cie_oidc/relying_party/templates/echo_attributes.html
index e6e296dd..ed0d00a2 100644
--- a/spid_cie_oidc/relying_party/templates/echo_attributes.html
+++ b/spid_cie_oidc/relying_party/templates/echo_attributes.html
@@ -62,7 +62,7 @@