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

Commit 573fef3

Browse files
committed
The jti parameter in a JWS should be used to catch reuse.
Need a database to hold all the jtis I've seen. Audience in a private_key_jwt JWS is endpoint dependent. It should not be possible to use a private_key_jwt constructed to be used at one endpoint to be used at another.
1 parent 7f713a7 commit 573fef3

File tree

6 files changed

+155
-33
lines changed

6 files changed

+155
-33
lines changed

src/oidcendpoint/client_authn.py

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
from cryptojwt.jwt import utc_time_sans_frac
99
from cryptojwt.utils import as_bytes
1010
from cryptojwt.utils import as_unicode
11-
from oidcmsg.oidc import RegistrationRequest
1211
from oidcmsg.oidc import verified_claim_name
1312

1413
from oidcendpoint import JWT_BEARER
1514
from oidcendpoint import sanitize
15+
from oidcendpoint.exception import MultipleUsage
1616
from oidcendpoint.exception import NotForMe
1717

1818
logger = logging.getLogger(__name__)
@@ -78,8 +78,8 @@ def verify(self, request, authorization_info, **kwargs):
7878
client_info = basic_authn(authorization_info)
7979

8080
if (
81-
self.endpoint_context.cdb[client_info["id"]]["client_secret"]
82-
== client_info["secret"]
81+
self.endpoint_context.cdb[client_info["id"]]["client_secret"]
82+
== client_info["secret"]
8383
):
8484
return {"client_id": client_info["id"]}
8585
else:
@@ -96,8 +96,8 @@ class ClientSecretPost(ClientSecretBasic):
9696

9797
def verify(self, request, **kwargs):
9898
if (
99-
self.endpoint_context.cdb[request["client_id"]]["client_secret"]
100-
== request["client_secret"]
99+
self.endpoint_context.cdb[request["client_id"]]["client_secret"]
100+
== request["client_secret"]
101101
):
102102
return {"client_id": request["client_id"]}
103103
else:
@@ -141,18 +141,30 @@ def verify(self, request, **kwargs):
141141
authtoken = sanitize(ca_jwt.to_dict())
142142
logger.debug("authntoken: {}".format(authtoken))
143143

144+
_endpoint = kwargs.get("endpoint")
145+
if _endpoint is None:
146+
if self.endpoint_context.issuer in ca_jwt["aud"]:
147+
pass
148+
else:
149+
raise NotForMe("Not for me!")
150+
else:
151+
if self.endpoint_context.endpoint[_endpoint].full_path in ca_jwt["aud"]:
152+
pass
153+
else:
154+
raise NotForMe("Not for me!")
155+
156+
# If there is a jti use it to make sure one-time usage is true
157+
_jti = ca_jwt.get('jti')
158+
if _jti:
159+
_key = "{}:{}".format(ca_jwt['iss'], _jti)
160+
if _key in self.endpoint_context.jti_db:
161+
raise MultipleUsage("Have seen this token once before")
162+
else:
163+
self.endpoint_context.jti_db.set(_key, utc_time_sans_frac())
164+
144165
request[verified_claim_name("client_assertion")] = ca_jwt
145166
client_id = kwargs.get("client_id") or ca_jwt["iss"]
146167

147-
# I should be among the audience
148-
# could be either my issuer id or the token endpoint
149-
if self.endpoint_context.issuer in ca_jwt["aud"]:
150-
pass
151-
elif self.endpoint_context.endpoint["token"].full_path in ca_jwt["aud"]:
152-
pass
153-
else:
154-
raise NotForMe("Not for me!")
155-
156168
return {"client_id": client_id, "jwt": ca_jwt}
157169

158170

@@ -192,7 +204,8 @@ def valid_client_info(cinfo):
192204

193205

194206
def verify_client(
195-
endpoint_context, request, authorization_info=None, get_client_id_from_token=None
207+
endpoint_context, request, authorization_info=None, get_client_id_from_token=None,
208+
endpoint=""
196209
):
197210
"""
198211
Initiated Guessing !
@@ -206,7 +219,8 @@ def verify_client(
206219
"""
207220

208221
# fixes request = {} instead of str
209-
# "AttributeError: 'dict' object has no attribute 'startswith'" in oidcendpoint/endpoint.py(158)client_authentication()
222+
# "AttributeError: 'dict' object has no attribute 'startswith'" in oidcendpoint/endpoint.py(
223+
# 158)client_authentication()
210224
if isinstance(authorization_info, dict):
211225
strings_parade = ("{} {}".format(k, v) for k, v in authorization_info.items())
212226
authorization_info = " ".join(strings_parade)
@@ -216,7 +230,7 @@ def verify_client(
216230
auth_info = ClientSecretPost(endpoint_context).verify(request)
217231
auth_info["method"] = "client_secret_post"
218232
elif "client_assertion" in request:
219-
auth_info = JWSAuthnMethod(endpoint_context).verify(request)
233+
auth_info = JWSAuthnMethod(endpoint_context).verify(request, endpoint=endpoint)
220234
# If symmetric key was used
221235
# auth_method = 'client_secret_jwt'
222236
# If asymmetric key was used
@@ -259,9 +273,9 @@ def verify_client(
259273
# store what authn method was used
260274
if auth_info.get("method"):
261275
if (
262-
endpoint_context.cdb[client_id].get("auth_method")
263-
and request.__class__.__name__
264-
in endpoint_context.cdb[client_id]["auth_method"]
276+
endpoint_context.cdb[client_id].get("auth_method")
277+
and request.__class__.__name__
278+
in endpoint_context.cdb[client_id]["auth_method"]
265279
):
266280
endpoint_context.cdb[client_id]["auth_method"][
267281
request.__class__.__name__

src/oidcendpoint/endpoint.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def parse_request(self, request, auth=None, **kwargs):
224224
# Verify that the client is allowed to do this
225225
_client_id = ""
226226
try:
227-
auth_info = self.client_authentication(req, auth, **kwargs)
227+
auth_info = self.client_authentication(req, auth, endpoint=self.name, **kwargs)
228228
except UnknownOrNoAuthnMethod:
229229
# If there is no required client authentication method
230230
if not self.client_authn_method:
@@ -268,9 +268,12 @@ def client_authentication(self, request, auth=None, **kwargs):
268268
:return: client_id or raise an exception
269269
"""
270270

271+
_endpoint = kwargs.get("endpoint")
272+
271273
try:
272274
authn_info = verify_client(
273-
self.endpoint_context, request, auth, self.get_client_id_from_token
275+
self.endpoint_context, request, auth, self.get_client_id_from_token,
276+
endpoint=_endpoint
274277
)
275278
except UnknownOrNoAuthnMethod:
276279
if self.client_authn_method is None:

src/oidcendpoint/endpoint_context.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from oidcendpoint import rndstr
1313
from oidcendpoint.client_authn import CLIENT_AUTHN_METHOD
1414
from oidcendpoint.id_token import IDToken
15+
from oidcendpoint.in_memory_db import InMemoryDataBase
1516
from oidcendpoint.session import create_session_db
1617
from oidcendpoint.sso_db import SSODb
1718
from oidcendpoint.template_handler import Jinja2TemplateHandler
@@ -95,6 +96,7 @@ def __init__(
9596
httpc=None,
9697
cookie_name=None,
9798
jwks_uri_path=None,
99+
jti_db=None,
98100
):
99101
self.conf = conf
100102
self.keyjar = keyjar or KeyJar()
@@ -142,6 +144,11 @@ def __init__(
142144
else:
143145
self.set_session_db(sso_db)
144146

147+
if jti_db:
148+
self.set_jti_db(db=jti_db)
149+
else:
150+
self.set_jti_db()
151+
145152
if cookie_name:
146153
self.cookie_name = cookie_name
147154
elif "cookie_name" in conf:
@@ -233,6 +240,9 @@ def set_session_db(self, sso_db=None, db=None):
233240
self.do_userinfo()
234241
logger.debug("Session DB: {}".format(self.sdb.__dict__))
235242

243+
def set_jti_db(self, db=None):
244+
self.jti_db = db or InMemoryDataBase()
245+
236246
def do_add_on(self):
237247
if self.conf.get("add_on"):
238248
for spec in self.conf["add_on"].values():
@@ -264,9 +274,7 @@ def do_userinfo(self):
264274
self.userinfo = init_user_info(_conf, self.cwd)
265275
self.sdb.userinfo = self.userinfo
266276
else:
267-
logger.warning(
268-
("Cannot init_user_info if any " "session_db was provided.")
269-
)
277+
logger.warning("Cannot init_user_info if no session_db was provided.")
270278

271279
def do_id_token(self):
272280
_conf = self.conf.get("id_token")

src/oidcendpoint/exception.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ class ToOld(OidcEndpointError):
2626
pass
2727

2828

29+
class MultipleUsage(OidcEndpointError):
30+
pass
31+
32+
2933
class FailedAuthentication(OidcEndpointError):
3034
pass
3135

tests/test_02_client_authn.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
from oidcendpoint.client_authn import basic_authn
1919
from oidcendpoint.client_authn import verify_client
2020
from oidcendpoint.endpoint_context import EndpointContext
21+
from oidcendpoint.exception import MultipleUsage
2122
from oidcendpoint.exception import NotForMe
23+
from oidcendpoint.oidc.authorization import Authorization
2224
from oidcendpoint.oidc.token import AccessToken
2325

2426
KEYDEFS = [
@@ -35,7 +37,10 @@
3537
"grant_expires_in": 300,
3638
"refresh_token_expires_in": 86400,
3739
"verify_ssl": False,
38-
"endpoint": {"token": {"path": "token", "class": AccessToken, "kwargs": {}}},
40+
"endpoint": {
41+
"token": {"path": "token", "class": AccessToken, "kwargs": {}},
42+
"authorization": {"path": "auth", "class": Authorization, "kwargs": {}}
43+
},
3944
"template_dir": "template",
4045
"jwks": {
4146
"private_path": "own/jwks.json",
@@ -85,6 +90,7 @@ def test_client_secret_jwt():
8590
client_keyjar.add_symmetric("", client_secret, ["sig"])
8691

8792
_jwt = JWT(client_keyjar, iss=client_id, sign_alg="HS256")
93+
_jwt.with_jti = True
8894
_assertion = _jwt.pack({"aud": [conf["issuer"]]})
8995

9096
request = {"client_assertion": _assertion, "client_assertion_type": JWT_BEARER}
@@ -105,6 +111,7 @@ def test_private_key_jwt():
105111
endpoint_context.keyjar.import_jwks(_jwks, client_id)
106112

107113
_jwt = JWT(client_keyjar, iss=client_id, sign_alg="RS256")
114+
_jwt.with_jti = True
108115
_assertion = _jwt.pack({"aud": [conf["issuer"]]})
109116

110117
request = {"client_assertion": _assertion, "client_assertion_type": JWT_BEARER}
@@ -115,6 +122,54 @@ def test_private_key_jwt():
115122
assert "jwt" in authn_info
116123

117124

125+
def test_private_key_jwt_reusage_other_endpoint():
126+
# Own dynamic keys
127+
client_keyjar = build_keyjar(KEYDEFS)
128+
# The servers keys
129+
client_keyjar[conf["issuer"]] = KEYJAR.issuer_keys[""]
130+
131+
_jwks = client_keyjar.export_jwks()
132+
endpoint_context.keyjar.import_jwks(_jwks, client_id)
133+
134+
_jwt = JWT(client_keyjar, iss=client_id, sign_alg="RS256")
135+
_jwt.with_jti = True
136+
_assertion = _jwt.pack({"aud": [endpoint_context.endpoint["token"].full_path]})
137+
138+
request = {"client_assertion": _assertion, "client_assertion_type": JWT_BEARER}
139+
140+
# This should be OK
141+
authn_info = PrivateKeyJWT(endpoint_context).verify(request, endpoint="token")
142+
143+
# This should NOT be OK
144+
with pytest.raises(NotForMe):
145+
PrivateKeyJWT(endpoint_context).verify(request, endpoint="authorization")
146+
147+
# This should NOT be OK
148+
with pytest.raises(MultipleUsage):
149+
PrivateKeyJWT(endpoint_context).verify(request, endpoint="token")
150+
151+
152+
def test_private_key_jwt_auth_endpoint():
153+
# Own dynamic keys
154+
client_keyjar = build_keyjar(KEYDEFS)
155+
# The servers keys
156+
client_keyjar[conf["issuer"]] = KEYJAR.issuer_keys[""]
157+
158+
_jwks = client_keyjar.export_jwks()
159+
endpoint_context.keyjar.import_jwks(_jwks, client_id)
160+
161+
_jwt = JWT(client_keyjar, iss=client_id, sign_alg="RS256")
162+
_jwt.with_jti = True
163+
_assertion = _jwt.pack({"aud": [endpoint_context.endpoint["authorization"].full_path]})
164+
165+
request = {"client_assertion": _assertion, "client_assertion_type": JWT_BEARER}
166+
167+
authn_info = PrivateKeyJWT(endpoint_context).verify(request, endpoint="authorization")
168+
169+
assert authn_info["client_id"] == client_id
170+
assert "jwt" in authn_info
171+
172+
118173
def test_wrong_type():
119174
with pytest.raises(AuthnFailure):
120175
ClientSecretBasic(endpoint_context).verify({}, "Foppa toffel")
@@ -208,7 +263,7 @@ def test_jws_authn_method_aud_token_endpoint():
208263

209264
request = {"client_assertion": _assertion, "client_assertion_type": JWT_BEARER}
210265

211-
assert JWSAuthnMethod(endpoint_context).verify(request)
266+
assert JWSAuthnMethod(endpoint_context).verify(request, endpoint="token")
212267

213268

214269
def test_jws_authn_method_aud_not_me():

0 commit comments

Comments
 (0)