Hardening for production
Externalised issuer and CORS, health probes, and a persistent security audit trail.
You will build
configurable issuer and CORS, liveness and readiness probes, and an audited, ADMIN-readable event log
You will understand
why the issuer is part of your security identity, the difference between liveness and readiness, and what must never be written to an audit log
- one migration, one audit service
Protocol correctness is not enough. A deployable authorization server also needs predictable runtime configuration, narrow cross-origin behavior, operational probes, and an audit trail.
Externalize the issuer
Instead of hardcoding:
http://localhost:9000
use:
authorization-server:
issuer: ${AUTHORIZATION_SERVER_ISSUER:http://localhost:9000}
and inject it into AuthorizationServerSettings.
A production issuer should be the stable external HTTPS URL clients see, for example:
https://auth.example.com
The issuer participates in discovery and token validation. It is part of your security identity, not a cosmetic base URL.
Forwarded headers
For reverse-proxy deployments:
server:
forward-headers-strategy: framework
The proxy itself must also be configured correctly and trusted appropriately. Do not treat arbitrary client-supplied forwarded headers as authoritative at the network boundary.
Explicit CORS
Configuration:
authorization-server:
cors:
allowed-origins: ${AUTHORIZATION_SERVER_CORS_ALLOWED_ORIGINS:http://localhost:3000,http://localhost:5173}
Build a CorsConfigurationSource using exact configured origins.
Allowed methods might include:
GET POST PUT DELETE OPTIONS
Allowed headers:
Authorization Content-Type Accept
Do not use * casually on an authorization server.
For the stateless bearer-token APIs in this template:
configuration.setAllowCredentials(false);
Then enable CORS in each relevant security filter chain.
Test preflight requests
Allowed origin:
curl -i \
-X OPTIONS \
http://localhost:9000/api/v1/clients \
-H "Origin: http://localhost:5173" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: Authorization"
Verify:
Access-Control-Allow-Origin: http://localhost:5173
Unknown origin should not receive a matching allow-origin response.
Add Actuator with minimal exposure
Add the Actuator starter, then:
management:
endpoints:
web:
exposure:
include: health
endpoint:
health:
probes:
enabled: true
show-details: never
Expose publicly only:
/actuator/health
/actuator/health/liveness
/actuator/health/readiness
Do not automatically expose sensitive diagnostic endpoints such as /env or /configprops to the public internet.
Health semantics
flowchart LR
LB[Load balancer / orchestrator]
L["/liveness"]
R["/readiness"]
App[Authorization Server]
LB --> L --> App
LB --> R --> App
Liveness answers roughly: should the process be restarted?
Readiness answers: should traffic currently be sent to this instance?
Those questions are operationally different.
Persistent security audit events
Create the table in V12__create_security_audit_events.sql:
CREATE TABLE security_audit_events (
id UUID NOT NULL PRIMARY KEY,
event_type VARCHAR(100) NOT NULL,
actor VARCHAR(200),
target_type VARCHAR(100),
target_id VARCHAR(200),
outcome VARCHAR(20) NOT NULL,
ip_address VARCHAR(64),
user_agent VARCHAR(500),
details TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Indexes on created_at, actor, and event_type make common operational queries cheaper.
What should be audited?
Examples:
LOGIN_SUCCESS
LOGIN_FAILURE
PASSWORD_CHANGED
PASSWORD_RESET_BY_ADMIN
OAUTH_CLIENT_CREATED
OAUTH_CLIENT_UPDATED
OAUTH_CLIENT_SECRET_ROTATED
OAUTH_CLIENT_DELETED
What must not be audited?
Never place these in audit details:
raw password
password hash
access token
refresh token
authorization code
raw client secret
private signing key
An audit table often has a long retention period and broad operational visibility. Treat it as sensitive security data, not a dumping ground for request objects.
Capture actor and request context
A small audit service can obtain the authenticated actor from SecurityContextHolder and, when an HTTP request is available, record:
remote IP
user-agent
Be careful interpreting remoteAddr behind proxies; production IP attribution depends on a correctly configured trusted proxy chain.
ADMIN audit API
Add:
GET /api/v1/audit-events
under the existing ADMIN bearer security chain.
Return a pageable safe response that excludes fields you do not need to expose through the API.
Audit login events
Our existing authentication event listener is a natural place to record:
LOGIN_FAILURE
LOGIN_SUCCESS
Again, record the login name and outcome—not the password.
Audit management operations
After successful client mutation:
OAUTH_CLIENT_CREATED
OAUTH_CLIENT_UPDATED
OAUTH_CLIENT_SECRET_ROTATED
OAUTH_CLIENT_DELETED
Notice the word after. An audit event labelled SUCCESS should represent a mutation that actually completed.
Operational tests
Automate:
health endpoint is public → 200
unauthenticated audit endpoint → 401
ROLE_USER audit endpoint → 403
ROLE_ADMIN audit endpoint → 200
configured CORS origin → allowed
unknown CORS origin → not allowed
Figure: production-facing surfaces
flowchart TB
Internet[Internet]
Proxy[HTTPS Reverse Proxy]
AS[Authorization Server]
DB[(PostgreSQL)]
Audit[(Audit Events)]
Keys[Signing Keys]
Ops[Health Probes]
Internet --> Proxy --> AS
AS --> DB
AS --> Audit
AS --> Keys
Ops --> AS
The code
This part corresponds to these commits in the repository:
a977668feat: harden runtime and CORS configurationd2781fffeat: add health monitoring and security audit logs
Check out the last one to see the project exactly as this part leaves it:
git checkout d2781ff
References
- OAuth Authorization Server Metadata — RFC 8414: https://www.rfc-editor.org/rfc/rfc8414
- Spring Authorization Server settings: https://docs.spring.io/spring-authorization-server/reference/configuration-model.html
- Spring Boot Actuator: https://docs.spring.io/spring-boot/reference/actuator/
- Spring Security CORS: https://docs.spring.io/spring-security/reference/servlet/integrations/cors.html
Next: package the server for Docker, run the real integration suite in CI, and cut the v1.0.0 release.