summaryrefslogtreecommitdiffstats
path: root/doc/howto/rp.rst
blob: 8479ff2df3c7e6422939ede424a648feb4b630ee (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
.. _howto_rp:

How to set up an OpenID Connect Relying Party (RP)
==================================================

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'.

This 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 take that as
we walk through the message flow.

Client registration and Provider information
::::::::::::::::::::::::::::::::::::::::::::

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 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
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.
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
--------------------------------

OIDC uses webfinger (http://tools.ietf.org/html/rfc7033)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
probably the e-mail address syntax. It's something the looks like an e-mail
address (local@domain) but not necessarily is one.

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

    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::

    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 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
you query for that::

    provider_info = client.provider_config(issuer)

A description of the whole set of metadata can be found here:
http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata

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.
Since a RP can potentially talk to more than one OP during it's life time
the provider information is store using the issuer name as the key::

    >> client.provider_info.keys()
    ['https://example.com/op']
    >> client.provider_info["https://example.com/op"]["scopes_supported"]
    ['openid', 'profile', 'email']


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'.
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 e-mail addresses of people responsible for this Client.

The whole list of possible parameters can be found here:
http://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata

The only absolutely required information is the **redirect_uris**

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"])

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).

.. Note:: The basic Client class is made 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 can not 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 )

    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")

    client.client_info = client_reg


Authorization query
-------------------

Once the client knows about the OP and the OP knows about the client we can
start doing business, that is get information about users.

The request you then want to make is the authentication request.

.. Note:: This might be slightly confusing. In OAuth2 (RFC 6749) the initial
    request is called authorization request and you do it at the authorization
    endpoint. In OIDC the request is renamed to authentication request.
    For historical reasons I've kept the name authorization request for the
    method that handles that request.

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.
* 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::

    import hashlib
    import hmac
    from oic.oauth2 import rndstr

    client.state = rndstr()
    _nonce = rndstr()
    args = {
        "client_id": client.client_id,
        "response_type": "code",
        "scope": ["openid"],
        "nonce": hmac.new(_nonce, digestmod=hashlib.sha224),
        "redirect_uri": client.redirect_uris[0]
    }

    result = client.do_authorization_request(state=client.state,
                                             request_args=args)

The arguments *state* are use to keep track on responses to
outstanding requests (state).

*nonce* is a string value used to associate a Client session with an ID Token,
and to mitigate replay attacks.

Most probable the response to this request will be a redirect to some other
URL where the authentication is performed.

Eventually a response is sent to the URL given as the redirect_uri.

You can parse this response by doing::

    from oic.oic.message import AuthorizationResponse

    aresp = client.parse_response(AuthorizationResponse, info=response,
                                  sformat="urlencoded")

    code = aresp["code"]
    assert aresp["state"] == client.state

*aresp* is an instance of an AuthorizationResponse or an ErrorResponse.
The later if an error was return from the OP.
Among other things you should get back in the authorization response is
the same state value as you used
when sending the request. If you used the response_type='code' then you
should also receive a grant code which you then can use to get the access
token::

    args = {
        "code": aresp["code"],
        "redirect_uri": client.redirect_uris[0],
        "client_id": client.client_id,
        "client_secret": client.client_secret
    }

    resp = client.do_access_token_request(scope="openid",
                                          state=aresp["state"],
                                          request_args=args,
                                          authn_method="client_secret_post"
                                          )


'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.

The resp you get back is an instance of an AccessTokenResponse or again possibly
an ErrorResponse instance.

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.
And then the final request, the user info request::

    userinfo = client.do_user_info_request(state=aresp["state"])

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.
actually only the *sub* parameter.

Implicit Flow
^^^^^^^^^^^^^

When using the Implicit Flow, all tokens are returned from the Authorization
Endpoint; the Token Endpoint is not used.

So::

    import hashlib
    import hmac
    from oic.oauth2 import rndstr

    client.state = rndstr()
    _nonce = rndstr()
    args = {
        "client_id": client.client_id,
        "response_type": "token",
        "scope": ["openid"],
        "nonce": hmac.new(_nonce, digestmod=hashlib.sha224),
        "redirect_uri": client.redirect_uris[0]
    }

    result = client.do_authorization_request(state=client.state,
                                             request_args=args)


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.

Again the response can be parse by doing::

    from oic.oic.message import AuthorizationResponse

    aresp = client.parse_response(AuthorizationResponse, info=response,
                                  sformat="urlencoded")

    assert aresp["state"] == client.state

Now *aresp* will not contain any code reference but instead an access token and
an ID token. The access token can be used as described above to fetch user
information.

Using Implicit Flow instead of Authorization Code Flow will save you a
round trip but at the same time you will get an access token and no
refresh_token. So in order to get a new access token you have to perform another
authorization request.