-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #71 from IdentityPython/develop
OAuth2 Client code refactoring and linting
- Loading branch information
Showing
205 changed files
with
10,739 additions
and
3,893 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,315 @@ | ||
# Usage stories | ||
|
||
This is a set of usage stories. | ||
Here to display what you can do with IdpyOIDC using OAuth2 or OIDC. | ||
|
||
Every story follows the same pattern it starts by initiating one client/RP and | ||
one AS/OP. | ||
After that a sequence of requests/responses are performed. Each one follows this | ||
pattern: | ||
|
||
- The client/RP constructs the request and possible client authentication information | ||
- The request and client authentication information is printed | ||
- The AS/OP does client authentication based on the authentication information received | ||
- The AS/OP parses and verifies the client request | ||
- The AS/OP constructs the server response | ||
- The client/RP parses and verifies the server response | ||
- The parsed and verified response is printed | ||
|
||
This pattern is repeated for each request/response in the sequence. | ||
|
||
To understand the descriptions below you have to remember that an AS/OP provides | ||
**endpoints** while a client/RP accesses **services**. An endpoint can | ||
support more than one service. A service can only reside at one endpoint. | ||
|
||
## Basic OAuth2 Stories | ||
|
||
These are based on the two basic OAuth2 RFCs; | ||
* [The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749) | ||
* [The OAuth 2.0 Authorization Framework: Bearer Token Usage](https://www.rfc-editor.org/rfc/rfc6750) | ||
|
||
### Client Credentials Grant (oauth2_cc.py) | ||
|
||
Displays the usage of the | ||
[client credentials grant](https://www.rfc-editor.org/rfc/rfc6749#section-4.4) . | ||
|
||
The client can request an access token using only its client | ||
credentials (or other supported means of authentication). | ||
|
||
The request/response sequence only contains the client credential exchange. | ||
|
||
The client is statically registered with the AS. | ||
|
||
#### configuration | ||
|
||
The server configuration expresses these points: | ||
|
||
- The server needs only one endpoint, the token endpoint. | ||
- The token released form the token endpoint is a signed JSON Web token (JWT) | ||
- The server deals only with access tokens. The default lifetime of a token is 3600 | ||
seconds. | ||
- The server can deal with 2 client authentication methods at the token endpoint: | ||
client_secret_basic and client_secret_post | ||
- In this example the audience for the token (the resource server) is statically set. | ||
|
||
|
||
"endpoint": { | ||
"token": { | ||
"path": "token", | ||
"class": Token, | ||
"kwargs": { | ||
"client_authn_method": ["client_secret_basic", "client_secret_post"], | ||
}, | ||
}, | ||
}, | ||
"token_handler_args": { | ||
"jwks_defs": {"key_defs": KEYDEFS}, | ||
"token": { | ||
"class": "idpyoidc.server.token.jwt_token.JWTToken", | ||
"kwargs": { | ||
"lifetime": 3600, | ||
"aud": ["https://example.org/appl"], | ||
} | ||
} | ||
} | ||
|
||
The client configuration | ||
|
||
- lists only one service - client credentials | ||
- specifies client ID and client secret since the client is statically | ||
registered with the server. | ||
|
||
|
||
"client_id": "client_1", | ||
"client_secret": "another password", | ||
"base_url": "https://example.com", | ||
"services": { | ||
"client_credentials": { | ||
"class": "idpyoidc.client.oauth2.client_credentials.CCAccessTokenRequest" | ||
} | ||
} | ||
|
||
**services** is a dictionary. The keys in that dictionary is for your usage only. | ||
Internally the software uses identifiers that are statically assigned to every Service class. | ||
This means that you can not have two instances of the same class in a _services_ | ||
definition. | ||
|
||
### Resource Owners Password Credentials (oauth2_ropc.py) | ||
|
||
**NOTE** Resource Owners Password Credentials is not part of OAuth2.1 | ||
|
||
Displays the usage of the | ||
[resource owners username and password](https://www.rfc-editor.org/rfc/rfc6749#section-4.3) | ||
for doing authorization. | ||
|
||
The resource owner password credentials grant type is suitable in | ||
cases where the resource owner has a trust relationship with the | ||
client, such as the device operating system or a highly privileged application. | ||
|
||
#### Configuration | ||
|
||
The big difference between Client Credentials and Resource Owners Passsword credentials | ||
is that the server also most support user authentication. Therefor this | ||
part is added to the server configuration: | ||
|
||
"authentication": { | ||
"user": { | ||
"acr": "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword", | ||
"class": "idpyoidc.server.user_authn.user.UserPass", | ||
"kwargs": { | ||
"db_conf": { | ||
"class": "idpyoidc.server.util.JSONDictDB", | ||
"kwargs": {"filename": full_path("passwd.json")} | ||
} | ||
} | ||
} | ||
} | ||
|
||
This allows for a very simple username/password check against a static file. | ||
|
||
On the client side the change is that the service configuration now looks | ||
like this: | ||
|
||
services = { | ||
"ropc": { | ||
"class": "idpyoidc.client.oauth2.resource_owner_password_credentials.ROPCAccessTokenRequest" | ||
} | ||
} | ||
|
||
|
||
### Authorization Code Grant (oauth2_code.py) | ||
|
||
The | ||
[authorization code grant](https://www.rfc-editor.org/rfc/rfc6749#section-4.1) | ||
is used to obtain both access tokens and possibly refresh tokens and is optimized | ||
for confidential clients. | ||
|
||
Since this is a redirection-based flow, the client must be capable of | ||
interacting with the resource owner's user-agent (typically a web | ||
browser) and capable of receiving incoming requests (via redirection) | ||
from the authorization server. | ||
|
||
In the demo implementation the response is transmitted directly from the server | ||
to the client no user agent is involved. | ||
|
||
In this story the flow contains three request/responses | ||
|
||
- Fetching server metadata | ||
- Authorization | ||
- Access token | ||
|
||
#### Configuration | ||
|
||
Let's take it part by part. | ||
First the endpoints, straight forward support for the sequence of exchanges we | ||
want to exercise. | ||
|
||
"endpoint": { | ||
"metadata": { | ||
"path": ".well-known/oauth-authorization-server", | ||
"class": "idpyoidc.server.oauth2.server_metadata.ServerMetadata", | ||
"kwargs": {}, | ||
}, | ||
"authorization": { | ||
"path": "authorization", | ||
"class": "idpyoidc.server.oauth2.authorization.Authorization", | ||
"kwargs": {}, | ||
}, | ||
"token": { | ||
"path": "token", | ||
"class": "idpyoidc.server.oauth2.token.Token", | ||
"kwargs": {}, | ||
} | ||
}, | ||
|
||
Next comes the type of tokens the grant manager can issue. | ||
In this case authorization codes and access tokens. | ||
|
||
"token_handler_args": { | ||
"key_conf": {"key_defs": KEYDEFS}, | ||
"code": { | ||
"lifetime": 600, | ||
"kwargs": { | ||
"crypt_conf": CRYPT_CONFIG | ||
} | ||
}, | ||
"token": { | ||
"class": "idpyoidc.server.token.jwt_token.JWTToken", | ||
"kwargs": { | ||
"lifetime": 3600, | ||
"aud": ["https://example.org/appl"], | ||
}, | ||
} | ||
}, | ||
|
||
The software can produce 3 types of tokens. | ||
|
||
- An encrypted value, unreadable by anyone but the server | ||
- A signed JSON Web Token following the pattern described in | ||
[JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens](https://datatracker.ietf.org/doc/rfc9068/) | ||
- An IDToken which only is used to represent ID Tokens. | ||
|
||
In this example only the two first types are used since no ID Tokens are produced. | ||
|
||
The next part is about the grant manager. | ||
|
||
"authz": { | ||
"class": AuthzHandling, | ||
"kwargs": { | ||
"grant_config": { | ||
"usage_rules": { | ||
"authorization_code": { | ||
"supports_minting": ["access_token"], | ||
"max_usage": 1, | ||
}, | ||
"access_token": { | ||
"expires_in": 600, | ||
} | ||
} | ||
} | ||
}, | ||
}, | ||
|
||
What this says is that an authorization code can only be used once and | ||
only to mint an access token. The lifetime for an authorization code is | ||
the default which is 300 seconds (5 minutes). | ||
The access token can not be used to mint anything. Note that in the | ||
token handler arguments the lifetime is set to 3600 seconds for a token | ||
while in the authz part and access tokens lifetime is defined to be | ||
600 seconds. It's the later that is used since it is more specific. | ||
|
||
"authentication": { | ||
"anon": { | ||
"acr": INTERNETPROTOCOLPASSWORD, | ||
"class": "idpyoidc.server.user_authn.user.NoAuthn", | ||
"kwargs": {"user": "diana"}, | ||
} | ||
}, | ||
|
||
It's convenient to use this no-authentication method in this context since we | ||
can't deal with user interaction. | ||
What happens is that authentication is assumed to have happened and that | ||
it resulted in that **diana** was authenticated. | ||
|
||
## OAuth2 Extension Stories | ||
|
||
The stories display support for a set of OAuth2 extension RFCs | ||
|
||
### PKCE (oauth2_add_on_pkce.py) | ||
|
||
[Proof Key for Code Exchange by OAuth Public Clients](https://datatracker.ietf.org/doc/rfc7636/). | ||
A technique to mitigate against the authorization code interception attack through | ||
the use of Proof Key for Code Exchange (PKCE). | ||
|
||
#### Configuration | ||
|
||
On the server side only one thing is added: | ||
|
||
"add_ons": { | ||
"pkce": { | ||
"function": "idpyoidc.server.oauth2.add_on.pkce.add_support", | ||
"kwargs": {}, | ||
}, | ||
} | ||
|
||
Similar on the client side: | ||
|
||
"add_ons": { | ||
"pkce": { | ||
"function": "idpyoidc.client.oauth2.add_on.pkce.add_support", | ||
"kwargs": { | ||
"code_challenge_length": 64, | ||
"code_challenge_method": "S256" | ||
}, | ||
}, | ||
} | ||
|
||
### JAR (oauth2_add_on_jar.py) | ||
|
||
[JWT-Secured Authorization Request (JAR)](https://datatracker.ietf.org/doc/rfc9101/) | ||
This document introduces the ability to send request parameters in a | ||
JSON Web Token (JWT) instead, which allows the request to be signed | ||
with JSON Web Signature (JWS) and encrypted with JSON Web Encryption | ||
(JWE) so that the integrity, source authentication, and | ||
confidentiality properties of the authorization request are attained. | ||
The request can be sent by value or by reference. | ||
|
||
#### Configuration | ||
|
||
On the server side nothing has to be done. The support for the | ||
request and request_uri parameters are built in to begin with. | ||
The reason for this is that OIDC had this from the beginning. | ||
|
||
On the client side this had to be added: | ||
|
||
"add_ons": { | ||
"jar": { | ||
"function": "idpyoidc.client.oauth2.add_on.jar.add_support", | ||
"kwargs": { | ||
'request_type': 'request_parameter', | ||
'request_object_signing_alg': "ES256", | ||
'expires_in': 600 | ||
}, | ||
}, | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import os | ||
|
||
BASEDIR = os.path.abspath(os.path.dirname(__file__)) | ||
|
||
|
||
def full_path(local_file): | ||
return os.path.join(BASEDIR, local_file) | ||
|
||
|
||
CRYPT_CONFIG = { | ||
"kwargs": { | ||
"keys": { | ||
"key_defs": [ | ||
{"type": "OCT", "use": ["enc"], "kid": "password"}, | ||
{"type": "OCT", "use": ["enc"], "kid": "salt"}, | ||
] | ||
}, | ||
"iterations": 1, | ||
} | ||
} | ||
|
||
SESSION_PARAMS = {"encrypter": CRYPT_CONFIG} | ||
|
||
KEYDEFS = [ | ||
{"type": "RSA", "key": "", "use": ["sig"]}, | ||
{"type": "EC", "crv": "P-256", "use": ["sig"]}, | ||
] |
Oops, something went wrong.