Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How do I configure CORS in Caddy security so that I can sign in less frequently? #90

Open
rubydotexe opened this issue Apr 20, 2022 · 22 comments
Assignees
Labels
cors question Further information is requested

Comments

@rubydotexe
Copy link

Hello again!

This is the behavior I'm having trouble with:

Basically, when I am using FoundryVTT the connection to my assets disconnect until I sign in the Caddy Security portal again. Problem is that this happens frequently. When I refresh the tab, I am sent to the auth portal, I sign in, and the problem is resolved for about less then five minutes where then the cycle repeats.

I checked the JS console log:

15:03:13.429 Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://github.com/login/oauth/authorize?client_id=[omited]c7&redirect_uri=https%3A%2F%2Fauth.example.com%2Foauth2%2Fgithub%2Fauthorization-code-callback&scope=read%3Auser&state=[omited]. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 302.

15:03:13.446 Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://github.com/login/oauth/authorize?client_id=[omited]&redirect_uri=https%3A%2F%2Fauth.[omited].xyz%2Foauth2%2Fgithub%2Fauthorization-code-callback&scope=read%3Auser&state=[omited]. (Reason: CORS request did not succeed). Status code: (null).

15:03:13.447 Uncaught (in promise) TypeError: NetworkError when attempting to fetch resource.
[No packages detected]

I think it has to do with my Caddy configuration? As you can see, I basically pulled things out of my bum and have no idea what I'm doing. I tried looking up examples for CORS in Caddy and I don't think I'm doing it right. If I'm doing something wrong (and I most certainly am) can someone provide an example of what it should look like?

{
	order authenticate before respond
	order authorize before basicauth
	#acme_ca https://acme-staging-v02.api.letsencrypt.org/directory


	security {
		oauth identity provider github {env.GITHUB_CLIENT_ID} {env.GITHUB_CLIENT_SECRET}

		authentication portal myportal {
			enable identity provider github

			ui {
				links {
					"My Website" https://example.com icon "las la-star"
					"My Identity" "/whoami" icon "las la-user"
				}
				password_recovery_enabled no
			}
			transform user {
				match origin local
				action add role authp/user
				ui link "Portal Settings" /settings icon "las la-cog"
			}
			transform user {
				match realm github
				match sub github.com/rubydotexe
				action add role authp/user
			}
		}
		authorization policy users_policy {
			set auth url https://auth.example.com:443/
			allow roles authp/admin authp/user
			acl rule {
				comment allow users
				match role authp/user
				allow stop log info
			}
			acl rule {
				comment default deny
				match any
				deny log warn
			}
		}

		authorization policy admins_policy {
			set auth url https://auth.example.com:443/
			allow roles authp/admin authp/user
			acl rule {
				comment allow users
				match role authp/user
				allow stop log info
			}
			acl rule {
				comment default deny
				match any
				deny log warn
			}
		}
	}
}

(tls_config) {
	tls {
		dns googleclouddns {
			gcp_project {env.GCP_PROJECT}
			gcp_application_default {env.GCP_APPLICATION_DEFAULT}
		}
	}
}

(cors) {
	@origin{args.0} header Origin {args.0}
	header @origin{args.0} Access-Control-Allow-Origin "{args.0}"
	header @origin{args.0} Vary Origin
}

(wildcard) {
	Access-Control-Request-Method = method
	Access-Control-Request-Headers = 1#field-name

	wildcard = "*"
	Access-Control-Allow-Origin = origin-or-null / wildcard
	Access-Control-Allow-Credentials = %s"true" ; case-sensitive
	Access-Control-Expose-Headers = #field-name
	Access-Control-Max-Age = delta-seconds
	Access-Control-Allow-Methods = #method
	Access-Control-Allow-Headers = #field-name
}

(options) {
	header Access-Control-Allow-Methods "POST, GET, OPTIONS"
	@options {
		method OPTIONS
	}
	respond @options 204
	import cors https://example.com
	import cors https://www.example.com
	import cors https://auth.example.com
	import cors https://dnd.example.com
	import cors https://files.example.com
	import cors https://neko.example.com
	import cors https://5etools.example.com
	import cors https://wiki.example.com
	import cors https://orcpub.example.com
}

(wiki) {
	reverse_proxy app:3333
}

(errors) {
	handle /ads.txt {
		respond 404
	}
	handle /robots.txt {
		respond 404
	}
	handle /wp-content/ {
		respond 404
	}
	handle /wp-includes/ {
		respond 404
	}
	handle /wp-admin/ {
		respond 404
	}
	handle /wp-plugins/ {
		respond 404
	}
	handle /_ts_ {
		respond 404
	}
	handle /_ts/ {
		respond 404
	}
	handle /_sites/ {
		respond 404
	}
}

auth.example.com {
	import tls_config
	import options
	authenticate with myportal
	root * /usr/share/caddy
	file_server
}

example.com {
	import tls_config
	import options
	authorize with users_policy
	reverse_proxy flame:5005
}

*.example.com {
	import tls_config
	import options

	@dnd host dnd.example.com
	handle @dnd {
		authorize with users_policy
		reverse_proxy foundry:30000
		encode zstd gzip
	}

	@pub host orcpub.example.com
	handle @dnd {
		#authorize with users_policy
		reverse_proxy orcpub:8890
		route /homebrew.orcbrew {
			root /homebrew.orcbrew /srv/orcpub
			file_server
		}
	}

	@cron host cron.example.com
	handle @cron {
		authorize with users_policy
		reverse_proxy crontab-ui:8000
		encode zstd gzip
	}

	@wiki host wiki.example.com
	handle @wiki {
		authorize with users_policy
		reverse_proxy raneto:3000
	}

	@neko host neko.example.com
	handle @neko {
		authorize with users_policy
		reverse_proxy neko:8080 {
			header_up Host {host}
			header_up X-Real-IP {remote_host}
			header_up X-Forwarded-For {remote_host}
		}
	}

	@tools host 5etools.example.com
	handle @tools {
		authorize with users_policy
		reverse_proxy 5etools:80
	}

	@files host files.example.com
	handle @files {
		authorize with users_policy
		reverse_proxy filebrowser:80
	}

	# Fallback for otherwise unhandled domains
	handle {
		abort
	}
}
@rubydotexe rubydotexe added need triage question Further information is requested labels Apr 20, 2022
@greenpau
Copy link
Owner

I think it has to do with my Caddy configuration? As you can see, I basically pulled things out of my bum and have no idea what I'm doing. I tried looking up examples for CORS in Caddy and I don't think I'm doing it right. If I'm doing something wrong (and I most certainly am) can someone provide an example of what it should look like?

@rubydotexe , thank you for the question! 👍

Here are some snippets relevant to your CORS configuration:

auth.example.com {
	import tls_config
>	import options
	authenticate with myportal
	root * /usr/share/caddy
	file_server
}

(options) {
	header Access-Control-Allow-Methods "POST, GET, OPTIONS"
	@options {
		method OPTIONS
	}
	respond @options 204
>	import cors https://example.com
	import cors https://www.example.com
	import cors https://auth.example.com
	import cors https://dnd.example.com
	import cors https://files.example.com
	import cors https://neko.example.com
	import cors https://5etools.example.com
	import cors https://wiki.example.com
	import cors https://orcpub.example.com
}

(cors) {
	@origin{args.0} header Origin {args.0}
	header @origin{args.0} Access-Control-Allow-Origin "{args.0}"
	header @origin{args.0} Vary Origin
}

Please provide output of the following command. Let's see what headers it returns.

curl -v http://auth.example.com

It would be very interesting how the options and cors imports unwrap.

@rubydotexe
Copy link
Author

Hello Mr. Greenburg! I appreciate your time very much.

This is my Caddyfile now:

{
	order authenticate before respond
	order authorize before basicauth
	#acme_ca https://acme-staging-v02.api.letsencrypt.org/directory


	security {
		oauth identity provider github {env.GITHUB_CLIENT_ID} {env.GITHUB_CLIENT_SECRET}

		authentication portal myportal {
			enable identity provider github

			ui {
				links {
					"My Website" https://example.com icon "las la-star"
					"My Identity" "/whoami" icon "las la-user"
				}
				password_recovery_enabled no
			}
			transform user {
				match origin local
				action add role authp/user
				ui link "Portal Settings" /settings icon "las la-cog"
			}
			transform user {
				match realm github
				match sub github.com/rubydotexe
				action add role authp/user
			}
		}
		authorization policy users_policy {
			set auth url https://auth.example.com:443/
			allow roles authp/admin authp/user
			acl rule {
				comment allow users
				match role authp/user
				allow stop log info
			}
			acl rule {
				comment default deny
				match any
				deny log warn
			}
		}

		authorization policy admins_policy {
			set auth url https://auth.example.com:443/
			allow roles authp/admin authp/user
			acl rule {
				comment allow users
				match role authp/user
				allow stop log info
			}
			acl rule {
				comment default deny
				match any
				deny log warn
			}
		}
	}
}

(tls_config) {
	tls {
		dns googleclouddns {
			gcp_project {env.GCP_PROJECT}
			gcp_application_default {env.GCP_APPLICATION_DEFAULT}
		}
	}
}

(options) {
	header Access-Control-Allow-Methods "POST, GET, OPTIONS"
	@options {
		method OPTIONS
	}
	respond @options 204
	import cors https://example.com
	import cors https://www.example.com
	import cors https://auth.example.com
	import cors https://dnd.example.com
	import cors https://files.example.com
	import cors https://neko.example.com
	import cors https://5etools.example.com
	import cors https://wiki.example.com
	import cors https://orcpub.example.com
	import cors https://cron.example.com
}

(cors) {
	@origin{args.0} header Origin {args.0}
	header @origin{args.0} Access-Control-Allow-Origin "{args.0}"
	header @origin{args.0} Vary Origin
}

auth.example.com {
	import tls_config
	import options
	authenticate with myportal
	root * /usr/share/caddy
	file_server
}

example.com {
	import tls_config
	import options
	authorize with users_policy
	reverse_proxy flame:5005
}

*.example.com {
	import tls_config
	import options

	@dnd host dnd.example.com
	handle @dnd {
		authorize with users_policy
		reverse_proxy foundry:30000
		encode zstd gzip
	}

	@pub host orcpub.example.com
	handle @dnd {
		#authorize with users_policy
		reverse_proxy orcpub:8890
		route /homebrew.orcbrew {
			root /homebrew.orcbrew /srv/orcpub
			file_server
		}
	}

	@cron host cron.example.com
	handle @cron {
		authorize with users_policy
		reverse_proxy crontab-ui:8000
		encode zstd gzip
	}

	@wiki host wiki.example.com
	handle @wiki {
		authorize with users_policy
		reverse_proxy raneto:3000
	}

	@neko host neko.example.com
	handle @neko {
		authorize with users_policy
		reverse_proxy neko:8080 {
			header_up Host {host}
			header_up X-Real-IP {remote_host}
			header_up X-Forwarded-For {remote_host}
		}
	}

	@tools host 5etools.example.com
	handle @tools {
		authorize with users_policy
		reverse_proxy 5etools:80
	}

	@files host files.example.com
	handle @files {
		authorize with users_policy
		reverse_proxy filebrowser:80
	}

	# Fallback for otherwise unhandled domains
	handle {
		abort
	}
}

And this is what I got in response to curl -v http://auth.example.com:


ruby@HOST:~$ curl -v http://auth.example.com
*   Trying [omitted]:80...
* TCP_NODELAY set
* Connected to auth.example.com ([omitted]) port 80 (#0)
> GET / HTTP/1.1
> Host: auth.example.com
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 308 Permanent Redirect
< Connection: close
< Location: https://auth.example.com/
< Server: Caddy
< Date: Thu, 21 Apr 2022 13:03:00 GMT
< Content-Length: 0
<
* Closing connection 0

And just out of curiosity I curled HTTPS as well:


ruby@CYBORG:~$ curl -v https://auth.example.com
*   Trying [omitted]:443...
* TCP_NODELAY set
* Connected to auth.example.com ([omitted]) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=auth.example.com
*  start date: Apr  3 15:20:16 2022 GMT
*  expire date: Jul  2 15:20:15 2022 GMT
*  subjectAltName: host "auth.example.com" matched cert's "auth.example.com"
*  issuer: C=US; O=Let's Encrypt; CN=R3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x55726970b880)
> GET / HTTP/2
> Host: auth.example.com
> user-agent: curl/7.68.0
> accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 302
< access-control-allow-methods: POST, GET, OPTIONS
< cache-control: no-store
< location: https://auth.example.com/login
< pragma: no-cache
< server: Caddy
< set-cookie: AUTHP_SESSION_ID=[omited]; Domain=example.com; Path=/; Secure; HttpOnly;
< content-length: 0
< date: Thu, 21 Apr 2022 13:03:09 GMT
<
* Connection #0 to host auth.example.com left intact

@greenpau
Copy link
Owner

@rubydotexe , for testing, please do the following:

auth.example.com {
	import tls_config
        # import options
        header Access-Control-Allow-Origin "*"
        header Access-Control-Allow-Methods "*"
	authenticate with myportal
	root * /usr/share/caddy
	file_server
}

Then curl the following way:

curl -v -L https://auth.example.com

@rubydotexe
Copy link
Author

rubydotexe commented Apr 22, 2022

@greenpau Here you go:

*   Trying [omited]:443...
* TCP_NODELAY set
* Connected to auth.example.com ([omited]) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=auth.example.com
*  start date: Apr  3 15:20:16 2022 GMT
*  expire date: Jul  2 15:20:15 2022 GMT
*  subjectAltName: host "auth.example.com" matched cert's "auth.example.com"
*  issuer: C=US; O=Let's Encrypt; CN=R3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0xaaaabda25730)
> GET / HTTP/2
> Host: auth.example.com
> user-agent: curl/7.68.0
> accept: */*
> 
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 302 
< access-control-allow-methods: *
< access-control-allow-origin: *
< cache-control: no-store
< location: https://auth.example.com/login
< pragma: no-cache
< server: Caddy
< set-cookie: AUTHP_SESSION_ID=[omitted]; Domain=example.com; Path=/; Secure; HttpOnly;
< content-length: 0
< date: Fri, 22 Apr 2022 13:34:13 GMT
< 
* Connection #0 to host auth.example.com left intact
* Issue another request to this URL: 'https://auth.example.com/login'
* Found bundle for host auth.example.com: 0xaaaabda19940 [can multiplex]
* Re-using existing connection! (#0) with host auth.example.com
* Connected to auth.example.com ([omited]) port 443 (#0)
* Using Stream ID: 3 (easy handle 0xaaaabda25730)
> GET /login HTTP/2
> Host: auth.example.com
> user-agent: curl/7.68.0
> accept: */*
> 
< HTTP/2 200 
< access-control-allow-methods: *
< access-control-allow-origin: *
< content-type: text/html
< server: Caddy
< set-cookie: AUTHP_SESSION_ID=[omitted]; Domain=example.com; Path=/; Secure; HttpOnly;
< content-length: 1856
< date: Fri, 22 Apr 2022 13:34:13 GMT
< 
<!doctype html>
<html lang="en">
  <head>
    <title>Sign In</title>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="Authentication Portal">
    <meta name="author" content="Paul Greenberg github.com/greenpau">
    <link rel="shortcut icon" href="/assets/images/favicon.png" type="image/png">
    <link rel="icon" href="/assets/images/favicon.png" type="image/png">

    <!-- Matrialize CSS -->
    <link rel="stylesheet" href="/assets/materialize-css/css/materialize.css" />
    <link rel="stylesheet" href="/assets/google-webfonts/roboto.css" />
    <link rel="stylesheet" href="/assets/line-awesome/line-awesome.css" />
    <link rel="stylesheet" href="/assets/css/styles.css" />
    
  </head>
  <body class="app-body">
    <div class="container">
      <div class="row">
        <div class="col s12 m8 offset-m2 l6 offset-l3 xl4 offset-xl4 app-card-container">
          <div class="row app-header center">
            
            <div class="col s4">
              <img class="d-block mx-auto mb-2" src="/assets/images/logo.svg" alt="Authentication Portal" width="72" height="72">
            </div>
            <div class="col s8">
              <h4>Sign In</h4>
            </div>
            
          </div>
          
          
          <div class="row">
            
            
            <a class="waves-effect waves-light grey darken-3 app-btn btn" href="oauth2/github">
              <i class="lab la-github app-btn-icon"></i><span class="app-btn-text">Github</span>
            </a>
            
          </div>
          
        </div>
      </div>
    </div>
    <!-- Optional JavaScript -->
    <script src="/assets/materialize-css/js/materialize.js"></script>
    
    
  </body>
* Connection #0 to host auth.example.com left intact

@greenpau
Copy link
Owner

Here you go:

@rubydotexe , do you still get Cross-Origin Request Blocked?

@rubydotexe
Copy link
Author

Here you go:

@rubydotexe , do you still get Cross-Origin Request Blocked?

The console errors are at least slightly different, but I'm still having to login at short intervals.

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://github.com/login/oauth/authorize?client_id=[redacted]&redirect_uri=https%3A%2F%2Fauth.example.com%2Foauth2%2Fgithub%2Fauthorization-code-callback&scope=read%3Auser&state=[redacted]. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 302.



Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://github.com/login/oauth/authorize?client_id=[redacted]&redirect_uri=https%3A%2F%2Fauth.example.com%2Foauth2%2Fgithub%2Fauthorization-code-callback&scope=read%3Auser&state=[redacted]. (Reason: CORS request did not succeed). Status code: (null).

@greenpau
Copy link
Owner

The console errors are at least slightly different, but I'm still having to login at short intervals.

@rubydotexe , please use Chrome and collect logs (HAR) from your session. Then, email them to me.

image

@LeonardMeyer
Copy link

LeonardMeyer commented May 16, 2022

@greenpau I have the exact same problem. I tried playing around with the CORS settings in my Caddyfile but to no avail. I have the HAR archive if you need it, what's your email ?

@greenpau
Copy link
Owner

@LeonardMeyer , greenpau|outlook.com

@LeonardMeyer
Copy link

Sent ! For context I have a Caddy container reverse proxying to subdomains pointing to several other containers .

@LeonardMeyer
Copy link

LeonardMeyer commented May 16, 2022

@greenpau Actually trying your last suggestion got me forward. I added the Access-Control headers one by one as I was told they were missing from the browser console. Then I ended up having some generic CORS error with a null message like above. MDN ended up being pretty useful , especially the "Preflight requests and credentials" part, because apparently I was missing the Access-Control-Allow-Credentials: true header. YMMV though because it seems very dependent on your browser, ad-blockers and your apps.

auth.example.com {
  route {
    header Access-Control-Allow-Origin "*"
    header Access-Control-Allow-Methods "*"
    header Access-Control-Allow-Headers "*"
    header Access-Control-Allow-Credentials "true"
    authenticate * with myportal
  }
}

I'm not fond of using the wildcard so I'll try with more specific headers (which seems not so straightforward), but it seems to work.

@greenpau
Copy link
Owner

I'm not fond of using the wildcard so I'll try with more specific headers (which seems not so straightforward), but it seems to work.

@LeonardMeyer , you don't have to use wilcards. They are for testing only. Now, you can set whatever domain you need it to be.

See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin

Access-Control-Allow-Origin: https://developer.mozilla.org

@greenpau greenpau added cors and removed need triage labels May 16, 2022
@LeonardMeyer
Copy link

LeonardMeyer commented May 17, 2022

@greenpau Nevermind, it looked better because I saw some preflight requests going through but it just took longer to fail. Browser console is saying things like:

Same origin policy disallows reading the remote resource https://example.com/?redirect_url=https%3A%2F%2Fsubdomain.example.com%2Fapi%2Fv1%2FindexerProxy. Reason: CORS request external redirect not allowed

So this was the first error and it matches that page. Origin of the preflight request was https://subdomain.example.com and not https://subdomain.example.com/api/v3/command, maybe that would explain that error ?

I tested in another app and what I see is this (I have the HAR if needed) :

  1. Subdomain makes a request
GET /api/v1/indexerProxy HTTP/2
Host: subdomain.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:100.0) Gecko/20100101 Firefox/100.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate, br
X-Api-Key: <redacted>
X-Requested-With: XMLHttpRequest
DNT: 1
Connection: keep-alive
Referer: https://subdomain.example.com/settings/indexers
Cookie: AUTHP_SESSION_ID=<redacted>; AUTHP_REDIRECT_URL=https://subdomain.example.com/index.js.map
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
TE: trailers
  1. Then for some reason server goes nope and respond with HTTP 302 to the auth url and CORSExternalRedirectNotAllowed is logged in console.
HTTP/2 302 Found
location: https://example.com?redirect_url=https%3A%2F%2Fsubdomain.example.com%2Fapi%2Fv1%2FindexerProxy
server: Caddy
content-type: text/plain; charset=utf-8
content-length: 5
date: Tue, 17 May 2022 12:38:02 GMT
X-Firefox-Spdy: h2
  1. A preflight request is then issued because CORS:
OPTIONS /?redirect_url=https%3A%2F%2Fsubdomain. HTTP/2
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:100.0) Gecko/20100101 Firefox/100.0
Accept: */*
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate, br
Access-Control-Request-Method: GET
Access-Control-Request-Headers: x-api-key,x-requested-with
Referer: https://subdomain.example.com/
Origin: https://subdomain.example.com
DNT: 1
Connection: keep-alive
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
  1. Preflight response is issued and CORSPreflightDidNotSucceed is logged in console.
HTTP/2 302 Found
access-control-allow-credentials: true
access-control-allow-headers: *
access-control-allow-methods: *
access-control-allow-origin: *
cache-control: no-store
location: https://example.com/login
pragma: no-cache
server: Caddy
set-cookie: AUTHP_SESSION_ID=<redacted>; Domain=example.stream; Path=/; Secure; HttpOnly;
content-length: 0
date: Tue, 17 May 2022 12:38:02 GMT
X-Firefox-Spdy: h2

@LeonardMeyer
Copy link

LeonardMeyer commented May 17, 2022

@greenpau After some tinkering I ended up with the same trick to fool preflight request as @rubydotexe, drawing inspiration from this, in order to avoid the preflight requests and redirect mess.

This is my Caddyfile:

{
  order authenticate before respond

  security {

    local identity store localdb {
      realm local
      path {$CADDY_AUTH_USERS_PATH}
    }

    authentication portal myportal {
      enable identity store localdb
      cookie domain {$DOMAIN}
      cookie lifetime 43200
      ui {
        links {
          "Sonarr"    https://subdomain1.{$DOMAIN} icon "las la-star"
          "Radarr"    https://subdomain2.{$DOMAIN} icon "las la-star"
        }
      }
    }

    authorization policy admin_policy {
        set auth url https://{$DOMAIN}
        allow roles authp/admin
        acl rule {
          comment allow admins
          match role authp/admin
          allow stop log info
        }
        acl rule {
          comment default deny
          match any
          deny log warn
        }
    }
  }
}

(protected_route) {
  {args.0}.{$DOMAIN} {
    route {
      authorize with admin_policy
      reverse_proxy {args.1}
    }
  }
}

{$DOMAIN} {
  route {
    header Access-Control-Allow-Origin "*"
    header Access-Control-Allow-Methods "*"
    header Access-Control-Allow-Headers "*"
    header Access-Control-Allow-Credentials "true"
    header Access-Control-Max-Age 86400

    @options method OPTIONS
    handle @options {
      header Content-Type "text/plain charset=UTF-8"
      header Content-Length 0
      respond 204
    }
    handle {
      authenticate * with myportal
    }
  }
}
import protected_route subdomain1 subdomain1:8989
import protected_route subdomain2 subdomain2:7878

It might be bad, I'm pretty new at Caddy. With this I don't have any CORS errors anymore, but at some point it will trigger some random JS errors in the console specific to the app which failed. I can't make sense of the requests I'm seeing when one error occurs. See an example:

First request
GET https://subdomain1.example.com/api/v1/notification
Response

HTTP/2 302 Found
location: https://example.com/login?login_hint=webadmin%40localdomain.local&redirect_url=https%3A%2F%2Fsubdomain1.example.com%2Fapi%2Fv1%2Fnotification
server: Caddy
set-cookie: access_token=delete; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT
content-type: text/plain; charset=utf-8
content-length: 5
date: Tue, 17 May 2022 22:19:31 GMT
X-Firefox-Spdy: h2

Second request
OPTIONS https://example.com/login?login_hint=webadmin@localdomain.local&redirect_url=https://subdomain1.example.com/api/v1/notification
Response

HTTP/2 204 No Content
access-control-allow-credentials: true
access-control-allow-headers: *
access-control-allow-methods: *
access-control-allow-origin: *
access-control-max-age: 86400
content-type: text/plain charset=UTF-8
server: Caddy
content-length: 0
date: Tue, 17 May 2022 22:19:31 GMT
X-Firefox-Spdy: h2

Third request
GET https://example.com/login?login_hint=webadmin@localdomain.local&redirect_url=https://subdomain1.example.com/api/v1/notification
Response

HTTP/2 200 OK
access-control-allow-credentials: true
access-control-allow-headers: *
access-control-allow-methods: *
access-control-allow-origin: *
access-control-max-age: 86400
content-type: text/html
server: Caddy
set-cookie: AUTHP_SESSION_ID=<redacted>; Domain=example.com; Path=/; Secure; HttpOnly;
set-cookie: AUTHP_REDIRECT_URL=https://subdomain1.example.com/api/v1/notification; Domain=example.com; Path=/; Max-Age=43200; Secure; HttpOnly;
content-length: 2788
date: Tue, 17 May 2022 22:19:31 GMT
X-Firefox-Spdy: h2

I don't understand why the first request is answered 302 ? Is this a caddy-security thing ? It looks like something expired and I have to authenticate again ? Last response body is the login page HTML by the way. Following that some JS is crashing in the console and the page just fails loading the view.

@greenpau
Copy link
Owner

I don't understand why the first request is answered 302 ?

I recommend creating two different routes. One for API endpoints and another one for non-API endpoint.
In non-API (app) polict you would set the redirect to work.
In API policy, you would return 403 only, i.e. no redirect.
Here is an example.

                authorization policy appPolicy {
                        set auth url /auth
                        crypto key verify CHANGE_ME
                        acl rule {
                                comment allow admins and users
                                match role authp/admin authp/user
                                allow stop log info
                        }
                        acl rule {
                                comment default deny
                                match any
                                deny log warn
                        }
                }
                authorization policy apiPolicy {
                        # do not redirect to /auth, return 403.
                        disable auth redirect
                        crypto key verify CHANGE_ME
                        acl rule {
                                comment allow admins and users
                                match role authp/admin authp/user
                                allow stop log info
                        }
                        acl rule {
                                comment default deny
                                match any
                                deny log warn
                        }
                }
(protected_route) {
  {args.0}.{$DOMAIN} {
    route /api* {
      authorize with apiPolicy
      reverse_proxy {args.1}
    }
    route {
      authorize with appPolicy
      reverse_proxy {args.1}
    }
  }
}

@LeonardMeyer
Copy link

LeonardMeyer commented May 17, 2022

Thanks for your answer @greenpau

  1. The thing is I can't say /api* because I have several subdomains and thus containers reverse proxied, and they could have any path. I could try /* I suppose but getting a 401/403 isn't the goal here anyway. Can't I just check the authorization and let the request through ?
  2. Why is it detecting an unauthenticated user ? My token has a pretty long lifetime

@greenpau
Copy link
Owner

@LeonardMeyer , i totally misunderstood the above. You get 302 because authorizer did not find token, or it is expired.

@LeonardMeyer
Copy link

LeonardMeyer commented May 18, 2022

@greenpau Ok actually I just saw #24 and that was why it was expiring so fast. CORS issue are also solved (as in no console errors). There's just one minor thing that is bothering me... With my Caddyfile and what happens with the 3 request/response above when token expires, the redirection to login happens only if I refresh the page. Otherwise the current page just breaks but stays on.

@greenpau
Copy link
Owner

There's just one minor thing that is bothering me... With my Caddyfile and what happens with the #90 (comment) above when token expires, the redirection to login happens only if I refresh the page. Otherwise the current page just breaks but stays on.

@LeonardMeyer , please elaborate. What is the desired behavior.

P.S. you totally hijacked the issue 😄 next time, open a new one and reference the issue that is similar to yours. Just a suggestion.

@LeonardMeyer
Copy link

@greenpau My bad, I did have a CORS issue initially 😅

The desired behavior should be that the redirect actually work as soon as the token is expired. But maybe CORS and preflight request is messing with said redirect.

@greenpau
Copy link
Owner

@LeonardMeyer , did you have a chance to review this #24 (comment)? i.e. there is a difference between:

  crypto default token lifetime ...
  cookie lifetime ...

@LeonardMeyer
Copy link

@greenpau Yes, as I said in my previous comment. Session lifespan is fine now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cors question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants