Skip to main content

Define WebSockets rules

This guide shows how to use Ory Oathkeeper with WebSockets.

tip

WebSockets bypass Ory Oathkeeper after the first request and thus Ory Oathkeeper only validates cookies once. It is up to your service to make sure that WebSocket connections expire within a reasonable time frame so the session cookie is still active and valid.

Let's create a simple echo WebSocket service that sends back an accepted message. We'll use the Gin Web framework to build our application, and Ory Kratos to handle user login, sign-up, and verification flows.

Install Ory Kratos and Ory Oathkeeper

You can create any directory for testing and create a docker-compose.yml file with the following content:

version: "3.7"
services:
oathkeeper:
image: oryd/oathkeeper:<version-you-want>
depends_on:
- kratos
ports:
- 8080:4455
- 4456:4456
command:
serve proxy -c "/etc/config/oathkeeper/oathkeeper.yml"
environment:
- LOG_LEVEL=debug
restart: on-failure
networks:
- intranet
volumes:
- ./oathkeeper:/etc/config/oathkeeper
postgres-kratos:
image: postgres:9.6
environment:
- POSTGRES_USER=kratos
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=kratos
networks:
- intranet
kratos-migrate:
image: oryd/kratos:<version-you-want>
links:
- postgres-kratos:postgres-kratos
environment:
- DSN=postgres://kratos:secret@postgres-kratos:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4
networks:
- intranet
volumes:
- type: bind
source: ./kratos
target: /etc/config/kratos
command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes
kratos:
image: oryd/kratos:<version-you-want>
links:
- postgres-kratos:postgres-kratos
environment:
- DSN=postgres://kratos:secret@postgres-kratos:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4
ports:
- '4433:4433'
- '4434:4434'
volumes:
- type: bind
source: ./kratos
target: /etc/config/kratos
networks:
- intranet
command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier
kratos-selfservice-ui-node:
image: oryd/kratos-selfservice-ui-node:latest
environment:
- KRATOS_PUBLIC_URL=http://kratos:4433/
- KRATOS_BROWSER_URL=http://127.0.0.1:4433/
networks:
- intranet
ports:
- "4455:3000"
restart: on-failure
mailslurper:
image: oryd/mailslurper:latest-smtps
ports:
- '4436:4436'
- '4437:4437'
networks:
- intranet
networks:
intranet:

This example uses the following network architecture:

  • 4433 port is the public ("browser") API of Ory Kratos.
  • 4434 is the admin API of Ory Kratos.
  • 4455 is a port for the user interface implemented by the reference self-service UI.
  • 8080 is a port of Ory Oathkeeper.

Other ports and services are available only in the internal network.

Configure Ory Oathkeeper and Ory Kratos

  1. Create a kratos folder and fetch configuration files:
mkdir kratos
wget https://raw.githubusercontent.com/ory/kratos/<version-you-want>/contrib/quickstart/kratos/email-password/identity.schema.json -O kratos/identity.schema.json
wget https://raw.githubusercontent.com/ory/kratos/<version-you-want>/contrib/quickstart/kratos/email-password/kratos.yml -O kratos/kratos.yml
  1. Create a oathkeeper folder and oathkeeper/oathkeeper.yml with the following content:
log:
level: debug
format: json

serve:
proxy:
cors:
enabled: true
allowed_origins:
- http://127.0.0.1:8080
allowed_methods:
- POST
- GET
- PUT
- PATCH
- DELETE
allowed_headers:
- Authorization
- Content-Type
exposed_headers:
- Content-Type
allow_credentials: true
debug: true

errors:
fallback:
- json

handlers:
redirect:
enabled: true
config:
to: http://127.0.0.1:4455/login
when:
- error:
- unauthorized
- forbidden
request:
header:
accept:
- text/html
json:
enabled: true
config:
verbose: true

access_rules:
matching_strategy: glob
repositories:
- file:///etc/config/oathkeeper/access-rules.yml

authenticators:
anonymous:
enabled: true
config:
subject: guest

cookie_session:
enabled: true
config:
check_session_url: http://kratos:4433/sessions/whoami
preserve_path: true
extra_from: "@this"
subject_from: "identity.id"
only:
- ory_kratos_session

noop:
enabled: true

authorizers:
allow:
enabled: true

mutators:
noop:
enabled: true
  1. Create oathkeeper/access-rules.yml with the following content:
- id: "ws:protected"
upstream:
preserve_host: true
url: "http://ws:8080"
match:
url: "http://127.0.0.1:8080/<**>"
methods:
- GET
- POST
authenticators:
- handler: cookie_session
mutators:
- handler: noop
authorizer:
handler: allow
errors:
- handler: redirect
config:
to: http://127.0.0.1:4455/login

This configuration of Ory Oathkeeper uses the cookie authenticator against Ory Kratos and proxies only authenticated requests to http://ws:8080 upstream. The ws hostname is resolved through the Docker network. If you aren't deploying your application within Docker, this would just be your localhost IP.

WebSocket service

  1. Let's create a folder ws and create our WebSocket service using Go and Gin framework. Create ws/main.go file with the following content:
// Copyright © 2022 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package main

import (
"fmt"

"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)

func main() {

r := gin.Default()
r.LoadHTMLFiles("index.html")

r.GET("/", func(c *gin.Context) {
c.HTML(200, "index.html", nil)
return
})

r.GET("/ws", func(c *gin.Context) {
var wsupgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
conn, err := wsupgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
fmt.Printf("Failed to set websocket upgrade: %+v\n", err)
return
}

for {
t, msg, err := conn.ReadMessage()
if err != nil {
break
}
conn.WriteMessage(t, msg)
}
return
})

r.Run(":8080")
}
  1. We need to initialize go modules by running the following commands:
cd ws
go mod init ws
go mod tidy
  1. Create ws/index.html file with the following content:
<html>
<head>
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
</head>

<body>
<h3>WebSocket Go</h3>
<pre id="output"></pre>

<script>
url = "ws://127.0.0.1:8080/ws"
c = new WebSocket(url)

send = function (data) {
$("#output").append(new Date() + " ==> " + data + "\n")
c.send(data)
}

c.onmessage = function (msg) {
$("#output").append(new Date() + " <== " + msg.data + "\n")
console.log(msg)
}

c.onopen = function () {
setInterval(function () {
send("ping")
}, 1000)
}
</script>
</body>
</html>
  1. Create ws/Dockerfile with the following content:
FROM golang as builder

RUN mkdir /build

ADD . /build

WORKDIR /build
RUN GOOS=linux GOARCH=amd64 go build -o ws main.go

FROM alpine
EXPOSE 8090

COPY --from=builder /build/ws /ws
COPY index.html /index.html
ENTRYPOINT ["/ws"]
  1. We need to add our ws service to the docker-compose.yml
services:
---
ws:
build:
context: "ws"
networks:
- intranet

Testing

  1. Run docker-compose up.
  2. Wait for services to be ready.
  3. Open http://127.0.0.1:4455.
  4. Create a new account.
  5. Open http://127.0.0.1:8080.