Skip to content

Commit 7c8409d

Browse files
authored
Merge pull request #54 from IdentityPython/fedservice
Fedservice
2 parents 6623da6 + 949ebd2 commit 7c8409d

File tree

212 files changed

+9476
-5561
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

212 files changed

+9476
-5561
lines changed

archdoc/docs/client/index.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# The IdpyOIDC client
2+
3+
A client can send requests to an endpoint and deal with the response.
4+
5+
IdpyOIDC assumes that there is one Relying Party(RP)/Client instance per
6+
OpenID Connect Provider(OP)/Authorization Server (AS).
7+
8+
If you have a service that expects to talk to several OPs/ASs
9+
then you must use **idpyoidc.client.rp_handler.RPHandler** to manage the RPs.
10+
11+
RPHandler has methods like:
12+
- begin()
13+
- finalize()
14+
- refresh_access_token()
15+
- logout()
16+
17+
More about RPHandler at the end of this section.
18+
19+
## Client
20+
21+
A client is configured to talk to a set of services each of them represented by
22+
a Service Instance.
23+
24+
# Context
25+
26+
# Service
27+
28+
A Service instance is expected to be able to:
29+
30+
1. Collect all the request arguments
31+
2. If necessary collect and add authentication information to the request attributes or HTTP header
32+
3. Formats the message
33+
4. chooses HTTP method
34+
5. Add HTTP headers
35+
36+
and then after having received the response:
37+
38+
1. Parses the response
39+
2. Gather verification information and verify the response
40+
3. Do any special post-processing.
41+
3. Store information from the response
42+
43+
Doesn't matter which service is considered they all have to be able to do this.
44+
45+
## Request
46+
47+
## Response
48+
49+
# AddOn
50+
51+
# Endpoints
52+
53+
## OAuth2
54+
55+
- Access Token
56+
- Authorization
57+
- Refresh Access Token
58+
- Server Metadata
59+
- Token Exchange

archdoc/docs/combo/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# An entity that can act both as a server and a client

archdoc/docs/server/index.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
# Service
3+
4+
## Request
5+
6+
## Response
7+
8+
# Context
9+
10+
# AddOn

doc/server/contents/conf.rst

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,19 @@ An example::
411411
]
412412
}
413413
},
414+
"revocation": {
415+
"path": "revoke",
416+
"class": "idpyoidc.server.oauth2.revocation.Revocation",
417+
"kwargs": {
418+
"client_authn_method": [
419+
"client_secret_post",
420+
"client_secret_basic",
421+
"client_secret_jwt",
422+
"private_key_jwt",
423+
"bearer_header"
424+
]
425+
}
426+
},
414427
"end_session": {
415428
"path": "session",
416429
"class": "idpyoidc.server.oidc.session.Session",
@@ -875,6 +888,106 @@ For example::
875888
return request
876889

877890

891+
==============
892+
Token revocation
893+
==============
894+
895+
In order to enable the token revocation endpoint a dictionary with key `token_revocation` should be placed
896+
under the `endpoint` key of the configuration.
897+
898+
If present, the token revocation configuration should contain a `policy` dictionary
899+
that defines the behaviour for each token type. Each token type
900+
is mapped to a dictionary with the keys `callable` (mandatory), which must be a
901+
python callable or a string that represents the path to a python callable, and
902+
`kwargs` (optional), which must be a dict of key-value arguments that will be
903+
passed to the callable.
904+
905+
The key `""` represents a fallback policy that will be used if the token
906+
type can't be found. If a token type is defined in the `policy` but is
907+
not in the `token_types_supported` list then it is ignored.
908+
909+
"token_revocation": {
910+
"path": "revoke",
911+
"class": "idpyoidc.server.oauth2.token_revocation.TokenRevocation",
912+
"kwargs": {
913+
"token_types_supported": ["access_token"],
914+
"client_authn_method": [
915+
"client_secret_post",
916+
"client_secret_basic",
917+
"client_secret_jwt",
918+
"private_key_jwt",
919+
"bearer_header"
920+
],
921+
"policy": {
922+
"urn:ietf:params:oauth:token-type:access_token": {
923+
"callable": "/path/to/callable",
924+
"kwargs": {
925+
"audience": ["https://example.com"],
926+
"scopes": ["openid"]
927+
}
928+
},
929+
"urn:ietf:params:oauth:token-type:refresh_token": {
930+
"callable": "/path/to/callable",
931+
"kwargs": {
932+
"resource": ["https://example.com"],
933+
"scopes": ["openid"]
934+
}
935+
},
936+
"": {
937+
"callable": "/path/to/callable",
938+
"kwargs": {
939+
"scopes": ["openid"]
940+
}
941+
}
942+
}
943+
}
944+
}
945+
946+
For the per-client configuration a similar configuration scheme should be present in the client's
947+
metadata under the `token_revocation` key.
948+
949+
For example::
950+
951+
"token_revocation":{
952+
"token_types_supported": ["access_token"],
953+
"policy": {
954+
"urn:ietf:params:oauth:token-type:access_token": {
955+
"callable": "/path/to/callable",
956+
"kwargs": {
957+
"audience": ["https://example.com"],
958+
"scopes": ["openid"]
959+
}
960+
},
961+
"urn:ietf:params:oauth:token-type:refresh_token": {
962+
"callable": "/path/to/callable",
963+
"kwargs": {
964+
"resource": ["https://example.com"],
965+
"scopes": ["openid"]
966+
}
967+
},
968+
"": {
969+
"callable": "/path/to/callable",
970+
"kwargs": {
971+
"scopes": ["openid"]
972+
}
973+
}
974+
}
975+
}
976+
}
977+
978+
The policy callable accepts a specific argument list and handles the revocation appropriately and returns
979+
an :py:class:`idpyoidc.message.oauth2..TokenRevocationResponse` or raises an exception.
980+
981+
For example::
982+
983+
def custom_token_revocation_policy(token, session_info, **kwargs):
984+
if some_condition:
985+
return TokenErrorResponse(
986+
error="invalid_request", error_description="Some error occured"
987+
)
988+
response_args = {"response_args": {}}
989+
return oauth2.TokenRevocationResponse(**response_args)
990+
878991
==================================
879992
idpyoidc\.server\.configure module
880993
==================================

example/flask_op/views.py

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def verify(authn_method):
119119
auth_args = authn_method.unpack_token(kwargs['token'])
120120
authz_request = AuthorizationRequest().from_urlencoded(auth_args['query'])
121121

122-
endpoint = current_app.server.server_get("endpoint", 'authorization')
122+
endpoint = current_app.server.get_endpoint('authorization')
123123
_session_id = endpoint.create_session(authz_request, username, auth_args['authn_class_ref'],
124124
auth_args['iat'], authn_method)
125125

@@ -133,8 +133,7 @@ def verify(authn_method):
133133

134134
@oidc_op_views.route('/verify/user', methods=['GET', 'POST'])
135135
def verify_user():
136-
authn_method = current_app.server.server_get(
137-
"endpoint_context").authn_broker.get_method_by_id('user')
136+
authn_method = current_app.server.get_context().authn_broker.get_method_by_id('user')
138137
try:
139138
return verify(authn_method)
140139
except FailedAuthentication as exc:
@@ -143,8 +142,7 @@ def verify_user():
143142

144143
@oidc_op_views.route('/verify/user_pass_jinja', methods=['GET', 'POST'])
145144
def verify_user_pass_jinja():
146-
authn_method = current_app.server.server_get(
147-
"endpoint_context").authn_broker.get_method_by_id('user')
145+
authn_method = current_app.server.get_context().authn_broker.get_method_by_id('user')
148146
try:
149147
return verify(authn_method)
150148
except FailedAuthentication as exc:
@@ -154,9 +152,9 @@ def verify_user_pass_jinja():
154152
@oidc_op_views.route('/.well-known/<service>')
155153
def well_known(service):
156154
if service == 'openid-configuration':
157-
_endpoint = current_app.server.server_get("endpoint", 'provider_config')
155+
_endpoint = current_app.server.get_endpoint('provider_config')
158156
elif service == 'webfinger':
159-
_endpoint = current_app.server.server_get("endpoint", 'discovery')
157+
_endpoint = current_app.server.get_endpoint('discovery')
160158
else:
161159
return make_response('Not supported', 400)
162160

@@ -166,45 +164,45 @@ def well_known(service):
166164
@oidc_op_views.route('/registration', methods=['GET', 'POST'])
167165
def registration():
168166
return service_endpoint(
169-
current_app.server.server_get("endpoint", 'registration'))
167+
current_app.server.get_endpoint('registration'))
170168

171169

172170
@oidc_op_views.route('/registration_api', methods=['GET', 'DELETE'])
173171
def registration_api():
174172
if request.method == "DELETE":
175173
return service_endpoint(
176-
current_app.server.server_get("endpoint", 'registration_delete'))
174+
current_app.server.get_endpoint('registration_delete'))
177175
else:
178176
return service_endpoint(
179-
current_app.server.server_get("endpoint", 'registration_read'))
177+
current_app.server.get_endpoint('registration_read'))
180178

181179

182180
@oidc_op_views.route('/authorization')
183181
def authorization():
184182
return service_endpoint(
185-
current_app.server.server_get("endpoint", 'authorization'))
183+
current_app.server.get_endpoint('authorization'))
186184

187185

188186
@oidc_op_views.route('/token', methods=['GET', 'POST'])
189187
def token():
190188
return service_endpoint(
191-
current_app.server.server_get("endpoint", 'token'))
189+
current_app.server.get_endpoint('token'))
192190

193191
@oidc_op_views.route('/introspection', methods=['POST'])
194192
def introspection_endpoint():
195193
return service_endpoint(
196-
current_app.server.server_get("endpoint", 'introspection'))
194+
current_app.server.get_endpoint('introspection'))
197195

198196
@oidc_op_views.route('/userinfo', methods=['GET', 'POST'])
199197
def userinfo():
200198
return service_endpoint(
201-
current_app.server.server_get("endpoint", 'userinfo'))
199+
current_app.server.get_endpoint('userinfo'))
202200

203201

204202
@oidc_op_views.route('/session', methods=['GET'])
205203
def session_endpoint():
206204
return service_endpoint(
207-
current_app.server.server_get("endpoint", 'session'))
205+
current_app.server.get_endpoint('session'))
208206

209207

210208
IGNORE = ["cookie", "user-agent"]
@@ -298,7 +296,7 @@ def check_session_iframe():
298296
req_args = dict([(k, v) for k, v in request.form.items()])
299297

300298
if req_args:
301-
_context = current_app.server.server_get("endpoint_context")
299+
_context = current_app.server.get_context()
302300
# will contain client_id and origin
303301
if req_args['origin'] != _context.issuer:
304302
return 'error'
@@ -314,15 +312,15 @@ def check_session_iframe():
314312

315313
@oidc_op_views.route('/verify_logout', methods=['GET', 'POST'])
316314
def verify_logout():
317-
part = urlparse(current_app.server.server_get("endpoint_context").issuer)
315+
part = urlparse(current_app.server.get_context().issuer)
318316
page = render_template('logout.html', op=part.hostname,
319317
do_logout='rp_logout', sjwt=request.args['sjwt'])
320318
return page
321319

322320

323321
@oidc_op_views.route('/rp_logout', methods=['GET', 'POST'])
324322
def rp_logout():
325-
_endp = current_app.server.server_get("endpoint", 'session')
323+
_endp = current_app.server.get_endpoint('session')
326324
_info = _endp.unpack_signed_jwt(request.form['sjwt'])
327325
try:
328326
request.form['logout']

example/flask_rp/application.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,14 @@ def init_oidc_rp_handler(app):
2323
_path = ''
2424
_kj.httpc_params = _rp_conf.httpc_params
2525

26-
rph = RPHandler(_rp_conf.base_url, _rp_conf.clients, services=_rp_conf.services,
27-
hash_seed=_rp_conf.hash_seed, keyjar=_kj, jwks_path=_path,
28-
httpc_params=_rp_conf.httpc_params)
26+
rph = RPHandler(base_url=_rp_conf.base_url,
27+
client_configs=_rp_conf.clients,
28+
services=_rp_conf.services,
29+
keyjar=_kj,
30+
hash_seed=_rp_conf.hash_seed,
31+
httpc_params=_rp_conf.httpc_params,
32+
jwks_path=_path,
33+
)
2934

3035
return rph
3136

example/flask_rp/views.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def finalize(op_identifier, request_args):
100100
logger.error(rp.response[0].decode())
101101
return rp.response[0], rp.status_code
102102

103-
_context = rp.client_get("service_context")
103+
_context = rp.get_context()
104104
session['client_id'] = _context.get('client_id')
105105

106106
session['state'] = request_args.get('state')
@@ -123,7 +123,7 @@ def finalize(op_identifier, request_args):
123123
raise excp
124124

125125
if 'userinfo' in res:
126-
_context = rp.client_get("service_context")
126+
_context = rp.get_context()
127127
endpoints = {}
128128
for k, v in _context.provider_info.items():
129129
if k.endswith('_endpoint'):
@@ -197,7 +197,7 @@ def session_iframe(): # session management
197197
logger.debug('session_iframe request_args: {}'.format(request.args))
198198

199199
_rp = get_rp(session['op_identifier'])
200-
_context = _rp.client_get("service_context")
200+
_context = _rp.get_context()
201201
session_change_url = "{}/session_change".format(_context.base_url)
202202

203203
_issuer = current_app.rph.hash2issuer[session['op_identifier']]
@@ -237,7 +237,7 @@ def session_change():
237237
def session_logout(op_identifier):
238238
_rp = get_rp(op_identifier)
239239
logger.debug('post_logout')
240-
return "Post logout from {}".format(_rp.client_get("service_context").issuer)
240+
return "Post logout from {}".format(_rp.get_context().issuer)
241241

242242

243243
# RP initiated logout
@@ -267,7 +267,7 @@ def frontchannel_logout(op_identifier):
267267
_rp = get_rp(op_identifier)
268268
sid = request.args['sid']
269269
_iss = request.args['iss']
270-
if _iss != _rp.client_get("service_context").get('issuer'):
270+
if _iss != _rp.get_context().get('issuer'):
271271
return 'Bad request', 400
272272
_state = _rp.session_interface.get_state_by_sid(sid)
273273
_rp.session_interface.remove_state(_state)

0 commit comments

Comments
 (0)