Traefik middleware - Forward authentication
Published: 2022-09-19 13:38:28

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](https://doc.traefik.io/traefik/middlewares/http/forwardauth/#forwardauth).

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.



```javascript
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:

```pug
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:

```pug
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:

```yaml
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:

```yaml
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:

```yaml
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:

```yaml
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