Josheli
  • Home
  • Blog
    • Knob
    • Running
    • Soccer
    • Technology
  • Projects
    • Overview
    • No Instagram
    • Google Photos WordPress Plugin
    • Plex Channels
    • Sh***y Game
    • Soccer In Colorado
    • Statrat
    • The Dot Game
    • Vox cPanel Hacks
    • WW Points Calculator
  • About
Knob , Technology

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

by dv February 24, 2021 20 Comments

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:

  • When you first go to one of the sub-domains, say two.example.com, the “proxy-auth” import will be invoked, and it will check to see if the “myid” cookie is set to a certain value. I’ve used a header_regexp matcher because the string varies in my case, but you could hard code it and use just a header matcher.
  • Since the “myid” cookie is not set, a new “myreferer” cookie will be set with the current URL you are trying to access, and you will be redirected to the “virtual” auth.example.com sub-domain. This “auth” sub-domain is “virtual” in the sense that there is no actual service; it only exists as Caddy configuration.
  • Upon redirection to the auth.example.com sub-domain, you will be prompted for the basic authentical credentials of the “basic-auth” snippet. When you successfully authenticate, the “myreferer” cookie will be deleted, a new “myid” cookie will be set with your “token”, and you will be redirected back to the original URL you were trying to access (the URL was stored in the “myreferer” cookie).
  • Now, back at the original URL and sub-domain, the “proxy-auth” import will be invoked again, but this time, the “myid” cookie is set with the correct token, and should match the header_regexp, thus bypassing the “no-auth” route, and letting you access the upstream service.
  • Subsequently navigating to any of the sub-domains protected with “proxy-auth” should check the “myid” cookie token, see that it matches, and let you in without having to authenticate again.

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.

  • How secure is it? I wouldn’t put my bank services behind it, but it’s as secure as basic authentication. Someone could guess your “myid” Cookie value, and spoof that, but they could also guess your basic authentication credentials. So make both of them hard to guess. Like I said, I’ve made the token in the “myid” cookie on my setup quite long and somewhat random, and use a regular expression to match the value. Caddy doesn’t support a lot of variables in snippets, or hashing in configuration, so it’s difficult to be too secure, but I’d say it’s moderately secure.
  • It only works if all of your services are sub-domains on the same domain. The cookies use the bare domain (example.com), and thus are valid on any sub-domain of example.com. If you have a second domain, the cookies won’t be sent, and the “proxy-auth” will never match.
  • Adjust the cookie values to your use case. I set the cookie lifetime for “myid” to an hour, and the myreferer cookie lifetime to 30 seconds.
  • You can still use the “basic-auth” snippet on other sub-domains, say for a service with an API.

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.

It's only fair to share...Share on facebook
Facebook
Share on twitter
Twitter
Share on email
Email

Related Content:

  • A New, Old Hobby: Self-hosted Services by Dv February 21, 2021 I've recently returned to a hobby of mine ... self-hosting various software services and web applications on a server I…
  • Dockerizing PHP apps and deploying to AWS Fargate Part 1 by Dv February 6, 2019 For the day job, I maintain four custom PHP apps in addition to several Drupal sites. Up until a couples…
  • Stupidly Simple, Static, Startpage for Self-hosted Services by Dv February 21, 2021 I mentioned the other day that I've been self-hosting some software services on a server in my basement. Well, I…
  • Ignoring God, Beginning a Blog by Dv May 22, 2006 [UPDATE 8/22/06:  I saw the lady again a few weeks later, and nothing has aired on Fox or YouTube yet,…
  • Migrating a Laravel application to Symfony by Dv April 15, 2019 When I started at the day job about 6.5 years ago, I inherited a jumble of custom PHP/MySql web applications.…
  • Previous Stupidly Simple, Static, Startpage for Self-hosted Services2 years ago
  • Next Running, writing, and YouTubing2 years ago

20 Replys to “Single Sign-on in Caddy Server Using only the Caddyfile and Basic Authentication”

  1. Tristan says:
    June 20, 2021 at 9:48 am

    I really like the idea and would love for it to work since a whole LDAP setup is borderline torture.

    Unfortunately though there is a loophole which makes this not very helpful. If the just cancels the basic auth dialog the myid cookie gets set nonetheless, thereby bypassing the system and gaining full access. I stumbled upon this whilst playing around with your solution.

  2. dv says:
    June 20, 2021 at 11:40 am

    If true, this would be bad, but I can’t reproduce this. Canceling the auth dialog doesn’t set myid on my system. I’ve tested on Chrome, Firefox and Safari and I’m not seeing this behavior. Can you provide any more detail? Maybe post your Caddyfile, or the relevant bits?

  3. Tristan says:
    June 20, 2021 at 12:52 pm

    I am testing on Firefox 89.0 on Voidlinux.
    Seems like I screwed something up then.

    This is my Caddyfile:

    (basic-auth) {
    basicauth / {
    user 123456
    }
    }

    (proxy-auth) {
    @no-auth {
    not header_regexp mycookie Cookie myid=asdf
    }
    route @no-auth {
    header Set-Cookie “myreferer={scheme}://{host}{uri}; Domain=example.com; Path=/; Max-Age=30; HttpOnly; SameSite=Lax”
    redir http://auth.example.com
    }
    }

    auth.example.com:80 {
    route {
    import basic-auth
    header Set-Cookie “myid=asdf; Domain=example.com; Path=/; Max-Age=3600; HttpOnly; SameSite=Lax”
    header +Set-Cookie “myreferer=null; Domain=example.com; Path=/; Expires=Thu, 25 Sep 1971 12:00:00 GMT; HttpOnly; SameSite=Lax”
    redir {http.request.cookie.myreferer}
    }
    }

    one.example.com:80 {
    reverse_proxy one:8080
    }

    two.example.com:80 {
    reverse_proxy two:8080
    }

  4. dv says:
    June 21, 2021 at 8:46 am

    Just for posterity, Tristan was correct about the bug, which he helped me fix. Loophole should be closed now, and I’ve updated the code in the post.

  5. Mel says:
    September 22, 2021 at 12:10 pm

    Hi
    just stumbled upon this which i find very useful – yet i do not seem to be able to re-direct from the auth.domain.com page when successfully logging in. Has the declaration {http.request.cookie.myreferer} changed?
    Thanks

  6. Mel says:
    September 23, 2021 at 7:24 am

    Hi Josh
    Conscious I commented yesterday, i just wanted to follow up. I originally did not see that the string I randomly created had special characters and broke your code. It is now working fine. Thanks a lot for that excellent piece of code.
    Best
    M

  7. Dan says:
    January 19, 2022 at 11:24 am

    Hi Josh,

    Not sure if I’m implementing this right but the Caddy v2 is challenging the auth.domain.com site and coming up empty for establishing a secure site. Is there a TLS skip verify command that I’m missing, or something similar that allows the pseudo site to be created?

    Thank you,
    Dan

  8. dv says:
    January 22, 2022 at 11:20 am

    Hi, Dan. If I’m understanding your issue correctly, you do need to have SSL set up on your subdomain, or change the redirection location to http. I proxy all of my sub-domains (including “auth.domain.com”) through Cloudflare and have a self-signed cert.

    If you don’t have that sub-domain set up with SSL, you’ll need to do that, or maybe change that line to not use https (but I’d only do that if you never left an internal network:

    redir http://auth.example.com

    Hopefully this helps.

  9. Marc says:
    January 25, 2022 at 12:39 am

    HI dv,
    I have been trying to use the configuration that you have provided, but I don’t undersant why it gives me the error “To many redirects”. I’m new into Caddy, so I don’t really understand all the code you have provided. I give you my code, so maybe you can help me.

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

    # 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=
    # 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 {
    tls internal
    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.”
    }

    dashboard.example.com {
    import proxy-auth

    #basicauth * {
    #user my-password-hashed
    #}
    tls internal

    # Set this path to your site’s directory.
    root * /home/test/

    # Enable the static file server.
    file_server

    # Another common task is to set up a reverse proxy:
    # reverse_proxy localhost:8080

    # Or serve a PHP site through php-fpm:
    # php_fastcgi localhost:9000
    }

    # Refer to the Caddy docs for more information:
    # https://caddyserver.com/docs/caddyfile

    Thanks

  10. dv says:
    January 25, 2022 at 4:33 pm

    Hey, Marc.

    A couple things:

    – Make sure anywhere the config refers to “example.com” is changed to your actual domain
    – The regex for myid on line 11 needs to match whatever you set myid equal to on line 29

  11. Dan says:
    January 29, 2022 at 3:33 pm

    Hi DV,
    I feel like I’m missing some caddy directive and should have the auth site in a separate section- by any chance DV do you have an example how you used your auth site to get Caddy2 ssl. I don’t use cloudflare and don’t know even if I self cert how to use the auth section correctly? It keeps timing out on challenges with letsencrypt and zerossl.
    Thank you,
    Dan

  12. Dan says:
    February 2, 2022 at 3:28 pm

    Okay, DV – I’ve since got the site up and running with auth now directing to login page securely.

    I’m trying to use the myID as the username and the hashed password (both hashed version and prior to hash version) as the password, but getting no where with the login screen – any advice?

    Thank you,
    Dan

  13. dv says:
    February 2, 2022 at 4:07 pm

    Hi, Dan. I sent you an email the other day. If you share your config with me I might be able to offer more help.

  14. Dan says:
    February 23, 2022 at 12:17 pm

    Hi DV,

    I’ve sent my redacted config, have you had a chance to look at it? I sure would appreciate the assistance!

    Thank you,
    Dan

  15. Armin says:
    April 22, 2022 at 3:55 am

    Great stuff and working out of the box for me :-)

    One question, though: I’ve yet to find a way to clear cookies in a way that I’m logged out again (eventually for a logout button). Just removing the myid cookie on any of the “real” domains doesn’t help and I can’t specifically delete it from the proxy-auth domain, can I?

    Only solution for now seems to be to clear the entire browser history/data…

  16. dv says:
    April 22, 2022 at 1:45 pm

    Hmm, I haven’t run into that, maybe because I’m always in a private browser and it just removes all cookies when I exit? But the myid cookie by default has a max-age of 1hr, so it should expire, no?

  17. Jose A. Hernandez says:
    April 24, 2022 at 11:46 am

    Is it be possible to modify this to only ask for basic auth when requests are not coming from local networks, so I’m not asked to authenticate when I’m inside my home network?

  18. dv says:
    April 24, 2022 at 12:41 pm

    Jose,

    Maybe try adding an IP check? something like (adjust for your private IP range):

    @no-auth {
    not {
    remote_ip 192.168.0.0/16
    header_regexp mycookie Cookie myid=
    }
    }

  19. Jose A. Hernandez says:
    April 24, 2022 at 3:58 pm

    Thanks, I initially had solved it by having a similar solution, but on the (basic-auth) block:

    (basic-auth) {
    @local_networks {
    not remote_ip 192.168.0.0/24
    }
    basicauth @local_networks {
    myuser mypasshash
    }
    }

    Your solution works better because by having the condition on the proxy-auth block instead, the redirection to the auth.example.com virtual host is skipped altogether.

  20. Armin says:
    April 26, 2022 at 3:31 am

    Regarding the logout/timeout:

    It seems it doesn’t forget the cookie after those 3600secs (I had kept that browser tab idle for more than an hour then did a reload, no password dialog). Also, I deleted all cookies for my test domain – still it let me in without basic auth – which I don’t understand at all. (yes, private session behaves differently) This was on Firefox.

    So would you confirm that with this approach a Logout button is not possible?

Leave a Reply

Your email address will not be published. Required fields are marked *

Popular Posts

  • Josheli, What Happened? (28,499)
  • Install Ubuntu on HP Laptop with UEFI and new SSD Hard Drive (14,582)
  • Running a Plex Media Server on an Old Laptop (13,451)
  • Simple Google Photos: A WordPress Plugin (11,755)
  • More Janky Snowboarding Video (11,053)

Random Read

Running a Plex Media Server on an Old Laptop
I've been running a Plex Media Server off on old laptop for more than a…

Read More

Google Photo
Google Photo
Google Photo
Google Photo

Social Things

  • Family Photos
  • Juiskie’s Instagram
  • Scooter’s Facebook
  • Scooter’s Instagram
  • YouTube Videos
  • DV’s Github
  • Tweet Tweet

RSS From Familyvance

  • Snowshoeing at Brainard Lake
  • Fishing and Hiking at Golden Gate Canyon State Park
  • Rainbow Trout Fishing Report at Waneka Lake
  • Weightless Texas-Rig Plastic Worms at Sawhill Ponds and Coot Lake
  • Sawhill Ponds Fishing Report
2023 Josheli. Donna Theme powered by WordPress