Introduction to oidcmsg¶
The OpenID Connect and OAuth2 standards both defines lots of messages. Requests that are sent from clients to servers and responses from servers to clients.
For each of these messages a number of parameters (claims) are listed, some of them required and some optional. Each parameter are also assigned a data type.
What is also defined in the standard is the on-the-wire representation of these messages. Like if they are the fragment component of a redirect URI or a JSON document transferred in the body of a response.
The oidcmsg.message.Message
class is supposed to capture all of this.
Using this class you should be able to:
- build a message,
- verify that a message’s parameters are correct, that all that are marked as required are present and all (required and optional) are of the right type
- serialize the message into the correct on-the-wire representation
- deserialize a received message from the on-the-wire representation into a
oidcmsg.message.Message
instance.
I will try to walk you through these steps below using example from RFC6749 (section 4.1 and 4.2).
The oidcmsg.message.Messag
class is the base class the oidcmsg
package contains subclasses representing all the messages defined in
OpenID Connect and OAuth2.
What oidcmsg also contains are tools for handling keys.
There is the oidcmsg.key_bundle.KeyBundle
class that can handle
keys that have the same origin. That for instance comes from one file or has
been fetched from a web site.
The oidcmsg.key_jar.KeyJar
class stores keys from many issuers.
Where the keys for each issuer is kept in one or more KeyBundle instances.
And finally there is the oidcmsg.jwt.JWT
class who is a sub class
of the cryptojwt.jwt
that knows about KeyJars.
This intro hopes to give you an overview of what you can do with the package. More specific descriptions can be found under howto.
Entity sending a message¶
Going from a set of attributes with values how would you go about creating an authorization request ? You would do something like this:
from oidcmsg.oauth2 import AuthorizationRequest
request_parameters = {
"response_type": "code",
"client_id": "s6BhdRkqt3",
"state": "xyz",
"redirect_uri": "https://client.example.com/cb"
}
message = AuthorizationRequest(**request_parameters)
authorization_endpoint = "https://server.example.com/authorize"
authorization_request = message.request(authorization_endpoint)
The resulting request will look like this
https://server.example.com/authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb
If we continue with the client sending an access token request there is a pattern emerging:
from oidcmsg.oauth2 import AccessTokenRequest
request = {
'grant_type':'authorization_code',
'code':'SplxlOBeZQQYbYS6WxSbIA',
'redirect_uri':'https://client.example%2Ecom%2Fcb'
}
message = AccessTokenRequest(**request)
access_token_request = message.to_urlencoded()
The resulting request will look like this:
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=https%3A%2F%2Fclient.example%252Ecom%252Fcb
Ready to be put in the HTTP POST body sent to the token endpoint.
The pattern is:
- Collect the parameters (with values) that are to appear in the request
- Chose the appropriate Message subclass
- Initiate the sub class with the collected information
- Serialize the message into whatever format is appropriate
Now, I have given examples on how a client would construct a request but of course there is not difference between this and a server constructing a response. The set of parameters is different and the message sub class to be used is different but the process is the same.
Entity receiving a message¶
Now the other side of the coin. An entity receives a message from its opponent. What to do ?
Again I’ll start with an example and again we’ll take the view of the client. The client has sent an authorization request, the user has been redirected to authenticate and decide on what permissions to grant and finally the server has redirect the user-agent back to the client by sending the HTTP response:
https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz
On the client it would get hold of the query part and then go from there:
from oidcmsg.oauth2 import AuthorizationResponse
query_conponent = 'code=SplxlOBeZQQYbYS6WxSbIA&state=xyz'
response = AuthorizationResponse().from_urlencoded(query_conponent)
print(response.verify())
print(response)
The result of this will be:
True
{'code': 'SplxlOBeZQQYbYS6WxSbIA', 'state': 'xyz'}
Similar when it comes to the response from the token endpoint:
from oidcmsg.oauth2 import AccessTokenResponse
http_response_body = '{"access_token":"2YotnFZFEjr1zCsicMWpAA",' \
'"token_type":"example","expires_in":3600,' \
'"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",' \
'"example_parameter":"example_value"}'
response = AccessTokenResponse().from_json(http_response_body)
print(response.verify())
print(response)
and this time the result will be:
True
{'access_token': '2YotnFZFEjr1zCsicMWpAA', 'token_type': 'example', 'expires_in': 3600, 'refresh_token': 'tGzv3JOkF0XG5Qx2TlKWIA', 'example_parameter': 'example_value'}
The processing pattern on the receiving end is:
- Pick out the protocol message part of the response
- Initiate the correct message subclass and run the appropriate
- deserializer method.
- Verify the correctness of the response
What if the response reecived was an error message ?¶
All the response subclasses are subclasses of
oidcmsg.oauth2.ResponseMessage
and that class provides you with one
method that is useful in this case:
>>> from oidcmsg.oauth2 import AccessTokenResponse
>>> response = {'error':'invalid_client'}
>>> message = AccessTokenResponse(**response)
>>> message.is_error_message()
True
Serialization methods¶
oidcmsg supports 3 different serialization/deserialization methods:
- urlencoded
- URL encoding converts characters into a format that can be transmitted over the Internet. URL encoding is described in RFC 3986
- json
- JavaScript Object Notation is a lightweight data-interchange format (https://www.json.org/)
- jwt
- Json Web Token specified in RFC 7519
There is a forth but that is just for internal use and that is to/from a python dictionary.
To use either of these there are a number of direct methods you can use:
- to_urlencoded/from_urlencoded
- to_json/from_json
- to_jwt/from_jwt
An example:
>>> from oidcmsg.oic import AccessTokenRequest
>>> params = {
... 'grant_type':'authorization_code',
... 'code':'SplxlOBeZQQYbYS6WxSbIA',
... 'redirect_uri':'https://client.example%2Ecom%2Fcb'
... }
>>> request = AccessTokenRequest(**params)
>>> print(request.to_urlencoded())
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=https%3A%2F%2Fclient.example%252Ecom%252Fcb
>>> print(request.to_json())
{"grant_type": "authorization_code", "code": "SplxlOBeZQQYbYS6WxSbIA", "redirect_uri": "https://client.example%2Ecom%2Fcb"}
to_jwt is a little bit more difficult since you need a couple of arguments. Starting with the same request as in the example above and using symmetric key crypto:
>>> from cryptojwt.jwk import SYMKey
>>> keys = [SYMKey(key="A1B2C3D4")]
>>> print(request.to_jwt(keys, algorithm="HS256")
eyJhbGciOiJIUzI1NiJ9.eyJncmFudF90eXBlIjogImF1dGhvcml6YXRpb25fY29kZSIsICJjb2RlIjogIlNwbHhsT0JlWlFRWWJZUzZXeFNiSUEiLCAicmVkaXJlY3RfdXJpIjogImh0dHBzOi8vY2xpZW50LmV4YW1wbGUlMkVjb20lMkZjYiJ9.PuzT0r7iEV99fRA9d6zz0Farf2qhQR2Tua0Z4Luar9g
Deserializing¶
Deserializing is as easy as serializing:
>>> from oidcmsg.oic import AccessTokenRequest
>>> params = {
... 'grant_type':'authorization_code',
... 'code':'SplxlOBeZQQYbYS6WxSbIA',
... 'redirect_uri':'https://client.example%2Ecom%2Fcb'
... }
>>> request = AccessTokenRequest(**params)
>>> msg_url = request.to_urlencoded()
>>> parsed_urlenc = AccessTokenRequest().from_urlencoded(msg_url)
>>> print(parsed_urlenc)
{'grant_type': 'authorization_code', 'code': 'SplxlOBeZQQYbYS6WxSbIA', 'redirect_uri': 'https://client.example%2Ecom%2Fcb'}
>>> msg_json = request.to_json()
>>> parsed_json = AccessTokenRequest().from_json(msg_json)
>>> print(parsed_json)
{'grant_type': 'authorization_code', 'code': 'SplxlOBeZQQYbYS6WxSbIA', 'redirect_uri': 'https://client.example%2Ecom%2Fcb'}
>>> from cryptojwt.jwk.hmac import SYMKey
>>> keys = [SYMKey(key="A1B2C3D4")]
>>> msg_jws = request.to_jwt(keys, algorithm="HS256")
>>> parsed_jwt = AccessTokenRequest().from_jwt(msg_jws, keys)
>>> print(parsed_jwt)
{'grant_type': 'authorization_code', 'code': 'SplxlOBeZQQYbYS6WxSbIA', 'redirect_uri': 'https://client.example%2Ecom%2Fcb'}
>>> print(parsed_jwt.jws_header)
>>> {'alg': 'HS256'}
Note the last line. When you have parsed a signed JWT the resulting class
instance contains as extra information the header of the signed JWT.
Note also that a signed JWT constructed this way will not contain any
extra information beside the information in the request.
If you want to create a signed JWT which contains issuer, intended audience
and more then you should use the oidcmsg.jwt.JWT
class.
More about that below.
Json Web Token¶
There as cases in OpenID connect where you want to fill a signed JWT with
a lot of metadata. One such is when you construct an ID Token.
The to_jwt method in oidcmsg.message.Message
will not add
any extra information for you.
oidcmsg.jwt.JWT
does.
Nothing beats an example:
>>> BOB = 'https://bob.example.com'
>>> kj = KeyJar()
>>> kj.add_symmetric(owner='', key='client_secret', usage=['sig'])
>>> alice = JWT(kj, iss=ALICE, alg="HS256")
>>> payload = {'sub': 'subject_id'}
>>> _jws = alice.pack(payload=payload, recv=BOB)
>>> kj[ALICE] = kj['']
>>> bob = JWT(kj, iss=BOB, alg='HS256)
>>> info = bob.unpack(_jws)
>>> print(info)
{'iss': 'https://alice.example.org', 'iat': 1518619782, 'aud': ['https://bob.example.com'], 'sub': 'subject_id'}
>>> type(info)
<class 'oidcmsg.oic.JsonWebToken'>
>>> print(info.jws_header)
{'alg': 'HS256'}
To walk through what’s happening about. We first need a
oidcmsg.key_jar.KeyJar
instance with the needed keys.
We only have one in this example a symmetric key.
This keyjar is what alice uses when she wants to sign the JWT.
When she initiates the oidcmsg.jwt.JWT
she sets a set of default
values, like signing algorithm and her own issuer ID.
When constructing the signed JWT she uses the pack method that as
arguments takes payload and receiver.
Now we turn to Bob. He has his own keyjar containing the symmetric key marked to belong to alice. This is important since that binding will be used when unpacking the signed JWT. The method will look inside the payload to find the issuer and from there find usable keys in the keyjar.
To set the issuer to BOB when initiating the JWT is necessary because the value on that will be matched against the audience of the signed JWT.
Let’s assume that Eve wanted to listen in and had access to the key:
>>> eve = JWT(kj, iss='https://eve.example.com')
>>> info = eve.unpack(_jws)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/cryptojwt-0.0.1-py3.6.egg/cryptojwt/jwt.py", line 297, in unpack
_info = self.verify_profile(_msg_cls, _info, **vp_args)
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/cryptojwt-0.0.1-py3.6.egg/cryptojwt/jwt.py", line 234, in verify_profile
if not _msg.verify(**kwargs):
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/oidcmsg-0.0.1-py3.6.egg/oidcmsg/oic/__init__.py", line 946, in verify
raise NotForMe('Not among intended audience')
oidcmsg.exception.NotForMe: Not among intended audience
Now Eve probably wouldn’t care but there you are.