{"componentChunkName":"component---src-templates-blog-post-js","path":"/blog/securing-internal-services-behind-oauth2-with-caddy","result":{"data":{"allGhostPost":{"edges":[{"node":{"title":"Securing Internal Services Behind an OAuth2 Provider with Caddy","html":"

Last updated on Mar. 1, 2019

\n
\n \n
\n

In the video above, you can see that secret.int.localtest.me† is secured behind a Google OAuth2 login. Unauthenticated requests are redirected to a login portal, auth.int.localtest.me, which asks the user to log in using Google. With this, we are able to authenticate once for all sites under int.localtest.me.

\n

† localtest.me as well as any of its subdomains resolve to 127.0.0.1. This makes testing local services much easier.

\n

⚠ Note: This post applies to Caddy 1 which was discontinued around mid-2020. Do not follow this if you are using Caddy 2.

\n

πŸ€” How does this work?

\n

Behind the scenes, authentication is handled using JWT Tokens set as cookies on the int.localtest.me hostname, making them available to all subdomains. The JWT token is checked on every request to authenticated URLs. This is handled by the http.jwt plugin.

\n

The login portal is served using the http.loginsrv plugin, which is a Caddy binding to tarent/loginsrv.

\n

πŸ›  How do I set this up?

\n

ℹ️ UPDATE Mar. 1, 2019 Due to Google+ being deprecated, it's no longer necesary to enable its API and OAuth scopes. I've removed the sections relating to that below.

\n

We will assume the following:

\n\n

πŸ”“ Base unsecured environment

\n

We will start with an internal service, publicly accessible:

\n

Caddyfile

\n
secret.int.domain.tld {\n    ...\n}\n
\n

πŸ“Έ Install the Caddy plugins

\n

πŸ‘‹πŸΌ I prefer to run Caddy inside a Docker container. But, you don't have to! You can skip this section, but make sure that your Caddy binary has the http.jwt and http.login plugins compiled. You can either compile the binary yourself or download a pre-built binary from Caddy's website making sure to include the plugins I mentioned.

\n

The abiosoft/caddy image doesn't include the http.jwt and http.login plugins, so we will need to build our own image. To do that, run:

\n
docker build -t caddy-jwt-login --build-arg plugins="jwt,login" github.com/abiosoft/caddy-docker.git\n
\n

Make sure to include any other plugins you might need if you are hosting other sites on the same Caddy instance. Replace caddy-jwt-login with a different tag if it makes sense for you.

\n

This will be the image that we will base our Caddy container on, instead of abiosoft/caddy.

\n

🌟 I've built and published an image that you can use, but I can't promise that it will always be up to date! kamaln7/caddy-jwt-login. As of right now, the latest version is kamaln7/caddy-jwt-login:0.11.0.

\n

πŸ”‘ Set up the Google OAuth2 provider

\n

In order to be able to use Google as an OAuth2 provider, we'll need to create a project in the Developers Console and add an OAuth2 service to it. Start by creating a new project here.

\n

Then, browse to the developer console. Make sure your project is selected in the top-left corner.

\n

Go to the Credentials page, accessible from the left sidebar. Click on the "Oauth consent screen" tab and fill in "Application name".

\n

Scroll down to "Authorized domains" and enter your top-level domain name in the field (domain.tld). Save and return to the Credentials page.

\n

In the Credentials tab, click on the blue Create credentials button and select OAuth client ID.

\n
\"screenshot\"
\n

The Application type will be Web application. Name it whatever you want, and add https://auth.int.domain.tld/login/google to the Authorized redirect URIs list. Don't click Create right after filling out the field. Click anywhere else on the page to add it to the list and then click Create. Dumb UX, I know. But it's necessary.

\n
\"screenshot\"
\n

On the following page, you will get a popup with your Client ID and secret. Save those somewhere because we will need them later.

\n

🌐 Create the login portal

\n

Add the following to your Caddy config. Replace YOURCLIENTID with your Client ID and YOURCLIENTSECRET with the secret.

\n
auth.int.domain.tld {\n        tls your@email.address\n        redir 302 {\n                if {path} is /\n                / /login\n        }\n\n        login {\n                google client_id=YOURCLIENTID,client_secret=YOURCLIENTSECRET\n                redirect_check_referer false\n                redirect_host_file /redirect_hosts.txt\n                cookie_domain int.domain.tld\n        }\n}\n
\n

This will configure the http.login plugin with the Google OAuth2 provider you just created, set the cookies on int.domain.tld instead of auth.int.domain.tld, and allow rediretion to hosts in the redirect_hosts.txt file. More on redirections below.

\n

βœ… Now, when you browse to https://auth.int.domain.tld, you will be able to log in using your Google account. Note that anyone will be able to log in here, but not everyone will have access to the protected services. We will limit service access to specific email addresses in the following section.

\n

πŸ” Secure the internal service

\n

All that is left is securing the internal service using the http.jwt plugin. We will extract this config into its own snippet so we can easily re-use it with other services.

\n

Caddyfile

\n
(int-auth) {\n        jwt {\n                path /\n                redirect https://auth.int.domain.tld/login?backTo=https%3A%2F%2F{host}{rewrite_uri_escaped}\n                allow sub your@email.address\n                allow sub otherpeson@gmail.com\n        }\n}\n
\n

This will enable JWT auth on / (every request) and redirect unauthenticated requests to our login portal. The backTo parameter instructs the login portal to redirect back to the internal service on successful login. Repeat the allow sub ... directives for each email address you want to allow access.

\n

πŸ’­ Remember we talked about a redirect_hosts.txt file? This is where it comes in. This file will contain a list of hosts that the login portal is allowed to redirect back to. By default, it won't allow any external URLs outside of auth.int.domain.tld. By setting redirect_check_referer false and providing a list of approved hosts, we are able to redirect users back to our internal services.

\n

Create a file with each internal service on a line and add it to your Docker image or wherever Caddy is running. Just make sure to update the path to it in the Caddy config above (the login {} block).

\n

redirect_hosts.txt

\n
secret.int.domain.tld\nanother-service.int.domain.tld\n
\n

✨ Finally, import this snippet in the internal service config:

\n

Caddyfile

\n
secret.int.domain.tld {\n    import int-auth\n    ...\n}\n
\n

What now?

\n

That's it! Your internal service(s) are secured using the login portal.

\n

OPTIONAL ⚑ Session persistance across Caddy restarts

\n

JWT tokens are signed using a secret. This allows the server to validate that the tokens weren't tampered with (by anyone who doesn't have access to the secret). http.login looks for a secret stored in the JWT_SECRET environment variable:

\n\n

When validating requests, http.jwt uses the JWT_SECRET environment variable. This is all fine, because the default secret is randomly generated on startup and not a hard-coded string. However, this means that the secret changes when Caddy restarts, which causes all users to be logged out as their tokens become invalidated.

\n

To make sessions persist across restarts, we need to set our own fixed secret. You can generate two random 16-character-long strings on random.org and put them together as a 32-character-long secret. (You need a paid random.org account to generate strings longer than 20 characters). Set the JWT_SECRET environment variable to this token and make it available to Caddy.

\n

πŸ“š Resources

\n\n","published_at":"2018-09-15T21:31:23.000+03:00","slug":"securing-internal-services-behind-oauth2-with-caddy","tags":[],"plaintext":"Last updated on Mar. 1, 2019\n\nIn the video above, you can see that secret.int.localtest.me† is secured behind\na Google OAuth2 login. Unauthenticated requests are redirected to a login\nportal, auth.int.localtest.me, which asks the user to log in using Google. With\nthis, we are able to authenticate once for all sites under int.localtest.me.\n\n† localtest.me as well as any of its subdomains resolve to 127.0.0.1. This\nmakes testing local services much easier.\n\n⚠ Note: This post applies to Caddy 1 which was discontinued around mid-2020. Do\nnot follow this if you are using Caddy 2.\n\nπŸ€” How does this work?\nBehind the scenes, authentication is handled using JWT Tokens set as cookies on\nthe int.localtest.me hostname, making them available to all subdomains. The JWT\ntoken is checked on every request to authenticated URLs. This is handled by the \nhttp.jwt plugin [https://caddyserver.com/docs/http.jwt].\n\nThe login portal is served using the http.loginsrv plugin\n[https://caddyserver.com/docs/http.login], which is a Caddy binding to \ntarent/loginsrv [https://github.com/tarent/loginsrv].\n\nπŸ›  How do I set this up?\nℹ️ UPDATE Mar. 1, 2019 Due to Google+ being deprecated, it's no longer necesary\nto enable its API and OAuth scopes. I've removed the sections relating to that\nbelow.\n\nWe will assume the following:\n\n * All internal services are hosted on *.int.domain.tld\n * The login portal will be served on login.int.domain.tld\n * The OAuth2 provider is Google with a list of approved email addresses\n * Caddy is used as the front-facing proxy or webserver for all internal\n services\n\nπŸ”“ Base unsecured environment\nWe will start with an internal service, publicly accessible:\n\nCaddyfile\n\nsecret.int.domain.tld {\n ...\n}\n\n\nπŸ“Έ Install the Caddy plugins\nπŸ‘‹πŸΌ I prefer to run Caddy inside a Docker container. But, you don't have to! \nYou can skip this section, but make sure that your Caddy binary has the http.jwt \n and http.login plugins compiled. You can either compile the binary yourself or\ndownload a pre-built binary from Caddy's website\n[https://caddyserver.com/download] making sure to include the plugins I\nmentioned.\n\nThe abiosoft/caddy image doesn't include the http.jwt and http.login plugins,\nso we will need to build our own image. To do that, run:\n\ndocker build -t caddy-jwt-login --build-arg plugins=\"jwt,login\" github.com/abiosoft/caddy-docker.git\n\n\nMake sure to include any other plugins you might need if you are hosting other\nsites on the same Caddy instance. Replace caddy-jwt-login with a different tag\nif it makes sense for you.\n\nThis will be the image that we will base our Caddy container on, instead of \nabiosoft/caddy.\n\n🌟 I've built and published an image that you can use, but I can't promise that\nit will always be up to date! kamaln7/caddy-jwt-login. As of right now, the\nlatest version is kamaln7/caddy-jwt-login:0.11.0.\n\nπŸ”‘ Set up the Google OAuth2 provider\nIn order to be able to use Google as an OAuth2 provider, we'll need to create a\nproject in the Developers Console and add an OAuth2 service to it. Start by\ncreating a new project here\n[https://console.developers.google.com/projectcreate].\n\nThen, browse to the developer console\n[https://console.developers.google.com/apis/dashboard]. Make sure your project\nis selected in the top-left corner.\n\nGo to the Credentials page\n[https://console.developers.google.com/apis/credentials], accessible from the\nleft sidebar. Click on the \"Oauth consent screen\" tab and fill in \"Application\nname\".\n\nScroll down to \"Authorized domains\" and enter your top-level domain name in the\nfield (domain.tld). Save and return to the Credentials page.\n\nIn the Credentials tab, click on the blue Create credentials button and select\nOAuth client ID.\n\nThe Application type will be Web application. Name it whatever you want, and add\n https://auth.int.domain.tld/login/google to the Authorized redirect URIs list.\n Don't click Create right after filling out the field. Click anywhere else on\nthe page to add it to the list and then click Create. Dumb UX, I know. But it's\nnecessary.\n\nOn the following page, you will get a popup with your Client ID and secret. Save\nthose somewhere because we will need them later.\n\n🌐 Create the login portal\nAdd the following to your Caddy config. Replace YOURCLIENTID with your Client\nID and YOURCLIENTSECRET with the secret.\n\nauth.int.domain.tld {\n tls your@email.address\n redir 302 {\n if {path} is /\n / /login\n }\n\n login {\n google client_id=YOURCLIENTID,client_secret=YOURCLIENTSECRET\n redirect_check_referer false\n redirect_host_file /redirect_hosts.txt\n cookie_domain int.domain.tld\n }\n}\n\n\nThis will configure the http.login plugin with the Google OAuth2 provider you\njust created, set the cookies on int.domain.tld instead of auth.int.domain.tld,\nand allow rediretion to hosts in the redirect_hosts.txt file. More on\nredirections below.\n\nβœ… Now, when you browse to https://auth.int.domain.tld, you will be able to log\nin using your Google account. Note that anyone will be able to log in here, but\nnot everyone will have access to the protected services. We will limit service\naccess to specific email addresses in the following section.\n\nπŸ” Secure the internal service\nAll that is left is securing the internal service using the http.jwt plugin. We\nwill extract this config into its own snippet so we can easily re-use it with\nother services.\n\nCaddyfile\n\n(int-auth) {\n jwt {\n path /\n redirect https://auth.int.domain.tld/login?backTo=https%3A%2F%2F{host}{rewrite_uri_escaped}\n allow sub your@email.address\n allow sub otherpeson@gmail.com\n }\n}\n\n\nThis will enable JWT auth on / (every request) and redirect unauthenticated\nrequests to our login portal. The backTo parameter instructs the login portal\nto redirect back to the internal service on successful login. Repeat the allow\nsub ... directives for each email address you want to allow access.\n\nπŸ’­ Remember we talked about a redirect_hosts.txt file? This is where it comes\nin. This file will contain a list of hosts that the login portal is allowed to\nredirect back to. By default, it won't allow any external URLs outside of \nauth.int.domain.tld. By setting redirect_check_referer false and providing a\nlist of approved hosts, we are able to redirect users back to our internal\nservices.\n\nCreate a file with each internal service on a line and add it to your Docker\nimage or wherever Caddy is running. Just make sure to update the path to it in\nthe Caddy config above (the login {} block).\n\nredirect_hosts.txt\n\nsecret.int.domain.tld\nanother-service.int.domain.tld\n\n\n✨ Finally, import this snippet in the internal service config:\n\nCaddyfile\n\nsecret.int.domain.tld {\n import int-auth\n ...\n}\n\n\nWhat now?\nThat's it! Your internal service(s) are secured using the login portal.\n\nOPTIONAL ⚑ Session persistance across Caddy restarts\nJWT tokens are signed using a secret. This allows the server to validate that\nthe tokens weren't tampered with (by anyone who doesn't have access to the\nsecret). http.login looks for a secret stored in the JWT_SECRET environment\nvariable:\n\n * if it exists, it uses it\n * otherwise, it uses loginsrv's default secret and updates the environment\n variable. The default secret is a randomly generated string\n\nWhen validating requests, http.jwt uses the JWT_SECRET environment variable.\nThis is all fine, because the default secret is randomly generated on startup\nand not a hard-coded string. However, this means that the secret changes when\nCaddy restarts, which causes all users to be logged out as their tokens become\ninvalidated.\n\nTo make sessions persist across restarts, we need to set our own fixed secret.\nYou can generate two random 16-character-long strings on random.org\n[https://www.random.org/strings/?num=2&len=16&digits=on&upperalpha=on&loweralpha=on&unique=on&format=html&rnd=new] \n and put them together as a 32-character-long secret. (You need a paid\nrandom.org account to generate strings longer than 20 characters). Set the \nJWT_SECRET environment variable to this token and make it available to Caddy.\n\nπŸ“š Resources\n * tarent/loginsrv [https://github.com/tarent/loginsrv]\n * http.login [https://caddyserver.com/docs/http.login] - Caddy plugin\n * http.jwt [https://caddyserver.com/docs/http.jwt] - Caddy plugin\n * loginsrv ← OAuth2","meta_description":null}}]}},"pageContext":{"slug":"securing-internal-services-behind-oauth2-with-caddy","prev":"dropin-chatops-turning-existing-workflows-and-scripts-into-slack-commands","next":"how-to-set-global-shortcut-open-new-iterm2-window"}},"staticQueryHashes":["3649515864"]}