How to Implement a Client¶
According to the OpenID Connect (OIDC) Core document, a Relying Party is an ‘OAuth 2.0 Client application requiring End-User Authentication and Claims from an OpenID Provider’. The goal of this document is to show how you can build a RP using the PyOIDC library.
There are a couple of choices you have to make, but we’ll make them as we walk through the message flow.
Before I start you should know that the basic code flow in OpenID Connect consists of a sequence of request-responses, namely these:
Issuer discovery using WebFinger
Provider Info discovery
Client registration
Authentication Request
Access Token Request
Userinfo Request
In the example below I will go through all the steps and will use the basic
Client
class because it provides interfaces to all of them.
So lets start with instantiating a client:
from oic.oic import Client
from oic.utils.authn.client import CLIENT_AUTHN_METHOD
client = Client(client_authn_method=CLIENT_AUTHN_METHOD)
The first choice is really not yours. It’s the OpenID Connect Provider (OP) that has to decide on whether it supports dynamic provider information gathering and/or dynamic client registration.
If the OP doesn’t support client registration then you have to statically register your client with the provider. Typically, this is accomplished using a web page provided by the organization that runs the OP. I can’t help you with this since each provider does it differently. What you eventually must get from the provider is a client id and a client secret.
If the provider does not support dynamic OP information lookup, then the necessary information will probably appear on some web page somewhere. Again, turn to the provider. Going through the dynamic process below you will learn what information to look for.
Issuer discovery¶
OIDC uses webfinger to do the OP discovery. In very general terms this means that the user that accesses the RP provides an identifier. There are a number of different syntaxes that this identifier can adhere to. The most common is probably the email address syntax. It looks like an email address (local@domain) but is not necessarily one.
At this point in time let us assume that you will instantiate an OIDC RP.
As stated above, depending on the OP and the return_type you will use, some of these steps may be left out or replaced with an out-of-band process.
Using pyoidc this is how you would do it:
uid = "foo@example.com"
issuer = client.discover(uid)
The discover method will use webfinger to find the OIDC OP given the user identifier provided. If the user identifier follows another syntax/scheme the same method can still be used, you just have to preface the ‘uid’ value with the scheme used. The returned issuer must, according to the standard, be an HTTPS URL, but some implementers have decided differently on this, so you may get a HTTP URL.
Provider Info discovery¶
When you have the provider info URL you want to get information about the OP, so you query for that:
provider_info = client.provider_config(issuer)
A description of the whole set of metadata can be found here:
The resulting provider_info is a dictionary, hence you can easily find the necessary information:
>> provider_info["issuer"]
'https://example.com/op'
>> provider_info["authorization_endpoint"]
'https://example.com/op/authz_endp'
The provider info is also automatically stored in the client instance:
>> client.provider_info["scopes_supported"]
['openid', 'profile', 'email']
For the simple Client it is expected it will only talk to one OP during its lifetime.
Now, you know all about the OP. The next step would be to register the client with the OP.
Client registration¶
To do that you need to know the ‘registration_endpoint’. And you have to decide on a couple of things about the RP.
Things like:
- redirect_uris
REQUIRED. Array of Redirection URI values used by the Client.
- response_types
OPTIONAL. JSON array containing a list of the OAuth 2.0 response_type values that the Client is declaring that it will restrict itself to using. If omitted, the default is that the Client will use only the code Response Type.
- contacts
OPTIONAL. Array of email addresses of people responsible for this Client.
The whole list of possible parameters can be found here:
The only absolutely required information is the redirect_uris
Provide the parameters as arguments to the method:
args = {
"redirect_uris": ['https://example.com/rp/authz_cb'],
"contacts": ["foo@example.com"]
}
registration_response = client.register(
provider_info["registration_endpoint"], **args)
or a combination of the two.
If the OP requires to authenticate at the Registration Endpoint, you can pass the Initial Access Token
as a keyword argument to the Client.register()
method:
registration_response = client.register(
provider_infop["registration_endpoint"],
registration_token="my token", **args)
Provided the registration went flawlessly you will get the registration response (an instance of RegistrationResponse) as a result. The response will also be stored in the client instance (registration_response attribute) and some of the parameters will be unpacked and set as attributes on the client instance.
Note
The basic Client class is expected to only talk to one OP. If your service needs to talk to several OPs that are a couple of patterns you could use. One is to instantiate one RP per OP, another to keep the OP specific information like provider information and client registration information outside the RP and then setup the RP every time you want to talk to a new OP.
Now back to the static variant. If you cannot do the Provider discovery dynamically you have to get the information out-of-band and then configure the RP accordingly. And this is how you would do that:
from oic.oic.message import ProviderConfigurationResponse
op_info = ProviderConfigurationResponse(
version="1.0", issuer="https://example.org/OP/1",
authorization_endpoint="https://example.org/OP/1/authz",
token_endpoint="https://example.org/OP/1/token",
... and so on )
# or
# op_info = ProviderConfigurationResponse(**info)
# if you have the provider info in the form of a dictionary
client.handle_provider_config(op_info, op_info['issuer'])
Likewise, if the client registration has been done out-of-band:
from oic.oic.message import RegistrationResponse
info = {"client_id": "1234567890", "client_secret": "abcdefghijklmnop"}
client_reg = RegistrationResponse(**info)
client.store_registration_info(client_reg)