Skip to main content

Security Context and Security Response Headers

This chapter covers two related but independent capabilities: first, the security context bridging provided by the security feature module (coco-feature-security), which bridges principal information written by a trusted upstream into the current thread; and second, the security response headers and CORS cross-origin configuration provided by the Web module (coco-feature-web).

Security Context Bridging

Overview

CocoSecurityContext holds the security principal of the current caller (CocoSecurityPrincipal: principal identifier, display name, role set, permission set, additional attributes) and the authentication state, for authorization, audit, tenant, data permission, and other modules to read. It is stored in the ThreadLocal of CocoSecurityContextHolder; the entry adapter is responsible for setting it when a request enters and clearing it when the request ends, while business code only reads it.

import io.github.coco.feature.security.context.CocoSecurityContextHolder;
import io.github.coco.feature.security.context.CocoSecurityContext;

// Read the current context; returns Optional.empty() when absent
CocoSecurityContextHolder.current()
.map(CocoSecurityContext::principal)
.ifPresent(principal -> log(principal.id()));

// Require an existing context; throws an unauthenticated exception when absent
CocoSecurityContext context = CocoSecurityContextHolder.requireCurrent();

In the Web scenario, bridging is performed by CocoSecurityWebFilter, which calls CocoWebSecurityContextResolver to resolve the context from the request. The resolver registered by the framework by default is HeaderCocoWebSecurityContextResolver.

Building the Context from Trusted HTTP Headers

HeaderCocoWebSecurityContextResolver does not authenticate; it only consumes the request headers already written by a trusted upstream (gateway, authentication filter, business infrastructure) and assembles them into an authenticated CocoSecurityContext:

Request HeaderDefault NameMaps To
Principal identifierX-Coco-Principal-Idprincipal.id (the context is not built if absent)
Principal display nameX-Coco-Principal-Nameprincipal.name (falls back to the principal identifier when absent)
Role setX-Coco-Rolesprincipal.roles (split by delimiter)
Permission setX-Coco-Permissionsprincipal.permissions (split by delimiter)

Roles and permissions are separated by a comma , by default. This resolver is disabled by default (coco.security.web.header.enabled=false) to avoid directly trusting external client input.

Must Be Deployed Behind a Trusted Gateway

This is the most critical boundary of this capability: once trusted request-header resolution is enabled, any request that can reach the application directly can forge headers such as X-Coco-Principal-Id to impersonate any principal. Therefore this resolver must be deployed behind a trusted gateway, with the gateway responsible for authentication and for overwriting/stripping these headers; the application must never be exposed directly to the public internet.

To highlight this risk, when coco.security.web.header.enabled=true, CocoSecurityAutoConfiguration prints a warning log at startup, noting that this resolver trusts upstream request headers and must be placed behind a trusted gateway, and recommends providing a custom CocoWebSecurityContextResolver bean to eliminate the warning.

Replacing with a Custom Resolver

If the principal information comes from a JWT, session, or another authentication mechanism, simply provide a custom CocoWebSecurityContextResolver bean to replace the default implementation (the default resolver is annotated with @ConditionalOnMissingBean and also suppresses the startup warning above):

import io.github.coco.feature.security.context.CocoSecurityContext;
import io.github.coco.feature.security.context.CocoSecurityPrincipal;
import io.github.coco.feature.security.web.CocoWebSecurityContextResolver;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Optional;

@Configuration
public class MySecurityConfig {

@Bean
public CocoWebSecurityContextResolver jwtSecurityContextResolver() {
return request -> resolveFromJwt(request);
}

private Optional<CocoSecurityContext> resolveFromJwt(HttpServletRequest request) {
// Build the principal after validating the JWT
CocoSecurityPrincipal principal = /* ... */ null;
return Optional.ofNullable(principal).map(CocoSecurityContext::authenticated);
}
}

Key Configuration Items

Configuration ItemDefaultDescription
coco.security.web.enabledtrueWhether to register the Web security context bridging filter
coco.security.web.header.enabledfalseWhether to enable trusted request-header resolution (enabling it trusts upstream headers; must be placed behind a trusted gateway)
coco.security.web.header.principal-id-header-nameX-Coco-Principal-IdPrincipal identifier request header
coco.security.web.header.principal-name-header-nameX-Coco-Principal-NamePrincipal display name request header
coco.security.web.header.roles-header-nameX-Coco-RolesRole set request header
coco.security.web.header.permissions-header-nameX-Coco-PermissionsPermission set request header
coco.security.web.header.authority-delimiter,Delimiter for the roles and permissions request headers

Security Response Headers

Overview

CocoSecurityHeadersFilter writes a set of security response headers at the very front of the filter chain, so that subsequent filters, business code, and downstream-generated error responses (signature 401, rate-limiting 429, unhandled exception 500, etc.) all carry these headers. Writing them at the very front is deliberate: once a response is committed it is too late to write them, and subsequent code can still override the framework's defaults with setHeader. Any response header whose value is null or blank is not written.

They fall into two categories:

  • Enabled by default (providing secure defaults): X-Content-Type-Options, X-Frame-Options, Referrer-Policy.
  • Not written by default (requiring explicit application configuration): Content-Security-Policy, Permissions-Policy, Strict-Transport-Security. These three either have no universally safe default (a misconfiguration would directly break the application) or are strongly application-specific, so they must be written by the application itself.

How to Enable and Configure

Controlled by coco.web.security-headers, enabled by default.

coco:
web:
security-headers:
enabled: true
content-type-options: nosniff
frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
content-security-policy: "default-src 'self'"
permissions-policy: "geolocation=(), camera=()"
strict-transport-security: "max-age=31536000; includeSubDomains"

Key Configuration Items

Configuration ItemDefaultDescription
coco.web.security-headers.enabledtrueWhether to enable the security response headers filter
coco.web.security-headers.orderhighest priorityFilter execution order
coco.web.security-headers.content-type-optionsnosniffX-Content-Type-Options; reverts to default when blank
coco.web.security-headers.frame-optionsDENYX-Frame-Options; reverts to default when blank
coco.web.security-headers.referrer-policystrict-origin-when-cross-originReferrer-Policy; reverts to default when blank
coco.web.security-headers.content-security-policyempty (not written)Content-Security-Policy
coco.web.security-headers.permissions-policyempty (not written)Permissions-Policy
coco.web.security-headers.strict-transport-securityempty (not written)Strict-Transport-Security

HSTS Is Written Only over HTTPS

Strict-Transport-Security is written only when HttpServletRequest.isSecure() is true (a secure connection); over plaintext HTTP, browsers ignore it, and writing it would only mask deployment configuration issues.

Note in particular: in deployments where TLS is terminated by a front-end proxy, isSecure() reflects the client's original protocol only if the application has set server.forward-headers-strategy=framework (or native); otherwise HSTS is silently skipped.

CORS Cross-Origin

Overview

CocoCorsProperties configures the allowed origins, methods, request headers, exposed response headers, credentials, and preflight cache of the global CORS filter. The CORS filter is disabled by default and is only registered when coco.web.cors.enabled=true (the auto-configuration is annotated with @ConditionalOnProperty(name = "enabled", havingValue = "true")).

How to Enable and Configure

coco:
web:
cors:
enabled: true
allowed-origins: ["https://app.example.com"]
allowed-methods: [GET, POST, PUT, DELETE, OPTIONS]
allowed-headers: ["*"]
exposed-headers: [X-Trace-Id]
allow-credentials: false
max-age: 1800

Key Configuration Items

Configuration ItemDefaultDescription
coco.web.cors.enabledfalseWhether to enable the CORS cross-origin filter
coco.web.cors.allowed-origins["*"]Allowed cross-origin origins
coco.web.cors.allowed-methodsGET, POST, PUT, DELETE, OPTIONSAllowed HTTP methods
coco.web.cors.allowed-headers["*"]Allowed request headers
coco.web.cors.exposed-headersemptyResponse headers exposed to the client
coco.web.cors.allow-credentialsfalseWhether to allow sending credentials (cookies, etc.)
coco.web.cors.max-age1800Preflight request cache time (seconds)

Notes

Per the CORS specification, allow-credentials: true and allowed-origins: ["*"] cannot be used together. When enabling credentials, narrow allowed-origins down to an explicit list of origins.