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

Commit b8cb833

Browse files
authored
Merge branch 'master' into refactor-introspection
2 parents fe8f38f + f554b7d commit b8cb833

16 files changed

+232
-119
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def run_tests(self):
6666
'quality': ['pylama', 'isort', 'eradicate', 'mypy', 'black', 'bandit'],
6767
},
6868
install_requires=[
69-
"oidcmsg>=0.6.8",
69+
"oidcmsg>=0.6.9",
7070
"jinja2",
7171
"pyyaml",
7272
"requests",

src/oidcendpoint/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
except ImportError:
77
import random as rnd
88

9-
__version__ = "0.12.3"
9+
__version__ = "0.13.1"
1010

1111

1212
DEF_SIGN_ALG = {

src/oidcendpoint/client_authn.py

Lines changed: 138 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import base64
22
import logging
3+
from urllib.parse import unquote_plus
34

45
from cryptojwt.exception import BadSignature
56
from cryptojwt.exception import Invalid
@@ -8,6 +9,7 @@
89
from cryptojwt.jwt import utc_time_sans_frac
910
from cryptojwt.utils import as_bytes
1011
from cryptojwt.utils import as_unicode
12+
from oidcmsg.oidc import JsonWebToken
1113
from oidcmsg.oidc import verified_claim_name
1214

1315
from oidcendpoint import JWT_BEARER
@@ -16,6 +18,7 @@
1618
from oidcendpoint.exception import MultipleUsage
1719
from oidcendpoint.exception import NotForMe
1820
from oidcendpoint.exception import UnknownClient
21+
from oidcendpoint.util import importer
1922

2023
logger = logging.getLogger(__name__)
2124

@@ -52,7 +55,17 @@ def verify(self, **kwargs):
5255
:param kwargs:
5356
:return:
5457
"""
55-
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()
5669

5770

5871
def basic_authn(authn):
@@ -62,7 +75,7 @@ def basic_authn(authn):
6275
_tok = as_bytes(authn[6:])
6376
# Will raise ValueError type exception if not base64 encoded
6477
_tok = base64.b64decode(_tok)
65-
part = as_unicode(_tok).split(":")
78+
part = [unquote_plus(p) for p in as_unicode(_tok).split(":")]
6679
if len(part) == 2:
6780
return dict(zip(["id", "secret"], part))
6881
else:
@@ -75,14 +88,17 @@ class ClientSecretBasic(ClientAuthnMethod):
7588
Server, authenticate with the Authorization Server in accordance with
7689
Section 3.2.1 of OAuth 2.0 [RFC6749] using HTTP Basic authentication scheme.
7790
"""
91+
tag = "client_secret_basic"
92+
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
7897

79-
def verify(self, request, authorization_info, **kwargs):
98+
def verify(self, authorization_info, **kwargs):
8099
client_info = basic_authn(authorization_info)
81100

82-
if (
83-
self.endpoint_context.cdb[client_info["id"]]["client_secret"]
84-
== client_info["secret"]
85-
):
101+
if self.endpoint_context.cdb[client_info["id"]]["client_secret"] == client_info["secret"]:
86102
return {"client_id": client_info["id"]}
87103
else:
88104
raise AuthnFailure()
@@ -95,12 +111,18 @@ class ClientSecretPost(ClientSecretBasic):
95111
Section 3.2.1 of OAuth 2.0 [RFC6749] by including the Client Credentials in
96112
the request body.
97113
"""
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
98122

99123
def verify(self, request, **kwargs):
100-
if (
101-
self.endpoint_context.cdb[request["client_id"]]["client_secret"]
102-
== request["client_secret"]
103-
):
124+
if self.endpoint_context.cdb[request["client_id"]]["client_secret"] == request[
125+
"client_secret"]:
104126
return {"client_id": request["client_id"]}
105127
else:
106128
raise AuthnFailure("secrets doesn't match")
@@ -109,38 +131,70 @@ def verify(self, request, **kwargs):
109131
class BearerHeader(ClientSecretBasic):
110132
"""
111133
"""
134+
tag = "bearer_header"
112135

113-
def verify(self, request, authorization_info, **kwargs):
114-
if not authorization_info.startswith("Bearer "):
115-
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
116140

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

119144

120145
class BearerBody(ClientSecretPost):
121146
"""
122147
Same as Client Secret Post
123148
"""
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
124155

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

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+
131167

132168
class JWSAuthnMethod(ClientAuthnMethod):
133-
def verify(self, request, **kwargs):
134-
_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)
135178
try:
136179
ca_jwt = _jwt.unpack(request["client_assertion"])
137180
except (Invalid, MissingKey, BadSignature) as err:
138181
logger.info("%s" % sanitize(err))
139182
raise AuthnFailure("Could not verify client_assertion.")
140183

141-
authtoken = sanitize(ca_jwt)
142-
if hasattr(ca_jwt, "to_dict") and callable(ca_jwt, "to_dict"):
143-
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())
144198
logger.debug("authntoken: {}".format(authtoken))
145199

146200
_endpoint = kwargs.get("endpoint")
@@ -178,12 +232,28 @@ class ClientSecretJWT(JWSAuthnMethod):
178232
The HMAC (Hash-based Message Authentication Code) is calculated using the
179233
bytes of the UTF-8 representation of the client_secret as the shared key.
180234
"""
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
181243

182244

183245
class PrivateKeyJWT(JWSAuthnMethod):
184246
"""
185247
Clients that have registered a public key sign a JWT using that key.
186248
"""
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
187257

188258

189259
CLIENT_AUTHN_METHOD = {
@@ -207,9 +277,12 @@ def valid_client_info(cinfo):
207277

208278

209279
def verify_client(
210-
endpoint_context, request, authorization_info=None, get_client_id_from_token=None,
211-
endpoint=None, also_known_as=None
212-
):
280+
endpoint_context,
281+
request,
282+
authorization_info=None,
283+
get_client_id_from_token=None,
284+
endpoint=None,
285+
also_known_as=None):
213286
"""
214287
Initiated Guessing !
215288
@@ -228,34 +301,28 @@ def verify_client(
228301
strings_parade = ("{} {}".format(k, v) for k, v in authorization_info.items())
229302
authorization_info = " ".join(strings_parade)
230303

231-
if authorization_info is None:
232-
if "client_id" in request and "client_secret" in request:
233-
auth_info = ClientSecretPost(endpoint_context).verify(request)
234-
auth_info["method"] = "client_secret_post"
235-
elif "client_assertion" in request:
236-
auth_info = JWSAuthnMethod(endpoint_context).verify(request, endpoint=endpoint)
237-
# If symmetric key was used
238-
# auth_method = 'client_secret_jwt'
239-
# If asymmetric key was used
240-
auth_info["method"] = "private_key_jwt"
241-
elif "access_token" in request:
242-
auth_info = BearerBody(endpoint_context).verify(request)
243-
auth_info["method"] = "bearer_body"
244-
else:
245-
raise UnknownOrNoAuthnMethod()
246-
else:
247-
if authorization_info.startswith("Basic "):
248-
auth_info = ClientSecretBasic(endpoint_context).verify(
249-
request, authorization_info
250-
)
251-
auth_info["method"] = "client_secret_basic"
252-
elif authorization_info.startswith("Bearer "):
253-
auth_info = BearerHeader(endpoint_context).verify(
254-
request, authorization_info
255-
)
256-
auth_info["method"] = "bearer_header"
257-
else:
258-
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
259326

260327
if also_known_as:
261328
client_id = also_known_as[auth_info.get("client_id")]
@@ -280,19 +347,14 @@ def verify_client(
280347

281348
# store what authn method was used
282349
if auth_info.get("method"):
283-
if (
284-
endpoint_context.cdb[client_id].get("auth_method")
285-
and request.__class__.__name__
286-
in endpoint_context.cdb[client_id]["auth_method"]
287-
):
288-
endpoint_context.cdb[client_id]["auth_method"][
289-
request.__class__.__name__
290-
] = auth_info["method"]
350+
_request_type = request.__class__.__name__
351+
_used_authn_method = endpoint_context.cdb[client_id].get("auth_method")
352+
if _used_authn_method:
353+
endpoint_context.cdb[client_id]["auth_method"][_request_type] = auth_info["method"]
291354
else:
292355
endpoint_context.cdb[client_id]["auth_method"] = {
293-
request.__class__.__name__: auth_info["method"]
356+
_request_type: auth_info["method"]
294357
}
295-
296358
elif not client_id and get_client_id_from_token:
297359
if not _token:
298360
logger.warning("No token")
@@ -307,3 +369,16 @@ def verify_client(
307369
raise ValueError("Unknown token")
308370

309371
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/common/authorization.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import logging
22
from urllib.parse import parse_qs
3-
from urllib.parse import splitquery
43
from urllib.parse import unquote
54
from urllib.parse import urlencode
65
from urllib.parse import urlparse
@@ -15,6 +14,7 @@
1514
from oidcendpoint.exception import RedirectURIError
1615
from oidcendpoint.exception import UnknownClient
1716
from oidcendpoint.user_info import SCOPE2CLAIMS
17+
from oidcendpoint.util import split_uri
1818

1919
logger = logging.getLogger(__name__)
2020

@@ -76,9 +76,9 @@ def verify_uri(endpoint_context, request, uri_type, client_id=None):
7676
if part.fragment:
7777
raise URIError("Contains fragment")
7878

79-
(_base, _query) = splitquery(_redirect_uri)
80-
if _query:
81-
_query = parse_qs(_query)
79+
(_base, _query) = split_uri(_redirect_uri)
80+
# if _query:
81+
# _query = parse_qs(_query)
8282

8383
match = False
8484
# Get the clients registered redirect uris

0 commit comments

Comments
 (0)