Skip to content

Commit 55d16fc

Browse files
authored
Merge pull request #70 from IdentityPython/stand_alone_client
Stand alone client
2 parents 63f6ed3 + 2669489 commit 55d16fc

38 files changed

+3746
-844
lines changed

example/flask_rp/views.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ def repost_fragment():
186186
return finalize(op_identifier, args)
187187

188188

189-
@oidc_rp_views.route('/authz_im_cb')
189+
@oidc_rp_views.route('/authz_tok_cb')
190190
def authz_im_cb(op_identifier='', **kwargs):
191191
logger.debug('implicit_hybrid_flow kwargs: {}'.format(kwargs))
192192
return render_template('repost_fragment.html', op_identifier=op_identifier)
@@ -244,9 +244,10 @@ def session_logout(op_identifier):
244244
@oidc_rp_views.route('/logout')
245245
def logout():
246246
logger.debug('logout')
247-
_info = current_app.rph.logout(state=session['state'])
248-
logger.debug('logout redirect to "{}"'.format(_info['url']))
249-
return redirect(_info['url'], 303)
247+
_request_info = current_app.rph.logout(state=session['state'])
248+
_url = _request_info["url"]
249+
logger.debug(f'logout redirect to "{_url}"')
250+
return redirect(_url, 303)
250251

251252

252253
@oidc_rp_views.route('/bc_logout/<op_identifier>', methods=['GET', 'POST'])

src/idpyoidc/client/claims/transform.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"subject_type": "subject_types_supported",
2222
"token_endpoint_auth_method": "token_endpoint_auth_methods_supported",
2323
"response_types": "response_types_supported",
24+
"response_modes": "response_modes_supported",
2425
"grant_types": "grant_types_supported",
2526
# In OAuth2 but not in OIDC
2627
"scope": "scopes_supported",

src/idpyoidc/client/current.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,14 @@ def get_set(
5656
) -> dict:
5757
"""
5858
59-
@param key: The key to a seet of current claims
59+
@param key: The key to a set of current claims
6060
@param message: A message class
6161
@param claim: A list of claims
6262
@return: Dictionary
63+
@raise KeyError if no such key
6364
"""
6465

65-
try:
66-
_current = self.get(key)
67-
except KeyError:
68-
return {}
66+
_current = self.get(key)
6967

7068
if message:
7169
_res = {k: _current[k] for k in message.c_param.keys() if k in _current}

src/idpyoidc/client/defaults.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,7 @@
3131
"response_types": [
3232
"code",
3333
"id_token",
34-
"id_token token",
3534
"code id_token",
36-
"code id_token token",
37-
"code token",
3835
],
3936
"token_endpoint_auth_method": "client_secret_basic",
4037
"scopes_supported": ["openid"],
@@ -48,6 +45,7 @@
4845
# Using PKCE is default
4946
DEFAULT_CLIENT_CONFIGS = {
5047
"": {
48+
"client_type": "oidc",
5149
"preference": DEFAULT_CLIENT_PREFERENCES,
5250
"add_ons": {
5351
"pkce": {
@@ -71,6 +69,8 @@
7169
}
7270

7371
OIDCONF_PATTERN = "{}/.well-known/openid-configuration"
72+
OAUTH2_SERVER_METADATA_URL = "{}/.well-known/oauth-authorization-server"
73+
7474
CC_METHOD = {
7575
"S256": hashlib.sha256,
7676
"S384": hashlib.sha384,
@@ -92,3 +92,13 @@
9292
SAML2_BEARER_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:saml2-bearer"
9393

9494
BASECHR = string.ascii_letters + string.digits
95+
96+
DEFAULT_RESPONSE_MODE = {
97+
"code": "query",
98+
"id_token": "fragment",
99+
"token": "fragment",
100+
"code token": "fragment",
101+
"code id_token": "fragment",
102+
"id_token token": "fragment",
103+
"code id_token token": "fragment",
104+
}

src/idpyoidc/client/oauth2/__init__.py

Lines changed: 59 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from json import JSONDecodeError
21
import logging
2+
from json import JSONDecodeError
33
from typing import Callable
44
from typing import Optional
55
from typing import Union
@@ -12,8 +12,8 @@
1212
from idpyoidc.client.exception import OidcServiceError
1313
from idpyoidc.client.exception import ParseError
1414
from idpyoidc.client.service import REQUEST_INFO
15-
from idpyoidc.client.service import SUCCESSFUL
1615
from idpyoidc.client.service import Service
16+
from idpyoidc.client.service import SUCCESSFUL
1717
from idpyoidc.client.util import do_add_ons
1818
from idpyoidc.client.util import get_deserialization_method
1919
from idpyoidc.configure import Configuration
@@ -26,8 +26,6 @@
2626

2727
logger = logging.getLogger(__name__)
2828

29-
Version = "2.0"
30-
3129

3230
class ExpiredToken(Exception):
3331
pass
@@ -40,20 +38,20 @@ class Client(Entity):
4038
client_type = "oauth2"
4139

4240
def __init__(
43-
self,
44-
keyjar: Optional[KeyJar] = None,
45-
config: Optional[Union[dict, Configuration]] = None,
46-
services: Optional[dict] = None,
47-
httpc: Optional[Callable] = None,
48-
httpc_params: Optional[dict] = None,
49-
context: Optional[OidcContext] = None,
50-
upstream_get: Optional[Callable] = None,
51-
key_conf: Optional[dict] = None,
52-
entity_id: Optional[str] = "",
53-
verify_ssl: Optional[bool] = True,
54-
jwks_uri: Optional[str] = "",
55-
client_type: Optional[str] = "",
56-
**kwargs
41+
self,
42+
keyjar: Optional[KeyJar] = None,
43+
config: Optional[Union[dict, Configuration]] = None,
44+
services: Optional[dict] = None,
45+
httpc: Optional[Callable] = None,
46+
httpc_params: Optional[dict] = None,
47+
context: Optional[OidcContext] = None,
48+
upstream_get: Optional[Callable] = None,
49+
key_conf: Optional[dict] = None,
50+
entity_id: Optional[str] = "",
51+
verify_ssl: Optional[bool] = True,
52+
jwks_uri: Optional[str] = "",
53+
client_type: Optional[str] = "",
54+
**kwargs
5755
):
5856
"""
5957
@@ -70,7 +68,11 @@ def __init__(
7068
:return: Client instance
7169
"""
7270

73-
if not client_type:
71+
if client_type:
72+
self.client_type = client_type
73+
elif config and 'client_type' in config:
74+
client_type = self.client_type = config["client_type"]
75+
else:
7476
client_type = self.client_type
7577

7678
if verify_ssl is False:
@@ -80,6 +82,8 @@ def __init__(
8082
else:
8183
httpc_params = {"verify": False}
8284

85+
jwks_uri = jwks_uri or config.get('jwks_uri', '')
86+
8387
Entity.__init__(
8488
self,
8589
keyjar=keyjar,
@@ -106,12 +110,12 @@ def __init__(
106110
do_add_ons(_add_ons, self._service)
107111

108112
def do_request(
109-
self,
110-
request_type: str,
111-
response_body_type: Optional[str] = "",
112-
request_args: Optional[dict] = None,
113-
behaviour_args: Optional[dict] = None,
114-
**kwargs
113+
self,
114+
request_type: str,
115+
response_body_type: Optional[str] = "",
116+
request_args: Optional[dict] = None,
117+
behaviour_args: Optional[dict] = None,
118+
**kwargs
115119
):
116120
_srv = self._service[request_type]
117121

@@ -134,14 +138,14 @@ def set_client_id(self, client_id):
134138
self.get_context().set("client_id", client_id)
135139

136140
def get_response(
137-
self,
138-
service: Service,
139-
url: str,
140-
method: Optional[str] = "GET",
141-
body: Optional[dict] = None,
142-
response_body_type: Optional[str] = "",
143-
headers: Optional[dict] = None,
144-
**kwargs
141+
self,
142+
service: Service,
143+
url: str,
144+
method: Optional[str] = "GET",
145+
body: Optional[dict] = None,
146+
response_body_type: Optional[str] = "",
147+
headers: Optional[dict] = None,
148+
**kwargs
145149
):
146150
"""
147151
@@ -177,14 +181,14 @@ def get_response(
177181
return self.parse_request_response(service, resp, response_body_type, **kwargs)
178182

179183
def service_request(
180-
self,
181-
service: Service,
182-
url: str,
183-
method: Optional[str] = "GET",
184-
body: Optional[dict] = None,
185-
response_body_type: Optional[str] = "",
186-
headers: Optional[dict] = None,
187-
**kwargs
184+
self,
185+
service: Service,
186+
url: str,
187+
method: Optional[str] = "GET",
188+
body: Optional[dict] = None,
189+
response_body_type: Optional[str] = "",
190+
headers: Optional[dict] = None,
191+
**kwargs
188192
) -> Message:
189193
"""
190194
The method that sends the request and handles the response returned.
@@ -312,17 +316,20 @@ def dynamic_provider_info_discovery(client: Client, behaviour_args: Optional[dic
312316
:param behaviour_args:
313317
:param client: A :py:class:`idpyoidc.client.oidc.Client` instance
314318
"""
319+
320+
if client.client_type == 'oidc' and client.get_service("provider_info"):
321+
service = 'provider_info'
322+
elif client.client_type == 'oauth2' and client.get_service('server_metadata'):
323+
service = 'server_metadata'
324+
else:
325+
raise ConfigurationError("Can not do dynamic provider info discovery")
326+
327+
_context = client.get_context()
315328
try:
316-
client.get_service("provider_info")
329+
_context.set("issuer", _context.config["srv_discovery_url"])
317330
except KeyError:
318-
raise ConfigurationError("Can not do dynamic provider info discovery")
319-
else:
320-
_context = client.get_context()
321-
try:
322-
_context.set("issuer", _context.config["srv_discovery_url"])
323-
except KeyError:
324-
pass
331+
pass
325332

326-
response = client.do_request("provider_info", behaviour_args=behaviour_args)
327-
if is_error_message(response):
328-
raise OidcServiceError(response["error"])
333+
response = client.do_request(service, behaviour_args=behaviour_args)
334+
if is_error_message(response):
335+
raise OidcServiceError(response["error"])

src/idpyoidc/client/oauth2/authorization.py

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ class Authorization(Service):
4040
}
4141

4242
_callback_path = {
43-
"redirect_uris": { # based on response_types
44-
"code": "authz_cb",
45-
"implicit": "authz_im_cb",
43+
"redirect_uris": { # based on response_mode
44+
"query": "authz_cb",
45+
"fragment": "authz_im_cb",
4646
# "form_post": "form"
4747
}
4848
}
@@ -100,14 +100,21 @@ def post_parse_response(self, response, **kwargs):
100100
pass
101101
return response
102102

103-
def _do_flow(self, flow_type, response_types):
104-
if flow_type == "code":
103+
def _do_flow(self, flow_type, response_types, context) -> str:
104+
if flow_type == "query":
105105
if "code" in response_types:
106-
return True
107-
elif flow_type in ["implicit", "hybrid"]:
106+
return "query"
107+
elif flow_type == "fragment":
108108
if implicit_response_types(response_types):
109-
return True
110-
return False
109+
return "fragment"
110+
elif flow_type == 'form_post':
111+
rm = context.get_preference('response_modes_supported')
112+
if rm and 'form_post' in rm:
113+
if context.config.conf.get("separate_form_post_cb", True):
114+
return "form_post"
115+
else:
116+
return "query"
117+
return ''
111118

112119
def _do_redirect_uris(self, base_url, hex, context, callback_uris, response_types):
113120
_redirect_uris = context.get_preference("redirect_uris", [])
@@ -116,23 +123,27 @@ def _do_redirect_uris(self, base_url, hex, context, callback_uris, response_type
116123
# the same redirect_uris for all flow types
117124
callback_uris["redirect_uris"] = {}
118125
for flow_type in self._callback_path["redirect_uris"].keys():
119-
if self._do_flow(flow_type, response_types):
126+
if self._do_flow(flow_type, response_types, context):
120127
callback_uris["redirect_uris"][flow_type] = _redirect_uris
121128
elif callback_uris:
122129
if "redirect_uris" in callback_uris:
123130
pass
124131
else:
125132
callback_uris["redirect_uris"] = {}
126-
for flow_type, path in self._callback_path["redirect_uris"].items():
127-
if self._do_flow(flow_type, response_types):
133+
for flow_type in self._callback_path["redirect_uris"].keys():
134+
_var = self._do_flow(flow_type, response_types, context)
135+
if _var:
136+
_path = self._callback_path["redirect_uris"][_var]
128137
callback_uris["redirect_uris"][flow_type] = [
129-
self.get_uri(base_url, path, hex)
138+
self.get_uri(base_url, _path, hex)
130139
]
131140
else:
132141
callback_uris["redirect_uris"] = {}
133-
for flow_type, path in self._callback_path["redirect_uris"].items():
134-
if self._do_flow(flow_type, response_types):
135-
callback_uris["redirect_uris"][flow_type] = [self.get_uri(base_url, path, hex)]
142+
for flow_type in self._callback_path["redirect_uris"].keys():
143+
_var = self._do_flow(flow_type, response_types, context)
144+
if _var:
145+
_path = self._callback_path["redirect_uris"][_var]
146+
callback_uris["redirect_uris"][flow_type] = [self.get_uri(base_url, _path, hex)]
136147
return callback_uris
137148

138149
def construct_uris(

src/idpyoidc/client/oauth2/server_metadata.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from cryptojwt.key_jar import KeyJar
66

7+
from idpyoidc.client.defaults import OAUTH2_SERVER_METADATA_URL
78
from idpyoidc.client.defaults import OIDCONF_PATTERN
89
from idpyoidc.client.exception import OidcServiceError
910
from idpyoidc.client.service import Service
@@ -23,6 +24,7 @@ class ServerMetadata(Service):
2324
synchronous = True
2425
service_name = "server_metadata"
2526
http_method = "GET"
27+
url_pattern = OAUTH2_SERVER_METADATA_URL
2628

2729
_supports = {}
2830

@@ -41,9 +43,9 @@ def get_endpoint(self):
4143
_iss = self.endpoint
4244

4345
if _iss.endswith("/"):
44-
return OIDCONF_PATTERN.format(_iss[:-1])
46+
return self.url_pattern.format(_iss[:-1])
4547

46-
return OIDCONF_PATTERN.format(_iss)
48+
return self.url_pattern.format(_iss)
4749

4850
def get_request_parameters(self, method="GET", **kwargs):
4951
"""

0 commit comments

Comments
 (0)