OAuth2 implementation with ORY Hydra, Vapor 3 and iOS 12
Part 3: Setup Identity Provider
Note: This post was originally written in 2018 and has not been updated since then. So this is not state of the art anymore, but maybe it can still be helpful to some people.
After we successfully implemented the user management in Part 2, we’ll now need to add the implementation of the Identity Provider for ORY Hydra on our Vapor backend.
Prerequisites
You should checkout the previous parts of the series:
Create Identity provider
As explained in Part 1 of this tutorial, ORY Hydra expects its identity provider to react to GET
calls on /auth/register
or /auth/login
and /auth/consent
. When one of the first two endpoints is called, we want to present the user with a web interface where he can register/login if necessary. For the consent endpoint we don’t want to present any UI, but just accept the consent request as we are working with a first party app. ORY Hydra will decide whether or not the user needs to login again (see Authorization Flow diagram in Part 1).
To make sure our Vapor backend reacts to those endpoints, we need to add them to the boot
function in our AuthController
and add the related functions.
You’ll see three errors because the functions declare to return Future<Response>
even though they are not doing that yet. We’ll get to that a little later.
Getting and accepting the LoginRequest
When the login and register endpoints are called, the first thing we need to do is to ask ORY Hydra whether or not we are supposed to present the user with a UI to login or if that is not needed. We can do that via accessing Hydra’s administrative API endpoint for receiving a login request /oauth2/auth/requests/login
.
Create Model and Service
As we now start interacting with Hydra quite a lot and we don’t want to mess up our AuthController
with all that code, the first thing we gonna do is to create a new class called HydraService
.
To be able to access that service we’ll need to register it in configure.swift
.
First thing we want to be able to do is receive a login request from Hydra. Let’s create a model for that request.
Hydra is able to return even more stuff to us when asking for a login request, but these are the three parameters we care about.
challenge
: This is the identifier of the authentication request. It is used to identify the session.skip
: If true, this Bool indicates that there is no need for the user to login again. So we can use this to know whether or not to show the login/register UI.subject
: This String usually contains the user ID of the user who asked for the login request. We’ll need that information in a later step of the auth flow.
Get LoginRequest from Hydra
Now we should create a function in the HydraService
to get the login request from Hydra:
That is quite a lot of code, let’s go through it step by step.
- first we create the
baseUrl
which works with an environment variable you need to add in therun
scheme in XCode. Via this URL the administrative API of ORY Hydra can be reached.
- then we create a small function to deliver the URL for the login request endpoint with a certain challenge
- then we create a function to perform to get the login request
- in the end we create another function to extract the
challenge
from the request - We also add a convenience function on
Future
in a new helper file to map the response:
Render UI for register + login
Now we can go back to the AuthController
and use the HydraService
in the renderRegister
and renderLogin
functions. In both cases the interaction with Hydra is the same, just the UI we show to the user and our user management is different.
First we are creating a HydraService
instance and extract the challenge
from the request. The we try to get the LoginRequest via the HydraService
. We ignore the skip
flag for now and just always try to render the authentication UI by adding the following:
The above code just looks for an HTML file called either register
or login
. We used Leaf for creating the HTML files. You can check them out in the Github repository, but we won’t go into detail on how to create them and what needs to be done to add Leaf to the Vapor project. It is just two basic forms that trigger POST
calls on our user management endpoints for registering and logging in a user when they are submitted. The only specific detail you need to know is that they need to include the challenge
as a hidden input field, so we can extract it from there again later.
Now we don’t want to ignore the skip
value anymore, but only want to show the UI if skip
is false. If it is true, we’ll need to tell Hydra to accept the login request and trigger the consent step afterwards.
Accepting the LoginRequest
There is quite a few things we need to do to accept a login request. Let’s start in the AuthController
again and change the renderAuth
function to handle the skip
flag:
What we changed in here is the following:
- we check if the login UI should be shown via the
skip
flag in the login request - if
skip
is false, we show the UI as before - if
skip
is true, we want to accept the login request and trigger the consent step afterwards
Now we need to switch back to the HydraService
and implement acceptLoginRequest
.
In the HydraService
we added another convenience function for receiving the URL for the accept login request endpoint of Hydra. Then we created the actual function to accept the request. We map the response into a HydraRedirect
object, which only includes the redirect URL.
Create Payload model for accepting LoginRequest
In the request we need to include some parameters that Hydra expects to get. To be able to include them this way, we need to add another struct in our HydraLoginRequest
model:
Our new struct HydraAcceptLoginRequestPayload
includes three properties:
remember
: this Bool, if true, tells Hydra to remember the user by storing a cookie with authentication data in the browserremember_for
: this tells Hydra how long it should remember the user. When set to 0, it will be remembered indefinitelysubject
: the samesubject
as in the login request
In addition to this, we need to add two more convenience functions in extensions on Client
and Request
. Create two new files for these in the Helpers
folder and add the following code:
In case we want to skip the register/login UI and directly accept the login request, we are done. We still need to accept the login request after the user registered/logged in though.
Accept LoginRequest after registration/login
To do so, we need to adjust the authenticate
function in the AuthController
to accept the login request after a successful register/login. Create a new function in the AuthController
called loginOrRegister
that wraps the code for the user management:
Now change the authenticate
function to accept the login request:
- We changed the return type to
Future<Response>
. We need to do that to be able to callrenderAuth
again in case accepting the login request fails. - We first do the user management part of registering/logging in as before.
- We then tell the
HydraService
to accept the login request. You might notice that the signature of this function is different than the one we used before. We’ll get to that soon. - In case of an error, we want to return the user to the register UI and show an error message. To be able to do so, we need to pass the challenge to the
renderAuth
function like this:
As mentioned, the acceptLoginRequest
function in the HydraService
is not the same we used before. This is because we don’t have access to the HydraLoginRequest
in this case. Therefore we need to add another function for accepting the login request with a different signature in the HydraService
.
As you can see, we now try to access the login challenge via the request’s payload. To be able to do so, we need to adjust our LoginPayload
to contain the challenge.
We just added a new property called challenge
of type String and added a validation to make sure this property can not be empty.
That’s it for the case where the user has to actively login. The next step will be to implement something similar for the consent part.
Getting and accepting the ConsentRequest
After we successfully accepted the login request, Hydra will start the consent step by making a GET
request to /auth/consent
on our API. So we’ll need to handle that request. The difference compared to the login flow is that this time we never want to show a UI to the user, but always directly accept the consent request as our auth server is only interacting with our own app for now.
Create ConsentRequest model
The first step is o create a model for the HydraConsentRequest
similar to the login request we already have. Create a new file and add the following:
The only difference compared to the HydraLoginRequest
is that we don’t have the subject
anymore, but instead the requested_scope
and grant_scope
. Both of these describe the scope that the user grants access to.
Getting + accepting the ConsentRequest in Service
Now go back to the HydraService
and add functions for getting and accepting the consent request. To do so, we need to adjust our convenience functions that return the necessary URLs to work with login and consent like this:
We introduced a new type called HydraRequestType
and use that in the convenience functions to return proper URLs for both, login and consent. Next we are going to add functions for getting and accepting the consent request, which work similarly to the login functions.
Go back to the AuthController
and adjust the skipConsent
function.
What we do in here is the following:
- we get the consent request from Hydra
- independant of what we receive in the response, we always directly accept the request afterwards
- after that we respond with a redirect
After the consent request is accepted, Hydra will redirect the user back into the iOS app with the requested tokens.
Protection against CSRF
In general, we would now be done with the authorization flow implementation on our Vapor backend. There is one more thing we should add for security reasons though. We should protect the user from Cross-site request forgery (CSRF).
Add CSRF library
To do so we need to add CSRF tokens to our HTML forms as hidden input fields. There is a convenience library for Vapor that takes care of creating those tokens called CSRF. Let’s add that to our project as a dependency.
To be able to use it, we’ll need to adjust our configure.swift
file to register CSRF
as a service and add it and SessionMiddleware
as middleware.
Before implementing adding a CSRF token to our login/register forms, let’s roughly go through what the CSRF
library does. It can be used to create and validate CSRF tokens. As we registered it as a middleware for our requests, we don’t need to actively trigger the validation, the library will take care of that automatically. What we do need to do though is to create a CSRF token and add it to our HTML forms as a hidden input field.
Add CSRF token to login + register forms
So, we need to add the CSRF token to our login/register forms in the AuthController
. Open it and create an instance of CSRF
at the top of the file:
Then navigate to the renderAuthForm
function and change it to the following:
What do we do here exactly?
- We first get the session out of the request and save it.
- Then we check if the request’s cookies contain a CSRF token, that was generated by Hydra. We need that token to be able to verify the request came from our own service.
- We save Hydra’s CSRF token under a key specific to the
CSRF
library in the session. - Then we create a new token from this request using the
CSRF
library and pass it as a view variable to our HTML form.
CSRF
will now get Hydra’s CSRF token out of the session, use it to create a token and make sure that the token passed to the HTML form is matching the one created using Hydra’s one. This way we can be sure, the request came from our own service.
This is it! We implemented all parts necessary to run the authorization flow with our vapor backend. In the next part we’ll have a look at the implementation of the iOS client.
Further reading
Hydra Node.js example for Identity provider:
https://github.com/ory/hydra-login-consent-node
What is CSRF:
https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)
Resources
You can find the fully implemented Vapor backend for this tutorial on Github!
Stay updated
If you liked this tutorial and are interested in further articles from us, follow us here or on Twitter and checkout our website!