Skip to content

Commit 71ba659

Browse files
authored
Merge pull request #17 from IdentityPython/develop
Better handling on error in authentication
2 parents 48145b8 + c2b88e9 commit 71ba659

File tree

10 files changed

+200
-27
lines changed

10 files changed

+200
-27
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
77

88
[metadata]
99
name = "idpyoidc"
10-
version = "1.0.11"
10+
version = "1.0.12"
1111
author = "Roland Hedberg"
1212
author_email = "[email protected]"
1313
description = "Everything OAuth2 and OIDC"

src/idpyoidc/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
__author__ = "Roland Hedberg"
2-
__version__ = "1.0.11"
2+
__version__ = "1.0.12"
33

44
import os
55
from typing import Dict

src/idpyoidc/server/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from cryptojwt import KeyJar
88

99
from idpyoidc.impexp import ImpExp
10-
from idpyoidc.message.oidc import RegistrationRequest
1110
from idpyoidc.server import authz
1211
from idpyoidc.server.client_authn import client_auth_setup
1312
from idpyoidc.server.configure import ASConfiguration

src/idpyoidc/server/cookie_handler.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
from cryptojwt.jws.hmac import HMACSigner
1616
from cryptojwt.jwt import utc_time_sans_frac
1717
from cryptojwt.key_jar import init_key_jar
18+
from cryptojwt.utils import as_bytes
1819

20+
from idpyoidc.encrypter import init_encrypter
1921
from idpyoidc.server.util import lv_pack
2022
from idpyoidc.server.util import lv_unpack
2123
from idpyoidc.time_util import epoch_in_a_while
@@ -37,17 +39,25 @@ def __init__(
3739
keys: Optional[dict] = None,
3840
sign_alg: [str] = "SHA256",
3941
name: Optional[dict] = None,
42+
crypt_config: Optional[dict] = None,
4043
**kwargs,
4144
):
45+
self.sign_key = None
46+
self.enc_key = None
47+
self.crypt = None
4248

4349
if keys:
4450
key_jar = init_key_jar(**keys)
45-
_keys = key_jar.get_signing_key(key_type="oct", kid="sig")
51+
_keys = key_jar.get_signing_key(key_type="oct")
4652
if _keys:
4753
self.sign_key = _keys[0]
48-
_keys = key_jar.get_encrypt_key(key_type="oct", kid="enc")
54+
_keys = key_jar.get_encrypt_key(key_type="oct")
4955
if _keys:
5056
self.enc_key = _keys[0]
57+
elif crypt_config:
58+
_crypt = init_encrypter(crypt_config)
59+
self.crypt = _crypt["encrypter"]
60+
self.crypt_config = _crypt["conf"]
5161
else:
5262
if sign_key:
5363
if isinstance(sign_key, SYMKey):
@@ -132,6 +142,11 @@ def _sign_enc_payload(self, payload: str, timestamp: Optional[Union[int, str]] =
132142
base64.b64encode(ctx),
133143
base64.b64encode(tag),
134144
]
145+
elif self.crypt:
146+
msg = lv_pack(timestamp, payload)
147+
cookie_payload = [
148+
bytes_timestamp,
149+
base64.b64encode(self.crypt.encrypt(msg.encode()))]
135150
else:
136151
cookie_payload = [bytes_timestamp, bytes_load, base64.b64encode(mac)]
137152

@@ -147,6 +162,15 @@ def _ver_dec_content(self, parts):
147162

148163
if parts is None:
149164
return None
165+
elif len(parts) == 2:
166+
t0, enc_payload = parts
167+
if not self.crypt:
168+
raise VerificationError("Can not decrypt")
169+
msg = self.crypt.decrypt(base64.b64decode(as_bytes(enc_payload)))
170+
t1, payload = lv_unpack(msg.decode("utf-8"))
171+
if t0 != t1:
172+
raise VerificationError('Suspicious timestamp')
173+
return payload, t1
150174
elif len(parts) == 3:
151175
# verify the cookie signature
152176
timestamp, payload, b64_mac = parts
@@ -255,15 +279,15 @@ def parse_cookie(self, name: str, cookies: List[dict]) -> Optional[List[dict]]:
255279
LOGGER.debug("Looking for '{}' cookies".format(name))
256280
res = []
257281
for _cookie in cookies:
258-
LOGGER.debug("Cookie: {}".format(_cookie))
282+
LOGGER.debug(f"Cookie: {_cookie}")
259283
if "name" in _cookie and _cookie["name"] == name:
260284
_content = self._ver_dec_content(_cookie["value"].split("|"))
261285
if _content:
262-
payload, timestamp = self._ver_dec_content(_cookie["value"].split("|"))
286+
payload, timestamp = _content
263287
value, typ = payload.split("::")
264288
res.append({"value": value, "type": typ, "timestamp": timestamp})
265289
else:
266-
LOGGER.debug(f"Could not verify {name} cookie")
290+
LOGGER.debug(f"Could not verify '{name}' cookie")
267291
return res
268292

269293

src/idpyoidc/server/oauth2/authorization.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -603,21 +603,22 @@ def setup_auth(
603603
if _csi.is_active() is False:
604604
identity = None
605605

606-
authn_args = authn_args_gather(request, authn_class_ref, cinfo, **kwargs)
607606
_mngr = _context.session_manager
608607
_session_id = ""
609608

610609
# To authenticate or Not
611610
if not identity: # No!
612611
logger.info("No active authentication")
613-
logger.debug("Known clients: {}".format(list(_context.cdb.keys())))
612+
# logger.debug("Known clients: {}".format(list(_context.cdb.keys())))
614613

615614
if "prompt" in request and "none" in request["prompt"]:
616615
# Need to authenticate but not allowed
617616
return self._login_required_error(redirect_uri, request)
618617
else:
618+
authn_args = authn_args_gather(request, authn_class_ref, cinfo, **kwargs)
619619
return {"function": authn, "args": authn_args}
620620
else:
621+
authn_args = authn_args_gather(request, authn_class_ref, cinfo, **kwargs)
621622
logger.info(f"Active authentication: {identity}")
622623
if re_authenticate(request, authn):
623624
# demand re-authentication
@@ -994,7 +995,7 @@ def process_request(
994995
cinfo = _context.cdb[_cid]
995996
# logger.debug("client {}: {}".format(_cid, cinfo))
996997

997-
# this apply the default optionally deny_unknown_scopes policy
998+
# this applies the default optionally deny_unknown_scopes policy
998999
check_unknown_scopes_policy(request, _cid, _context)
9991000

10001001
if http_info is None:
@@ -1004,7 +1005,11 @@ def process_request(
10041005
if _cookies:
10051006
logger.debug("parse_cookie@process_request")
10061007
_session_cookie_name = _context.cookie_handler.name["session"]
1007-
_my_cookies = _context.cookie_handler.parse_cookie(_session_cookie_name, _cookies)
1008+
try:
1009+
_my_cookies = _context.cookie_handler.parse_cookie(_session_cookie_name, _cookies)
1010+
except Exception as err:
1011+
logger.info(f"Parse cookie failed due to: {err}")
1012+
_my_cookies = {}
10081013
else:
10091014
_my_cookies = {}
10101015

src/idpyoidc/server/user_authn/user.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
from idpyoidc.server.exception import FailedAuthentication
1515
from idpyoidc.server.exception import ImproperlyConfigured
16+
from idpyoidc.server.exception import InconsistentDatabase
17+
from idpyoidc.server.exception import NoSuchClientSession
18+
from idpyoidc.server.exception import NoSuchGrant
1619
from idpyoidc.server.exception import OnlyForTestingWarning
1720
from idpyoidc.time_util import utc_time_sans_frac
1821
from idpyoidc.util import instantiate
@@ -64,8 +67,8 @@ def authenticated_as(self, client_id, cookie=None, **kwargs):
6467
return None, 0
6568
else:
6669
_info = self.cookie_info(cookie, client_id)
67-
logger.debug("authenticated_as: cookie info={}".format(_info))
6870
if _info:
71+
logger.debug("authenticated_as: cookie info={}".format(_info))
6972
if "max_age" in kwargs and kwargs["max_age"] != 0:
7073
_max_age = kwargs["max_age"]
7174
_now = utc_time_sans_frac()
@@ -74,6 +77,9 @@ def authenticated_as(self, client_id, cookie=None, **kwargs):
7477
"Too old by {} seconds".format(_now - (_info["timestamp"] + _max_age))
7578
)
7679
return None, 0
80+
else:
81+
logger.info("Failed to find session based on cookie")
82+
7783
return _info, utc_time_sans_frac()
7884

7985
def verify(self, *args, **kwargs):
@@ -109,9 +115,18 @@ def cookie_info(self, cookie: List[dict], client_id: str) -> dict:
109115
for val in cookie:
110116
_info = json.loads(val["value"])
111117
_info["timestamp"] = int(val["timestamp"])
118+
119+
# verify session ID
120+
try:
121+
_context.session_manager[_info["sid"]]
122+
except (KeyError, ValueError, InconsistentDatabase,
123+
NoSuchClientSession, NoSuchGrant) as err:
124+
logger.info(f"Verifying session ID fail due to {err}")
125+
return {}
126+
112127
session_id = _context.session_manager.decrypt_session_id(_info["sid"])
113128
logger.debug("cookie_info: session id={}".format(session_id))
114-
# _, cid, _ = _context.session_manager.decrypt_session_id(_info["sid"])
129+
115130
if session_id[1] != client_id:
116131
continue
117132
else:
@@ -138,13 +153,13 @@ class UserPassJinja2(UserAuthnMethod):
138153
url_endpoint = "/verify/user_pass_jinja"
139154

140155
def __init__(
141-
self,
142-
db,
143-
template_handler,
144-
template="user_pass.jinja2",
145-
server_get=None,
146-
verify_endpoint="",
147-
**kwargs,
156+
self,
157+
db,
158+
template_handler,
159+
template="user_pass.jinja2",
160+
server_get=None,
161+
verify_endpoint="",
162+
**kwargs,
148163
):
149164

150165
super(UserPassJinja2, self).__init__(server_get=server_get)

tests/private/cookie_jwks.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"keys": [{"kty": "oct", "use": "enc", "kid": "enc", "k": "XlZGhRrHFOQwgIJZSV8g23cxVoL27xyv"}, {"kty": "oct", "use": "sig", "kid": "sig", "k": "TD6UZ7GmkUeC1oejdCGTf2YAp7FHeGfd"}]}
1+
{"keys": [{"kty": "oct", "use": "enc", "kid": "enc", "k": "G6JN24md0JKKBD16Aq02wPpvW6QrlgzI"}, {"kty": "oct", "use": "sig", "kid": "sig", "k": "8YmUB_TM3eW3-uZHUqgsuiXSkMpgboUk"}]}

tests/test_server_13_user_authn.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pytest
55

66
from idpyoidc.server import Server
7+
from idpyoidc.server.authn_event import create_authn_event
78
from idpyoidc.server.configure import OPConfiguration
89
from idpyoidc.server.user_authn.authn_context import INTERNETPROTOCOLPASSWORD
910
from idpyoidc.server.user_authn.authn_context import UNSPECIFIED
@@ -80,6 +81,20 @@ def create_endpoint_context(self):
8081
}
8182
self.server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR)
8283
self.endpoint_context = self.server.endpoint_context
84+
self.session_manager = self.endpoint_context.session_manager
85+
self.user_id = "diana"
86+
87+
def _create_session(self, auth_req, sub_type="public", sector_identifier=""):
88+
if sector_identifier:
89+
authz_req = auth_req.copy()
90+
authz_req["sector_identifier_uri"] = sector_identifier
91+
else:
92+
authz_req = auth_req
93+
client_id = authz_req["client_id"]
94+
ae = create_authn_event(self.user_id)
95+
return self.session_manager.create_session(
96+
ae, authz_req, self.user_id, client_id=client_id, sub_type=sub_type
97+
)
8398

8499
def test_authenticated_as_without_cookie(self):
85100
authn_item = self.endpoint_context.authn_broker.pick(INTERNETPROTOCOLPASSWORD)
@@ -93,12 +108,11 @@ def test_authenticated_as_with_cookie(self):
93108
method = authn_item[0]["method"]
94109

95110
authn_req = {"state": "state_identifier", "client_id": "client 12345"}
111+
_sid = self._create_session(authn_req)
96112
_cookie = self.endpoint_context.new_cookie(
97113
name=self.endpoint_context.cookie_handler.name["session"],
98114
sub="diana",
99-
sid=self.endpoint_context.session_manager.encrypted_session_id(
100-
"diana", "client 12345", "abcdefgh"
101-
),
115+
sid=_sid,
102116
state=authn_req["state"],
103117
client_id=authn_req["client_id"],
104118
)

tests/test_server_20g_cookie_handler.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from idpyoidc.server.cookie_handler import CookieHandler
55
from idpyoidc.server.cookie_handler import compute_session_state
6+
from tests import CRYPT_CONFIG
67

78
KEYDEFS = [
89
{"type": "OCT", "kid": "sig", "use": ["sig"]},
@@ -233,3 +234,56 @@ def test_mult_cookie(self):
233234
def test_compute_session_state():
234235
hv = compute_session_state("state", "salt", "client_id", "https://example.com/redirect")
235236
assert hv == "d21113fbe4b54661ae45f3a3233b0f865ccc646af248274b6fa5664267540e29.salt"
237+
238+
239+
class TestCookieHandlerFernetEnc(object):
240+
@pytest.fixture(autouse=True)
241+
def make_cookie_content_handler(self):
242+
cookie_conf = {
243+
"crypt_config": CRYPT_CONFIG,
244+
}
245+
246+
self.cookie_handler = CookieHandler(**cookie_conf)
247+
248+
def test_make_cookie_content(self):
249+
_cookie_info = self.cookie_handler.make_cookie_content("idpyoidc.server", "value", "sso")
250+
assert _cookie_info
251+
assert set(_cookie_info.keys()) == {"name", "value", "samesite", "httponly", "secure"}
252+
assert len(_cookie_info["value"].split("|")) == 2
253+
254+
def test_make_cookie_content_max_age(self):
255+
_cookie_info = self.cookie_handler.make_cookie_content(
256+
"idpyoidc.server", "value", "sso", max_age=3600
257+
)
258+
assert _cookie_info
259+
assert set(_cookie_info.keys()) == {
260+
"name",
261+
"value",
262+
"max-age",
263+
"samesite",
264+
"httponly",
265+
"secure",
266+
}
267+
assert len(_cookie_info["value"].split("|")) == 2
268+
269+
def test_read_cookie_info(self):
270+
_cookie_info = [self.cookie_handler.make_cookie_content("idpyoidc.server", "value", "sso")]
271+
returned = [{"name": c["name"], "value": c["value"]} for c in _cookie_info]
272+
_info = self.cookie_handler.parse_cookie("idpyoidc.server", returned)
273+
assert len(_info) == 1
274+
assert set(_info[0].keys()) == {"value", "type", "timestamp"}
275+
assert _info[0]["value"] == "value"
276+
assert _info[0]["type"] == "sso"
277+
278+
def test_mult_cookie(self):
279+
_cookie = [
280+
self.cookie_handler.make_cookie_content("idpyoidc.server", "value", "sso"),
281+
self.cookie_handler.make_cookie_content("idpyoidc.server", "session_state", "session"),
282+
]
283+
assert len(_cookie) == 2
284+
_c_info = self.cookie_handler.parse_cookie("idpyoidc.server", _cookie)
285+
assert len(_c_info) == 2
286+
assert _c_info[0]["value"] == "value"
287+
assert _c_info[0]["type"] == "sso"
288+
assert _c_info[1]["value"] == "session_state"
289+
assert _c_info[1]["type"] == "session"

0 commit comments

Comments
 (0)