Building a Quarkus application with Ory Kratos

2021-03-06

Quarkus plus Ory Kratos plus Kotlin equals safety

Building a new web application is fun and exiting. Building user management and password handling for the eleventh time… not so much. You will open up a huge can of worms for yourself if you try to roll your own security.

This is why “IDaaS” (Identity as a service) providers are on the rise. While Keycloak is the most dominant player in the open source IDaaS market, there is another up and coming alternative. Ory offers an open-source suite of tools for authentication, authorization, and user management. In contrast to the monolithic approach of Keycloak Ory offers lightweight building blocks which seem to be a better fit for the age of Docker.

Here I pick one of Ory’s building blocks, Kratos, and try to integrate it in a Quarkus app written in Kotlin. The outcome is by no means complete, full-featured, production-ready or anything else. It’s a quick sketch for me to get familiar with Kratos.

Spare me the words, show me the code

Here you go: quarkus_kratos_example

Getting up and running

Things I will build:

  • User registration
  • Login
  • Logout
  • Some secured resource Things I wont build: Everything else

Since I want to keep it simple I will not use any frontend framework. HTML will be rendered server-side with Quarkus’ home-made templating engine, Qute.

Setting up the project

We will need the following Quarkus extensions:

  • resteasy
  • kotlin
  • resteasy-jackson
  • quarkus-rest-cliet
  • qute

To set up your project and get the extensions run the following Maven command.

$ mvn io.quarkus:quarkus-maven-plugin:1.12.0.Final:create \
    -DprojectGroupId=me.hauke \
    -DprojectArtifactId=quarkuskratos \
    -Dextensions="resteasy,kotlin,resteasy-jackson,io.quarkus:quarkus-rest-client,io.quarkus:quarkus-resteasy-qute"

After deleting the example files we get the following directory structure:

$ tree .

.
├── README.md
├── mvnw
├── mvnw.cmd
├── pom.xml
├── quarkuskratos.iml
└── src
    └── main
        ├── docker
        │   ├── Dockerfile.jvm
        │   ├── Dockerfile.legacy-jar
        │   ├── Dockerfile.native
        │   └── Dockerfile.native-distroless
        ├── kotlin
        │   └── me
        │       └── hauke
        │           └── quarkuskratos
        │               └── index
        └── resources
            ├── META-INF
            │   └── resources
            ├── application.properties
            └── templates
                └── index

13 directories, 10 files

Baby’s first index.html

Lets create a index page and endpoint that will be available at http://127.0.0.1:8080/

We create a simple html template at src/main/resources/templates/index/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{title}</title>
</head>
<body>
    <div>{text}</div>
</body>
</html>

{title} and {text} are the value expressions that are evaluated when Qute renders the template.

To get the template rendered we create an IndexEndpoint:

@Path("") // ①
class IndexEndpoint @Inject constructor(
        @ResourcePath("index/index.html") val index: Template // ②
) {

    @GET
    @Produces(MediaType.TEXT_HTML)
    fun index(): TemplateInstance =
            index.data(
                    "title", "My first template", // ③
                    "text", "Hello 👋🏻"
            )
}

: Here we define the path where the template should be rendered.

: Where can Qute find the template (relative to src/main/resources/templates)?

: Data for value expressions can be filled with pairs of placeholder names and data or by providing an object with matching attributes. In this case something like the following data class would be the correct “view model”:

data class IndexModel(
    val title: String,
    val text: String
)

Running the project with ./mvnw clean quarkus:dev and opening http://127.0.0.1:8080/ in our browser renders the template. Blank browser window with text “Hello 👋🏻”

Nice.

Setting up Kratos

Kratos in conveniently available via Docker. For development and testing purposes you don’t even need a full database server since it can store everything in sqlite.

For getting started we create the following three files in a kratos folder in our project root directory:

  • kratos-quickstart.yml: Docker compose file for running Kratos and its components (completed file)
  • identity.schema.json: Schema for user information (completed file)
  • kratos.yml: Kratos configuration file (completed file)

Now we can run Kratos for the first time:

$ docker-compose -f kratos/kratos-quickstart.yml up --build --force-recreate

Kratos is API-only so we don’t have a fancy GUI for administration. If we would want one, we need to build it ourself.

For now we query Kratos via it’s admin API:

$ curl --request GET --url http://127.0.0.1:4434/health/alive

{"status":"ok"}

Kratos runs two endpoints at once: 127.0.0.1:4433 (public endpoint) and 127.0.0.1:4434 (admin endpoint). We will use the public or “self service” endpoint, since we want our users to register themself.

Building a registration form

To register a new user in Kratos the following events will happen:

  1. Browser GETs Kratos self service registration url
  2. Kratos creates new registration flow
  3. Kratos redirects browser to configured Quarkus registration endpoint
  4. Browser GETs Quarkus registration endpoint with flowId as parameter
  5. Quarkus takes flowId and GETs details of flow from Kratos
  6. Kratos gives flow Details to Quarkus
  7. Quarkus renders form and delivers form to browser with csrf_token_ 8. User fills out form in browser, browser POSTs form to Kratos
  8. Kratos creates user and executes post registration hook

To start the flow we add the Kratos self service registration url as a link to our index.html template:

{#include base}
{#title}Welcome{/title}
{#body}
<div>
    {text}
</div>
<div>
    <a href="http://127.0.0.1:4433/self-service/registration/browser">Sign up for secret recipes</a>
</div>
{/body}
{/include}

There are more changes to the template. We moved the outer part (<html></html> and so on) to an base.html template file:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{#insert title}Default Title{/}</title>
</head>
<body>
{#insert body}No body!{/}
</body>
</html> 

Qute automatically collects the {#include} files and builds the template as before.

In the Kratos configuration we defined the registration.ui_url to be http://127.0.0.1:8080/auth/registration. So we need to add the matching endpoint and service to our Quarkus application:

RegistrationEndpoint.kt:

@Path("/auth/registration")
class RegistrationEndpoint @Inject constructor(
        val registrationService: RegistrationService,
) {
    @GET
    @Produces(MediaType.TEXT_HTML)
    fun registration(
            @QueryParam("flow") flow: String): TemplateInstance =
            registrationService.registration(flow)
}

The endpoint takes the flow id from the query parameters, calls the service and returns the rendered template.

RegistrationService.kt:

@ApplicationScoped
class RegistrationService @Inject constructor(
        @RestClient val kratosClient: KratosClient,
        @ResourcePath("auth/registration.html") val registrationTemplate: Template,
) {
    fun registration(flowId: String): TemplateInstance {
        val response = kratosClient.getRegistrationFlow(flowId)
       
        val model = RegistrationModel(/*...*/)

        return registrationTemplate.data(model)
    }
}

(see full file here Github RegistrationService.kt)

Our service uses a (soon to be build) KratosClient to get the registration flow details. These details are used to build the data model for the registrationTemplate.

The Quarkus application needs a way to ask Kratos for detail of the flow. Therefore we need the KratosClient:

@RegisterRestClient(configKey = "kratos")
interface KratosClient {
    @GET
    @Path("/self-service/registration/flows")
    fun getRegistrationFlow(@QueryParam("id") flowId: String): KratosRegistrationResponse

    @JsonIgnoreProperties(ignoreUnknown = true)
    data class KratosRegistrationResponse(
            val id: String,
            val type: String,
            val messages: List<KratosMessage>?,
            val methods: Map<String, KratosMethod>
    ) {

        data class KratosMethod(
                val method: String,
                val config: KratosMethodConfig
        )

        data class KratosMethodConfig(
                val action: String,
                val method: String,
                val fields: List<KratosField>
        )

        data class KratosField(
                val name: String,
                val type: String,
                val required: Boolean = false,
                val value: String?,
                val messages: List<KratosMessage>?
        )

        data class KratosMessage(
                val id: String,
                val text: String,
                val type: String
        )
    }
}

To make the client as simple as possible we use the Microprofile Rest Client.

In application.properties we define configuration parameters for the client:

kratos/mp-rest/url=http://127.0.0.1:4433/
kratos/mp-rest/scope=javax.inject.Singleton
kratos/mp-rest/hostnameVerifier=io.quarkus.restclient.NoopHostnameVerifier

All that is left to do for registration is to define a auth/registration.html template:

{#include base}
{#title}Registration{/title}
{#body}
{#with model}
<div>
    <p>Welcome to registration</p>
    <p>Flow: {flow}</p>
    <div>{message}</div>
    <div>
        <form action="{action}" method="POST">
            {#for field in fields}
            {#if field.name is 'csrf_token'}
            <input type="hidden" name="csrf_token" value="{field.value}">
            {#else}
            <div>
                <label for="{field.id}">{field.name}</label>
                <input type="{field.type}" name="{field.name}" id="{field.id}">
                {#if field.message}
                <div>{field.message}</div>
                {/if}
            </div>
            {/if}
            {/for}
            <button type="submit">Sign up for cookies</button>
        </form>
    </div>
    <div>
        <a href="/">Go home</a>
    </div>
</div>
{/with}
{/body}
{/include}

Now we have a link to the registration page at the start page: Browser window with text “Hello 👋🏻”. Below that a link “Sign up for secret recipes”

And a registration form: Browser window with unstyled html form. Inputs for password, email, and username. Sign up button below that.

If we try to register a user with a very short password like asdf Kratos will tell us that this is not a good idea. Unstyled HTML registration form with Kratos error message: “The password can not be used because password length must be at least 6 characters but only got 4.”

Same with lovely ones like Password123! that other people have used quite often before. Unstyled HTML registration form with Kratos error message: “The password can not be used because the password has been found in at least 126927 data beaches and must no longer be used…”

For details on the password policy, see:https://www.ory.sh/kratos/docs/concepts/security/#password-policy

When we register with an acceptable password user is created and Kratos runs the post registration hooks.

We configured them as:

password:
  default_browser_return_url: http://127.0.0.1:8080/ # ①
  hooks:
    - hook: session # ②

So we are redirected () to the main page of our application and we automatically logged in (). Nice!

How do we know that we are logged in? Lets take a look at the cookies: Browser window with developer tools open. Application storage tab open. Cookie section selected. Two cookies for 127.0.0.1 are visible: ory_kratos session and csrf_token Here we have the ory_kratos_session cookie and also a csrf_token.

Now that we are logged in we can take a look a how we can secure a page by checking for the ory_kratos_session cookie.

Grandma’s secret cookies

Let’s say I want to share my grandma’s secret cookie recipe, but only with my logged in users and not with any anonymous cookie thief on the internet.

First we create another endpoint RecipeEndpoint and a recipe.html Qute template: RecipeEndpoint.kt

@Path("/recipe")
class RecipeEndpoint @Inject constructor(
        @ResourcePath("recipe/recipe.html") val recipe: Template
) {
    @GET
    @Produces(MediaType.TEXT_HTML)
    fun get(): TemplateInstance =
            recipe.instance()
}

recipe.html

{#include base}
{#title}My Title{/title}
{#body}
<div>
    <p>Hello 👋</p>
    <p>The perfect recipe for 🍪s is still in the making. Please check again later.</p>
</div>
<div>
    <a href="/">Go home</a>
</div>
{/body}
{/include}

We also create a link from the index.html template to the recipe page. index.html

...
<div>
    <p>Grandma's super secret cookie recipe</p>
    <a href="/recipe">Get it</a>
    <p>(You must be logged in)</p>
</div>
...

So far so insecure. Now anybody could access the recipe.

Browser window with unstyled HTMl text: “Hello 👋 The perfect recipe for 🍪s is still in the making. Please check again later.” Below that a link titled “Go home”.

Lets fix that!

Securing an endpoint

Since we don’t want to secure every endpoint with a login we define an annotation that defines which endpoint is secured: AuthenticationFilter.kt

@NameBinding
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class Secured

For the authentication we define an implementation of a ContainerRequestFilter that adopts the @Secured annotation.
AuthenticationFilter.kt

@Secured
@Provider
@Priority(Priorities.AUTHENTICATION)
class AuthenticationFilter : ContainerRequestFilter {
    @Context
    lateinit var request: HttpServerRequest

    @Inject
    @RestClient
    lateinit var kratosClient: KratosClient

    companion object {
        private val LOG = Logger.getLogger(AuthenticationFilter::class.java)
    }

    override fun filter(context: ContainerRequestContext) {
        val cookie: Cookie? = context.cookies["ory_kratos_session"] // ①

        if (cookie == null) {
            LOG.warn("No cookie found")
            throw NotAuthorizedException("please login")
        }

        LOG.info("Mmmmmh, Cookie: $cookie")

        val response = try {
            kratosClient.getWhoAmI(cookie) // ②
        } catch (e: WebApplicationException) {
            LOG.warn(e)
            throw NotAuthorizedException("please login")
        }

        LOG.info(response) // ③
    }
}

: Here we read the session cookie from the request context.

: We need to verify with Kratos if the cookie belongs to a logged in user.

: For this testcase we log the whole response with all the user information. Don’t ever do this in real life please.

Now every endpoint that is annotated with @Secured will be protected by our AuthenticationFilter.

To ask Kratos about the logged in user we add the following to KratosClient.kt:

@GET
@Path("/sessions/whoami")
@Throws(WebApplicationException::class)
fun getWhoAmI(@CookieParam("ory_kratos_session") kratosSession: Cookie): String

Kratos will answer with 200 and additional information about the logged in user (username, e-mail verification status, roles, …) if the user is logged in. If no session is found 401 is returned.

To secure the recipe endpoint we add the @Secured annotation. RecipeEndpoint.kt

...
@Secured
@GET
@Produces(MediaType.TEXT_HTML)
fun get(): TemplateInstance = 
...

Since we don’t want to show a blank 401 page to unauthenticated users we add an exception mapper that redirects to the start page:

AuthenticationFilter.kt

@Provider
class PermissionExceptionHandler : ExceptionMapper<NotAuthorizedException> {
    override fun toResponse(e: NotAuthorizedException?): Response {
        return Response.temporaryRedirect(URI("http://127.0.0.1:8080/")).build()
    }
}

Whenever you try to access the super secret endpoint you now need to pass through the AuthenticationFilter and authenticate yourself with the session cookie. Without this cookie you are simply redirected to the start page.

Logging out

So far we only have one method of logging out: Wait 24 hours until the session cookie expires.

Since we might want to log out earlier we need to add an explicit way to log out.

Logging out is done by simply calling an endpoint in Kratos. So we just add a link in our start page:

<div>
    <a href="http://127.0.0.1:4433/self-service/browser/flows/logout">Log out</a>
</div>

A little problem here: We don’t have any CSRF protection on this link. Anybody who places this link anywhere can log out our user. This is a problem that Kratos plans to address.

Logging in

Our users are already logged in when they register and can log out again. But what about logging in?

We just repeat the steps we already did for registering new users. Only the Kratos URLs are different ones.

First we add a link to the Kratos login endpoint to the index page: index.html

<div>
    Please <a href="http://127.0.0.1:4433/self-service/login/browser">login</a>
</div>

The login flow is quite similar to the registration flow:

So we add a login page template:

login.html

{#include base}
{#title}Login{/title}
{#body}
{#with model}
<div>
    <p>Welcome to Login</p>
    <p>Flow: {flow}</p>
    <div>{message}</div>
    <div>
        <form action="{action}" method="POST">
            {#for field in fields}
            {#if field.name is 'csrf_token'}
            <input type="hidden" name="csrf_token" value="{field.value}">
            {#else}
            <div>
                <label for="{field.id}">{field.name}</label>
                <input type="{field.type}" name="{field.name}" id="{field.id}">
                {#if field.message}
                <div>{field.message}</div>
                {/if}
            </div>
            {/if}
            {/for}
            <button type="submit">Sign in for cookies</button>
        </form>
    </div>
    <div>
        <a href="/">Go home</a>
    </div>
</div>
{/with}
{/body}
{/include}

This template is nearly identical to registration.html and in a more mature implementation I would avoid the duplicate code. But not today!

The login page is served by the login endpoint Kratos redirects to:

LoginEndpoint.kt

@Path("/auth/login")
class LoginEndpoint @Inject constructor(
        val loginService: LoginService
) {
    @GET
    @Produces(MediaType.TEXT_HTML)
    fun login(@QueryParam("flow") flow: String): TemplateInstance =
            loginService.login(flow)
}

To get login form data from Kratos we add the matching endpoint to the client:

KratosClient.kt

  @GET
  @Path("/self-service/login/flows")
  fun getLoginFlow(@QueryParam("id") flowId: String): KratosResponse

The response has the same structure as for the registration response so I use the same model . For consistency I renamed it from KratosRegistrationResponse to KratosResponse for consistency.

The endpoint needs a service to assemble the login template from the response of the client: LoginService.kt

@ApplicationScoped
class LoginService @Inject constructor(
        @RestClient val kratosClient: KratosClient,
        @ResourcePath("auth/login.html") val loginTemplate: Template,
) {
    fun login(flowId: String): TemplateInstance {
        val response = kratosClient.getLoginFlow(flowId)

        val model = LoginModel(
                flow = flowId,
                message = response.messages?.toString() ?: "",
                action = response.methods["password"]?.config?.action ?: "",
                fields = response.methods["password"]?.config?.fields?.map { field ->
                    LoginModel.LoginFieldModel(
                            id = field.name,
                            type = field.type,
                            value = field.value,
                            name = field.name,
                            message = field.messages?.joinToString(";") { it.text }
                    )
                } ?: emptyList()
        )

        return loginTemplate.data(model)
    }


    data class LoginModel(
            val flow: String,
            val message: String = "",
            val action: String,
            val fields: List<LoginFieldModel> = emptyList()
    ) {
        data class LoginFieldModel(
                val id: String,
                val type: String,
                val value: String?,
                val name: String,
                val message: String?
        )
    }
}

Now we can log in with our new form: Browser window with an unstyled HTML form. Inputs are “identifier” and “password”. Below that a button labelled “Sign in for cookies”.

And with that last step we are “feature complete” (in a very, very, very lax interpretation of the word) for this prototype.

Summary and next steps

So with just these few steps we created a user registration and login process. While a lot is missing from a fully featured user management we actually got pretty far, especially considering how little work we have done here.

As we have seen it’s quite easy to put Kratos and Quarkus together to create a minimal but working example of securing a page with a login.

Hi, this is a text you should only get, if you are using a screenreader. Thanks for reading my stuff. I would love to provide you a great experience. If you have any feedback on how I can make my stuff more accessible please leave me a note via mail.