Bypass Kratos CORS for local development
A while ago, I was developing a web application that relies on Ory Kratos for identity and session management. Kratos is an awesome lightweight IdP (Identity Provider) that is written in Go Lang, which makes it ideal to deploy on a cloud environment such as Kubernetes. Kratos, however, implements very strict security protocols that makes the development experience somewhat less pleasant.
Running Kratos on your local machine is quite straightforward as the team had prepared a quick guide to help you get started. Deploying an instance of Kratos on Kubernetes is also simple enough as you can make use of the Helm chart provided, or refer to it when writing your own YAML files (I’ll probably write an article on this soon).
Backend engineers who are familiar with micro-services development should not face much complexity when dealing with Kratos; on the other hand frontend engineers may generally be used to interacting with an online development server or environment. Regardless of whether you’re a backend, frontend or full-stack engineer, it’s quite common to be in a situation where only part of the application you’re currently editing is running on your local machine while the rest exist in some development environment somewhere on the internet.
CORS or Cross-Origin Resource Sharing is a common issue faced by frontend developers where a server under a domain foo.com would reject requests coming from your local domain localhost. Instinctively, a frontend engineer would notify the backend engineer to allow CORS from all origins - an easy fix that requires perhaps a few lines of code beginning with Access-Control-Allow-Origin: * that can be added to your NGINX web server.
This quick fix however would not work with Kratos as its application server handles CORS separately, and it’s quite strict with no way to override or disable it. When you think about it, it can be a good thing since Kratos is after all an authentication service that is supposed to provide security to your application. In this article, I’m going to demonstrate how I managed to resolve this issue so you can continue developing your frontend application on your local machine while interacting with a Kratos instance on the cloud.
Prerequisites
Before we proceed any further in this demo, I assume you already have the following:
- An instance of Kratos running on a cloud service like Digital Ocean
- Kratos is running in development mode
- A domain name or subdomain pointing to your Kratos instance. For this demo I’ll use kratos.foo.com
- No forced SSL redirection. Http should be enabled
- Some knowledge in GoLang
- Familiarity with Kratos APIs
Understanding the problem
Regardless the type of web application you’re working on, Kratos can detect that you’re using a web browser as they include the Origin header in every request. Whether you like it or not, this behaviour cannot be modified and it’s for the best. Using your local development machine to develop a Single Page Application (SPA) using Angular, for instance, would serve the application using localhost:4200 causing your origin header to be set to localhost:4200 automatically.
From my experience, I was able to initiate a login or registration flow which returned a CSRF cookie that is set to HTTPOnly; which means you won't be able to access this cookie from the client side using Javascript but you can include this cookie in subsequent HTTP requests using the flag withCredentials: true.
The problem occurs when attempting to complete the flow. The CSRF cookie would automatically be sent back to kratos.foo.com, and as you might have guessed, you’d face a CORS error response telling you the localhost origin does not match the origin of the server.
I’d recommend not wasting your time trying to configure Kratos to allow the origin of localhost:4200 as its implementation simply does not allow it. Continue reading what worked for me instead.
The solution
There’s only one solution to this issue which is to have both backend and frontend on the same origin i.e. foo.com. This should not be an issue in a production environment, but on a local development machine this can be achieved by tricking your browser into thinking that localhost is foo.com.
To trick your browser, you can modify the /etc/hosts file, and then have Angular serve on foo.com:80. It's a trick that I’m not fond of as I tend to work on multiple projects, and having to change my DNS configurations in this manner can be troublesome.
Instead, I prefer to reverse proxy 'kratos.foo.com' and have it served via localhost:3000. This way, both backend and frontend are available on the same origin. To understand how this works, you only need to remember that http clients other than the ones in web browsers such as a programming language do not set an Origin header, thus convincing Kratos that the request is originated from a backend service or a mobile client.
That’s pretty much it. Enough with words, let’s write some code to make this possible.
Creating a simple reverse proxy
Step 1: Create a new Go package and open up your favourite IDE. You’ll need to add the main function as all Go packages require an entry point.
Step 2: Create a simple web server as follows:
http.HandleFunc("/", func(response http.ResponseWriter, request *http.Request) {})
err := http.ListenAndServe(":3000", nil)
if err != nil {
panic(err)
}
Step 3: Now we need to implement the handler that’s going to forward our requests to kratos.foo.com .
url, parseErr := url.Parse("http://kratos.foo.com")
If parseErr != nil {
panic(parseErr)
}
proxy := httputil.NewSingleHostReverseProxy(url)
request.URL.Host = url.Host
request.URL.Scheme = url.Scheme
request.Header.set("X-Forwarded-Host", request.Header.Get("Host"))
request.Host = url.Host
proxy.ServeHTTP(response, request)
Step 4: You can now run your Go server.
$ go run .
Step 5: Set your angular baseUrl to http://localhost:3000.
That's great! You can now continue enjoying the familiar experience of developing locally while interacting with an online development service.
Complete code
Here’s the complete code if you wish to copy and paste.
package main
import (
"net/http"
"net/http/httputil"
"net/url"
)
func main() {
http.HandleFunc("/", func(response http.ResponseWriter, request *http.Request) {
url, parseErr := url.Parse("http://kratos.foo.com")
If parseErr != nil {
panic(parseErr)
}
proxy := httputil.NewSingleHostReverseProxy(url)
request.URL.Host = url.Host
request.URL.Scheme = url.Scheme
request.Header.set("X-Forwarded-Host", request.Header.Get("Host"))
request.Host = url.Host
proxy.ServeHTTP(response, request)
})
err := http.ListenAndServe(":3000", nil)
if err != nil {
panic(err)
}
}
I’ve also created a simple reverse proxy client that is reusable in case this is something you plan on doing in the long run.
Final thoughts
Kratos is a very good identity management service similar to KeyCloak. Unlike KeyCload, Kratos does not include any UI which makes it very lightweight and ideal for customisation. However, its development experience is not as pleasant as it implements very strict CORS rules that require you to run a local instance while developing your frontend.
An easy fix to this problem involves creating a reverse proxy that forwards your requests to your online instance while having it served using localhost. By doing so, you end up having both backend and frontend hosted on the same origin thus bypassing any CORS restrictions. This article explains how you can do that exactly, and I hope it has saved you a few hours of debugging.