What

  • CORS blocks non-whitelisted origins in AppSecurityConfig.
  • JWT is validated for protected endpoints in JwtAuthenticationFilter using AuthenticationManagerJWT.
  • API tiers restrict access even with a valid token; higher tiers unlock more endpoints.

Flow (request lifecycle):

  1. Incoming request → CORS applied → non-whitelisted origins blocked.
  2. Public/No-Auth endpoints bypass JWT checks.
  3. Protected endpoints → JWT extracted and validated.
  4. Tier authorization matched against requested API tier.
  5. 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:

  • allowedOrigins strictly whitelists domains that can call the APIs; everything else is blocked at the edge.

  • allowedHeaders/exposedHeaders explicitly permit and reveal only what clients need (tracking ID, API tier), reducing leakage.

  • allowCredentials = true enables 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: SecurityContextHolder gets 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 an ApiTier using constants (APIS_TIER_*, NO_AUTH_APIS).

  • The token’s sub (subject) determines its tier (TOKEN_TIRE_ONE is 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_APIS via isNoAuthAPI().
    • Everything else requires a valid token and passes tier checks.
  • 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.