In the information age, nothing is more important than the security of our services—and more importantly, the security of our users and their data. Ensuring security within a (micro)services architecture can quickly become complex and burdensome. As services grow and more spring to life within the ecosystem, maintaining consistent and reliable access control becomes a top priority. To streamline access control, we can leverage Forward Authentication, which centralizes the authentication mechanism and simplifies securing our (micro)services.

While many reverse proxies offer functionality for routing and authentication, Traefik stands out with its ease of use and flexibility. In addition to efficiently routing traffic, Traefik’s integration with Forward Authentication (ForwardAuth) enables us to centralize user authentication across all services. Using a centralized authentication not only makes the access management easier, but also strengthens the security of our services at the gateway level, making Traefik an excellent choice for the task.

In this post, we will look into the process of setting up and configuring ForwardAuth with Traefik and explore some advantages of using this authentication method within a (micro)services architecture to secure access to a protected Order resource in our hypothetical application.

What is Authentication

First a quick look into what authentication actually is. When we ask our users to authenticate, we are essentially asking them to prove to our application that they are who they say they are. In the most basic form of authentication, the users often prove their identity by supplying our application with an email address, or a unique username, and a password. Since the communication between the user/client and the server in the application leveraging the HTTP protocol is more often than not stateless, the user must prove their identity with every request.

If an attacker can obtain the users password which they are using to prove their identity with, the attacker can now identify as the user to our application at any time, until the user changes their password. And since users must prove their identity with the password on every request, the attacker has a lot of chances to get to the users password. To mitigate this, the access control system in our application will usually issue a short lived access token to our user, after they have proven their identity with their password. After this point, the user can use this access token to prove their identity, until the token expires, and they have to obtain a new one.

What is Forward Authentication

When our application now receives a request to the protected GET /order/{id} resource endpoint, it verifies that the request also contains the access token, and it ensures that the token is valid and belonging to a user. Since we are in a (micro)services architecture and we are using multiple services to serve different resources to our users, as in example, the request to retrieve an order with the ID {id} is handled by the order service, we would typically need to implement identity verification in each service, or rather implement an authentication service which will verify the identity of the users for us through the POST /auth/verify endpoint.

Instead of having each service verify user identities by calling the authentication service to verify the users identity, which quickly becomes cumbersome and can lead to inconsistencies between services, we can employ the Forward Authentication mechanism in our reverse proxy, Traefik. When using ForwardAuth each request that is received to a protected URI, like in example GET /order/{id}, Traefik will automatically forward the request to our authentication service for user authentication, before sending the request onward to the resource service, in our example, order.

sequenceDiagram
    actor user as User
    participant traefik as Traefik
    participant auth as Authentication Service
    participant order as Order Service

    user->>traefik: GET /order/{id}
    Note right of traefik: ForwardAuth request

    traefik-->>auth: POST /auth/verify

    alt Authentication failed
        activate auth
        Note over auth: Verify user identity
        auth-->>user: 401 Unauthorized
        deactivate auth
    else Authentication succesful
        activate auth
        Note over auth: Verify user identity
        auth-->>traefik: 200 OK
        deactivate auth

        traefik->>order: GET /order/{id}
        order->>user: 200 OK
    end

Configuring ForwardAuth in Traefik

Below we have hypothetical service and http router definitions for our authentication and order services and routers in Traefik:

[http]
  [http.routers]
    [http.routers.order]
      rule = "Host(`order.domain.tld`) && PathPrefix(`/order`)"
      service = "order"
    [http.routers.authentication]
      rule = "Host(`order.domain.tld`) && PathPrefix(`/login`)"
      service = "order"

  [http.services]
    [http.services.order.loadbalancer]
      [[http.services.order.loadbalancer.server]]
        port = 8000
    [http.services.authentication.loadbalancer]
      [[http.services.authentication.loadbalancer.server]]
        port = 8000

In this configuration we have configured 2 services that we are running with docker, order and authentication, and Traefik will route all requests to https://order.domain.tld/order to the order service and all requests made to https://order.domain.tld/login the authentication service. We do not need to route the /auth/verify route, since we are going to be calling this only inside the same docker network bridge.

With this setup, the order service would have to manually call https://authentication:8000/auth/verify manually on each request made to the /order/{id} endpoint, and of course any other endpoint or service would need to do the same. To centralize the authentication and not require each service to handle it individually, we are going to use ForwardAuth by defining a middleware in Traefik and add it to our router for the order service:

[http]
  [http.middlewares]
    [http.middlewares.auth-user.forwardauth]
      address = "https://authentication:8000/auth/verify"

  [http.routers]
    [http.routers.order-secure]
      rule = "Host(`order.domain.tld`) && PathRegexp(`/order/[0-9]+`)"
      service = "order"
      middlewares = ["auth-user"]
    [http.routers.order]
      # kept the same as before

  # services defined as before

We have not defined a new ForwardAuth middleware with the name auth-user, and instructed Traefik to redirect all requests made to all routers that employ this middleware to the https://authentication:8000/auth/verify address. Further on, we have now added a new router, order-secure which uses the auth-user middleware, and catches only requests that are made to https://order.domain.tld/order/{id} address while keeping the https://order.domain.tld/order requests non-authenticated.

Now whenever the order service receives a request to the /order/{id} URI, we know we can trust this request as it was already forwarded to our authentication service for user identity verification by Traefik and we can proceed servicing the request right away.

Sharing information between the services

In real-world scenarios, we might also want to share some user information from the authentication service to the order service once the user has been authenticated, since the authentication service will already have probably accessed the database, and we want to limit the access to the database in the downstream services in order to help maintain high performance.

To achieve this, we have a couple of options:

  • use of JSON Web Tokens or JWT
  • set any required data to the response headers with the authentication service

Use of JSON Web Tokens

If we are using JWTs for authentication of the users, we can already keep the required user data in the JWT itself, since it was designed for exactly this. When the user sends the JWT in the request headers, Traefik will naturally forward this JWT to the authentication service through the ForwardAuth middleware, as well as send this JWT on to the order service, where the authentication service can check the validity of the JWT token and the order service can extract user data from it.

Setting required data to response headers after authentication

Since JWTs expose user data, we might want to avoid that, and can therefore set any user data that the downstream services will require in the response headers of the /auth/verify call and instruct Traefik to add data from that specific header to the follow-up request headers that will be sent to the order service. Ideally in a way where the downstream services will be able to verify that the data was indeed set by the authentication service. We achieve this by adding a slight modification in our ForwardAuth middleware:

[http]
  [http.middlewares]
    [http.middlewares.auth-user.forwardauth]
      address = "https://authentication:8000/auth/verify"
      authResponseHeaders = ["X-Authentication-User"]

# all else remains the same

Putting it all together

Now let’s put everything we have learned together:

[http]
  [http.middlewares]
    [http.middlewares.auth-user.forwardauth]
      address = "https://authentication:8000/auth/verify"
      authResponseHeaders = ["X-Authentication-User"]

  [http.routers]
    [http.routers.order-secure]
      rule = "Host(`order.domain.tld`) && PathRegexp(`/order/[0-9]+`)"
      service = "order"
      middlewares = ["auth-user"]
    [http.routers.order]
      rule = "Host(`order.domain.tld`) && PathPrefix(`/order`)"
      service = "order"
    [http.routers.authentication]
      rule = "Host(`order.domain.tld`) && PathPrefix(`/login`)"
      service = "order"

  [http.services]
    [http.services.order.loadbalancer]
      [[http.services.order.loadbalancer.server]]
        port = 8000
    [http.services.authentication.loadbalancer]
      [[http.services.authentication.loadbalancer.server]]
        port = 8000

Note: the above configuration is omitting HTTPS configuration as it is beyond the scope of this post.

And here we have it, congratulations! We now have a secure GET /order/{id} endpoint in our order service, and to secure any more services or endpoints, we simply need to add the middleware to their route definitions. Now let’s take look at the lifecycle of the GET /order/{id} request:

sequenceDiagram
    actor user as User
    participant traefik as Traefik
    participant auth as Authentication Service
    participant order as Order Service

    user->>traefik: GET /order/{id}
    Note right of user: Bearer: jwt-token
    Note right of traefik: ForwardAuth request

    activate auth
    traefik-->>auth: POST /auth/verify
    Note right of traefik: Bearer: jwt-token

    Note over auth: Verify user identity
    Note over auth: Generate internal-jwt-token
    auth-->>traefik: 200 OK
    Note left of auth: X-Authentication-User: internal-jwt-token
    deactivate auth

    activate order
    traefik->>order: GET /order/{id}
    Note right of traefik: X-Authentication-User: internal-jwt-token
    Note over order: Verify signature of internal jwt-token
    Note over order: Service the order request
    order->>user: 200 OK
    Note left of order: Bearer: jwt-token
    deactivate order