Skip to content
118 changes: 118 additions & 0 deletions doc/server/contents/conf.rst
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,19 @@ An example::
]
}
},
"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"
]
}
},
"end_session": {
"path": "session",
"class": "idpyoidc.server.oidc.session.Session",
Expand Down Expand Up @@ -874,6 +887,12 @@ For example::

return request

==================
Client Credentials
==================
There are two possible ways to enable Client Credentials in OIDC-OP, globally and per-client.
For both cases the the Client Credentials handler is enabled throught the `client_credentials`
dictionary in token's `grant_types_supported`.

==================================
idpyoidc\.server\.configure module
Expand Down Expand Up @@ -948,3 +967,102 @@ 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)
57 changes: 57 additions & 0 deletions doc/server/contents/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,60 @@ The [RFC-8693](https://datatracker.ietf.org/doc/html/rfc8693) describes the `aud
defines the authorized targets of a token exchange request.
If `subject_token = urn:ietf:params:oauth:token-type:refresh_token` then `audience` should not be
included in the token exchange request.

Revocation endpoint
----------------------

Here an example about how to use a idpyoidc OP revocation endpoint.
This example uses a client with an HTTP Basic Authentication::

import base64
import requests

TOKEN = "eyJhbGciOiJFUzI1NiIsImtpZCI6IlQwZGZTM1ZVYUcxS1ZubG9VVTQwUXpJMlMyMHpjSHBRYlMxdGIzZ3hZVWhCYzNGaFZWTlpTbWhMTUEifQ.eyJzY29wZSI6IFsib3BlbmlkIiwgInByb2ZpbGUiLCAiZW1haWwiLCAiYWRkcmVzcyIsICJwaG9uZSJdLCAiYXVkIjogWyJvTHlSajdzSkozWHZBWWplRENlOHJRIl0sICJqdGkiOiAiOWQzMjkzYjZiYmNjMTFlYmEzMmU5ODU0MWIwNzE1ZWQiLCAiY2xpZW50X2lkIjogIm9MeVJqN3NKSjNYdkFZamVEQ2U4clEiLCAic3ViIjogIm9MeVJqN3NKSjNYdkFZamVEQ2U4clEiLCAic2lkIjogIlowRkJRVUZCUW1keGJIVlpkRVJKYkZaUFkxQldaa0pQVUVGc1pHOUtWWFZ3VFdkZmVEY3diMVprYmpSamRrNXRMVzB4YTNnelExOHlRbHBHYTNRNVRHZEdUUzF1UW1sMlkzVnhjRE5sUm01dFRFSmxabGRXYVhJeFpFdHVSV2xtUzBKcExWTmFaRzV3VjJodU0yNXlSbTU0U1ZWVWRrWTRRM2x2UWs1TlpVUk9SazlGVlVsRWRteGhjWGx2UWxWRFdubG9WbTFvZGpORlVUSnBkaTFaUTFCcFptZFRabWRDVWt0YVNuaGtOalZCWVhkcGJFNXpaV2xOTTFCMk0yaE1jMDV0ZGxsUlRFc3dObWxsYUcxa1lrTkhkemhuU25OaWFWZE1kVUZzZDBwWFdWbzFiRWhEZFhGTFFXWTBPVzl5VjJOUk4zaGtPRDA9IiwgInR0eXBlIjogIlQiLCAiaXNzIjogImh0dHBzOi8vMTI3LjAuMC4xOjgwMDAiLCAiaWF0IjogMTYyMTc3NzMwNSwgImV4cCI6IDE2MjE3ODA5MDV9.pVqxUNznsoZu9ND18IEMJIHDOT6_HxzoFiTLsniNdbAdXTuOoiaKeRTqtDyjT9WuUPszdHkVjt5xxeFX8gQMuA"

data = {
'token': TOKEN,
'token_type_hint': 'access_token'
}

_basic_secret = base64.b64encode(
f'{"oLyRj7sJJ3XvAYjeDCe8rQ"}:{"53fb49f2a6501ec775355c89750dc416744a3253138d5a04e409b313"}'.encode()
)
headers = {
'Authorization': f"Basic {_basic_secret.decode()}"
}

requests.post('https://127.0.0.1:8000/revoke', verify=False, data=data, headers=headers)


The idpyoidc OP will return a HTTP 200 response containing an empty json.

Client Credentials
--------------

Here an example about how a client can request a new access token using only its credentials::

import requests

CLIENT_ID=""
CLIENT_SECRET=""

data = {
"grant_type" : "client_credentials",
"client_id" : f"{CLIENT_ID}",
"client_secret" : f"{CLIENT_SECRET}"
}
headers = {'Content-Type': "application/x-www-form-urlencoded" }
response = requests.post(
'https://example.com/OIDC/token', verify=False, data=data, headers=headers
)

The idpyoidc OP will return a json response like this::

{
"access_token": "eyJhbGciOiJFUzI1NiIsI...Bo6aQcOKEN-1U88jjKxLb-9Q",
"token_type": "example",
"expires_in": 86400
}

24 changes: 24 additions & 0 deletions src/idpyoidc/message/oauth2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,30 @@ class TokenIntrospectionResponse(Message):
"jti": SINGLE_OPTIONAL_STRING,
}

# RFC 7009
class TokenRevocationRequest(Message):
c_param = {
"token": SINGLE_REQUIRED_STRING,
"token_type_hint": SINGLE_OPTIONAL_STRING,
# The ones below are part of authentication information
"client_id": SINGLE_OPTIONAL_STRING,
"client_secret": SINGLE_OPTIONAL_STRING,
}


class TokenRevocationResponse(Message):
pass

class TokenRevocationErrorResponse(ResponseMessage):
"""
Error response from the revocation endpoint
"""
c_allowed_values = ResponseMessage.c_allowed_values.copy()
c_allowed_values = {
"error": [
"unsupported_token_type"
]
}

# RFC 8693
class TokenExchangeRequest(Message):
Expand Down
3 changes: 1 addition & 2 deletions src/idpyoidc/server/client_authn.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,6 @@ def verify_client(
auth_info = {}
continue
break

# store what authn method was used
if "method" in auth_info and client_id:
_request_type = request.__class__.__name__
Expand All @@ -548,10 +547,10 @@ def verify_client(
endpoint_context.cdb[client_id]["auth_method"][_request_type] = auth_info["method"]
else:
endpoint_context.cdb[client_id]["auth_method"] = {_request_type: auth_info["method"]}

return auth_info



def client_auth_setup(server_get, auth_set=None):
if auth_set is None:
auth_set = CLIENT_AUTHN_METHOD
Expand Down
2 changes: 2 additions & 0 deletions src/idpyoidc/server/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ def client_authentication(self, request: Message, http_info: Optional[dict] = No
raise UnAuthorizedClient("Authorization failed")
if "client_id" not in authn_info and authn_info.get("method") != "none":
raise UnAuthorizedClient("Authorization failed")
if "client_id" not in authn_info and authn_info.get("method") != "none":
raise UnAuthorizedClient("Authorization failed")
return authn_info

def do_post_parse_request(
Expand Down
1 change: 0 additions & 1 deletion src/idpyoidc/server/oauth2/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ def _introspect(self, token, client_id, grant):
return None

if not token.is_active():
#
return None

scope = token.scope
Expand Down
4 changes: 3 additions & 1 deletion src/idpyoidc/server/oauth2/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
from cryptojwt.jwe.exception import JWEException

from idpyoidc.message import Message
from idpyoidc.message.oauth2 import AccessTokenResponse
from idpyoidc.message.oauth2 import AccessTokenResponse, CCAccessTokenRequest
from idpyoidc.message.oauth2 import ResponseMessage
from idpyoidc.message.oauth2 import TokenExchangeRequest
from idpyoidc.message.oidc import TokenErrorResponse
from idpyoidc.server.constant import DEFAULT_REQUESTED_TOKEN_TYPE
from idpyoidc.server.endpoint import Endpoint
from idpyoidc.server.exception import ProcessError
from idpyoidc.server.oauth2.token_helper import AccessTokenHelper
from idpyoidc.server.oauth2.token_helper import CCAccessTokenHelper
from idpyoidc.server.oauth2.token_helper import RefreshTokenHelper
from idpyoidc.server.oauth2.token_helper import TokenExchangeHelper
from idpyoidc.server.session import MintingNotAllowed
Expand All @@ -35,6 +36,7 @@ class Token(Endpoint):
default_capabilities = {"token_endpoint_auth_signing_alg_values_supported": None}
helper_by_grant_type = {
"authorization_code": AccessTokenHelper,
"client_credentials": CCAccessTokenHelper,
"refresh_token": RefreshTokenHelper,
}

Expand Down
Loading