summaryrefslogtreecommitdiff
path: root/yarns/300-end-user-auth.yarn
blob: 98295fb9acc327133759616c6e8822cdccaac6f1 (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
End-user interactive login
=============================================================================

We will be implementing the full [OpenId Connect authorization code
flow][] later on, but currently this is a tiny, insufficiently secure
subset of that. It's just enough for us to have some form of login, to
set up a continuous delivery pipeline for it, and to start building
the full thing.

OpenID Connect
-----------------------------------------------------------------------------

[OpenID Connect]: https://openid.net/specs/openid-connect-core-1_0.html
[OAuth2]: https://tools.ietf.org/html/rfc6749

[OpenID Connect][] (OIDC) is a standard protocol for authenticating
end-users. It is an extension of the [OAuth2][] protocol, which
provides authorization. The distinction is important: OAuth2 allows a
user let an application access the user's data on some service, but
does not itself provide a way to verify the user's identity. OIDC
provides a way for the service to do the identity verification
(authentication).

There are, in the OIDC context, several entities involved:

* The **end-user**, who owns some data on the resource server. Ownership
  here is in the legal sense.

* The end-user's user agent, also known as the browser. This is what
  the end-user interacts with directly.

* The **resource server**, where the actual data is. It is assumed the
  data is sensitive, and needs to be protected, but that some access
  should be allowed.

* The **facade application**, which accesses data on the resource
  server, and processes it in some way for the user's benefit.

* The **OpenID provider** authenticates the user and produces tokens
  for the facade application to access the user's data on the resource
  server. This is Qvisqve.

The OIDC authorization code flow works like this, at a very high
level:

* User initiates an authentication process. Typically by clicking on a
  login link, but might also happen when the facade notices
  authentication is needed, such as when its token expires. In either
  case, there is an HTTP request from the browser to the facade.

* The facade returns a 302 redirect to the browser. The redirect takes
  the browser to the OpenID provider, and supplies a number of
  parameters in the URL to identify the application, and what access
  it wants.

* The OpenID provider verifies the user's identify, such as by asking
  them for their username and password.

* Once the identity is verified, the OpenID provider redirects the
  browser back to the facade, and gives a unique, single-use
  authorization code as part of the redirected URL.

* The facade extracs the authorization code, and requests an access
  token from the OpenID provider. As part of that, the facade
  authenticates itself to the OpenID provider.

* The facade uses the access token to get data from the resource
  server, and shows it to the user via the browser.

Note that the access token never reaches the browser. Also, it is
given only to an authenticated facade application. Further, the facade
application never sees the user's login creentials. There's more
details in the protocol to mitigate replay attack, cross-site
forgeries and other shenanigans.

Test scenario
-----------------------------------------------------------------------------

This scenario shows the steps of authenticating the end-user.

    SCENARIO end-user interactive login

We need to have a Qvisqve, and there needs to be a user account
configured for it. Further, the facade application must also be
registerd, before the login process starts.

    GIVEN a Qvisqve configuration for "https://qvisqve"
    AND Qvisqve configuration has user account tomjon with password hunter2
    AND Qvisqve configuration has application facade
    ... with callback url https://facade/callback
    ... and secret happydays
    ... and allowed scopes read write
    AND a running Qvisqve instance

User goes to the facade's login URL and gets a redirect to Qvisqve's
/auth endpoint. We skip the request to the facade. The redirect to
/auth includes important parameters. The parameters are:

* `response_type=code` — identify this as the start of an
  authorization code flow
* `scope=openid+read` — space delimited list of scope names
  required by the facade, plus `openid` to indicate we want OIDC
* `client_id=facade` — identify the application; note that at
  this point Qvisqve can't verify this
* `state=RANDOM`— a unique, single-use, hard-to-guess value
* `redirect_uri=https://facade/callback` — where the browser
  should go when the user has been identified

Since the request to Qvisqve is always done as a result of an HTTP 302
redirect response from the facade, it's always a GET request, and the
parameters are part of the URL. The response should be login form. The
form will have a hidden field, with which Qvisqve will know which
authentication attempt is happening. This field will have a new,
unique, hard-to-guess value every time the user authenication starts
anew. Note that this should probably be different from the `state`
value from the facade (FIXME: why?).

    WHEN browser requests GET /auth?response_type=code&scope=openid+read&client_id=facade&state=RANDOM&redirect_uri=https://facade/callback
    THEN HTTP status code is 200 OK
    AND Content-Type is text/html
    AND body has an HTML form with field username
    AND body has an HTML form with field password
    AND body has an HTML form with field attempt_id
    AND HTML form field attempt_id is saved as ATTEMPTID

The user enters their login credentials, and presses submit. Qvisqve
verifies the credentials.

    WHEN browser requests POST /auth, with form values
    ... username=tomjon and password=wrong and attempt_id=${ATTEMPTID}
    THEN HTTP status code is 401 Unauthorized

FIXME: a 401 error is unfriendly, the user should be given an
opportunity to try again. Fix later. Anyway, let's say user can use
the back button and tries again with the correct password.

Qvisqve accepts the credentials this time, and generates an
"authorization code". This is a unique, hard-to-predict value that can
be used only once. The code is added to the facade callback URL, and
the browser is redirected to that. This is how the facade gets the
code. The browser also gets the code, but since using the code
requires the facade's client credentials, the browser can't use the
authorization code for anything, so it's acceptably safe to let the
browser see it.

    WHEN browser requests POST /auth, with form values
    ... username=tomjon and password=hunter2 and attempt_id=${ATTEMPTID}
    THEN HTTP status code is 302 Found
    AND HTTP Location header starts with https://facade/callback?
    AND HTTP Location header is saved as LOCATION
    AND authorization code from LOCATION is saved as CODE
    AND state from LOCATION is RANDOM

The browser follows the redirect to the facade. The facade extracts
the authorization code, and uses its own client credentials to
retrieve the access token corresponding to the code.

    WHEN facade requests POST /token, with
    ... form values grant_type=authorization_code and code=${CODE}
    ... using Basic Auth with username facade, password wrong
    THEN HTTP status code is 401 Unauthorized

    WHEN facade requests POST /token, with
    ... form values grant_type=authorization_code and code=${CODE}
    ... using Basic Auth with username facade, password happydays

Qvisqve returns the access token. It has the requested scope, minus
"openid".

FIXME: The aud field should have the facade as one of the values, but
the resource server needs to also be there. Or else the resource
server needs to ignore aud, which seems fishy, or it needs to accept
the facade as the aud, which seems tricky. I don't know how to handle
this. Needs research and thinking.

    THEN HTTP status code is 200 OK
    AND Content-Type is application/json
    AND JSON body has field access_token
    AND JSON body has field token_type, with value Bearer
    AND JSON body has field expires_in

    AND access token has a scope field set to read
    AND access token has a sub field set to tomjon

The authorization code can't be re-used.

    WHEN facade requests POST /token, with
    ... form values grant_type=authorization_code and code=${CODE}
    ... using Basic Auth with username facade, password happydays
    THEN HTTP status code is 400 Bad request

    FINALLY Qvisqve is stopped