diff --git a/requirements.txt b/requirements.txt index d7aa79dd..3babcaf3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -oidcmsg>=1.4.0 +oidcmsg>=1.6.0 pyyaml jinja2>=2.11.3 responses>=0.13.0 diff --git a/setup.py b/setup.py index a0af09fc..6b998e6e 100644 --- a/setup.py +++ b/setup.py @@ -72,7 +72,7 @@ def run_tests(self): "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules"], install_requires=[ - "oidcmsg==1.5.4", + "oidcmsg==1.6.0", "pyyaml", "jinja2>=2.11.3", "responses>=0.13.0" diff --git a/src/oidcop/configure.py b/src/oidcop/configure.py index 06332d22..d2c02b45 100755 --- a/src/oidcop/configure.py +++ b/src/oidcop/configure.py @@ -126,6 +126,8 @@ def __init__( conf = copy.deepcopy(conf) Base.__init__(self, conf, base_path, file_attributes, dir_attributes=dir_attributes) + self.key_conf = conf.get('key_conf') + for key in self.parameter.keys(): _val = conf.get(key) if not _val: @@ -150,9 +152,10 @@ def __init__( if key == "template_dir": _val = os.path.abspath(_val) if key == "keys": - key = "key_conf" - - setattr(self, key, _val) + if not self.key_conf: + setattr(self, "key_conf", _val) + else: + setattr(self, key, _val) class OPConfiguration(EntityConfiguration): diff --git a/src/oidcop/endpoint_context.py b/src/oidcop/endpoint_context.py index fd135395..d4f7b40d 100755 --- a/src/oidcop/endpoint_context.py +++ b/src/oidcop/endpoint_context.py @@ -162,6 +162,7 @@ def __init__( self.login_hint2acrs = None self.par_db = {} self.provider_info = {} + self.remove_token = None self.scope2claims = conf.get("scopes_to_claims", SCOPE2CLAIMS) self.session_manager = None self.sso_ttl = 14400 # 4h @@ -338,3 +339,18 @@ def create_providerinfo(self, capabilities): ) return _provider_info + + def set_remember_token(self): + ses_par = self.conf.get("session_params") or {} + + self.session_manager.remove_inactive_token = ses_par.get("remove_inactive_token", False) + + _rm = ses_par.get("remember_token", {}) + if "class" in _rm: + _kwargs = _rm.get("kwargs", {}) + self.session_manager.remember_token = init_service(_rm["class"], **_kwargs) + elif "function" in _rm: + if isinstance(_rm["function"], str): + self.session_manager.remember_token = importer(_rm["function"]) + else: + self.session_manager.remember_token = _rm["function"] diff --git a/src/oidcop/oauth2/introspection.py b/src/oidcop/oauth2/introspection.py index c298c12d..8f183d88 100644 --- a/src/oidcop/oauth2/introspection.py +++ b/src/oidcop/oauth2/introspection.py @@ -31,6 +31,7 @@ def _introspect(self, token, client_id, grant): return None if not token.is_active(): + # return None scope = token.scope diff --git a/src/oidcop/server.py b/src/oidcop/server.py index 1972aff6..8774d8c0 100644 --- a/src/oidcop/server.py +++ b/src/oidcop/server.py @@ -93,6 +93,7 @@ def __init__( self.endpoint_context.do_userinfo() # Must be done after userinfo self.do_login_hint_lookup() + self.endpoint_context.set_remember_token() for endpoint_name, endpoint_conf in self.endpoint.items(): _endpoint = self.endpoint[endpoint_name] diff --git a/src/oidcop/session/grant.py b/src/oidcop/session/grant.py index a7c6f8d0..cee78539 100644 --- a/src/oidcop/session/grant.py +++ b/src/oidcop/session/grant.py @@ -1,4 +1,5 @@ import logging +from typing import Callable from typing import Dict from typing import List from typing import Optional @@ -32,11 +33,11 @@ class GrantMessage(ImpExp): } def __init__( - self, - scope: Optional[str] = "", - authorization_details: Optional[dict] = None, - claims: Optional[list] = None, - resources: Optional[list] = None, + self, + scope: Optional[str] = "", + authorization_details: Optional[dict] = None, + claims: Optional[list] = None, + resources: Optional[list] = None, ): ImpExp.__init__(self) self.scope = scope @@ -99,6 +100,10 @@ def token_map_load(items: dict, **kwargs): return {k: importer(v) for k, v in items.items()} +def remember_token(token): + logger.info(str(token)) + + class Grant(Item): parameter = Item.parameter.copy() parameter.update( @@ -122,22 +127,24 @@ class Grant(Item): } def __init__( - self, - scope: Optional[list] = None, - claims: Optional[dict] = None, - resources: Optional[list] = None, - authorization_details: Optional[dict] = None, - authorization_request: Optional[Message] = None, - authentication_event: Optional[AuthnEvent] = None, - issued_token: Optional[list] = None, - usage_rules: Optional[dict] = None, - issued_at: int = 0, - expires_in: int = 0, - expires_at: int = 0, - revoked: bool = False, - token_map: Optional[dict] = None, - sub: Optional[str] = "", - extra: Optional[Dict[str, str]] = None, + self, + scope: Optional[list] = None, + claims: Optional[dict] = None, + resources: Optional[list] = None, + authorization_details: Optional[dict] = None, + authorization_request: Optional[Message] = None, + authentication_event: Optional[AuthnEvent] = None, + issued_token: Optional[list] = None, + usage_rules: Optional[dict] = None, + issued_at: int = 0, + expires_in: int = 0, + expires_at: int = 0, + revoked: bool = False, + token_map: Optional[dict] = None, + sub: Optional[str] = "", + extra: Optional[Dict[str, str]] = None, + remember_token: Optional[Callable] = None, + remove_inactive_token: Optional[bool] = False ): Item.__init__( self, @@ -157,6 +164,8 @@ def __init__( self.id = uuid1().hex self.sub = sub self.extra = extra or {} + self.remember_token = remember_token + self.remove_inactive_token = remove_inactive_token if token_map is None: self.token_map = TOKEN_MAP @@ -193,13 +202,13 @@ def add_acr_value(self, claims_release_point): return False def payload_arguments( - self, - session_id: str, - endpoint_context, - claims_release_point: str, - scope: Optional[dict] = None, - extra_payload: Optional[dict] = None, - secondary_identifier: str = "", + self, + session_id: str, + endpoint_context, + claims_release_point: str, + scope: Optional[dict] = None, + extra_payload: Optional[dict] = None, + secondary_identifier: str = "", ) -> dict: """ @@ -248,16 +257,16 @@ def payload_arguments( return payload def mint_token( - self, - session_id: str, - endpoint_context: object, - token_class: str, - token_handler: TokenHandler = None, - based_on: Optional[SessionToken] = None, - usage_rules: Optional[dict] = None, - scope: Optional[list] = None, - token_type: Optional[str] = "", - **kwargs, + self, + session_id: str, + endpoint_context: object, + token_class: str, + token_handler: TokenHandler = None, + based_on: Optional[SessionToken] = None, + usage_rules: Optional[dict] = None, + scope: Optional[list] = None, + token_type: Optional[str] = "", + **kwargs, ) -> Optional[SessionToken]: """ @@ -359,8 +368,12 @@ def get_token(self, value: str) -> Optional[SessionToken]: return None def revoke_token( - self, value: Optional[str] = "", based_on: Optional[str] = "", recursive: bool = True + self, + value: Optional[str] = "", + based_on: Optional[str] = "", + recursive: bool = True ): + remain = [] for t in self.issued_token: if not value and not based_on: t.revoked = True @@ -376,6 +389,17 @@ def revoke_token( if recursive: self.revoke_token(based_on=t.value) + if t.revoked: + if self.remove_inactive_token: + if self.remember_token: + self.remember_token(t) + else: + remain.append(t) + else: + remain.append(t) + + self.issued_token = remain + def get_spec(self, token: SessionToken) -> Optional[dict]: if self.is_active() is False or token.is_active is False: return None @@ -442,19 +466,19 @@ class ExchangeGrant(Grant): type = "exchange_grant" def __init__( - self, - scope: Optional[list] = None, - claims: Optional[dict] = None, - resources: Optional[list] = None, - authorization_details: Optional[dict] = None, - issued_token: Optional[list] = None, - usage_rules: Optional[dict] = None, - issued_at: int = 0, - expires_in: int = 0, - expires_at: int = 0, - revoked: bool = False, - token_map: Optional[dict] = None, - users: list = None, + self, + scope: Optional[list] = None, + claims: Optional[dict] = None, + resources: Optional[list] = None, + authorization_details: Optional[dict] = None, + issued_token: Optional[list] = None, + usage_rules: Optional[dict] = None, + issued_at: int = 0, + expires_in: int = 0, + expires_at: int = 0, + revoked: bool = False, + token_map: Optional[dict] = None, + users: list = None, ): Grant.__init__( self, diff --git a/src/oidcop/session/manager.py b/src/oidcop/session/manager.py index 3bddebc4..2db716b9 100644 --- a/src/oidcop/session/manager.py +++ b/src/oidcop/session/manager.py @@ -1,6 +1,7 @@ import hashlib import logging import os +from typing import Callable from typing import List from typing import Optional import uuid @@ -10,9 +11,9 @@ from oidcop import rndstr from oidcop.authn_event import AuthnEvent from oidcop.exception import ConfigurationError +from oidcop.session.database import NoSuchClientSession from oidcop.token import handler from oidcop.util import Crypt -from oidcop.session.database import NoSuchClientSession from .database import Database from .grant import Grant from .grant import SessionToken @@ -47,7 +48,7 @@ def __init__(self, salt: Optional[str] = "", filename: Optional[str] = ""): if os.path.isfile(filename): self.salt = open(filename).read() elif not os.path.isfile(filename) and os.path.exists( - filename + filename ): # Not a file, Something else raise ConfigurationError("Salt filename points to something that is not a file") else: @@ -82,10 +83,12 @@ class SessionManager(Database): init_args = ["handler"] def __init__( - self, - handler: TokenHandler, - conf: Optional[dict] = None, - sub_func: Optional[dict] = None, + self, + handler: TokenHandler, + conf: Optional[dict] = None, + sub_func: Optional[dict] = None, + remember_token: Optional[Callable] = None, + remove_inactive_token: Optional[bool] = False ): super(SessionManager, self).__init__() self.conf = conf or {} @@ -100,6 +103,8 @@ def __init__( self._init_db() self.token_handler = handler + self.remember_token = remember_token + self.remove_inactive_token = remove_inactive_token # this allows the subject identifier minters to be defined by someone # else then me. @@ -159,14 +164,14 @@ def find_token(self, session_id: str, token_value: str) -> Optional[SessionToken return None # pragma: no cover def create_grant( - self, - authn_event: AuthnEvent, - auth_req: AuthorizationRequest, - user_id: str, - client_id: Optional[str] = "", - sub_type: Optional[str] = "public", - token_usage_rules: Optional[dict] = None, - scopes: Optional[list] = None, + self, + authn_event: AuthnEvent, + auth_req: AuthorizationRequest, + user_id: str, + client_id: Optional[str] = "", + sub_type: Optional[str] = "public", + token_usage_rules: Optional[dict] = None, + scopes: Optional[list] = None, ) -> str: """ @@ -192,6 +197,8 @@ def create_grant( usage_rules=token_usage_rules, scope=scopes, claims=_claims, + remember_token=self.remember_token, + remove_inactive_token=self.remove_inactive_token ) self.set([user_id, client_id, grant.id], grant) @@ -199,14 +206,14 @@ def create_grant( return self.encrypted_session_id(user_id, client_id, grant.id) def create_session( - self, - authn_event: AuthnEvent, - auth_req: AuthorizationRequest, - user_id: str, - client_id: Optional[str] = "", - sub_type: Optional[str] = "public", - token_usage_rules: Optional[dict] = None, - scopes: Optional[list] = None, + self, + authn_event: AuthnEvent, + auth_req: AuthorizationRequest, + user_id: str, + client_id: Optional[str] = "", + sub_type: Optional[str] = "public", + token_usage_rules: Optional[dict] = None, + scopes: Optional[list] = None, ) -> str: """ Create part of a user session. The parts added are user- and client @@ -295,12 +302,6 @@ def get_grant(self, session_id: str) -> Grant: else: # pragma: no cover raise ValueError("Wrong type of item") - def _revoke_dependent(self, grant: Grant, token: SessionToken): - for t in grant.issued_token: - if t.based_on == token.value: - t.revoked = True # TODO: not covered yet! - self._revoke_dependent(grant, t) - def revoke_token(self, session_id: str, token_value: str, recursive: bool = False): """ Revoke a specific token that belongs to a specific user session. @@ -317,13 +318,13 @@ def revoke_token(self, session_id: str, token_value: str, recursive: bool = Fals token.revoked = True if recursive: # TODO: not covered yet! grant = self[session_id] - self._revoke_dependent(grant, token) + grant.revoke_token(value=token.value) def get_authentication_events( - self, - session_id: Optional[str] = "", - user_id: Optional[str] = "", - client_id: Optional[str] = "", + self, + session_id: Optional[str] = "", + user_id: Optional[str] = "", + client_id: Optional[str] = "", ) -> List[AuthnEvent]: """ Return the authentication events that exists for a user/client combination. @@ -393,10 +394,10 @@ def revoke_grant(self, session_id: str): self.set(_path, _info) def grants( - self, - session_id: Optional[str] = "", - user_id: Optional[str] = "", - client_id: Optional[str] = "", + self, + session_id: Optional[str] = "", + user_id: Optional[str] = "", + client_id: Optional[str] = "", ) -> List[Grant]: """ Find all grant connected to a user session @@ -417,13 +418,13 @@ def grants( return [self.get([user_id, client_id, gid]) for gid in _csi.subordinate] def get_session_info( - self, - session_id: str, - user_session_info: bool = False, - client_session_info: bool = False, - grant: bool = False, - authentication_event: bool = False, - authorization_request: bool = False, + self, + session_id: str, + user_session_info: bool = False, + client_session_info: bool = False, + grant: bool = False, + authentication_event: bool = False, + authorization_request: bool = False, ) -> dict: """ Returns information connected to a session. @@ -478,13 +479,13 @@ def _compatible_sid(self, sid): return sid def get_session_info_by_token( - self, - token_value: str, - user_session_info: bool = False, - client_session_info: bool = False, - grant: bool = False, - authentication_event: bool = False, - authorization_request: bool = False, + self, + token_value: str, + user_session_info: bool = False, + client_session_info: bool = False, + grant: bool = False, + authentication_event: bool = False, + authorization_request: bool = False, ) -> dict: _token_info = self.token_handler.info(token_value) sid = _token_info.get("sid") diff --git a/src/oidcop/session/token.py b/src/oidcop/session/token.py index 52ada60e..56554ee2 100644 --- a/src/oidcop/session/token.py +++ b/src/oidcop/session/token.py @@ -1,3 +1,4 @@ +import json from typing import Optional from uuid import uuid1 @@ -145,6 +146,19 @@ def supports_minting(self, token_class): else: return token_class in _supports_minting + def __str__(self): + _info = { + "token_class": self.token_class, + "value": self.value, + "based_on": self.based_on, + "id": self.id, + "scope": self.scope, + "claims": self.claims, + "resources": self.resources, + "name": self.name + } + return json.dumps(_info) + class AccessToken(SessionToken): parameter = SessionToken.parameter.copy() diff --git a/src/oidcop/token/handler.py b/src/oidcop/token/handler.py index c4fb2386..5bd827a9 100755 --- a/src/oidcop/token/handler.py +++ b/src/oidcop/token/handler.py @@ -24,11 +24,11 @@ class TokenHandler(ImpExp): parameter = {"handler": DLDict, "handler_order": [""]} def __init__( - self, - access_token: Optional[Token] = None, - authorization_code: Optional[Token] = None, - refresh_token: Optional[Token] = None, - id_token: Optional[Token] = None, + self, + access_token: Optional[Token] = None, + authorization_code: Optional[Token] = None, + refresh_token: Optional[Token] = None, + id_token: Optional[Token] = None, ): ImpExp.__init__(self) self.handler = {"authorization_code": authorization_code, "access_token": access_token} @@ -141,13 +141,13 @@ def default_token(spec): def factory( - server_get, - code: Optional[dict] = None, - token: Optional[dict] = None, - refresh: Optional[dict] = None, - id_token: Optional[dict] = None, - jwks_file: Optional[str] = "", - **kwargs + server_get, + code: Optional[dict] = None, + token: Optional[dict] = None, + refresh: Optional[dict] = None, + id_token: Optional[dict] = None, + jwks_file: Optional[str] = "", + **kwargs ) -> TokenHandler: """ Create a token handler diff --git a/tests/test_01_grant.py b/tests/test_01_grant.py index 67199372..b9410204 100644 --- a/tests/test_01_grant.py +++ b/tests/test_01_grant.py @@ -1,18 +1,19 @@ -import pytest from cryptojwt.key_jar import build_keyjar from oidcmsg.oidc import AuthorizationRequest +import pytest -from . import full_path from oidcop.authn_event import create_authn_event from oidcop.server import Server -from oidcop.session.grant import TOKEN_MAP from oidcop.session.grant import Grant +from oidcop.session.grant import TOKEN_MAP from oidcop.session.grant import find_token from oidcop.session.grant import get_usage_rules +from oidcop.session.grant import remember_token from oidcop.session.token import AuthorizationCode from oidcop.session.token import SessionToken from oidcop.token import DefaultToken from oidcop.user_authn.authn_context import INTERNETPROTOCOLPASSWORD +from . import full_path KEYDEFS = [ {"type": "RSA", "key": "", "use": ["sig"]}, @@ -21,7 +22,6 @@ KEYJAR = build_keyjar(KEYDEFS) - conf = { "issuer": "https://example.com/", "template_dir": "template", @@ -32,7 +32,7 @@ "class": "oidcop.oidc.authorization.Authorization", "kwargs": {}, }, - "token_endpoint": {"path": "token", "class": "oidcop.oidc.token.Token", "kwargs": {},}, + "token_endpoint": {"path": "token", "class": "oidcop.oidc.token.Token", "kwargs": {}, }, }, "authentication": { "anon": { @@ -46,6 +46,12 @@ "class": "oidcop.user_info.UserInfo", "kwargs": {"db_file": full_path("users.json")}, }, + "session_params": { + "remove_inactive_token": True, + "remember_token": { + "function": remember_token, + } + } } USER_ID = "diana" @@ -179,9 +185,7 @@ def test_grant(self): ) grant.revoke_token() - assert code.revoked is True - assert access_token.revoked is True - assert refresh_token.revoked is True + assert grant.issued_token == [] def test_get_token(self): session_id = self._create_session(AREQ) @@ -514,3 +518,70 @@ def test_assigned_scope_2nd(self): ) assert access_token.scope == refresh_token.scope + + def test_grant_remove_based_on_code(self): + session_id = self._create_session(AREQ) + session_info = self.endpoint_context.session_manager.get_session_info( + session_id=session_id, grant=True + ) + grant = session_info["grant"] + code = grant.mint_token( + session_id, + endpoint_context=self.endpoint_context, + token_class="authorization_code", + token_handler=TOKEN_HANDLER["authorization_code"], + ) + + access_token = grant.mint_token( + session_id, + endpoint_context=self.endpoint_context, + token_class="access_token", + token_handler=TOKEN_HANDLER["access_token"], + based_on=code, + ) + + refresh_token = grant.mint_token( + session_id, + endpoint_context=self.endpoint_context, + token_class="refresh_token", + token_handler=TOKEN_HANDLER["refresh_token"], + based_on=code, + ) + + grant.revoke_token(based_on=code.value) + assert len(grant.issued_token) == 1 + + def test_grant_remove_one_by_one(self): + session_id = self._create_session(AREQ) + session_info = self.endpoint_context.session_manager.get_session_info( + session_id=session_id, grant=True + ) + grant = session_info["grant"] + code = grant.mint_token( + session_id, + endpoint_context=self.endpoint_context, + token_class="authorization_code", + token_handler=TOKEN_HANDLER["authorization_code"], + ) + + access_token = grant.mint_token( + session_id, + endpoint_context=self.endpoint_context, + token_class="access_token", + token_handler=TOKEN_HANDLER["access_token"], + based_on=code, + ) + + refresh_token = grant.mint_token( + session_id, + endpoint_context=self.endpoint_context, + token_class="refresh_token", + token_handler=TOKEN_HANDLER["refresh_token"], + based_on=code, + ) + + grant.revoke_token(value=refresh_token.value) + assert len(grant.issued_token) == 2 + + grant.revoke_token(value=access_token.value) + assert len(grant.issued_token) == 1 diff --git a/tests/test_06_session_manager.py b/tests/test_06_session_manager.py index 34953c75..4b5cffc3 100644 --- a/tests/test_06_session_manager.py +++ b/tests/test_06_session_manager.py @@ -87,8 +87,8 @@ def create_session_manager(self): "token_endpoint": {"path": "{}/token", "class": Token, "kwargs": {}}, }, "session_params": { - "password": "ses_key", - "salt": "ses_salt" + "password": "ses_key", + "salt": "ses_salt" }, "template_dir": "template", "claims_interface": {"class": "oidcop.session.claims.ClaimsInterface", "kwargs": {}}, @@ -619,7 +619,11 @@ def test_revoke_dependent(self): code = self._mint_token("authorization_code", grant, _session_id) token = self._mint_token("access_token", grant, _session_id, code) - self.session_manager._revoke_dependent(grant, token) + + grant.remove_inactive_token = True + grant.revoke_token(value=token.value) + assert len(grant.issued_token) == 1 + assert grant.issued_token[0].token_class == "authorization_code" def test_grants(self): token_usage_rules = self.endpoint_context.authz.usage_rules("client_1")