How to write an application that integrates Kratos in Go
Building applications is hard, and developers need to maximise productivity, so they can focus their efforts on solving business problems. One way of achieving this is to use services provided by third-party providers. Read on for David Oram’s process of integrating Kratos in Go.
Using configurable services
This allows developers to offload essential application responsibilities; for example by using database services from AWS. This trend has been with us for many years, and has seen the rise in many services such as compute services, file storage and hosting. The same shift has been seen with application level services, where it has become more and more attractive to integrate services together to build business applications.
A common requirement for modern web based applications, is the provision for users to register, login, logout and manage their own accounts. This is where Kratos comes in. Kratos is an open source project that help takes the burden off providing these services in your own applications.
Benefits of integrating your app with Kratos
-
Providing the benefits of keeping up with security best practices
-
BYO user interface. Kratos allows you to define the look’n’feel of the UI. The user interface is implemented in your application, giving you full control
-
BYO data model. The identity data model that Kratos uses is provided by your application
-
Long term support. Kratos project is supported by some big players in the open source space such as ThoughtWorks and the Raspberry Pi project
This article explains how to write an application
kratos-selfservice-ui-go
, that integrates with Kratos, written in Go. Source code forkratos-selfservice-ui-go
is available on github
The structure of the application is relatively straightforward, and based on the Kratos self service example application written in Node. It provides the following self service UI pages:
-
Registration
-
Login
-
Logout
-
User settings:
— Update profile
— Change password
— Password reset
Once a user is logged in, they get access to the following additional page:
- Dashboard
Architecture
For convenience a demonstration system can be run using Docker compose.
Architecture overview
The components have these functions
-
[traefik
](https://traefik.io/traefik/) reverse proxy presents the Kratos and self service app together as a single unified website running under one host. This is important because Kratos and the self-service app share state through cookies -
kratos-migrate
service (not shown on the diagram) creates a sqlite database for Kratos, and runs database migrations -
kratos
is the Kratos server -
[mailhog
](https://github.com/mailhog/MailHog) is a self-contained email server which presents a simple web UI, as well as an API which is useful for integration testing -
kratos-selfservice-ui-go
is our Go sample application. It integrates with Kratos, presenting all the UI pages needed by Kratos, as well as providing a Dashboard page that can be accessed only when the user is logged in
Running the sample
To run the sample you will need [Docker](https://www.docker.com/)
installed, then run:
make quickstart
After a few seconds you should be able to navigate to the website at http://localhost/ and you should see a page like this:
Registration
Clicking on the Registration button starts the registration flow.
The registration web handler code from handlers/registration.go
is as follows:
// Registration directs the user to a page where they can sign-up or
// register to use the site
func (rp RegistrationParams) Registration(w http.ResponseWriter, r *http.Request) {
// Start the login flow with Kratos if required
flow := r.URL.Query().Get("flow")
if flow == "" {
log.Printf("No flow ID found in URL, initializing login flow, redirect to %s", rp.FlowRedirectURL)
http.Redirect(w, r, rp.FlowRedirectURL, http.StatusMovedPermanently)
return
}
// Call Kratos to retrieve the login form
params := public.NewGetSelfServiceRegistrationFlowParams()
params.SetID(flow)
log.Print("Calling Kratos API to get self service registration")
res, err := api_client.PublicClient().Public.GetSelfServiceRegistrationFlow(params)
if err != nil {
log.Printf("Error getting self service registration flow %v, redirecting to /", err)
http.Redirect(w, r, "/", http.StatusMovedPermanently)
return
}
dataMap := map[string]interface{}{
"config": res.GetPayload().Methods["password"].Config,
"flow": flow,
"fs": rp.FS,
"pageHeading": "Registration",
}
if err = GetTemplate(registrationPage).Render("layout", w, r, dataMap); err != nil {
ErrorHandler(w, r, err)
}
}
The handler is typical of the integration points that your app will have in Kratos. First it checks for the flow
URL parameter, and if not present redirects the browser to the Kratos endpoint http://127.0.0.1/self-service/registration/browser
which starts registration inside Kratos, and then redirects straight back to this handler in your app.
The next step is to call the NewGetSelfServiceRegistrationFlowParams
Kratos API, passing the flow
parameter, which will return a data structure representing the form structure that Kratos is expecting.
The data structure is as follows:
{
"action": "http://127.0.0.1/self-service/registration/methods/password?flow=373bdce4-7e06-4fad-a7a1-f5366f3bc509",
"method": "POST",
"fields": [
{
"name": "csrf_token",
"type": "hidden",
"required": true,
"value": "Xut/HbFdJTR2rNXsnWpznRVMezSyS7b+NUibcwIRR03+06Ag+yui/hgGaxdRM6XocWFBT7PRl4NZpdXR0pw7LA=="
},
{
"name": "password",
"type": "password",
"required": true
},
{
"name": "traits.email",
"type": "email"
},
...
]
}
It may not be obvious immediately, but this data structure has been designed so that it is deliberately easy to convert into an HTML form.
You simply contsruct an HTML <form>
element with the action
and method
attributes, and then iterate over the fields
, turning each one into an <input>
element with the attributes and value
provided. The only decision you need to make are the order that the fields are displayed on the form, and any styling you want to apply.
Here is the resulting form:
I’ve deliberately entered a weak password as part of my registration. When I submitted the form, Kratos responded with the same data structure as before, but contained validation errors. We can present that to the user and allow them to respond.
Errors are coded for easy translation, in this case 4000005
along with response text that described the error condition:
Correcting that error, and resubmitting the form, leads to a successful registration:
Kratos has some flexibility around what happens next, in my case I have configured Kratos to send a registration email asking the user to confirm their email address, but this is not required in order to login. By opening mailhog we can view the email:
After registration, the user is automatically logged in to the site.
Clicking on the Dashboard link passes the request through the KratoAuthMiddleware. The purpose of this middleware is to restrict access to pages that require the user to be logged in.
The KratoAuthMiddleware
calls the Kratos [WhoAmI
](https://www.ory.sh/kratos/docs/reference/api#check-who-the-current-http-session-belongs-to) endpoint with the csrf_token
and ory_kratos_session
cookies. WhoAmiI
returns a Session
object which contains a wealth of information about the logged in user.
Calling the WhoAmiI
API every time a protected page is accessed, is the recommended approach, to verifying that the session is still active. This means that Kratos has responsibility for managing the session lifecycle, see configuration (refer to session.lifespan
). On my laptop running everything locally this call only takes 3-8 ms, but it will be worth checking performance in your scenario with a production-sized database of users.
The other functions of the application are pretty self explanatory:
-
Update Profile presents a page with two forms; one to update your password, and the other to update name and email address
-
There is also an Account recovery page, linked from the login page, for the situation when the user forgets their password
Conclusions
Once you understand the general flow of the Kratos interactions, the integration work is relatively straightforward, and Kratos provides a lot of functionality and flexibility.
It’s a very attractive option for solving application authentication
Before starting it’s worth taking some time to look at all the configuration options that Kratos provides. This will allow you to evaluate how you might migrate existing user data from another system into Kratos, or how to implement a system for your back office that would allow a helpdesk user to find users in the system and help them with login issues.
There is much more to Kratos than is covered here. I encourage you to look through the Kratos docs to learn more.
Implementation notes
The application experiments with a few interesting features that you might use in a production quality Go app:
- The app is written in Go version 1.16
— All html templates are embedded in the binary. This means the entire app is contained in a single executable
— The Gorilla toolkit provides the URL multiplexor, useful middleware for logging and error handling, and session handling modules
— The HashFS library allows static assets (CSS, JS) to be correctly cached by appending SHA256 hashes to filenames
— Kratos supply a Go client api, which makes development easier
- I experimented with Tailwind CSS. I don’t normally get much chance to play with the look’n’feel of the app, so this was a fun part of the project for me
— Note that I haven’t applied any steps to minimise the CSS, but I would expect that to be relatively straightforward
-
There is a small amount of Javascript (opening and closing the menu), for which I used Stimulus, as a simpler alternative to using something like React
-
The Docker file uses a multi-stage build to compile the app, and then produce a smaller Docker image (approx 18 Mb)
-
A comprehensive test suite for the website is written using Cypress. I focussed on integration tests, rather than a lot of unit tests, because that’s what this project was all about. The tests cover most if not all of the integration points
— The application uses data-cy
attributes as test hooks, as recommended in the cypress best practices
— Tests use the mailhog
API to check programatically if emails are sent correctly.
— Tests run in Docker, so no need to install cypress
— It’s easy to run tests against newer (or older) browser releases, by altering the base image in the Dockerfile
— It’s easy to run the tests in headless or interactive mode. The interactive tests use a browser bundled with the cypress-included images, which means that it does not interfere or clash with the browser running on my laptop. See the make cypress
target to see how that runs in conjunction with an X server running on my laptop
— On the downside, an extra step is required to include the custom node modules required by the tests in the docker image — see make cypress-docker
Running the tests in headless mode:
Watch a video of running the tests in headless mode
Thanks for reading! I hope you find this guide useful as you use third-party services in your applications.
Read more from Abletech
-
Kate on React on Rails with Devise
-
Alex on refactoring code
-
Kate on design for developers
-
Alex on testing ActiveRecord concerns
-
Marcus on best-practice security