Traefik middleware - Forward authentication
Alienor
Posted on September 19, 2022
In this article we will explain how to use Traefik middlewares and routers to manage authentication to many applications on Kubernetes.
Custom authentication
Custom auth delegates management to an external server.
To manage custom authentication we will use the ForwardAuth Middleware.
In this exemple, we will create a NodeJS Express server that persist authentication with a cookie containing a JWT token.
For every request passing through the middleware it will check the JWT token.
If it is not present or expired, it will send a login form to the client with a 401
status code.
We will use our previous secret basic-auth-users-secret
to manage the accounts.
This is one kind of implementation, but you can imagine many authentication workflows.
We will not explain how to dockerize the NodeJS server, but you can find many tutorials.
Implement the server
The following environment variables need to be defined:
-
COOKIE
: the name of the cookie that will persists the JWT -
SECRET
: The JWT secret to manage JWT -
USERS_PATH
: the path to the users file -
VALIDITY
: the authentication validity duration in seconds
In this example, the form values are sent in a HTTP header because the body of the request is not forwarded the server.
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const atob = require('atob');
const htpasswd = require('htpasswd-js');
const cookie = process.env.COOKIE;
const key = process.env.SECRET;
const usersPath = process.env.USERS_PATH;
const validity = parseInt(process.env.VALIDITY);//in seconds
router.all('/', function (req, res, next) {
let forwarded = {
method: req.header("X-Forwarded-Method"),
protocol: req.header("X-Forwarded-Proto"),
host: req.header("X-Forwarded-Host"),
uri: req.header("X-Forwarded-Uri"),
ip: req.header("X-Forwarded-For"),
};
let url = `${forwarded.protocol}://${forwarded.host}${forwarded.uri}`;
let roles = req.query.roles;
try {
if (cookie in req.cookies) {
// Check JWT token
let decoded = jwt.verify(req.cookies[cookie], key);
res.sendStatus(200);
return;
}
else if (forwarded.method.toUpperCase() == "POST") {
// Check login/password
let form = req.header("Form-Content");
form = atob(form);
let [login, password] = form.split(":");
let loggedIn = htpasswd.authenticate({
username: login,
password,
file: usersPath
});
if (!loggedIn) throw new Error("Not logged in");
console.log(`Logged in ${login}. Redirect to ${url}`);
// Create JWT token
let val = (req.query.validity || validity) * 1000;
let expire = val + Date.now();
let token = jwt.sign({ user: login, exp: Math.floor(expire / 1000) }, key);
res.cookie(cookie, token, {
secure: true,
httpOnly: true,
expire: new Date(expire)
});
res.status(302);
res.header("Location", url);
// redirect to initial url
res.render('redirect', { title: 'Redirect', url });
return;
}
}
catch (er) {
console.error(er);
res.clearCookie(cookie);
}
res.status(401);
res.render('index', { title: 'Login', url , js: `function sendForm(e) {
var form = e.currentTarget;
var xhr = new XMLHttpRequest();
var formData = new FormData(form);
xhr.addEventListener('load', function(event) {
window.location.reload();
});
xhr.addEventListener('error', function(event) {
alert('Oups! Something went wrong.');
});
xhr.open('POST', '');
xhr.setRequestHeader('Form-Content', btoa(form.login.value+':'+form.password.value));
xhr.send(formData);
e.preventDefault();
e.stopPropagation();
return false;
}`});
});
module.exports = router;
Here is the index
view:
extends layout
block content
h1= "Login"
form(method="post", onsubmit="return sendForm(event);")
input(name="login")
input(name="password", type="password")
input(name="redirect", type="hidden", value= url)
button= "Login"
script= js
And here is the redirect
view:
extends layout
block content
h1= "Redirection en cours"
script= `window.location = "${url}";`
Deploy the authentication middleware
First we will define a Secret containing the needed environment variables:
apiVersion: v1
kind: Secret
metadata:
name: custom-auth-secret
stringData:
# The cookie name
COOKIE: my-cookie-name
# The JWT
SECRET: The jwt secret key
# The JWT and cookie validity in seconds
VALIDITY: "1800"
Let's define the server Deployment and Service:
kind: Deployment
apiVersion: apps/v1
metadata:
name: custom-auth
labels:
k8s-app: custom-auth
spec:
replicas: 1
selector:
matchLabels:
k8s-app: custom-auth
template:
metadata:
name: custom-auth
labels:
k8s-app: custom-auth
spec:
containers:
- name: custom-auth
image: custom-auth-server:latest
env:
- name: USERS_PATH
value: /auth/users
envFrom:
- secretRef:
name: custom-auth-secret
volumeMounts:
- name: users
mountPath: "/auth"
readOnly: true
volumes:
- name: users
secret:
secretName: basic-auth-users-secret
---
kind: Service
apiVersion: v1
metadata:
name: custom-auth
labels:
k8s-app: custom-auth
spec:
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 8080
selector:
k8s-app: custom-auth
Now we define the Traefik ForwardAuth Middleware in the same namespace of the server:
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: custom-auth
spec:
forwardAuth:
address: http://auth.traefik:8080
It can now be used in Traefik routers:
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: test-custom-auth
spec:
entryPoints:
- web
routes:
- kind: Rule
match: Host(`test-custom-auth.lenra.io`)
middlewares:
- name: test-custom-auth
# Define the middleware namespace if you use is it another one
# namespace: my-namespace
services:
- kind: Service
name: my-service
port: 8080
Go further
We can imagine many things to improve this authentication system:
- pass the allowed users to the custom authentication server by defining it in the middleware address field
- define the authentication cookie at the main domain in order to login only once
- manage authentication with a third-part OAuth provider, like Github
Posted on September 19, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.