Skip to content
This repository was archived by the owner on Jun 23, 2023. It is now read-only.

Commit d9157c1

Browse files
authored
Merge pull request #106 from IdentityPython/token_classes
To fix token backward compatibility.
2 parents afa043e + 3209ab0 commit d9157c1

3 files changed

Lines changed: 135 additions & 15 deletions

File tree

src/oidcop/token/__init__.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@
1515

1616
logger = logging.getLogger(__name__)
1717

18+
ALT_TOKEN_NAME = {
19+
"authorization_code": "A",
20+
"access_token": "T",
21+
"refresh_token": "R",
22+
"id_token": "I"
23+
}
24+
1825

1926
def is_expired(exp, when=0):
2027
if exp < 0:
@@ -28,6 +35,11 @@ def is_expired(exp, when=0):
2835
class Token(object):
2936
def __init__(self, token_class, lifetime=300, **kwargs):
3037
self.token_class = token_class
38+
try:
39+
self.alt_token_name = ALT_TOKEN_NAME[token_class]
40+
except KeyError:
41+
self.alt_token_name = ""
42+
3143
self.lifetime = lifetime
3244
self.kwargs = kwargs
3345

@@ -70,7 +82,8 @@ def __init__(self, password, token_class="", token_type="Bearer", **kwargs):
7082
self.crypt = Crypt(password)
7183
self.token_type = token_type
7284

73-
def __call__(self, session_id: Optional[str] = "", token_class: Optional[str] = "", **payload) -> str:
85+
def __call__(self, session_id: Optional[str] = "", token_class: Optional[str] = "",
86+
**payload) -> str:
7487
"""
7588
Return a token.
7689
@@ -112,9 +125,10 @@ def info(self, token: str) -> dict:
112125
:return: dictionary with info about the token
113126
"""
114127
_res = dict(zip(["_id", "token_class", "sid", "exp"], self.split_token(token)))
115-
if _res["token_class"] != self.token_class:
128+
if _res["token_class"] not in [self.token_class, self.alt_token_name]:
116129
raise WrongTokenClass(_res["token_class"])
117130
else:
131+
_res["token_class"] = self.token_class
118132
_res["handler"] = self
119133
return _res
120134

src/oidcop/token/jwt_token.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66

77
from oidcop.exception import ToOld
88
from oidcop.token import Crypt
9+
from oidcop.token.exception import WrongTokenClass
10+
911
from . import Token
1012
from . import is_expired
1113
from .exception import UnknownToken
1214

13-
1415
# TYPE_MAP = {"A": "code", "T": "access_token", "R": "refresh_token"}
1516

1617

@@ -58,10 +59,11 @@ def __call__(self, session_id: Optional[str] = "", token_class: Optional[str] =
5859
:param payload: A dictionary with information that is part of the payload of the JWT.
5960
:return: Signed JSON Web Token
6061
"""
61-
if not token_class and self.token_class:
62-
token_class = self.token_class
63-
else:
64-
token_class = "authorization_code"
62+
if not token_class:
63+
if self.token_class:
64+
token_class = self.token_class
65+
else:
66+
token_class = "authorization_code"
6567

6668
payload.update({"sid": session_id, "token_class": token_class})
6769
payload = self.load_custom_claims(payload)
@@ -86,14 +88,22 @@ def get_payload(self, token):
8688

8789
def info(self, token):
8890
"""
89-
Return type of Token (A=Access code, T=Token, R=Refresh token) and
90-
the session id.
91+
Return token information
9192
9293
:param token: A token
93-
:return: tuple of token type and session id
94+
:return: dictionary with token information
9495
"""
9596
_payload = self.get_payload(token)
9697

98+
_class = _payload.get("ttype")
99+
if _class is None:
100+
_class = _payload.get("token_class")
101+
102+
if _class not in [self.token_class, self.alt_token_name]:
103+
raise WrongTokenClass(_payload["token_class"])
104+
else:
105+
_payload["token_class"] = self.token_class
106+
97107
if is_expired(_payload["exp"]):
98108
raise ToOld("Token has expired")
99109
# All the token metadata

tests/test_35_oidc_token_endpoint.py

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import base64
12
import json
23
import os
34

4-
from oidcop.configure import OPConfiguration
55
import pytest
66
from cryptojwt import JWT
77
from cryptojwt.key_jar import build_keyjar
@@ -15,6 +15,7 @@
1515
from oidcop.authn_event import create_authn_event
1616
from oidcop.authz import AuthzHandling
1717
from oidcop.client_authn import verify_client
18+
from oidcop.configure import OPConfiguration
1819
from oidcop.cookie_handler import CookieHandler
1920
from oidcop.exception import UnAuthorizedClient
2021
from oidcop.oidc import userinfo
@@ -26,6 +27,7 @@
2627
from oidcop.session import MintingNotAllowed
2728
from oidcop.user_authn.authn_context import INTERNETPROTOCOLPASSWORD
2829
from oidcop.user_info import UserInfo
30+
from oidcop.util import lv_pack
2931

3032
KEYDEFS = [
3133
{"type": "RSA", "key": "", "use": ["sig"]},
@@ -113,7 +115,7 @@ def conf():
113115
},
114116
"refresh": {
115117
"class": "oidcop.token.jwt_token.JWTToken",
116-
"kwargs": {"lifetime": 3600, "aud": ["https://example.org/appl"],},
118+
"kwargs": {"lifetime": 3600, "aud": ["https://example.org/appl"], },
117119
},
118120
"id_token": {"class": "oidcop.token.id_token.IDToken", "kwargs": {}},
119121
},
@@ -127,8 +129,8 @@ def conf():
127129
"class": ProviderConfiguration,
128130
"kwargs": {},
129131
},
130-
"registration": {"path": "registration", "class": Registration, "kwargs": {},},
131-
"authorization": {"path": "authorization", "class": Authorization, "kwargs": {},},
132+
"registration": {"path": "registration", "class": Registration, "kwargs": {}, },
133+
"authorization": {"path": "authorization", "class": Authorization, "kwargs": {}, },
132134
"token": {
133135
"path": "token",
134136
"class": Token,
@@ -164,7 +166,7 @@ def conf():
164166
"usage_rules": {
165167
"authorization_code": {
166168
"expires_in": 300,
167-
"supports_minting": ["access_token", "refresh_token", "id_token",],
169+
"supports_minting": ["access_token", "refresh_token", "id_token", ],
168170
"max_usage": 1,
169171
},
170172
"access_token": {"expires_in": 600},
@@ -741,3 +743,97 @@ def test_configure_grant_types(self):
741743
assert len(self.token_endpoint.helper) == 1
742744
assert "access_token" in self.token_endpoint.helper
743745
assert "refresh_token" not in self.token_endpoint.helper
746+
747+
748+
class TestOldTokens(object):
749+
@pytest.fixture(autouse=True)
750+
def create_endpoint(self, conf):
751+
server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR)
752+
753+
endpoint_context = server.endpoint_context
754+
endpoint_context.cdb["client_1"] = {
755+
"client_secret": "hemligt",
756+
"redirect_uris": [("https://example.com/cb", None)],
757+
"client_salt": "salted",
758+
"endpoint_auth_method": "client_secret_post",
759+
"response_types": ["code", "token", "code id_token", "id_token"],
760+
}
761+
endpoint_context.keyjar.import_jwks(CLIENT_KEYJAR.export_jwks(), "client_1")
762+
self.session_manager = endpoint_context.session_manager
763+
self.token_endpoint = server.server_get("endpoint", "token")
764+
self.user_id = "diana"
765+
self.endpoint_context = endpoint_context
766+
767+
def _create_session(self, auth_req, sub_type="public", sector_identifier=""):
768+
if sector_identifier:
769+
authz_req = auth_req.copy()
770+
authz_req["sector_identifier_uri"] = sector_identifier
771+
else:
772+
authz_req = auth_req
773+
client_id = authz_req["client_id"]
774+
ae = create_authn_event(self.user_id)
775+
return self.session_manager.create_session(
776+
ae, authz_req, self.user_id, client_id=client_id, sub_type=sub_type
777+
)
778+
779+
def _mint_code(self, grant, client_id):
780+
session_id = self.session_manager.encrypted_session_id(self.user_id, client_id, grant.id)
781+
usage_rules = grant.usage_rules.get("authorization_code", {})
782+
_exp_in = usage_rules.get("expires_in")
783+
784+
# Constructing an authorization code is now done
785+
_code = grant.mint_token(
786+
session_id=session_id,
787+
endpoint_context=self.endpoint_context,
788+
token_class="authorization_code",
789+
token_handler=self.session_manager.token_handler["authorization_code"],
790+
usage_rules=usage_rules,
791+
)
792+
793+
if _exp_in:
794+
if isinstance(_exp_in, str):
795+
_exp_in = int(_exp_in)
796+
if _exp_in:
797+
_code.expires_at = utc_time_sans_frac() + _exp_in
798+
return _code
799+
800+
def test_old_default_token(self):
801+
session_id = self._create_session(AUTH_REQ)
802+
grant = self.session_manager[session_id]
803+
code = self._mint_code(grant, AUTH_REQ["client_id"])
804+
805+
# pack and unpack
806+
_handler = self.session_manager.token_handler.handler["authorization_code"]
807+
_res = dict(zip(["_id", "token_class", "sid", "exp"], _handler.split_token(code.value)))
808+
809+
_old_type_value = base64.b64encode(
810+
_handler.crypt.encrypt(lv_pack(_res["_id"], "A", _res["sid"], _res["exp"]).encode())
811+
).decode("utf-8")
812+
813+
_info = self.session_manager.token_handler.info(_old_type_value)
814+
assert _info["token_class"] == "authorization_code"
815+
816+
def test_old_jwt_token(self):
817+
session_id = self._create_session(AUTH_REQ)
818+
grant = self.session_manager[session_id]
819+
code = self._mint_code(grant, AUTH_REQ["client_id"])
820+
821+
_handler = self.session_manager.token_handler.handler["access_token"]
822+
_old_type_token = _handler(session_id=session_id, token_class="T")
823+
824+
_info = self.session_manager.token_handler.info(_old_type_token)
825+
assert _info["token_class"] == "access_token"
826+
827+
payload = {"sid": session_id, "ttype": "T"}
828+
payload = _handler.load_custom_claims(payload)
829+
830+
# payload.update(kwargs)
831+
_context = _handler.server_get("endpoint_context")
832+
signer = JWT(
833+
key_jar=_context.keyjar, iss=_handler.issuer, lifetime=300, sign_alg=_handler.alg,
834+
)
835+
836+
_old_type_token = signer.pack(payload)
837+
838+
_info = self.session_manager.token_handler.info(_old_type_token)
839+
assert _info["token_class"] == "access_token"

0 commit comments

Comments
 (0)