josheli

Single Sign-on in Caddy Server Using only the Caddyfile and Basic Authentication

As mentioned, I've started self-hosting a lot of services, and in front of them all I have a reverse proxy using the Caddy Server. As always, with all web services, authentication is one of the bigger pain points. Typically, every service implements their own authentication mechanism, which becomes tiresome when you have to log in separately to half-a-dozen different websites. There are many solutions to this problem, such as Authelia, that integrate with your reverse proxy and your upstream web services to provide single sign-on. There are even some third-party modules for Caddy that will implement an Auth Portal for your services.

But that's a lot of work.

What if we could just use a vanilla Caddy server with no extra modules, along with the standard Caddyfile, to implement some sort of proxy authentication or single sign-on? Well, let's try.

Caddy supports basic auth, so we could start by putting basic auth in front of all of the services. If we use the same user/password, that's only one thing to remember, but we still have to authenticate separately for each service.

one.example.com {
  basicauth / {
    user hashed-password
  }
  reverse\_proxy service1:8001
}

two.example.com {
  basicauth / {
    user hashed-password
  }
  reverse\_proxy service2:8002
}

three.example.com {
  basicauth / {
    user hashed-password
  }
  reverse\_proxy service3:8003
}
...

Caddy also supports "snippets" , reusable chunks of configuration that you can "import" elsewhere, so let's clean up the basic auth a bit. This is basically the same as above, but now the user and password are only in one place and easier to manage.

(basic-auth) {
  basicauth / {
    user hashed-password
  }
}

one.example.com {
  import basic-auth
  reverse\_proxy service1:8001
}

two.example.com {
  import basic-auth
  reverse\_proxy service2:8002
}

three.example.com {
  import basic-auth
  reverse\_proxy service3:8003
}
...

Ok, that's a little better, but not much as we still have to authenticate separately for each service. If only there were some way to log in once, and have Caddy recognize that one authentication for each service. Hmm...

(basic-auth) {
  basicauth / {
    user hashed-password
  }
}

# a snippet to check if a cookie token is set. if not, store the current page as the referer and redirect to auth site
(proxy-auth) {
  # if cookie not = some-token-nonsense
  @no-auth {
    not header\_regexp mycookie Cookie myid=<regex-to-match-id>
    # https://github.com/caddyserver/caddy/issues/3916
  }
  
  # store current time, page and redirect to auth
  route @no-auth {
    header Set-Cookie "myreferer={scheme}://{host}{uri}; Domain=example.com; Path=/; Max-Age=30; HttpOnly; SameSite=Strict; Secure"
    redir https://auth.example.com
  }
}

# a pseudo site that only requires basic auth, sets cookie, and redirects back to original site
auth.example.com {
  route / {
    # require authentication
    import basic-auth

    # upon successful auth, set a client token
    header Set-Cookie "myid=some-long-hopefully-random-string; Domain=example.com; Path=/; Max-Age=3600; HttpOnly; SameSite=Strict; Secure"
    
    #delete the referer cookie
    header +Set-Cookie "myreferer=null; Domain=example.com; Path=/; Expires=Thu, 25 Sep 1971 12:00:00 GMT; HttpOnly; SameSite=Strict; Secure"
    
    # redirect back to the original site
    redir {http.request.cookie.myreferer}
  }

  # fallback
  respond "Hi."
}

one.example.com {
  import proxy-auth
  reverse\_proxy service1:8001
}

two.example.com {
  import proxy-auth
  reverse\_proxy service2:8002
}

three.example.com {
  import proxy-auth
  reverse\_proxy service3:8003
}
...

Nice! With this new configuration, you'll only need to authenticate once for all sub-domains protected by "proxy-auth". So what's happening here? Let's walk through it:

Clear as mud, right? It's not the most robust system in the world, or the most secure, but it works, it's simple, and it only requires a Caddy configuration file. No need for third-party authentication services.

Caveats.

And there it is, a poor-man's single sign-on (or proxy authentication) for Caddy services using only configuration. A further step would be to pass the authenticated user name on to the upstream services and configure them to use that for authorization, but that's for someone else to figure out.

Hope this is useful, and let me know if you find faults, improvements, or just have a general comment.