Single Sign On (SSO) with subdomains using Caddy v2
Tags: security, tutorial, caddyserver, selfhosted
Published: 2021-05-29 11:30:43

**[Update May 2022]**: The Caddy plugins used have been updated since I initially published this post. Please check out an updated version of this blog post on my site at: https://blog.sjain.dev/caddy-sso/ for the latest instructions!

I've recently taken an interest in [self-hosting](https://github.com/awesome-selfhosted/awesome-selfhosted) simple open source applications — to have fun, take control of my privacy, and learn more about Linux, Docker and DevOps!

However, with this comes the need to add some form of authentication in front of all your services. For example, you probably don't want just anyone to be able to view all your RSS feeds, or use your markdown editor freely!

The most basic form is [HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication), which is a pain as it must be configured and re-entered for each subdomain/service. You also need to type it out for each service, which can be challenging on mobile.

[Single Sign On (SSO)](https://www.cloudflare.com/en-gb/learning/access-management/what-is-sso/) on the other hand allows you to authenticate with different services with a single login. For example, if you have a Google account, you can go to [youtube.com](http://youtube.com), [gmail.com](http://gmail.com), [drive.google.com](http://drive.google.com), etc., without having to enter your login details again for each site — and each site has the same login details, saving you precious time!

There are already a few really useful guides and write-ups on using Caddy to set up an SSO system (e.g., see [here](https://joshstrange.com/securing-your-self-hosted-apps-with-single-signon/) and [here](https://josheli.com/knob/2021/02/24/single-sign-on-in-caddy-server-using-only-the-caddyfile-and-basic-authentication/)), but I wasn't able to immediately figure out how to get this working with the latest Caddy v2 and use it on subdomains. I still wanted to stick with Caddy despite this, as it is extremely easy to set-up and provides automatic HTTPS certificates out-of-the-box, which is really useful when getting started!

This short blog post shows you how to configure a simple email/password SSO system with Caddy v2! By the end, it is as simple as adding *a single line* to add SSO to your subdomain!

### 1. Download Caddy v2 with plugins

You need the plugins [caddy-auth-portal](https://github.com/greenpau/caddy-auth-portal) and [caddy-auth-jwt](https://github.com/greenpau/caddy-auth-jwt). 

You can get Caddy with the plugins through a variety of options: manually building from source, downloading the pre-built version [from their download page](https://caddyserver.com/download), or (the easiest) using [this direct link](https://caddyserver.com/download?package=github.com%2Fgreenpau%2Fcaddy-auth-portal&amp;package=github.com%2Fgreenpau%2Fcaddy-auth-jwt) with the plugins pre-populated!

### 2. Set up caddy-auth-jwt

At the top of your `Caddyfile`, add the following:

    (sso) {
        jwt {
            trusted_tokens {
                status_secret {
                    token_name access_token
                    token_secret YOUR_SECRET_EHERE
                }
            }
    
            auth_url https://auth.mydomain.com
            allow roles user
        }
    }
    

This creates a special '[snippet](https://caddyserver.com/docs/caddyfile/concepts#snippets)' that can be reused and `import`ed in other blocks by simply using `import sso` (`sso` can be called whatever you want, as long as you're consistent with the naming throughout your `Caddyfile`!).

Make sure you replace `YOUR_SECRET_HERE` with a randomly generated secure secret, and update the `auth_url` to whatever you want (e.g., for me it could be `https://auth.sjain.dev`).

I'm not going to go over `roles` in this post, but the [official caddy-auth-portal examples](https://github.com/greenpau/caddy-auth-portal) show you exactly how to use it! For my case, I'm only using it to authenticate my own account.

Note: you should [read the docs](https://github.com/greenpau/caddy-auth-portal#jwt-tokens) to understand alternatives for managing your JWTs, which might suit your environment better. For example, you might choose to use environment variables, or a private key in a file.

### 3. Set up your Caddyfile's directive *order*

In Caddy v2, there's a [pre-set order of precedence for directives](https://caddyserver.com/docs/caddyfile/directives#directive-order). See [this issue on GitHub](https://github.com/caddyserver/caddy/issues/3438#issuecomment-633048762) for more details.

But `jwt` and `auth_portal` (the directives from the two plugins we installed) aren't in that pre-set; so we need to tell Caddy where they lie in the order of precedence.

Just after your `sso` snippet, add the following:

    {
        order jwt before reverse_proxy
        order auth_portal before jwt
    }
    

### 4. Configure your SSO!

Now you can use the `auth_portal` directive to configure your SSO setup:

    auth.mydomain.com {
        auth_portal {
            path /
            cookie_domain mydomain.com
            backends {
                local_backend {
                    method local
                    path /etc/caddy/auth/local/users.json
                    realm local
                }
            }
            jwt {
                token_name access_token
                token_secret YOUR_SECRET_HERE
            }
            ui {
                links {
                    "RSS" https://rss.mydomain.com
                    "Notes" https://notes.mydomain.com
                }
            }
            registration {
                dropbox /etc/caddy/auth/local/users.json
                code "YOUR_CODE_HERE"
            }
        }
    }
    

Make sure you set the `auth.mydomain.com` to be your actual domain (again, it doesn't have to be `auth.` — it can be whatever you want as long as you're consistent). You'll also need to update your domain's DNS settings to account for this new subdomain.

It's crucial to update the `cookie_domain` — this is what makes it work for your subdomains!

Update the `token_secret` to be what you said in the `jwt` directive.

If you want the users to be stored elsewhere then you can update the `path` directive of `local_backend`. You'll also need to update this file to change user roles.

The `ui` directive is optional: if enabled, when authenticated on `auth.mydomain.com`, you will be provided with a very handy list of all your services:

![List of URLs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jw0cw5c4uz5sz71r6ks7.png)

The `registration` directive is also optional: if enabled, there will be a registration option on the login screen, and one of the registration form fields will be the `code` as an extra step so only people you want (or who know the code) can register.

For simplicity, you might enable registration temporarily to create your own account, and then disable it — depending on any risk to your existing services! Alternatively, you can copy/paste the `superuser` in the JSON file you specified and update the duplicate to be your own details, e.g., change the password (using bcrypt), the roles, ID, username, etc.

### 5. Use your SSO!

That's all the config done! Now whenever you want to enable SSO for a subdomain/service, just `import sso`.

There's a caveat though: *one* of your subdomains/routes needs to be marked as `primary yes` (for reasons explained [here](https://github.com/greenpau/caddy-auth-jwt#overview)), but the `sso` snippet we defined didn't have this. So, you'll need to copy and paste the config into one of your routes and add `primary yes` before you can just use `import sso` in the rest.

For example, for the subdomain `mainservice.mydomain.com`:

    mainservice.mydomain.com {
        jwt {
            primary yes # XXX: this is the addition
            trusted_tokens {
                status_secret {
                    token_name access_token
                    token_secret YOUR_SECRET_EHERE
                }
            }
            auth_url https://auth.mydomain.com
            allow roles user
        }
    
        reverse_proxy http://localhost:1234
    }

And then for `rss.mydomain.com` (and any other subdomains):

    rss.mydomain.com {
        import sso
        reverse_proxy http://localhost:1234
    }

Now whenever anyone navigates to `rss.mydomain.com` or `mainservice.mydomain.com`, if they are not logged in, they will be redirected to `auth.mydomain.com` and will have to log in:

![Login screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j2j4jpfj8v4s19ycr4ww.png)
 
If you're already logged in (your browser will have a cookie), then it will just let you in!

The cookie will be shared across your subdomains, so you can freely switch between all your services without the pain of logging in every time!

What if you want some paths to be public but not all? You could do this:

    service.mydomain.com {
        @allow path /public /anothersafepath
        handle @allow {
            reverse_proxy http://localhost:1234
        }
            
        import sso
        reverse_proxy http://localhost:1234
    }
    

Because of the *order*s we set up earlier, this simply works from top-to-bottom because `handle` has higher precedence than `jwt` (from the `import sso`) and also `reverse_proxy`. [Here's the pre-defined order for reference](https://caddyserver.com/docs/caddyfile/directives#directive-order)!

If you wanted to instead only keep some paths secret, you could change the second line to `@reject not path /secret /anothersecretpath` (and change `@allow` to `@reject` in the third line!).

I hope this post helps setting up your SSO with Caddy. I'd highly recommend trying it out if you find yourself always needing to authenticate with different services on your domain – and check out [caddy-auth-portal's docs](https://github.com/greenpau/caddy-auth-portal) for even more advanced features!

*Disclaimer: authentication is extremely important for a variety of services so please ensure your configuration works for you. In this blog I detail how you might use SSO but am in no way accountable for any privacy or confidentiality breaches as this is dependent on your configuration.*