CORS and preflight¶
Cross-origin browser calls trigger a preflight OPTIONS request before the real request.
That preflight carries no credentials, so a deny-by-default rule table would block it and break
CORS. javalin-security has a narrow bypass for exactly this case, but it does not add CORS
response headers — that is still Javalin's CORS plugin's job.
Two independent concerns¶
| Concern | Handled by |
|---|---|
Sending Access-Control-Allow-* response headers. |
Javalin's CORS plugin (config.bundledPlugins.enableCors). |
Not blocking the preflight OPTIONS in the rule table. |
http.allowCorsPreflight = true. |
You typically need both.
Setup¶
import io.github.mzlnk.javalin.security.authorization.Rules
import io.github.mzlnk.javalin.security.security
import io.javalin.Javalin
Javalin.create { config ->
// 1. CORS response headers
config.bundledPlugins.enableCors { cors ->
cors.addRule { it.allowHost("https://app.example.com") }
}
// 2. Let the preflight through, protect the rest
config.security { security ->
security.rules.any("/api/*", Rules.authenticated())
security.http.authentication = myStrategy
security.http.allowCorsPreflight = true // don't block CORS preflight OPTIONS
security.http.fallback = Rules.deny()
}
config.routes.get("/api/data") { it.result("data") }
}
import io.github.mzlnk.javalin.security.JavalinSecurityPlugin;
import io.github.mzlnk.javalin.security.authorization.Rules;
import io.javalin.Javalin;
Javalin.create(config -> {
// 1. CORS response headers
config.bundledPlugins.enableCors(cors ->
cors.addRule(it -> it.allowHost("https://app.example.com")));
// 2. Let the preflight through, protect the rest
config.registerPlugin(new JavalinSecurityPlugin(security -> {
security.rules.any("/api/*", Rules.authenticated());
security.http.authentication = myStrategy;
security.http.allowCorsPreflight = true; // don't block CORS preflight OPTIONS
security.http.fallback = Rules.deny();
}));
config.routes.get("/api/data", ctx -> ctx.result("data"));
});
What the bypass exempts¶
allowCorsPreflight = true permits only requests that are:
- HTTP method
OPTIONS, and - carrying an
Access-Control-Request-Methodheader (the preflight marker).
The check runs before the rule table and fallback, so it works even with deny-by-default.
It is not a general OPTIONS allow
Regular OPTIONS traffic (without the preflight header) is still governed by your rules.
The bypass never grants access to the actual cross-origin request that follows — that
request carries credentials and is authorized normally.
Common pitfalls¶
- CORS "works" locally but fails in the browser. You enabled
allowCorsPreflightbut forgot the CORS plugin, so the preflight passes the guard but returns noAccess-Control-Allow-Originheader. Addconfig.bundledPlugins.enableCors { … }. - Preflight returns 401. You have deny-by-default but did not set
allowCorsPreflight = true. - Credentialed CORS. If the browser sends cookies or credentials, configure the CORS plugin
to allow credentials and a specific origin (not
*). Remember: the WebSocket Origin allow-list is a separate mechanism.
Next steps¶
- Authorization — how the rule table decides access.
- WebSocket security — Origin allow-listing for upgrades.