DEV Community

Shubham
Shubham

Posted on • Updated on • Originally published at blog.sjain.dev

Single Sign On (SSO) with subdomains using Caddy v2

[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 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, 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) 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, gmail.com, 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 and here), 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 and 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, or (the easiest) using this direct link 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
    }
}
Enter fullscreen mode Exit fullscreen mode

This creates a special 'snippet' that can be reused and imported 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 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 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. See this issue on GitHub 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
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

rss.mydomain.com {
    import sso
    reverse_proxy http://localhost:1234
}
Enter fullscreen mode Exit fullscreen mode

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

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

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!

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 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.

Top comments (0)