Skip to content

Commit

Permalink
Merge pull request #63 from IdentityPython/develop
Browse files Browse the repository at this point in the history
Version 2.0
  • Loading branch information
Giuseppe De Marco authored May 5, 2023
2 parents 90f6330 + ca7e310 commit c6b7f5c
Show file tree
Hide file tree
Showing 229 changed files with 12,425 additions and 6,920 deletions.
59 changes: 59 additions & 0 deletions archdoc/docs/client/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# The IdpyOIDC client

A client can send requests to an endpoint and deal with the response.

IdpyOIDC assumes that there is one Relying Party(RP)/Client instance per
OpenID Connect Provider(OP)/Authorization Server (AS).

If you have a service that expects to talk to several OPs/ASs
then you must use **idpyoidc.client.rp_handler.RPHandler** to manage the RPs.

RPHandler has methods like:
- begin()
- finalize()
- refresh_access_token()
- logout()

More about RPHandler at the end of this section.

## Client

A client is configured to talk to a set of services each of them represented by
a Service Instance.

# Context

# Service

A Service instance is expected to be able to:

1. Collect all the request arguments
2. If necessary collect and add authentication information to the request attributes or HTTP header
3. Formats the message
4. chooses HTTP method
5. Add HTTP headers

and then after having received the response:

1. Parses the response
2. Gather verification information and verify the response
3. Do any special post-processing.
3. Store information from the response

Doesn't matter which service is considered they all have to be able to do this.

## Request

## Response

# AddOn

# Endpoints

## OAuth2

- Access Token
- Authorization
- Refresh Access Token
- Server Metadata
- Token Exchange
1 change: 1 addition & 0 deletions archdoc/docs/combo/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# An entity that can act both as a server and a client
10 changes: 10 additions & 0 deletions archdoc/docs/server/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

# Service

## Request

## Response

# Context

# AddOn
132 changes: 132 additions & 0 deletions doc/server/contents/conf.rst
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,23 @@ An example::
"normal",
"aggregated",
"distributed"
],
"policy": {
"function": "/path/to/callable",
"kwargs": {}
}
}
},
"revocation": {
"path": "revoke",
"class": "idpyoidc.server.oauth2.revocation.Revocation",
"kwargs": {
"client_authn_method": [
"client_secret_post",
"client_secret_basic",
"client_secret_jwt",
"private_key_jwt",
"bearer_header"
]
}
},
Expand Down Expand Up @@ -734,6 +751,10 @@ the following::
"userinfo": {
"class": "oidc_provider.users.UserInfo",
"kwargs": {
"policy": {
"function": "/path/to/callable",
"kwargs": {}
},
"claims_map": {
"phone_number": "telephone",
"family_name": "last_name",
Expand All @@ -747,6 +768,17 @@ the following::
}
}

The policy for userinfo endpoint is optional and can also be configured in a client's metadata, for example::

"userinfo": {
"kwargs": {
"policy": {
"function": "/path/to/callable",
"kwargs": {}
}
}
}

================================
Special Configuration directives
================================
Expand Down Expand Up @@ -875,6 +907,106 @@ For example::
return request


==============
Token revocation
==============

In order to enable the token revocation endpoint a dictionary with key `token_revocation` should be placed
under the `endpoint` key of the configuration.

If present, the token revocation configuration should contain a `policy` dictionary
that defines the behaviour for each token type. Each token type
is mapped to a dictionary with the keys `callable` (mandatory), which must be a
python callable or a string that represents the path to a python callable, and
`kwargs` (optional), which must be a dict of key-value arguments that will be
passed to the callable.

The key `""` represents a fallback policy that will be used if the token
type can't be found. If a token type is defined in the `policy` but is
not in the `token_types_supported` list then it is ignored.

"token_revocation": {
"path": "revoke",
"class": "idpyoidc.server.oauth2.token_revocation.TokenRevocation",
"kwargs": {
"token_types_supported": ["access_token"],
"client_authn_method": [
"client_secret_post",
"client_secret_basic",
"client_secret_jwt",
"private_key_jwt",
"bearer_header"
],
"policy": {
"urn:ietf:params:oauth:token-type:access_token": {
"callable": "/path/to/callable",
"kwargs": {
"audience": ["https://example.com"],
"scopes": ["openid"]
}
},
"urn:ietf:params:oauth:token-type:refresh_token": {
"callable": "/path/to/callable",
"kwargs": {
"resource": ["https://example.com"],
"scopes": ["openid"]
}
},
"": {
"callable": "/path/to/callable",
"kwargs": {
"scopes": ["openid"]
}
}
}
}
}

For the per-client configuration a similar configuration scheme should be present in the client's
metadata under the `token_revocation` key.

For example::

"token_revocation":{
"token_types_supported": ["access_token"],
"policy": {
"urn:ietf:params:oauth:token-type:access_token": {
"callable": "/path/to/callable",
"kwargs": {
"audience": ["https://example.com"],
"scopes": ["openid"]
}
},
"urn:ietf:params:oauth:token-type:refresh_token": {
"callable": "/path/to/callable",
"kwargs": {
"resource": ["https://example.com"],
"scopes": ["openid"]
}
},
"": {
"callable": "/path/to/callable",
"kwargs": {
"scopes": ["openid"]
}
}
}
}
}

The policy callable accepts a specific argument list and handles the revocation appropriately and returns
an :py:class:`idpyoidc.message.oauth2..TokenRevocationResponse` or raises an exception.

For example::

def custom_token_revocation_policy(token, session_info, **kwargs):
if some_condition:
return TokenErrorResponse(
error="invalid_request", error_description="Some error occured"
)
response_args = {"response_args": {}}
return oauth2.TokenRevocationResponse(**response_args)

==================================
idpyoidc\.server\.configure module
==================================
Expand Down
34 changes: 16 additions & 18 deletions example/flask_op/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def verify(authn_method):
auth_args = authn_method.unpack_token(kwargs['token'])
authz_request = AuthorizationRequest().from_urlencoded(auth_args['query'])

endpoint = current_app.server.server_get("endpoint", 'authorization')
endpoint = current_app.server.get_endpoint('authorization')
_session_id = endpoint.create_session(authz_request, username, auth_args['authn_class_ref'],
auth_args['iat'], authn_method)

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

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

@oidc_op_views.route('/verify/user_pass_jinja', methods=['GET', 'POST'])
def verify_user_pass_jinja():
authn_method = current_app.server.server_get(
"endpoint_context").authn_broker.get_method_by_id('user')
authn_method = current_app.server.get_context().authn_broker.get_method_by_id('user')
try:
return verify(authn_method)
except FailedAuthentication as exc:
Expand All @@ -154,9 +152,9 @@ def verify_user_pass_jinja():
@oidc_op_views.route('/.well-known/<service>')
def well_known(service):
if service == 'openid-configuration':
_endpoint = current_app.server.server_get("endpoint", 'provider_config')
_endpoint = current_app.server.get_endpoint('provider_config')
elif service == 'webfinger':
_endpoint = current_app.server.server_get("endpoint", 'discovery')
_endpoint = current_app.server.get_endpoint('discovery')
else:
return make_response('Not supported', 400)

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


@oidc_op_views.route('/registration_api', methods=['GET', 'DELETE'])
def registration_api():
if request.method == "DELETE":
return service_endpoint(
current_app.server.server_get("endpoint", 'registration_delete'))
current_app.server.get_endpoint('registration_delete'))
else:
return service_endpoint(
current_app.server.server_get("endpoint", 'registration_read'))
current_app.server.get_endpoint('registration_read'))


@oidc_op_views.route('/authorization')
def authorization():
return service_endpoint(
current_app.server.server_get("endpoint", 'authorization'))
current_app.server.get_endpoint('authorization'))


@oidc_op_views.route('/token', methods=['GET', 'POST'])
def token():
return service_endpoint(
current_app.server.server_get("endpoint", 'token'))
current_app.server.get_endpoint('token'))

@oidc_op_views.route('/introspection', methods=['POST'])
def introspection_endpoint():
return service_endpoint(
current_app.server.server_get("endpoint", 'introspection'))
current_app.server.get_endpoint('introspection'))

@oidc_op_views.route('/userinfo', methods=['GET', 'POST'])
def userinfo():
return service_endpoint(
current_app.server.server_get("endpoint", 'userinfo'))
current_app.server.get_endpoint('userinfo'))


@oidc_op_views.route('/session', methods=['GET'])
def session_endpoint():
return service_endpoint(
current_app.server.server_get("endpoint", 'session'))
current_app.server.get_endpoint('session'))


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

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

@oidc_op_views.route('/verify_logout', methods=['GET', 'POST'])
def verify_logout():
part = urlparse(current_app.server.server_get("endpoint_context").issuer)
part = urlparse(current_app.server.get_context().issuer)
page = render_template('logout.html', op=part.hostname,
do_logout='rp_logout', sjwt=request.args['sjwt'])
return page


@oidc_op_views.route('/rp_logout', methods=['GET', 'POST'])
def rp_logout():
_endp = current_app.server.server_get("endpoint", 'session')
_endp = current_app.server.get_endpoint('session')
_info = _endp.unpack_signed_jwt(request.form['sjwt'])
try:
request.form['logout']
Expand Down
11 changes: 8 additions & 3 deletions example/flask_rp/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,14 @@ def init_oidc_rp_handler(app):
_path = ''
_kj.httpc_params = _rp_conf.httpc_params

rph = RPHandler(_rp_conf.base_url, _rp_conf.clients, services=_rp_conf.services,
hash_seed=_rp_conf.hash_seed, keyjar=_kj, jwks_path=_path,
httpc_params=_rp_conf.httpc_params)
rph = RPHandler(base_url=_rp_conf.base_url,
client_configs=_rp_conf.clients,
services=_rp_conf.services,
keyjar=_kj,
hash_seed=_rp_conf.hash_seed,
httpc_params=_rp_conf.httpc_params,
jwks_path=_path,
)

return rph

Expand Down
Loading

0 comments on commit c6b7f5c

Please sign in to comment.