identitypython / oidcendpoint Goto Github PK
View Code? Open in Web Editor NEWImplementation of OIDC OP/Oauth2 AS endpoints
License: Apache License 2.0
Implementation of OIDC OP/Oauth2 AS endpoints
License: Apache License 2.0
Currently there are two Endpoint
s (as in oidcendpoint classes) that implement the token endpoint, the AccessToken
(src/oidcendpoint/oidc/token.py
) and RefreshAccessToken
(src/oidcendpoint/oidc/refresh_token.py
).
In order to implement the token endpoint in SATOSA I would have to check the grant_type in the request and call the corresponding oidcendpoint endpoint (ie if grant_type=refresh_token
then use RefreshAccessToken
). This decision should be made in oidcendpoint since it is the one parsing the request.
Shouldn't these two classes be merged?
Is it possible to support scopes allowed per RP/client? So that each client is allowed to only ask for a subset of supported scopes. It could be configured/saved in the client database.
My present thinking is this:
When a user completes a successful authentication at an OP a session is created.
There is one session per user_id and client_id combination.
That means that if the user sends an authentication request from another client and SSO is used then a new session is created. The 2 sessions have an authentication event in common.
If SSO was not allowed then the 2 sessions, even if they concerned the same user_id, would not have a common authentication event.
This means that at the top of the session management tree we would have a number of authentication events.
Now within a session grants can be given. These grants can lead to authorization codes, access tokens and/or refresh tokens to be issued.
So we have a hierarchy:
This would allow us easy handling of single logout as well as dealing with grant management as described in
Grant Management
There are connections between grants, for instance it's useful to know which refresh token was used to issue which access token.
With the help of django-oidc-op/snippts/rp_hanlder.py here I post the debugging information regarding an ordinary oidcendpoint/oidcrp session.
in OAuth2 aud it's optional, as described here:
https://tools.ietf.org/html/rfc7519#section-4.1.3
In OIDC not: https://openid.net/specs/openid-connect-core-1_0.html#IDToken
python3 snippets/rp_handler.py -c example/data/oidc_rp/conf.django.yaml -u that_user -p that_password -iss django_oidc_op
Client registration done...
Connecting to Authorization url:
{
"url": "https://127.0.0.1:8000/authorization?redirect_uri=https%3A%2F%2F127.0.0.1%3A8099%2Fauthz_cb%2Fdjango_oidc_op&scope=openid+that_scope+profile+email+address+phone&response_type=code&nonce=wCn0Bncr7m6sRO10P5f7SA5o&state=ytSp5K8X5XvE5RCfEFmEpHqHZVn5kYgx&code_challenge=ycWJAoBgUEH9NyRPEsUJwvRtTUAsDRMKvMecaLs9d_8&code_challenge_method=S256&client_id=1UUl6cwNigmj",
"state": "ytSp5K8X5XvE5RCfEFmEpHqHZVn5kYgx"
}
The Authorization endpoint returns a HTML authentication form with a token
{
"token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJXdG9SekV4VXkxak9GVXlSV2hwZUdkbFREWlBaME55TW1ka05ERlFaakJSUzJreVQwaExVazVJUVEifQ.eyJhdXRobl9jbGFzc19yZWYiOiAib2lkY2VuZHBvaW50LnVzZXJfYXV0aG4uYXV0aG5fY29udGV4dC5JTlRFUk5FVFBST1RPQ09MUEFTU1dPUkQiLCAicXVlcnkiOiAicmVkaXJlY3RfdXJpPWh0dHBzJTNBJTJGJTJGMTI3LjAuMC4xJTNBODA5OSUyRmF1dGh6X2NiJTJGZGphbmdvX29pZGNfb3Amc2NvcGU9b3BlbmlkK3RoYXRfc2NvcGUrcHJvZmlsZStlbWFpbCthZGRyZXNzK3Bob25lJnJlc3BvbnNlX3R5cGU9Y29kZSZub25jZT13Q24wQm5jcjdtNnNSTzEwUDVmN1NBNW8mc3RhdGU9eXRTcDVLOFg1WHZFNVJDZkVGbUVwSHFIWlZuNWtZZ3gmY29kZV9jaGFsbGVuZ2U9eWNXSkFvQmdVRUg5TnlSUEVzVUp3dlJ0VFVBc0RSTUt2TWVjYUxzOWRfOCZjb2RlX2NoYWxsZW5nZV9tZXRob2Q9UzI1NiZjbGllbnRfaWQ9MVVVbDZjd05pZ21qIiwgInJldHVybl91cmkiOiAiaHR0cHM6Ly8xMjcuMC4wLjE6ODA5OS9hdXRoel9jYi9kamFuZ29fb2lkY19vcCIsICJpc3MiOiAiaHR0cHM6Ly8xMjcuMC4wLjE6ODAwMCIsICJpYXQiOiAxNTk1OTMyNzI3fQ.VKnZZmWuHuOZjgaUUn7A5X5TjZaGeuuv8AjpwMkYdtmpxr31GEEOnmltjU3burmIZV1qOZC4vnRTZntAXO8GflwRkjtKBPvewGqkz4etHVZEkHZ3nKMG8zolFuU7xdYuV9wUok0ZzNh52qWcLhGOHTvBsfHB5gN7JXYSKF33Ii1JlwYL--nJLuIQRvV2MjyzzS01GGJ_Zlk2zWaox7MsWQeTcFk4HBnfaGc1ugjVJsMqpNwRmWvronVvU-93MvfVK46lhUQlvJuZNRJ2tlHc3JVvCDYmTfFk-MVlt_LhuTk90_u1G35lpX0klLavdgkOorUheJVVPsqCj9aME0GdqQ",
"url": "verify/oidc_user_login/"
}
The Authorization returns a HttpRedirect (302) to https://127.0.0.1:8099/authz_cb/django_oidc_op?state=ytSp5K8X5XvE5RCfEFmEpHqHZVn5kYgx&scope=openid+that_scope+profile+email+address+phone&code=Z0FBQUFBQmZJQUE0YWFwUkdiMzhQM3oxNTkzNDZ4QlRQZjNlbUNIeXIwM1kwSkVZSHRzc0pueE01dndyZ2YxZXdzRVVGWGFlTXNmOFFGM3I3cW5iMEE5Uk9xXzFJQzRuN0tOd0ZrVzJwYlk3M2xIa3pCRGh4eUgySTRIaE9aVlhQSDFGenFwTHduR0NDc0tmSUJ3d3RsaXdLRldIMjQ1STcxRU5oWUE1WHIwb3B4ZWU2V1ZldndsRjBSWU1wOUF4N3owcDFWV2QzSDZtcUQzU0JKUW5qemxPdzFOdE5SWnJ4VXJ3N3hpM0dlYTZSYkROSmZyNURQWT0%3D&session_state=bc627c1120c4bb6fc3c6296d24fe926c9740b0f7944ce0e0c55c65b6055b5085.w9fW3DOoKcYD3nvU&iss=https%3A%2F%2F127.0.0.1%3A8000&client_id=1UUl6cwNigmj
{}
Bearer Access Token
"eyJhbGciOiJFUzI1NiIsImtpZCI6IlQwZGZTM1ZVYUcxS1ZubG9VVTQwUXpJMlMyMHpjSHBRYlMxdGIzZ3hZVWhCYzNGaFZWTlpTbWhMTUEifQ.eyJzaWQiOiAiYzBlY2QxMTFjMTM5MmM1N2M2YjE3MWZkMmNiYjJkMzFjMGM2NjUyOGVhN2QwZGFlZTNkODk2YTgiLCAidHR5cGUiOiAiVCIsICJzdWIiOiAiMDc2ZWNjYTk0ZmU0NTQ2N2I0NDM1ZDhlZWFkMjE4OGFkMzc3MWUxMGZmNjcyY2UxOTMwYzA0YWE4NjI0MTgxYyIsICJpc3MiOiAiaHR0cHM6Ly8xMjcuMC4wLjE6ODAwMCIsICJpYXQiOiAxNTk1OTMyNzI4LCAiZXhwIjogMTU5NTkzNjMyOCwgImF1ZCI6IFsiMVVVbDZjd05pZ21qIiwgImh0dHBzOi8vMTI3LjAuMC4xOjgwMDAiXX0.tAyozYfL6EpbZ0v_31_pm6MbeuD5RSILqZuIyObks_vJEzUOU1qqi4zxt4jz05s002u8y795NZPMqlgjpNNWFw"
Access Token
{
"sid": "c0ecd111c1392c57c6b171fd2cbb2d31c0c66528ea7d0daee3d896a8",
"ttype": "T",
"sub": "076ecca94fe45467b4435d8eead2188ad3771e10ff672ce1930c04aa8624181c",
"iss": "https://127.0.0.1:8000",
"iat": 1595932728,
"exp": 1595936328,
"aud": [
"1UUl6cwNigmj",
"https://127.0.0.1:8000"
]
}
ID Token
{
"sub": "076ecca94fe45467b4435d8eead2188ad3771e10ff672ce1930c04aa8624181c",
"auth_time": 1595932727,
"acr": "oidcendpoint.user_authn.authn_context.INTERNETPROTOCOLPASSWORD",
"nonce": "wCn0Bncr7m6sRO10P5f7SA5o",
"iss": "https://127.0.0.1:8000",
"iat": 1595932728,
"exp": 1595933028,
"aud": [
"1UUl6cwNigmj"
]
}
Userinfo endpoint result:
{
"email": "[email protected]",
"given_name": "Giuseppe",
"family_name": "De Marco",
"gender": "male",
"birthdate": "2020-07-26",
"updated_at": 1595931659,
"sub": "076ecca94fe45467b4435d8eead2188ad3771e10ff672ce1930c04aa8624181c"
}
Hello everybody, this is a long-term issue.
During the past year I lived on my skin many jwtconnect breakable upgrades and at this moment an user can still add some options in oidcendpoint configuration.yml, that do not belong anymore to the release he's currently using.
An example is http_params
changed then in httpc_params
and others as we seen from v0.13.0 to v1.0.1.
I think that a configuration schema validator could help as newcomers users as developers to get some warnings or, in best cases, exception when service starts. This would avoid to run oidcendpoint in a inconsistent way, no more errors during oauth2/oidc sessions would happens.
in django we also use a special command called diffsettings that shows up with configuration fields that would not belong to one's well known in the system core.
A configuration object with a validation method, that starts first of all.
This code would also put a base to the documentation that could be estracted directly from the configuration schema definition.
What else?
The info method tries to find the correct handler for the token by using a try-except and moving to the next token type (order
) in case of some exceptions. However, if the access or refresh token is configured to be a JWT, the exception raised is cryptojwt.exception.BadSyntax
which is not handled.
This is the point where cryptojwt.exception.BadSyntax
, but I am not sure if there are other exceptions that should be added too:
Is "none" authn method still supported? Because the recently introduced client_auth_setup requires a known method or a class and None raises an exception when it's about to be imported.
Check here:
oidcendpoint/src/oidcendpoint/client_authn.py
Line 382 in 46742a4
I can still configure an endpoint to not have client authentication by not providing a client_authn_method argument in the kwargs, however I can see the None method in the CLIENT_AUTHN_METHOD dict and also here:
oidcendpoint/src/oidcendpoint/endpoint.py
Line 286 in 46742a4
So I am not sure if it's still supported or not.
Suggested solution:
Handle None case in client_auth_setup.
The master and the develop branch have diverged. Master should be the state of develop that was last released. Someone should look into that and we should be careful not to push commits to master no more. We should only commit to develop through PRs and then merge to master when we want to release a new version.
We have found two (corner case) issues with PKCE:
In configurations with PKCE enabled as not essential, and plain
challenge method not supported, authentication requests without PKCE parameters fail. The reason behind this is that because the challenge method is missing, plain
is set and afterwards a check for the code_challenge_method
validity fails, because we do not support plain
. I suppose that this check must be done only in case code_challenge
is included in the request.
add_pkce_support
method assumes both authorization
and token
endpoints are configured so it makes no checks before trying to add PKCE hook methods. As a result it crashes if either is missing. I propose that instead of crashing we make the required checks and in the case either is missing, we log a warning and skip the configuration.
The authorization endpoint allows three possible response_modes
: query
, fragment
and form_post
.
According to https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html#FormPostResponseMode:
In this mode, Authorization Response parameters are encoded as HTML form values that are auto-submitted in the User Agent, and thus are transmitted via the HTTP POST method to the Client, with the result parameters being encoded in the body using the application/x-www-form-urlencoded format
BUT when an authorization request is made using response_mode = form_post
the endpoint will return a response with content-type = application/x-www-form-urlencoded
(it should be text/html
). The content-type is set in
oidcendpoint/src/oidcendpoint/endpoint.py
Line 345 in 4a761b1
I don't know if this affects any other endpoints, so I'm not sure how this should be fixed.
In https://github.com/IdentityPython/oidcendpoint/blob/master/src/oidcendpoint/endpoint_context.py#L352: ("code", 600), ("token", 3600), ("refresh", 86400) shouldn't be taken from general configuration?
We could do implement something like
get settings (general config) parameters, if any -> take this preset.
Is there any plan to support Token Exchange (https://tools.ietf.org/html/rfc8693)? It would be a nice feature to have and useful for our use cases.
If the user reloads during logout, logout_all_clients()
may get None
back from usids
and the call fails. Perhaps we should fail gracefully if no sessions are found?
I think there is a bug regarding the token introspection endpoint responses in the case of inactive/invalid tokens.
The TokenIntrospectionResponse class is only used when the token is valid (active = True) when it should be used in all cases. The return value should always be {"response_args": _resp}
.
To define custom scopes the userinfo endpoint needs to be defined, but in the case of implicit flow the userinfo isn't needed. Shouldn't custom scopes be decoupled from userinfo?
In addition, custom scopes are not taken into account when defining the supported scopes in the authorization endpoint (only the scopes defined in SCOPE2CLAIMS
are used there), is this intentional? IMO custom scopes should be added to the default supported scopes.
Could we think to configure wich scopes could be released to clients?
I didn't found this filter and not developed this in django-oidc-op. Is there something I missed or we could add this feature as well?
I'd like to add this feature as a many2one in the registration ui:
https://github.com/peppelinux/django-oidc-op/blob/master/images/rp.png
In authorization's post_authentication
method, there's some error handling before creating response_info
and filling the return_uri
entry etc., for example here:
The response_info is initialized in line 555 and in 566 the return_uri
is filled.
As a result, when do_response
is called, there's an exception raised in
oidcendpoint/src/oidcendpoint/endpoint.py
Line 422 in 5929fde
as return_uri is considered to always be there.
We could say that the app using oidcendpoint could fill the return_uri to avoid the error, but I feel it would be better if in post_authentication
the response_info
was filled at the beginning rather than in the middle of the method.
Right now, the token introspection endpoint responds with active = False in all requests even if, for example, the token parameter is missing. Should the case of invalid or malformed requests be handled in another way, e.g. with a HTTP 400?
The RFC states "Note that a properly formed and authorized query for an inactive or otherwise invalid token (or a token the protected resource is not allowed to know about) is not considered an error response by this specification."
, however a request without a token parameter probably should not be considered properly formed.
AFAIK code_verifier is required to be in the token request if the client had sent a code_challenge in the authorization request. Otherwise PKCE has no meaning.
Unfortunately, right now this is not the case:
An attacker can just not send a code verifier and will have no trouble getting an access token.
I think parse_request should always return a error_cls object instead of a dict.
Offending line is:
I believe that the client registration should be able to use the BearerHeader client_authn_method for simple authentication.
However, if configured to use it, it never checks the token as there is no get_client_id_from_token method implemented in oidcendpoint/oidc/registration.py. The method is called here
oidcendpoint/src/oidcendpoint/client_authn.py
Line 370 in 3192e60
As a result it returns "" and no exception is raised.
I tried implementing a get_client_id_from_token method similarly to userinfo's one, but then there's a problem with unauthenticated registration as the No token exception is raised, because it cannot find a client_id in the request (correctly) and there is a get_client_id_from_token implemented. I think the last check should be corrected.
oidcendpoint/src/oidcendpoint/client_authn.py
Line 363 in 3192e60
The claims that will be added in the id token are chosen via the by_schema
function in oidcendpoint/userinfo.py
. Which by default uses the oidcmsg.oidc.IdToken
class to choose whether a claim should be added or not.
I am not sure if this is intentional or not, because if extra claims/scopes are added then they will not be returned in the IDToken since they are not in the default OIDC claims, unless a custom id_token_schema
is provided in the configuration.
I think that the claims shouldn't be filtered like this (perhaps the call to by_schema
should be removed altogether).
I hope the problem I describe is clear, do you think this should be changed or should we simply extend the id_token_schema
to include our extra claims?
There are some issues regarding the token introspection endpoint:
self.endpoint_context.sdb[token]
in do_access_token raises KeyError in the case of not-existing token. This is not handled (the if/else could be changed to a try except)within its given time window of validity
, which means that the iss/nbf should be checked also to be before the current time.This may be a little off-topic, but we are also interested in allowing configurable claims per client for the introspection response.
I succesfully tested allowed_scopes and I found a default behaviour in oidcendpoint and oidcRP that I'd like to discuss.
When OidcRP have in its configuration some scopes not supported by OP (as found in Provider discovery) it simply omit them and send a authn request with which found available in provider discovery.
oidcendpoint when get a authz request with unavailable scopes, it simply ignore these and release its defaults (openid profile email address phone). The resulting access token could be used by a Client/RP (or a faulty RS) to give access to a resource for which the OP doesn't release the token for (simply ignoring the authz response and the auth code, no token introspection and the worst things ...).
The same behaviour if it have some allowed_scopes defined for a client_id that ask for a scope not configured for it.
In oidc.authorization
we put a filter that increase this check:
oidcendpoint/oidc/authorization.py(682)process_request()
-> _cid = request_info["client_id"]
(Pdb) ll
672 def process_request(self, request_info=None, **kwargs):
673 """ The AuthorizationRequest endpoint
674
675 :param request_info: The authorization request as a dictionary
676 :return: dictionary
677 """
678
679 if isinstance(request_info, AuthorizationErrorResponse):
680 return request_info
681
682 -> _cid = request_info["client_id"]
683 cinfo = self.endpoint_context.cdb[_cid]
we have request_info e cinfo as follow
(Pdb) request_info
<oidcmsg.oidc.AuthorizationRequest object at 0x7feeb9e2a3d0>
(Pdb) request_info.__dict__
{'_dict': {'redirect_uri': 'https://127.0.0.1:8099/authz_cb/django_oidc_op', 'scope': ['openid', 'that_scope', 'profile', 'email', 'address', 'phone'], 'response_type': ['code'], 'nonce': '1mWXHQc7WEpe4HCOimA3s6Ko', 'state': 'P1y9a0KTqm1znr4ALOu31sNN6t8yXhRg', 'code_challenge': 'hz3Nx5jHHPdgaDUN6zuI8v9e4JOwHxo34FUFsCAtp3k', 'code_challenge_method': 'S256', 'client_id': '1UUl6cwNigmj'}, 'lax': False, 'jwt': None, 'jws_header': None, 'jwe_header': None, 'verify_ssl': True}
and
(Pdb) cinfo
{'id': 1, 'client_id': '1UUl6cwNigmj', 'client_salt': 'vWRsHApW', 'registration_access_token': 'dyIinaJe3gWFwJPqL2dVUHKKGcZhJdrv', 'registration_client_uri': 'https://127.0.0.1:8000/registration_api?client_id=1UUl6cwNigmj', 'client_id_issued_at': 1595799959, 'client_secret': '78be88872d5877c4ddb209335f4eb2fc5118a481a195a454c8b2ebcb', 'client_secret_expires_at': 1598391959, 'application_type': 'web', 'token_endpoint_auth_method': 'client_secret_basic', 'jwks_uri': 'https://127.0.0.1:8099/static/jwks.json', 'contacts': ['[email protected]'], 'grant_types': ['authorization_code'], 'response_types': ['code'], 'post_logout_redirect_uris': [], 'redirect_uris': [('https://127.0.0.1:8099/authz_cb/django_oidc_op', {})], 'allowed_scopes': ['that_custom_scope', 'openid']}
@rohe I suggest to put a simple filter here, somethings like:
# this prevents that authz would be released for unavailable scopes
for scope in request_info['scope']:
if scope not in client_allowed_scopes:
_msg = '{} requested an unauthorized scope ({})'
logger.warning(_msg.format(cinfo['client_id'],
scope))
raise UnAuthorizedClientScope()
add_client_secret always calls client_secret_expiration_time and as a result there is no way to set the secrets to not expire. There should be a way to pass 0 as a client_secret_expiration_time in the config to make the secrets non-expiring. 0 should also be the default, imo.
The RFC [1] mentions some JWT specific attributes in the response, but token introspection should work with normal access tokens as well. The offending line:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.