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

Commit a4443b1

Browse files
committed
The new client auth process demands that endpoint is known.
Use the client_authn_setup function to initiate the client auth methods to be used. Fixed tests.
1 parent b762395 commit a4443b1

File tree

7 files changed

+151
-70
lines changed

7 files changed

+151
-70
lines changed

src/oidcendpoint/client_authn.py

Lines changed: 131 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from cryptojwt.jwt import utc_time_sans_frac
1010
from cryptojwt.utils import as_bytes
1111
from cryptojwt.utils import as_unicode
12+
from oidcmsg.oidc import JsonWebToken
1213
from oidcmsg.oidc import verified_claim_name
1314

1415
from oidcendpoint import JWT_BEARER
@@ -17,6 +18,7 @@
1718
from oidcendpoint.exception import MultipleUsage
1819
from oidcendpoint.exception import NotForMe
1920
from oidcendpoint.exception import UnknownClient
21+
from oidcendpoint.util import importer
2022

2123
logger = logging.getLogger(__name__)
2224

@@ -53,7 +55,17 @@ def verify(self, **kwargs):
5355
:param kwargs:
5456
:return:
5557
"""
56-
raise NotImplementedError
58+
raise NotImplementedError()
59+
60+
def is_usable(self, request=None, authorization_info=None):
61+
"""
62+
Verify that this authentication method is applicable.
63+
64+
:param request: The request
65+
:param authorization_info: Other authorization information
66+
:return: True/False
67+
"""
68+
raise NotImplementedError()
5769

5870

5971
def basic_authn(authn):
@@ -76,14 +88,17 @@ class ClientSecretBasic(ClientAuthnMethod):
7688
Server, authenticate with the Authorization Server in accordance with
7789
Section 3.2.1 of OAuth 2.0 [RFC6749] using HTTP Basic authentication scheme.
7890
"""
91+
tag = "client_secret_basic"
7992

80-
def verify(self, request, authorization_info, **kwargs):
93+
def is_usable(self, request=None, authorization_info=None):
94+
if authorization_info is not None and authorization_info.startswith("Basic "):
95+
return True
96+
return False
97+
98+
def verify(self, authorization_info, **kwargs):
8199
client_info = basic_authn(authorization_info)
82100

83-
if (
84-
self.endpoint_context.cdb[client_info["id"]]["client_secret"]
85-
== client_info["secret"]
86-
):
101+
if self.endpoint_context.cdb[client_info["id"]]["client_secret"] == client_info["secret"]:
87102
return {"client_id": client_info["id"]}
88103
else:
89104
raise AuthnFailure()
@@ -96,12 +111,18 @@ class ClientSecretPost(ClientSecretBasic):
96111
Section 3.2.1 of OAuth 2.0 [RFC6749] by including the Client Credentials in
97112
the request body.
98113
"""
114+
tag = "client_secret_post"
115+
116+
def is_usable(self, request=None, authorization_info=None):
117+
if request is None:
118+
return False
119+
if "client_id" in request and "client_secret" in request:
120+
return True
121+
return False
99122

100123
def verify(self, request, **kwargs):
101-
if (
102-
self.endpoint_context.cdb[request["client_id"]]["client_secret"]
103-
== request["client_secret"]
104-
):
124+
if self.endpoint_context.cdb[request["client_id"]]["client_secret"] == request[
125+
"client_secret"]:
105126
return {"client_id": request["client_id"]}
106127
else:
107128
raise AuthnFailure("secrets doesn't match")
@@ -110,38 +131,70 @@ def verify(self, request, **kwargs):
110131
class BearerHeader(ClientSecretBasic):
111132
"""
112133
"""
134+
tag = "bearer_header"
113135

114-
def verify(self, request, authorization_info, **kwargs):
115-
if not authorization_info.startswith("Bearer "):
116-
raise AuthnFailure("Wrong type of authorization token")
136+
def is_usable(self, request=None, authorization_info=None):
137+
if authorization_info is not None and authorization_info.startswith("Bearer "):
138+
return True
139+
return False
117140

141+
def verify(self, authorization_info, **kwargs):
118142
return {"token": authorization_info.split(" ", 1)[1]}
119143

120144

121145
class BearerBody(ClientSecretPost):
122146
"""
123147
Same as Client Secret Post
124148
"""
149+
tag = "bearer_body"
150+
151+
def is_usable(self, request=None, authorization_info=None):
152+
if request is not None and "access_token" in request:
153+
return True
154+
return False
125155

126156
def verify(self, request, **kwargs):
127-
try:
128-
return {"token": request["access_token"]}
129-
except KeyError:
157+
_token = request.get("access_token")
158+
if _token is None:
130159
raise AuthnFailure("No access token")
131160

161+
res = {"token": _token}
162+
_client_id = request.get("client_id")
163+
if _client_id:
164+
res["client_id"] = _client_id
165+
return res
166+
132167

133168
class JWSAuthnMethod(ClientAuthnMethod):
134-
def verify(self, request, **kwargs):
135-
_jwt = JWT(self.endpoint_context.keyjar)
169+
def is_usable(self, request=None, authorization_info=None):
170+
if request is None:
171+
return False
172+
if "client_assertion" in request:
173+
return True
174+
return False
175+
176+
def verify(self, request, key_type, **kwargs):
177+
_jwt = JWT(self.endpoint_context.keyjar, msg_cls=JsonWebToken)
136178
try:
137179
ca_jwt = _jwt.unpack(request["client_assertion"])
138180
except (Invalid, MissingKey, BadSignature) as err:
139181
logger.info("%s" % sanitize(err))
140182
raise AuthnFailure("Could not verify client_assertion.")
141183

142-
authtoken = sanitize(ca_jwt)
143-
if hasattr(ca_jwt, "to_dict") and callable(ca_jwt, "to_dict"):
144-
authtoken = sanitize(ca_jwt.to_dict())
184+
_sign_alg = ca_jwt.jws_header.get("alg")
185+
if _sign_alg and _sign_alg.startswith("HS"):
186+
if key_type == "private_key":
187+
raise AttributeError("Wrong key type")
188+
keys = self.endpoint_context.keyjar.get("sig", 'oct', ca_jwt["iss"],
189+
ca_jwt.jws_header.get("kid"))
190+
_secret = self.endpoint_context.cdb[ca_jwt["iss"]].get('client_secret')
191+
if _secret and keys[0].key != as_bytes(_secret):
192+
raise AttributeError("Oct key used for signing not client_secret")
193+
else:
194+
if key_type == "client_secret":
195+
raise AttributeError("Wrong key type")
196+
197+
authtoken = sanitize(ca_jwt.to_dict())
145198
logger.debug("authntoken: {}".format(authtoken))
146199

147200
_endpoint = kwargs.get("endpoint")
@@ -179,12 +232,28 @@ class ClientSecretJWT(JWSAuthnMethod):
179232
The HMAC (Hash-based Message Authentication Code) is calculated using the
180233
bytes of the UTF-8 representation of the client_secret as the shared key.
181234
"""
235+
tag = "client_secret_jwt"
236+
237+
def verify(self, request=None, **kwargs):
238+
res = JWSAuthnMethod.verify(self, request, key_type="client_secret",
239+
**kwargs)
240+
# Verify that a HS alg was used
241+
res['method'] = self.tag
242+
return res
182243

183244

184245
class PrivateKeyJWT(JWSAuthnMethod):
185246
"""
186247
Clients that have registered a public key sign a JWT using that key.
187248
"""
249+
tag = "private_key_jwt"
250+
251+
def verify(self, request=None, **kwargs):
252+
res = JWSAuthnMethod.verify(self, request, key_type="private_key",
253+
**kwargs)
254+
# Verify that an RS or ES alg was used
255+
res['method'] = self.tag
256+
return res
188257

189258

190259
CLIENT_AUTHN_METHOD = {
@@ -208,9 +277,12 @@ def valid_client_info(cinfo):
208277

209278

210279
def verify_client(
211-
endpoint_context, request, authorization_info=None, get_client_id_from_token=None,
212-
endpoint=None, also_known_as=None
213-
):
280+
endpoint_context,
281+
request,
282+
authorization_info=None,
283+
get_client_id_from_token=None,
284+
endpoint=None,
285+
also_known_as=None):
214286
"""
215287
Initiated Guessing !
216288
@@ -229,34 +301,28 @@ def verify_client(
229301
strings_parade = ("{} {}".format(k, v) for k, v in authorization_info.items())
230302
authorization_info = " ".join(strings_parade)
231303

232-
if authorization_info is None:
233-
if "client_id" in request and "client_secret" in request:
234-
auth_info = ClientSecretPost(endpoint_context).verify(request)
235-
auth_info["method"] = "client_secret_post"
236-
elif "client_assertion" in request:
237-
auth_info = JWSAuthnMethod(endpoint_context).verify(request, endpoint=endpoint)
238-
# If symmetric key was used
239-
# auth_method = 'client_secret_jwt'
240-
# If asymmetric key was used
241-
auth_info["method"] = "private_key_jwt"
242-
elif "access_token" in request:
243-
auth_info = BearerBody(endpoint_context).verify(request)
244-
auth_info["method"] = "bearer_body"
245-
else:
246-
raise UnknownOrNoAuthnMethod()
247-
else:
248-
if authorization_info.startswith("Basic "):
249-
auth_info = ClientSecretBasic(endpoint_context).verify(
250-
request, authorization_info
251-
)
252-
auth_info["method"] = "client_secret_basic"
253-
elif authorization_info.startswith("Bearer "):
254-
auth_info = BearerHeader(endpoint_context).verify(
255-
request, authorization_info
256-
)
257-
auth_info["method"] = "bearer_header"
258-
else:
259-
raise UnknownOrNoAuthnMethod(authorization_info)
304+
auth_info = {}
305+
_methods = []
306+
if endpoint:
307+
try:
308+
_methods = endpoint_context.endpoint[endpoint].client_authn_method
309+
except AttributeError:
310+
pass
311+
312+
for _method in _methods:
313+
if _method.is_usable(request, authorization_info):
314+
try:
315+
auth_info = _method.verify(request=request, authorization_info=authorization_info,
316+
endpoint=endpoint)
317+
except Exception as err:
318+
logger.warning("Verifying auth using {} failed: {}".format(_method.tag, err))
319+
else:
320+
if "method" not in auth_info:
321+
auth_info["method"] = _method.tag
322+
break
323+
324+
if not auth_info:
325+
return auth_info
260326

261327
if also_known_as:
262328
client_id = also_known_as[auth_info.get("client_id")]
@@ -303,3 +369,16 @@ def verify_client(
303369
raise ValueError("Unknown token")
304370

305371
return auth_info
372+
373+
374+
def client_auth_setup(auth_set, endpoint_context):
375+
res = []
376+
377+
for item in auth_set:
378+
_cls = CLIENT_AUTHN_METHOD.get(item)
379+
if _cls:
380+
res.append(_cls(endpoint_context))
381+
else:
382+
res.append(importer(item)(endpoint_context))
383+
384+
return res

src/oidcendpoint/endpoint.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111

1212
from oidcendpoint import sanitize
1313
from oidcendpoint.client_authn import UnknownOrNoAuthnMethod
14-
from oidcendpoint.client_authn import WrongAuthnMethod
14+
from oidcendpoint.client_authn import client_auth_setup
1515
from oidcendpoint.client_authn import verify_client
16+
from oidcendpoint.exception import UnAuthorizedClient
1617
from oidcendpoint.util import OAUTH2_NOCACHE_HEADERS
1718

1819
__author__ = "Roland Hedberg"
@@ -176,13 +177,13 @@ def __init__(self, endpoint_context, **kwargs):
176177
if _val:
177178
setattr(self, param, _val)
178179

179-
if "client_authn_method" in kwargs:
180-
self.client_authn_method = kwargs["client_authn_method"]
181-
elif self.default_capabilities is not None:
182-
if "client_authn_method" in self.default_capabilities:
183-
self.client_authn_method = self.default_capabilities[
184-
"client_authn_method"
185-
]
180+
_methods = kwargs.get("client_authn_method")
181+
if _methods:
182+
self.client_authn_method = client_auth_setup(_methods, endpoint_context)
183+
elif self.default_capabilities:
184+
_methods = self.default_capabilities.get("client_authn_method")
185+
if _methods:
186+
self.client_authn_method = client_auth_setup(_methods, endpoint_context)
186187

187188
self.endpoint_info = construct_endpoint_info(
188189
self.default_capabilities, **kwargs
@@ -284,10 +285,8 @@ def client_authentication(self, request, auth=None, **kwargs):
284285
else:
285286
raise
286287

287-
if authn_info["method"] not in self.client_authn_method:
288-
LOGGER.error("Wrong client authentication method was used: %s",
289-
authn_info["method"])
290-
raise WrongAuthnMethod("Wrong authn method: {}".format(authn_info["method"]))
288+
if authn_info == {} and self.client_authn_method and len(self.client_authn_method):
289+
raise UnAuthorizedClient("Authorization failed")
291290

292291
return authn_info
293292

src/oidcendpoint/oidc/userinfo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def parse_request(self, request, auth=None, **kwargs):
141141
request = {}
142142

143143
# Verify that the client is allowed to do this
144-
auth_info = self.client_authentication(request, auth, **kwargs)
144+
auth_info = self.client_authentication(request, auth, endpoint="userinfo", **kwargs)
145145
if isinstance(auth_info, ResponseMessage):
146146
return auth_info
147147
else:
File renamed without changes.

tests/test_25_oidc_token_endpoint.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from oidcendpoint.client_authn import verify_client
1010
from oidcendpoint.endpoint_context import EndpointContext
1111
from oidcendpoint.exception import MultipleUsage
12+
from oidcendpoint.exception import UnAuthorizedClient
1213
from oidcendpoint.oidc import userinfo
1314
from oidcendpoint.oidc.authorization import Authorization
1415
from oidcendpoint.oidc.provider_config import ProviderConfiguration
@@ -249,6 +250,6 @@ def test_process_request_using_private_key_jwt(self):
249250
_resp = self.endpoint.process_request(request=_req)
250251

251252
# 2nd time used
252-
with pytest.raises(MultipleUsage):
253+
with pytest.raises(UnAuthorizedClient):
253254
self.endpoint.parse_request(_token_request)
254255

tests/test_31_introspection.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from oidcendpoint.client_authn import WrongAuthnMethod
1818
from oidcendpoint.client_authn import verify_client
1919
from oidcendpoint.endpoint_context import EndpointContext
20+
from oidcendpoint.exception import UnAuthorizedClient
2021
from oidcendpoint.oauth2.authorization import Authorization
2122
from oidcendpoint.oauth2.introspection import Introspection
2223
from oidcendpoint.oidc.token_coop import TokenCoop
@@ -110,7 +111,7 @@ def create_endpoint(self):
110111
"class": Introspection,
111112
"kwargs": {
112113
"release": ["username"],
113-
"client_authn_method": {"client_secret_post": ClientSecretPost},
114+
"client_authn_method": ["client_secret_post"],
114115
},
115116
},
116117
"token": {
@@ -175,7 +176,7 @@ def test_parse_no_authn(self):
175176
self.introspection_endpoint.endpoint_context, AUTH_REQ, uid="diana"
176177
)
177178
_token = self._create_jwt("diana")
178-
with pytest.raises(UnknownOrNoAuthnMethod):
179+
with pytest.raises(UnAuthorizedClient):
179180
self.introspection_endpoint.parse_request({"token": _token})
180181

181182
def test_parse_with_client_auth_in_req(self):
@@ -203,7 +204,7 @@ def test_parse_with_wrong_client_authn(self):
203204
_basic_token = as_unicode(base64.b64encode(as_bytes(_basic_token)))
204205
_basic_authz = "Basic {}".format(_basic_token)
205206

206-
with pytest.raises(WrongAuthnMethod):
207+
with pytest.raises(UnAuthorizedClient):
207208
self.introspection_endpoint.parse_request({"token": _token}, _basic_authz)
208209

209210
def test_process_request(self):

0 commit comments

Comments
 (0)