What
- CORS blocks non-whitelisted origins in
AppSecurityConfig. - JWT is validated for protected endpoints in
JwtAuthenticationFilterusingAuthenticationManagerJWT. - API tiers restrict access even with a valid token; higher tiers unlock more endpoints.
Flow (request lifecycle):
- Incoming request → CORS applied → non-whitelisted origins blocked.
- Public/No-Auth endpoints bypass JWT checks.
- Protected endpoints → JWT extracted and validated.
- Tier authorization matched against requested API tier.
- On success, controller executes; on failure, error is routed to global handler.
Why
- Prevent misuse from unwanted domains via strict CORS.
- Reject bad tokens: expired, blocked, reused/refresh-misuse.
- Enforce progression: open → tier 4 → tier 3 → tier 2 → tier 1 as business rules demand.
How
- CORS configuration (
AppSecurityConfig.corsConfigurationSource()):
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val configuration = CorsConfiguration()
configuration.allowedOrigins = listOf(
"http://localhost:3000",
"https://yourdomain.com"
)
configuration.allowedMethods = listOf("GET","POST","PUT","DELETE","OPTIONS","PATCH")
configuration.allowedHeaders = listOf(
"Authorization","Content-Type","X-Requested-With","Accept","Origin",
KeywordsAndConstants.HEADER_TRACKING_ID,
KeywordsAndConstants.HEADER_API_KEY,
KeywordsAndConstants.HEADER_OTP,
KeywordsAndConstants.HEADER_AUTH_TOKEN
)
configuration.allowCredentials = true
configuration.exposedHeaders = listOf(
KeywordsAndConstants.HEADER_TRACKING_ID,
KeywordsAndConstants.HEADER_API_TIER
)
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", configuration)
return source
}
Explanation:
allowedOriginsstrictly whitelists domains that can call the APIs; everything else is blocked at the edge.allowedHeaders/exposedHeadersexplicitly permit and reveal only what clients need (tracking ID, API tier), reducing leakage.allowCredentials = trueenables cookies/authorization headers only for whitelisted origins.JWT filter flow (
JwtAuthenticationFilter):
override fun doFilterInternal(req: HttpServletRequest, res: HttpServletResponse, chain: FilterChain) {
try {
// Allow public and no-auth endpoints
val path = req.requestURI
if (isPublicEndpoint(path) || isNoAuthAPI(path)) {
chain.doFilter(req, res); return
}
// Require token for protected endpoints
val token = jwtUtilMaster.getJwtAuth(req, mustReturnTokenOrElseCrash = true)
if (token != null) {
val auth = JwtAuthenticationToken(
KeywordsAndConstants.TOKEN_PREFIX + token,
req.method,
path,
req.getHeader(KeywordsAndConstants.HEADER_TRACKING_ID) ?: ""
)
val authentication = authenticationManager.authenticate(auth)
SecurityContextHolder.getContext().authentication = authentication
chain.doFilter(req, res)
} else {
handlerExceptionResolver.resolveException(req, res, null, Exception("JWT token is required for this endpoint"))
}
} catch (e: Exception) {
handlerExceptionResolver.resolveException(req, res, null, e)
}
}
Explanation:
Public endpoints: swagger/docs/favicon; No-Auth endpoints: business endpoints whitelisted via
NO_AUTH_APIS.Missing/invalid token: routed to global handler via
handlerExceptionResolver(no raw container error pages).Successful authentication:
SecurityContextHoldergets the authenticated token and the request proceeds to the controller.Tier enforcement (
AuthenticationManagerJWT):
private fun checkJwtAuthorizations(subject: String, token: JwtAuthenticationToken) {
val apiTier = determineApiTier(token.path)
when (subject) {
KeywordsAndConstants.TOKEN_TIRE_FOUR -> if (apiTier in listOf(ApiTier.TIRE_THREE, ApiTier.TIRE_TWO, ApiTier.TIRE_ONE))
throw ForbiddenExceptionThrowable(
errorMessage = KeywordsAndConstants.SOFT_TOKEN_TIRE_FOUR_ERROR_GENERIC,
code = KeywordsAndConstants.SOFT_TOKEN_TIRE_FOUR_ERROR_GENERIC_CODE
)
KeywordsAndConstants.TOKEN_TIRE_THREE -> if (apiTier in listOf(ApiTier.TIRE_TWO, ApiTier.TIRE_ONE))
throw ForbiddenExceptionThrowable(
errorMessage = KeywordsAndConstants.SOFT_TOKEN_TIRE_THREE_ERROR,
code = KeywordsAndConstants.SOFT_TOKEN_TIRE_THREE_ERROR_CODE
)
KeywordsAndConstants.TOKEN_TIRE_TWO -> if (apiTier == ApiTier.TIRE_ONE)
throw ForbiddenExceptionThrowable(
errorMessage = KeywordsAndConstants.SOFT_TOKEN_TIRE_TWO_ERROR,
code = KeywordsAndConstants.SOFT_TOKEN_TIRE_TWO_ERROR_CODE
)
KeywordsAndConstants.TOKEN_TIRE_ONE -> { /* full access */ }
}
}
Explanation:
determineApiTier(path)maps the requested URL to anApiTierusing constants (APIS_TIER_*,NO_AUTH_APIS).The token’s
sub(subject) determines its tier (TOKEN_TIRE_ONEis highest). Lower-tier tokens are blocked from higher-tier APIs with specific, consistent error codes.Open vs protected:
- Open: swagger, docs, favicon via
isPublicEndpoint(). - No-auth business APIs: from
KeywordsAndConstants.NO_AUTH_APISviaisNoAuthAPI(). - Everything else requires a valid token and passes tier checks.
- Open: swagger, docs, favicon via
Extra hardening:
- Generate and propagate a tracking ID when missing; expose it to clients for tracing.
- Add client IP and an internal access-log ID to the request (not exposed to clients) using
HeaderEnhancedHttpServletRequest. - Validate “refresh vs access” token usage; block reused/blocked/expired tokens; route all failures via the resolver to the global handler.