{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreihfwbuucrvqthnmdyv3dk6wbzk2vy325lpguqfuszojfwvy3zfo3y",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3molyucbmawd2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreifpevwqgczrbtdtwiefrbctgnxref3u5wy2ubvirgghrddz554bhq"
    },
    "mimeType": "image/webp",
    "size": 444660
  },
  "path": "/jamilxt/spring-security-7-mfa-modular-config-and-what-breaks-l1b",
  "publishedAt": "2026-06-18T22:50:38.000Z",
  "site": "https://dev.to",
  "tags": [
    "java",
    "springboot",
    "security",
    "springio2026",
    "YouTube",
    "Spring Security What's New (7.0)",
    "Migrating to Spring Security 7",
    "Spring AI MCP Security Project",
    "@EnableMultiFactorAuthentication",
    "@Bean",
    "@HttpServiceExchange",
    "@GetExchange",
    "@RequestParam"
  ],
  "textContent": "Spring Security 7 dropped at Spring I/O 2026. Daniel Erno from the Spring Security team gave a talk covering the biggest changes. Here's what matters if you're building Java apps with Spring Boot.\n\n##  Multi-Factor Authentication (MFA) — The Big One\n\nThis feature was requested 12 years ago (issue #2603, opened November 2013). It finally shipped.\n\n**What it does:** You can now enforce MFA at the application level or per-endpoint. Spring Security tracks which authentication factors each user has completed and when they authenticated.\n\n**Key classes:**\n\n  * `FactorGrantedAuthority` — records what factor you used (password, one-time token, etc.) and when you used it\n  * `@EnableMultiFactorAuthentication` — enables MFA across your app\n  * `AllRequiredFactorsAuthorizationManager` — combine multiple MFA rules\n\n\n\n**Site-wide MFA example:**\n\n\n\n    @EnableMultiFactorAuthentication(authorities = {\n        FactorGrantedAuthority.PASSWORD_AUTHORITY,\n        FactorGrantedAuthority.OTT_AUTHORITY\n    })\n\n\nNow every protected endpoint requires both password login AND a one-time token.\n\n**Per-endpoint MFA (more practical):**\n\n\n\n    @Bean\n    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {\n        var mfa = new AllRequiredFactorsAuthorizationManager.Builder()\n            .requiredFactor(FactorGrantedAuthority.PASSWORD_AUTHORITY)\n            .requiredFactor(FactorGrantedAuthority.OTT_AUTHORITY)\n            .build();\n\n        http.authorizeHttpRequests(authZ ->\n            authZ\n                .requestMatchers(\"/public.html\").permitAll()\n                .requestMatchers(\"/admin.html\").access(mfa.hasRole(\"admin\"))\n                .anyRequest().authenticated()\n        );\n        return http.build();\n    }\n\n\n**Time-based rules:** You can require that an authentication is recent. For example, force users to re-authenticate before changing their password:\n\n\n\n    var recentPassword = AllRequiredFactorsAuthorizationManager.builder()\n        .requiredFactor(FactorGrantedAuthority.PASSWORD_AUTHORITY, Duration.ofMinutes(5))\n        .build();\n\n    http.authorizeHttpRequests(authZ ->\n        authZ.requestMatchers(\"/change-password\")\n            .access(recentPassword)\n    );\n\n\nIf a user logged in 30 minutes ago, they get redirected back to login before accessing the password change page.\n\n**Why this matters:** Before Spring Security 7, you had to implement MFA yourself or use a third-party library. Now it's built into the framework.\n\n##  Modular Configuration — Stop Replacing Boot Defaults\n\n**The old problem:** When you created a `SecurityFilterChain` bean, you replaced Spring Boot's entire security configuration. You had to manually add form login, session management, CSRF protection, and OAuth2 defaults. Miss one, and your app breaks.\n\n**The new way:** Instead of a full `SecurityFilterChain` bean, provide a customizer that changes only what you need:\n\n\n\n    // OLD: replaces ALL defaults\n    @Bean\n    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {\n        http.authorizeHttpRequests(authZ ->\n            authZ.requestMatchers(\"/public.html\").permitAll()\n                  .anyRequest().authenticated()\n        );\n        return http.build();\n    }\n\n    // NEW: keeps Boot defaults, only changes what you need\n    @Bean\n    Customizer<HttpSecurity> httpSecurityCustomizer() {\n        return http ->\n            http.authorizeHttpRequests(authZ ->\n                authZ.requestMatchers(\"/public.html\").permitAll()\n            );\n    }\n\n\nSpring Boot's form login, CSRF, session management, and OAuth2 Authorization Server defaults stay intact. You only customize the parts you change.\n\n**Targeted customizers:** You can also customize specific configurers without touching the full HTTP security:\n\n\n\n    // Change only OAuth2 Authorization Server defaults\n    @Bean\n    Customizer<OAuth2AuthorizationServerConfigurer> authServerCustomizer() {\n        return authz -> authz.oidc(oidc ->\n            oidc.providerConfiguration(pc ->\n                pc.claims(c -> c.claim(\"conference\", \"Spring I/O 2026\"))\n            )\n        );\n    }\n\n\nThis is cleaner. No more `SecurityFilterChain` beans unless you really need to override everything.\n\n##  What Got Removed\n\n**OAuth2 Password Grant — gone.** If you're using password grant type, migrate to authorization code. The OAuth 2.1 spec calls password grant \"legacy.\" Spring Security removed it from the client library.\n\n**`and()` DSL — gone.** The old chain-style configuration with `.and()` is removed. Use lambdas instead:\n\n\n\n    // OLD (removed)\n    http\n        .authorizeHttpRequests(authZ -> authZ\n            .requestMatchers(\"/admin\").hasRole(\"ADMIN\")\n            .anyRequest().authenticated())\n        .and()\n        .formLogin();\n\n    // NEW (lambda style)\n    http\n        .authorizeHttpRequests(authZ -> authZ\n            .requestMatchers(\"/admin\").hasRole(\"ADMIN\")\n            .anyRequest().authenticated())\n        .formLogin(Customizer.withDefaults());\n\n\nOpen Rewrite can automate this migration for you.\n\n**Module reorganization:** `AccessDecisionVoterManager` moved to `spring-security-access` package. Kerberos and Authorization Server modules now share the same version number (7.x).\n\n##  OAuth2 Changes\n\n**PKCE is now the default.** The Authorization Server enforces Proof Key for Code Exchange. Clients must send a code challenge. If you have legacy clients that can't support PKCE, you can turn it off:\n\n\n\n    http.oauth2AuthorizationServer(authz ->\n        authz.tokenEndpoint(token -> token\n            .requireProofKey(false) // disable PKCE (not recommended)\n        )\n    );\n\n\n**Dynamic Client Registration.** Authorization Servers can now register clients through a REST endpoint (`/oauth2/register`). This was added mainly for MCP (Model Context Protocol) support, where AI tools like Claude Desktop need to dynamically register themselves as clients.\n\nThe endpoint requires a token with `client.create` scope. Each token can register exactly one client. After registration, the token is discarded.\n\n**Warning:** Don't open this endpoint without access control. The spec authors recommend authenticated registration, not open registration. Open registration is a denial-of-service risk.\n\n##  HTTP Service Clients with OAuth2\n\nSpring Framework 7 introduced HTTP Service Clients (declarative, interface-based HTTP clients). Spring Security 7 adds OAuth2 token injection support.\n\n\n\n    // Define as an interface\n    @HttpServiceExchange(url = \"http://localhost:8090/api/hugo\")\n    interface HugoService {\n        @GetExchange\n        List<String> getNominees(@RequestParam int year);\n    }\n\n    // Register the bean with OAuth2 support\n    @Bean\n    HugoService hugoService(HttpServiceProxyFactory factory, OAuth2AuthorizedClientManager manager) {\n        return factory.createClient(HugoService.class);\n    }\n\n    // Enable OAuth2 token injection\n    @Bean\n    OAuth2RestClientHttpServiceGroupConfigurer oauth2Configurer(\n            OAuth2AuthorizedClientManager manager) {\n        return OAuth2RestClientHttpServiceGroupConfigurer.fromOAuth2AuthorizedClientManager(manager)\n            .clientRegistrationId(\"default-client\");\n    }\n\n\nNo more manual `RestClient` request interceptors. The token injection happens automatically.\n\n##  MCP Security\n\nModel Context Protocol (MCP) gets security support through Spring AI, not Spring Security core. The MCP spec changes too frequently for Spring Security's release cadence.\n\n**Start with Spring Initializr:** Add the MCP server starter with security:\n\n\n\n    spring-ai-starter-mcp-server-security\n\n\nFor clients:\n\n\n\n    spring-ai-starter-mcp-client-security\n\n\nBoth depend on the Authorization Server for dynamic client registration and token management. The security flow is OAuth2-based: metadata discovery, client registration, authorization code flow, token exchange.\n\n##  Should You Upgrade?\n\n**If you're on Spring Security 6:** Plan the migration now. The `and()` DSL removal and `SecurityFilterChain` changes will require code updates. Open Rewrite handles most of the migration automatically.\n\n**If you're starting a new project:** Spring Security 7 is the right choice. MFA is built in, configuration is simpler, and OAuth2 defaults follow current best practices (PKCE enforced, no password grant).\n\n**If you use Kerberos or OAuth2 Authorization Server:** Versions are now aligned with Spring Security 7. No more separate version tracking.\n\n##  Sources\n\n  * Spring I/O 2026 — Daniel Erno, \"Spring Security 7\" talk (YouTube)\n  * Spring Security What's New (7.0)\n  * Migrating to Spring Security 7\n  * Spring AI MCP Security Project\n\n",
  "title": "Spring Security 7: MFA, Modular Config, and What Breaks"
}