breakfix: CORS preflight did not succeed - no token found
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 , 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
@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.
@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 , i am referring to cors options.
I think you need to create Caddy “handler” to match options requests and respond accordingly.
@axi92 , the handler should be above “authorize” directive.
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.
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.
And if I remove the authorize with apipolicy on that api.domain.com cors works but not the auth ofc becaus its disabled
@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.