caddy-security icon indicating copy to clipboard operation
caddy-security copied to clipboard

breakfix: CORS preflight did not succeed - no token found

Open axi92 opened this issue 11 months ago • 9 comments

Describe the issue

I am using jwt tokens via query and header. Via query is working but when using header caddy prints no token found and in the browser I get the cors error preflight did not succeed.

Configuration

Paste full Caddyfile below:

{
	email [email protected]
	debug
	
	order authenticate before respond
	order authorize before basicauth

	security {
		oauth identity provider keycloak {
			driver generic
			realm keycloak
			client_id {env.KEYCLOAK_CLIENT_ID}
			client_secret {env.KEYCLOAK_CLIENT_SECRET}
			scopes openid email profile
			metadata_url https://keycloak.domain.com/realms/master/.well-known/openid-configuration
		}

		authentication portal myportal {
			crypto default token lifetime 3600
			crypto key sign-verify {env.JWT_SHARED_KEY}
			enable identity provider keycloak
			cookie domain domain.com
			ui {
				links {
					"My Identity" "/whoami" icon "las la-user"
				}
			}
			transform user {
				match origin keycloak
				action add role authp/user
			}
			transform user {
				match origin local
				action add role authp/user
				ui link "Portal Settings" /settings icon "las la-cog"
			}
		}

		authorization policy mypolicy {
			set auth url https://auth.domain.com/
			allow roles authp/admin authp/user
			crypto key verify {env.JWT_SHARED_KEY}
			bypass uri prefix /docs/
		}
		authorization policy apipolicy {
			#set token sources header query
			crypto key verify from directory /home/user/proxy/jwt-public-keys/api
			crypto key token name api_token
			allow roles default-roles-master consumer
			acl default deny
			validate path acl
			disable auth redirect
			inject headers with claims
		}
	}
}

(letls) {
	tls {
		issuer acme {
			disable_http_challenge
			disable_tlsalpn_challenge
			propagation_delay 30s
			resolvers ns1.digitalocean.com ns2.digitalocean.com ns3.digitalocean.com
			dns digitalocean ...
		}
	}
}

# Old config, not working
(cors) {
  header {
    Access-Control-Allow-Origin "{args.0}"
    Access-Control-Allow-Credentials true
    Access-Control-Allow-Methods *
    Access-Control-Allow-Headers *
    defer
  }
}

(cors2) {
	@origin header Origin {args.0}
		header @origin {
                Access-Control-Allow-Methods "GET, OPTIONS"
                Access-Control-Allow-Credentials true
                Access-Control-Allow-Origin {args.0}
                Access-Control-Allow-Headers "api_token"
                Vary: Origin
	}
}

auth.domain.com {
	authenticate with myportal
	import letls
}

api.domain.com {
	authorize with apipolicy
	import cors2 https://apidocs.domain.com
	import letls
	route /inventum/* {
		reverse_proxy http://10.64.192.146:8889
	}
}

apidocs.domain.com {
  import letls
  authorize with mypolicy
  file_server {
    root /opt/swagger
  }
}

Version Information

Provide output of caddy list-modules --versions | grep -E "(auth|security)" below:

http.authentication.hashes.bcrypt v2.9.1
http.authentication.providers.http_basic v2.9.1
http.handlers.authentication v2.9.1
tls.client_auth.verifier.leaf v2.9.1
http.authentication.providers.authorizer v1.1.29
http.handlers.authenticator v1.1.29
security v1.1.29

Expected behavior

It should find the token in the header as in the query param.

Additional context

Is there a way to see where caddy is searching for the token? Printing the context or something? I tried the isolated preflight request in curl too see if there is something that I can figure out:

$ curl 'https://api.domain.com/inventum/health' -X OPTIONS -H 'User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0' -H 'Accept: */*' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate, br, zstd' -H 'Access-Control-Request-Method: GET' -H 'Access-Control-Request-Headers: api_token' -H 'Referer: https://apidocs.domain.com/' -H 'Origin: https://apidocs.domain.com' -H 'DNT: 1' -H 'Sec-GPC: 1' -H 'Connection: keep-alive' -H 'Sec-Fetch-Dest: empty' -H 'Sec-Fetch-Mode: cors' -H 'Sec-Fetch-Site: same-site' -H 'Priority: u=4' -H 'Pragma: no-cache' -H 'Cache-Control: no-cache' -H 'TE: trailers' -vv

* Host api.domain.com:443 was resolved.
* Connected to api.domain.com (172.16.2.11) port 443
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://api.domain.com/inventum/health
* [HTTP/2] [1] [:method: OPTIONS]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: api.domain.com]
* [HTTP/2] [1] [:path: /inventum/health]
* [HTTP/2] [1] [user-agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0]
* [HTTP/2] [1] [accept: */*]
* [HTTP/2] [1] [accept-language: en-US,en;q=0.5]
* [HTTP/2] [1] [accept-encoding: gzip, deflate, br, zstd]
* [HTTP/2] [1] [access-control-request-method: GET]
* [HTTP/2] [1] [access-control-request-headers: api_token]
* [HTTP/2] [1] [referer: https://apidocs.domain.com/]
* [HTTP/2] [1] [origin: https://apidocs.domain.com]
* [HTTP/2] [1] [dnt: 1]
* [HTTP/2] [1] [sec-gpc: 1]
* [HTTP/2] [1] [sec-fetch-dest: empty]
* [HTTP/2] [1] [sec-fetch-mode: cors]
* [HTTP/2] [1] [sec-fetch-site: same-site]
* [HTTP/2] [1] [priority: u=4]
* [HTTP/2] [1] [pragma: no-cache]
* [HTTP/2] [1] [cache-control: no-cache]
* [HTTP/2] [1] [te: trailers]
> OPTIONS /inventum/health HTTP/2
> Host: api.domain.com
> User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0
> Accept: */*
> Accept-Language: en-US,en;q=0.5
> Accept-Encoding: gzip, deflate, br, zstd
> Access-Control-Request-Method: GET
> Access-Control-Request-Headers: api_token
> Referer: https://apidocs.domain.com/
> Origin: https://apidocs.domain.com
> DNT: 1
> Sec-GPC: 1
> Connection: keep-alive
> Sec-Fetch-Dest: empty
> Sec-Fetch-Mode: cors
> Sec-Fetch-Site: same-site
> Priority: u=4
> Pragma: no-cache
> Cache-Control: no-cache
> TE: trailers
> 
< HTTP/2 401 
< access-control-allow-credentials: true
< access-control-allow-headers: api_token
< access-control-allow-origin: https://apidocs.domain.com
< alt-svc: h3=":443"; ma=2592000
< server: Caddy
< vary: Origin
< content-length: 0
< date: Fri, 31 Jan 2025 11:31:39 GMT
< 
* Connection #0 to host api.domain.com left intact

axi92 avatar Jan 31 '25 12:01 axi92

@axi92 , using caddy trace is one thing to do.

How is your options config different from the config in https://github.com/greenpau/caddy-security/issues/353

greenpau avatar Jan 31 '25 19:01 greenpau

@axi92 , in caddy security debug output look for configuration. It tells you what it would do.

You can have your ow. Way to handle options. That’s not really a part of caddy security.

greenpau avatar Jan 31 '25 19:01 greenpau

@axi92 , using caddy trace is one thing to do.

How is your options config different from the config in #353

What options do you refer to?

I added the trace plugin. But I am having a hard time finding something in the logs.

I tried it like that:

api.domain.com {
	authorize with apipolicy
	import cors2 https://apidocs.domain.com
	import letls
	route /inventum/* {
		trace disabled=no tag="blub"
		reverse_proxy http://10.64.192.146:8889
	}
}

Could it be a caddy config issue so the security plugin won't get the token?

axi92 avatar Feb 03 '25 14:02 axi92

@axi92 , i am referring to cors options.

I think you need to create Caddy “handler” to match options requests and respond accordingly.

greenpau avatar Feb 03 '25 16:02 greenpau

@axi92 , the handler should be above “authorize” directive.

greenpau avatar Feb 03 '25 16:02 greenpau

Think through this … caddy security does not handle options method requests. In your config, the first thing that touches the request is authorize plugin. That is non starter. You need to respond to options request with the handler and not pass it to anything else.

greenpau avatar Feb 03 '25 16:02 greenpau

Wait so the order is important? If I authorize first and then import cors it wont work because the auth "stops" the flow? Or is it this order authenticate before respond?

I am confused on what front I need to debug. If I change too many things at once I might get new issues.

Edit: I saw the order trace before respond in your example I will try that.

axi92 avatar Feb 04 '25 12:02 axi92

And if I remove the authorize with apipolicy on that api.domain.com cors works but not the auth ofc becaus its disabled

axi92 avatar Feb 04 '25 14:02 axi92

@axi92 , I am not talking about the “order” directive.

I am referring to the statements inside “api.domain.com”. There are executed sequentially. Here, you have “authorize” directive which gets executed first. Then, you have “import cors2”. If you are unauthorize, your request never passes to it. Typically, credentials are not being passed together with options request. If the credentials do not pass, then the request cannot be authorized.

If you want, I can hop on a whatsup call to help you out. Please reach over linkedin.

greenpau avatar Feb 05 '25 04:02 greenpau