Traefik middleware - Forward authentication

lenra_io

Alienor

Posted on September 19, 2022

Traefik middleware - Forward authentication

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And here is the redirect view:

extends layout

block content
  h1= "Redirection en cours"
  script= `window.location = "${url}";`
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
đź’– đź’Ş đź™… đźš©
lenra_io
Alienor

Posted on September 19, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related