From b44f84eaab1af6a85f4313f3cedc628591eb7d0d Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Sat, 13 Dec 2014 15:20:53 +0100 Subject: [PATCH] Updated RP documentation. --- doc/howto/rp.rst | 139 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 102 insertions(+), 37 deletions(-) diff --git a/doc/howto/rp.rst b/doc/howto/rp.rst index 8479ff2..298b1ae 100644 --- a/doc/howto/rp.rst +++ b/doc/howto/rp.rst @@ -1,7 +1,7 @@ .. _howto_rp: -How to set up an OpenID Connect Relying Party (RP) -================================================== +Python Cookbook for OpenID Connect Public Client +================================================ According to the OpenID Connect (OIDC) Core document a Relying Party is an 'OAuth 2.0 Client application requiring End-User @@ -13,26 +13,43 @@ library. There are a couple of choices you have to make, but we'll take that as we walk through the message flow. -Client registration and Provider information -:::::::::::::::::::::::::::::::::::::::::::: +Before I start you should now that the basic code flow in OpenID Connect +consists of a sequence of request-responses, namely these: -The first choice is really not yours it's the OpenID Connect Provider (OP) +* Issuer discovery using WebFinger +* Provider Info discovery +* Client registration +* Authorization Request +* Access Token Request +* Userinfo Request + + +In the example below I will go through all the steps and I will use the basic +Client class because it will provide interfaces to all of them. +So lets start with instantiating a client:: + + from oic.oic import Client + + c = Client() + + +The first choices 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 static register your client with the provider. Typically this is accomplished using a web -page and form provider by the organization that runs the OP. Can't help +page provided by the organization that runs the OP. Can't help you with this since each provider does it differently. What you eventually must get from the service provide is a client id and a client secret. If the service provider does not support dynamic OP information lookup, then -the necessary information will probably appear on some webpage somewhere. +the necessary information will probably appear on some web page somewhere. Again look to the service provider. Going through the dynamic process below you will learn what information to look for. -OP discovery and RP registration --------------------------------- +Issuer discovery +---------------- OIDC uses webfinger (http://tools.ietf.org/html/rfc7033)to do the OP discovery. In very general terms this means @@ -45,24 +62,27 @@ At this point in time let us assume that you will instantiated a OIDC RP. .. Note::Oh, by the way I will probably alternate between talking about the RP and the client, don't get caught up on that, they are the same thing. -Instantiation at it's simplest can look like this:: - from oic.oic.consumer import Consumer +As stated above depending on 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. - client = Consumer() - -Now what about the users identifier how to get from that to knowing where the -OP are and what the OP can do. To find out where it is you can do this:: +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. +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 a https url, but some implementers have decided differently on this, so you may get a http url. -Anyway once you have the URL you want to get information about the OP, so + +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) @@ -70,6 +90,12 @@ you query for that:: A description of the whole set of metadata can be found here: http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata +.. Note::One parameter of the provider info is the issuer parameter this + is supposed to be *exactly* the same as the URL you used to fetch the + information. Now, this isn't valid for some providers. You can tell the + client to not care about this by setting + client.allow["issuer_mismatch"] = True + The resulting provider_info is a dictionary, hence you can easily find the necessary information:: @@ -89,7 +115,13 @@ the provider information is store using the issuer name as the key:: Now, you know all about the OP. The next step would be to register the -client with the OP. To do that you need to know the 'registration_endpoint'. +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: @@ -114,11 +146,26 @@ So, registering a client could then be accomplished doing:: client.redirect_uris = ['https://example.com/rp/authz_cb'] registration_response = client.register(provider_info["registration_endpoint"]) +You have two choices here, you can either assign the parameters with value to +the client instance as in the example above or you can provide them as an +argument 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. + Provided the registration went flawlessly you will get the registration response (an instance of a RegistrationResponse) as a result. But at the same time -tha response will be stored in the client instance (client_info parameter). +automatically the response will be stored in the client instance +(client_info parameter). -.. Note:: The basic Client class is made to only talk to one OP. If your service +.. 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 @@ -136,14 +183,18 @@ the RP accordingly. And this is how you would do that:: 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.provider_info = op_info Likewise if the client registration has been done out-of-band:: from oic.oic.message import RegistrationResponse - client_reg = RegistrationResponse( - client_id="1234567890", client_secret="abcdefghijklmnop") + info = {"client_id": "1234567890", "client_secret": "abcdefghijklmnop"} + client_reg = RegistrationResponse(**info) client.client_info = client_reg @@ -167,30 +218,30 @@ Before doing the request you have to decided on a couple of things: * which response type you want to use. You can read up on response types in the OAuth2 RFC. * the scope. The list of scopes must contain 'openid'. There is a list of - extra scopes that OIDC defines those can be found in the specification. + extra scopes that OIDC defines which can be found in the specification. * whether to use HTTP 'GET' or 'POST'. Either one is allowed. 'GET' is default. Authorization Code Flow ^^^^^^^^^^^^^^^^^^^^^^^ From the list redirect_uris you have to pick one to use for this request. -Given you have all that you now can send the request:: +Given you have all that, you now can send the request:: import hashlib import hmac from oic.oauth2 import rndstr - client.state = rndstr() - _nonce = rndstr() + session["state"] = rndstr() + session["nonce"] = rndstr() args = { "client_id": client.client_id, "response_type": "code", "scope": ["openid"], - "nonce": hmac.new(_nonce, digestmod=hashlib.sha224), + "nonce": session["nonce"], "redirect_uri": client.redirect_uris[0] } - result = client.do_authorization_request(state=client.state, + result = client.do_authorization_request(state=session["state"], request_args=args) The arguments *state* are use to keep track on responses to @@ -199,6 +250,9 @@ outstanding requests (state). *nonce* is a string value used to associate a Client session with an ID Token, and to mitigate replay attacks. +Since you will need both these arguments later in the process you probably +want to store them in a session object (assumed to look like a dictionary). + Most probable the response to this request will be a redirect to some other URL where the authentication is performed. @@ -208,11 +262,14 @@ You can parse this response by doing:: from oic.oic.message import AuthorizationResponse + # If you're in a WSGI environment + response = environ["QUERY_STRING"] + aresp = client.parse_response(AuthorizationResponse, info=response, sformat="urlencoded") code = aresp["code"] - assert aresp["state"] == client.state + assert aresp["state"] == session["state"] *aresp* is an instance of an AuthorizationResponse or an ErrorResponse. The later if an error was return from the OP. @@ -237,9 +294,13 @@ token:: 'scope' has to be the same as in the authorization request. + If you don't specify a specific client authentication method, then *client_secret_basic* is used. +You have to provide client_id and client_secret as arguments, how they are used +depends on the authentication method used. + The resp you get back is an instance of an AccessTokenResponse or again possibly an ErrorResponse instance. @@ -247,6 +308,9 @@ If it's an AccessTokenResponse the information in the response will be stored in the client instance with *state* as the key for future use. One if the items in the response will be the ID Token which contains information about the authentication. +One parameter (or claim as its also called) is the nonce you provider with +the authroization request. + And then the final request, the user info request:: userinfo = client.do_user_info_request(state=aresp["state"]) @@ -255,7 +319,7 @@ Using the *state* the client library will find the appropriate access token and based on the token type chose the authentication method. *userinfo* in an instance of OpenIDSchema or ErrorResponse. Given that you have -used openid as the scope *userinfo* will not contain a lot of information. +used openid as the scope, *userinfo* will not contain a lot of information. actually only the *sub* parameter. Implicit Flow @@ -266,21 +330,19 @@ Endpoint; the Token Endpoint is not used. So:: - import hashlib - import hmac from oic.oauth2 import rndstr - client.state = rndstr() - _nonce = rndstr() + seession["state"] = rndstr() + session["nonce"] = rndstr() args = { "client_id": client.client_id, - "response_type": "token", + "response_type": ["id_token", "token"], "scope": ["openid"], - "nonce": hmac.new(_nonce, digestmod=hashlib.sha224), + "nonce": session["nonce"], "redirect_uri": client.redirect_uris[0] } - result = client.do_authorization_request(state=client.state, + result = client.do_authorization_request(state=session["state"], request_args=args) @@ -288,6 +350,9 @@ As for the Authorization Code Flow the authentication part will begin with a redirect to a login page and end with a redirect back to the registered redirect_uri. +Since the response will be return as a fragment you need some special code +to catch that information. How you do that depends on your setup. + Again the response can be parse by doing:: from oic.oic.message import AuthorizationResponse