Spring Boot / Intermediate: data, security, and framework evidence / Lesson 9
Authentication, request rules, and object access
Build a layered security contract that separates identity, endpoint permission, object ownership, CSRF, and safe failure handling.
On this page
Lesson goals
- Distinguish authentication from authorization.
- Declare request and method rules explicitly.
- Test cross-tenant denial independently of valid input.
Identity is the beginning of an access decision
Authentication establishes a trusted identity under the selected credential mechanism; authorization decides what that identity may do. A logged-in user is not automatically permitted to read every product or mutate another tenant's order. Validate token/session credentials through a reviewed provider, then derive scope from trusted identity rather than a caller-selected tenant header or JSON field.
Spring Security's servlet filter chain can reject a request before MVC mapping or validation. That is why a controller advice is not a universal handler for401 and403 outcomes. Define authentication entry and access-denial behavior for the API and protect diagnostic causes separately. Do not log raw passwords, bearer tokens, or entire security contexts when investigating a rejected request.
Make allowed paths and fallthrough visible
The illustrative chain permits basic health access, requires catalog:read for product GETs, and denies everything else. The selected authority is exact; hasRole has a role-prefix convention and should not be casually substituted for hasAuthority. The fragment requires an authentication provider and intentionally does not supply real credentials. It remains non-runnable and exposes no listener.
Keeping a public health status does not authorize exposing detailed environment or component data; Actuator policy is developed later. HTTP Basic, if selected in a real deployment, needs appropriate transport security and credential lifecycle. Adding a custom chain changes auto-configuration expectations, so verify the complete intended policy rather than assuming Boot's earlier default still handles every unmatched route.
package training.catalog;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration(proxyBeanMethods = false)
class ApiSecurity {
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(rules -> rules
.requestMatchers("/actuator/health").permitAll()
.requestMatchers(HttpMethod.GET, "/products/**").hasAuthority("catalog:read")
.anyRequest().denyAll())
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
The identity provider is contextual and CSRF is not globally disabled. Actual filter behavior and authentication are unexecuted.
Enforce ownership beyond route access
An endpoint rule answers whether a caller can enter a route category; it may not answer whether product42 belongs to the caller's permitted tenant. Method security can delegate to a domain access capability when enabled, but the underlying query/mutation still needs consistent trusted scoping. Boot's security starter does not enable method authorization by itself. Proxy-boundary reasoning applies here too.
The complete dependency-free model checks authority plus a trusted tenant/resource comparison. It prints true for a matching tenant, false for another tenant, and false without authority. This proves only the selected Boolean policy for synthetic inputs, not Spring's filter, method interception, credential verification, or resistance to all application threats. Each requires its own evidence.
public class Main {
static boolean canRead(String trustedTenant,java.util.Set<String> authorities,String resourceTenant) {
return trustedTenant!=null && trustedTenant.equals(resourceTenant) && authorities.contains("catalog:read");
}
public static void main(String[] args) throws Exception {
var read=java.util.Set.of("catalog:read");
System.out.println(canRead("north",read,"north"));
System.out.println(canRead("north",read,"south"));
System.out.println(canRead("north",java.util.Set.of(),"north"));
}
}
true
false
false
Trusted north access succeeds only for the north resource and with the required authority. No Spring Security integration is exercised.
Do not collapse browser controls into authentication
CSRF protects relevant browser credential flows against unintended cross-site requests; CORS controls browser cross-origin access policy. Neither proves object ownership. A403 on a state-changing request can be CSRF rejection rather than an incorrect password. Do not disable CSRF globally just to make a test pass without analyzing how credentials are sent and which clients are supported.
A stateless token API may select a different reviewed CSRF policy from a session/browser form application, but βRESTβ in a class name is not sufficient justification. Test preflight handling, allowed origins, credential behavior, and real authentication/authorization boundaries separately. Avoid wildcard permission combined with an unexplained credential model. Keep selected policy explicit in configuration and tests.
Verify denials as deliberately as successes
Test anonymous, authenticated-without-authority, authorized, cross-tenant, malformed, and CSRF-relevant cases. Confirm rejection occurs before forbidden state change and that public failures disclose only the approved information. A mock principal in a test is useful policy evidence but does not prove production token validation, issuer/audience checks, password handling, or transport configuration.
Use a focused security-aware MVC fixture for request policy and direct domain tests for object rules, then authorized isolated integration tests for the full identity provider where appropriate. No provider or listener is started here. The result is a layered contract with known evidence, not the claim that one annotation or a passing login form makes every application data path secure.
Common errors and fixes
A valid identifier exposed another tenant's product
- Why it happens
- Route access was mistaken for object-level permission.
- How to fix it
- Enforce trusted scope in reads and writes.
- Prevent it next time
- Test well-formed cross-tenant requests.
PreAuthorize did not run
- Why it happens
- Method authorization was not enabled or the call bypassed the managed boundary.
- How to fix it
- Verify enablement and actual invocation.
- Prevent it next time
- Exercise the proxied method policy.
A403 was fixed by disabling CSRF everywhere
- Why it happens
- The rejection was suppressed without a credential-flow analysis.
- How to fix it
- Classify CSRF versus authority and select a reviewed policy.
- Prevent it next time
- Keep security tests faithful to supported clients.
Check your understanding
1. Does a successful mock-user GET test prove production token verification?
Show worked solution
No. It can verify request authorization with that synthetic identity. Signature, issuer, audience, expiry, credential transport, and identity-provider integration need separate controlled evidence. Keep the model and actual provider boundary distinct.
2. A caller has catalog:read but the product belongs to south while trusted identity is north. What should the model do?
Show worked solution
Deny. The authority is necessary but not sufficient for that resource. Enforce the same ownership rule in the actual repository/application path and verify no unauthorized representation or state change occurs.
Key takeaways
- Authentication, route permission, and ownership are different decisions.
- Method security needs enablement and real interception.
- Browser controls and synthetic identities have precise evidence limits.