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 allows user tomjon scopes foo bar 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+foo+yo&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 foo 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