Spring Boot Authentication Methods

This section documents OAuth 2.0 (RFC 6749) as amended by the OAuth 2.0 Security Best Current Practice (RFC 9700 / BCP 240), the OAuth 1.0 Protocol (RFC 5849) for historical context, and OpenID Connect Core 1.0, as published at the IETF Datatracker and the OpenID Foundation specifications — and, on the Spring pages, Spring Boot 4.1.x and Spring Security 7.1.x as published at the Spring Security reference documentation — which are the references these pages are written and verified against.

OAuth 2.1 is still an Internet-Draft (draft-ietf-oauth-v2-1-16, 3 September 2026) and is flagged as such everywhere it appears on these pages. It is a working-group consolidation in progress, not a published standard; nothing here should be read as saying otherwise.

This content was generated with the assistance of AI and should be verified against those official specifications before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Authentication Methods: Passwordless and 2FA describes, framework-neutrally, what an authorization server can use to identify a user. This page names the Spring Security 7.1 mechanism behind each of those methods for a service that runs its own authorization server — and, just as importantly, says plainly which ones Spring does not implement for you.

It keeps that page’s two axes rather than flattening them into one list:

A primary factor is what identifies the user in the first place — a password, or one of the passwordless methods below. 2FA/MFA is an additional security layer stacked on a primary factor: it is never a primary method in its own right, and it is never "passwordless" by itself. Enabling mfa() on a password login does not make that login passwordless — it makes it a hardened password login.

Form login with DaoAuthenticationProvider, UserDetailsService and PasswordEncoder — the password primary factor — is already documented in depth in Spring Security; it is not repeated here.

Passwordless primary methods: what Spring gives you

These replace the password as the primary factor.

Method (protocol page) Spring Security 7.1 mechanism Built in?

E-mail one-time code

oneTimeTokenLogin() + a custom OneTimeTokenGenerationSuccessHandler that mails the token value

Yes

E-mail magic link

oneTimeTokenLogin() + a handler that mails a link to /login/ott?token=…​

Yes

SMS one-time code

oneTimeTokenLogin() + a handler that sends the token over an SMS gateway

Yes (delivery is yours)

Push approval

 — (no built-in support; custom AuthenticationProvider/filter)

No

Passkeys (WebAuthn/FIDO2)

webAuthn() with PublicKeyCredentialUserEntityRepository + UserCredentialRepository

Yes

Device-bound credentials

rememberMe() — but it is a bearer cookie, not a device-bound key (see below)

Partial

Federated / social login

oauth2Login() — see Authorization Server & Social Login

Yes

Certificate / smart-card (mTLS)

x509(), contributing FactorGrantedAuthority.X509_AUTHORITY

Yes

Additional 2FA/MFA layers: what Spring gives you

None of these is a primary method. Each is stacked on one of the rows above.

Layer (protocol page) Spring Security 7.1 mechanism Built in?

Requiring more than one factor at all

@EnableMultiFactorAuthentication, or AuthorizationManagerFactories.multiFactor()

Yes

One-time token as a second factor

The same oneTimeTokenLogin(), required via FactorGrantedAuthority.OTT_AUTHORITY

Yes

Passkey as a second factor

The same webAuthn(), required via FactorGrantedAuthority.WEBAUTHN_AUTHORITY

Yes

TOTP/HOTP authenticator apps

 — (no built-in support; custom factor)

No

SMS/e-mail OTP as a second factor

oneTimeTokenLogin() reused in the second-factor role

Yes (delivery is yours)

Push approval as a second factor

 — (no built-in support; custom factor)

No

Recovery codes

 — (application concern)

No

One-time token login

oneTimeTokenLogin() is the mechanism behind the e-mail code, the magic link and the SMS code — and, when required as a second factor, behind OTP-as-2FA. Spring generates, stores and consumes the token; delivering it is deliberately left to you, because Spring has no opinion about your mail or SMS provider.

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/login/**", "/ott/**").permitAll()
            .anyRequest().authenticated())
        .oneTimeTokenLogin(ott -> ott
            .tokenGenerationSuccessHandler(magicLinkHandler()));
    return http.build();
}

The moving parts:

  • OneTimeTokenService — generates and consumes tokens. InMemoryOneTimeTokenService is the default and is suitable only for a single instance; use JdbcOneTimeTokenService in production so tokens survive a restart and are shared across instances.

  • OneTimeTokenGenerationSuccessHandler — handle(HttpServletRequest, HttpServletResponse, OneTimeToken). This is where you mail or text the token. It is the only piece you must write for a working magic link.

  • GenerateOneTimeTokenRequestResolver (default DefaultGenerateOneTimeTokenRequestResolver) — customizes the generate request, including the token’s time-to-live.

Default endpoints: POST /ott/generate (configurable with tokenGeneratingUrl(…​)) and the submit page at GET /login/ott (configurable with defaultSubmitPageUrl(…​)).

A magic-link handler that mails a link rather than a bare code:

@Bean
OneTimeTokenGenerationSuccessHandler magicLinkHandler() {
    return (request, response, oneTimeToken) -> {
        String link = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request))
                .replacePath(request.getContextPath() + "/login/ott")
                .replaceQuery("token=" + oneTimeToken.getTokenValue())
                .toUriString();

        mailSender.send(oneTimeToken.getUsername(), "Your sign-in link", link);   (1)

        response.setStatus(HttpStatus.OK.value());                                (2)
    };
}
1 Deliver out of band. Never write the token value into the HTTP response.
2 Answer identically whether or not the account exists — see the enumeration note below.

Three things Spring does not do for you here, all of them called out as requirements on the protocol page:

  • Rate limiting and lockout on POST /ott/generate. Without it the endpoint is a free mail/SMS cannon pointed at your users and your budget.

  • Enumeration-safe responses. Return the same status and body whether or not the address is known.

  • TTL tuning. Set it deliberately through the request resolver; a magic link that lives for hours is a standing credential in an inbox.

Passkeys

webAuthn() makes the service a WebAuthn relying party. Add spring-security-webauthn.

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .webAuthn(webAuthn -> webAuthn
            .rpId("example.com")                            (1)
            .rpName("Example")
            .allowedOrigins("https://example.com"));        (2)
    return http.build();
}
1 The relying-party ID is the registrable domain. It is what binds a credential to your origin, and it is why passkeys are phishing-resistant — a credential registered for example.com simply cannot be used by examp1e.com.
2 Must list every origin the ceremony may run from, including ports in development.

Persistence is two repositories, and the in-memory defaults are for demos only:

  • PublicKeyCredentialUserEntityRepository — the WebAuthn user handle. JdbcPublicKeyCredentialUserEntityRepository for production.

  • UserCredentialRepository — the registered credentials themselves. JdbcUserCredentialRepository for production.

  • PublicKeyCredentialCreationOptionsRepository — holds the in-flight registration challenge; defaults to the HttpSession, which is usually right.

Default endpoints: POST /webauthn/register/options and POST /webauthn/register to enrol, then POST /webauthn/authenticate/options and POST /login/webauthn to sign in. The generated login page offers passkey sign-in once webAuthn() is enabled.

Multi-factor authentication

Spring Security 7 models each completed authentication step as a factor authority on the resulting Authentication, taken from FactorGrantedAuthority: PASSWORD_AUTHORITY, OTT_AUTHORITY, WEBAUTHN_AUTHORITY, X509_AUTHORITY, AUTHORIZATION_CODE_AUTHORITY. Requiring more than one factor is then just an authorization rule, and Spring handles the redirecting between factor login pages.

The declarative form:

@Configuration
@EnableWebSecurity
@EnableMultiFactorAuthentication(authorities = {
        FactorGrantedAuthority.PASSWORD_AUTHORITY,
        FactorGrantedAuthority.OTT_AUTHORITY })
public class SecurityConfig { }

The bean form, which is the one to reach for when the rule is conditional:

@Bean
AuthorizationManagerFactory<Object> authz() {
    return AuthorizationManagerFactories.multiFactor()
            .requireFactors(
                    FactorGrantedAuthority.PASSWORD_AUTHORITY,
                    FactorGrantedAuthority.OTT_AUTHORITY)
            .build();
}

A factor can carry a validity window, which is how "you authenticated with your password an hour ago, do it again" is expressed:

AuthorizationManagerFactories.multiFactor()
        .requireFactor(factor -> factor
                .passwordAuthority()
                .validDuration(Duration.ofMinutes(30)))
        .build();

Passwordless and multi-factor

This is the combination the protocol page recommends, and Spring expresses it directly: accept either a passkey on its own or a one-time token plus a password — so a user with a passkey never types a password, and a user without one still gets two factors.

AllRequiredFactorsAuthorizationManager<Object> passkeyOnly =
        AllRequiredFactorsAuthorizationManager.<Object>builder()
                .requireFactor(factor -> factor.webauthnAuthority())
                .build();

AllRequiredFactorsAuthorizationManager<Object> passwordAndOtt =
        AllRequiredFactorsAuthorizationManager.<Object>builder()
                .requireFactor(factor -> factor.passwordAuthority())
                .requireFactor(factor -> factor.ottAuthority())
                .build();

DefaultAuthorizationManagerFactory<Object> mfa = new DefaultAuthorizationManagerFactory<>();
mfa.setAdditionalAuthorization(
        AllRequiredFactorsAuthorizationManager.anyOf(passkeyOnly, passwordAndOtt));

http.authorizeHttpRequests(auth -> auth
        .requestMatchers("/protected/**").access(mfa.authenticated())
        .anyRequest().authenticated());

Note which branch is which: passkeyOnly is passwordless, passwordAndOtt is multi-factor but not passwordless. Both are two-of-something in the security sense; only one of them removes the password from the account. Swapping passwordAuthority() for ottAuthority() plus webauthnAuthority() would give a passwordless and multi-factor branch.

For per-user rules — MFA for administrators only — use RequiredAuthoritiesAuthorizationManager with a RequiredAuthoritiesRepository, or .when(…​) on the factory.

Device trust, and what is not built in

rememberMe() is Spring’s "remember this device", and it is important to be precise about what it is: a long-lived bearer cookie, not a device-bound key pair. Anyone who copies the cookie is the device. It is reasonable for suppressing a second factor on a familiar browser; it is not a substitute for the device-bound credentials described on the protocol page, and it should never stand in for the primary factor on a sensitive operation — require a fresh factor there instead, via validDuration(…​) above or step-up.

Spring Security has no built-in support for these, in either the primary or the second-factor role:

  • TOTP/HOTP authenticator apps — the single most commonly assumed "it must be in there somewhere" feature. It is not. Implement it as a custom factor (an AuthenticationProvider plus a filter that contributes your own GrantedAuthority, plugged into the MFA rules above) or use a third-party library.

  • Push approval, in either role.

  • Recovery codes, which are an application concern — and, per the protocol page, the thing that quietly caps the assurance of every stronger method above them.

Saying this plainly matters more than it looks: a configuration that appears to enforce TOTP because it mentions mfa() enforces nothing at all unless something actually contributes that factor’s authority.

Reporting the result to clients

None of the above is visible to a client — that is the point of doing authentication inside the authorization server. What is visible is what you put in the token. Map the factor authorities onto the standard RFC 8176 amr values, and set acr and auth_time, with an OAuth2TokenCustomizer on your Spring Authorization Server:

@Bean
OAuth2TokenCustomizer<JwtEncodingContext> idTokenCustomizer() {
    return context -> {
        if (!OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) {
            return;
        }
        Collection<? extends GrantedAuthority> granted =
                context.getPrincipal().getAuthorities();

        List<String> amr = new ArrayList<>();
        if (hasFactor(granted, FactorGrantedAuthority.PASSWORD_AUTHORITY))  amr.add("pwd");
        if (hasFactor(granted, FactorGrantedAuthority.OTT_AUTHORITY))       amr.add("otp");
        if (hasFactor(granted, FactorGrantedAuthority.WEBAUTHN_AUTHORITY))  amr.add("swk");
        if (hasFactor(granted, FactorGrantedAuthority.X509_AUTHORITY))      amr.add("hwk");
        if (amr.size() > 1) {
            amr.add("mfa");                                                  (1)
        }

        context.getClaims()
                .claim("amr", amr)
                .claim("acr", amr.contains("swk") ? "urn:example:aal2" : "urn:example:aal1")
                .claim("auth_time", authenticationInstant(context).getEpochSecond());
    };
}
1 mfa means more than one factor was used — nothing more. It does not mean "no password was used". A client that needs to know the login was passwordless must check for the absence of pwd, not the presence of mfa. This is the single most common misreading of amr, and it is why the two axes are kept apart on both this page and the protocol page.

Resource servers then enforce an upgrade mid-session with an RFC 9470 step-up challenge — see Scopes, Claims and Permissions and Authentication Methods: Passwordless and 2FA.

Why none of this works when the client collects the password

Every mechanism on this page lives inside the authorization server, behind its own login pages. A client that collects credentials itself — the removed resource owner password credentials grant, or an embedded WebView in a native app — can reach none of it: no magic link, no passkey, no second factor, no step-up, no CAPTCHA, no risk engine. That is the concrete, product-level reason those patterns are deprecated, and it is why the authorization request has to happen in a browser the authorization server controls.