Class MfaConfigurer
A second factor at sign-in.
http.formLogin(Customizer.withDefaults())
.mfa(mfa -> mfa.totpService(totp).recoveryCodeService(recoveryCodes));
A user with a confirmed authenticator app -- see TotpService -- is no
longer signed in by their password alone. Their password is accepted, the
request stays anonymous, and they are sent to /login/mfa, which asks for a
code; posting a right one there signs them in, exactly as the password
would have without this. A user who has not enrolled signs in as before.
With nothing set the chain serves a plain page at GET /login/mfa and
takes the code at POST /login/mfa in the field code. Naming a
secondFactorPage hands the page to the application. Either way both are
open to everyone, whatever the authorization rules say: the user on them
is, as far as the chain knows, nobody yet.
The codes come from the TotpService given here or the application's bean
of that type, and recovery codes are accepted when there is a
RecoveryCodeService the same way.
How many guesses, and who can be kept out
Wrong codes are counted, and too many are answered 429. Three numbers decide how many:
cn1.security.mfa.attempts: wrong one-time codes for one user, from every address and session together. 5 unless set.cn1.security.mfa.attemptsPerAddress: how many of those one client network may use up, and how many wrong recovery codes it may try for that user. Half ofattemptsrounded up unless set: 3.cn1.security.mfa.attemptsWindowSeconds: the window both are counted in. 300 unless set.
A client network is the client's address, an IPv6 address counting by its
first 64 bits; see
RateLimitKeys.clientNetwork().
A code is counted before it is looked at and a right one hands its counts
back, so only wrong ones stay counted. A sign-in that completes -- by a
one-time code, a recovery code, or a passkey that verified the user --
clears what was counted against that user.
What that gives somebody who has a user's password and not their second factor, with the numbers above:
- At most 5 wrong one-time codes in a window, in total. Signing in again with the password, a new session or another address buys none: the count is the user's. At most 3 of the 5 from one network.
- Recovery codes: at most 3 wrong ones in a window from each network, with no total for the user. A code is ten characters out of 31, and a user has ten: a guess is right once in 8 x 10^13.
- They cannot keep the user out from one network. When they have used their 3 there, 2 of the user's 5 are left for everybody else, and a right code needs one.
- From two networks or more they can use all 5, and one-time codes are then refused for that user from everywhere until the window has run. The user still signs in with a recovery code, which has its own count at their own network, or with a passkey, which is not counted at all; either clears the count. The attacker can run it up again, so this lasts until the password is changed -- and a run of 429s for one user is the sign that it has to be.
- At the user's own network they can use up both counts, the one-time
codes' and the recovery codes'. A passkey is then the way in. A server
behind a proxy it has not been told to believe sees one address for every
client, which makes every client the user's own network: set
cn1.server.forwardHeaders.
attemptsPerAddress equal to attempts gives the old trade back: any one
client that knows the password can keep the user out. Larger is refused.
The window is whatever the limiter means by one. The count kept in the
process hands attempts back evenly -- after 5 at once, one every minute --
and a JdbcRateLimiter counts 5
from the first of each window.
Where the attempts are counted
Where they are counted depends on what the application declares. With
nothing, in this process. With one RateLimiter bean, wherever that bean
counts -- a JdbcRateLimiter
makes it one count for every process -- in two limiters the bean derives
for the purpose, with the limits above and not the bean's own; see
RateLimiter.derive(String, int, long). A limiter given to attemptLimiter(RateLimiter), or a bean that
derives none, counts by its own limit, and setting the keys as well is
then refused when the chain is built, since they would decide nothing.
One limiter has one limit, so it cannot hold a client network to less than
the user's total: give attemptLimiter(RateLimiter, RateLimiter) two to
keep the guarantees above. Clearing a count needs RateLimiter.reset(String); with
a limiter that has none, a right code costs an attempt and nothing is
cleared.
With several RateLimiter beans the one marked @Primary is the one, as it
would be for an injection. With none marked, or more than one, the chain is
refused when it is built, with a message that names the beans: counting in
this process instead would look exactly like the shared count the
application asked for, and not be it. Mark one @Primary, or inject the
one meant -- @Qualifier names it -- and give it to attemptLimiter(RateLimiter).
What holds a sign-in back is the chain's SecondFactorPolicy, which every
sign-in that ends in a session consults; see SessionSignIn.
Every way of presenting a first factor
A second factor that one mechanism of the chain skipped would be optional, so each mechanism has a rule for a user who has one:
HttpSecurity.formLogin(Customizer)andHttpSecurity.oauth2Login(Customizer): the sign-in is held back for the code.HttpSecurity.webAuthn(Customizer): a passkey whose authenticator verified the user is two factors in one step and signs in; one that did not is held back.HttpSecurity.httpBasic(Customizer): refused, with a 401 that says why once the password has been checked. Credentials sent with every request have no second step to present a code in. A chain can exempt it, by name:HttpBasicConfigurer.secondFactorExempt.HttpSecurity.rememberMe(Customizer): the cookie signs the user in only when the sign-in that issued it passed the second factor. Any other cookie of theirs -- one from before they enrolled -- is withdrawn.HttpSecurity.oauth2ResourceServer(Customizer)andHttpSecurity.apiKey(Customizer): accepted. A token or a key is not a user signing in: nobody is there to type a code, and it stands for a sign-in that already happened -- the one that had the token issued, which went through this policy if it was made here -- or for a decision of whoever minted the key. What limits them is their own lifetime and revocation.
A filter or provider of the application's own that makes a request a
user's is outside all of this; it can ask SecondFactorPolicy.requires(Authentication).
-
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final StringThe setting that holds how many wrong one-time codes a user has in one window, from everywhere together; 5 unless set.static final StringThe setting that holds how many of those one client network may use up, and how many wrong recovery codes it may try for the user; half ofATTEMPTS, rounded up, unless set.static final StringThe setting that holds the length of that window in seconds; 300 unless set. -
Method Summary
Modifier and TypeMethodDescriptionattemptLimiter(RateLimiter attemptLimiter) What counts attempts, in place of the application'sRateLimiterbean and of the count kept in this process; null to count none.attemptLimiter(RateLimiter perUser, RateLimiter perAddress) What counts attempts, as two limits:perUsera user's wrong one-time codes from everywhere, andperAddresstheir wrong codes at one client network -- one-time codes and recovery codes apart.The clock the pending sign-in's lifetime is read from; for tests.codeParameter(String codeParameter) The form field the code is in;codeunless set.voidconfigure(HttpSecurity http) Adds this part's filters; nothing by default.defaultSuccessUrl(String defaultSuccessUrl) Where a user goes after the code when no page asked for the sign-in.voidinit(HttpSecurity http) Shares what other parts need to know; nothing by default.pendingValiditySeconds(int pendingValiditySeconds) How long a user has to enter their code after their password was accepted; 300 seconds unless set.processingUrl(String processingUrl) Where the code is posted; the page's path unless set.recoveryCodeService(RecoveryCodeService recoveryCodeService) What checks recovery codes, in place of the application's bean.secondFactorPage(String secondFactorPage) The application's own page that asks for the code: a path it serves.successHandler(AuthenticationSuccessHandler successHandler) Answers a completed sign-in itself, instead of the redirect.totpService(TotpService totpService) What checks one-time codes, in place of the application's bean.Methods inherited from class SecurityConfigurer
disable, getBuilder
-
Field Details
-
ATTEMPTS
The setting that holds how many wrong one-time codes a user has in one window, from everywhere together; 5 unless set.- See Also:
-
ATTEMPTS_PER_ADDRESS
-
ATTEMPTS_WINDOW
The setting that holds the length of that window in seconds; 300 unless set.- See Also:
-
-
Method Details
-
totpService
What checks one-time codes, in place of the application's bean. -
recoveryCodeService
What checks recovery codes, in place of the application's bean. -
secondFactorPage
The application's own page that asks for the code: a path it serves. -
processingUrl
Where the code is posted; the page's path unless set. -
codeParameter
The form field the code is in;codeunless set. -
pendingValiditySeconds
How long a user has to enter their code after their password was accepted; 300 seconds unless set. -
attemptLimiter
What counts attempts, in place of the application's
RateLimiterbean and of the count kept in this process; null to count none.The one limiter counts a user's attempts and their attempts at each client network, under keys of their own and by its one limit. A client network is then allowed all of a user's attempts, and one client that knows the password can keep the user out; see
attemptLimiter(RateLimiter, RateLimiter). -
attemptLimiter
What counts attempts, as two limits:
perUsera user's wrong one-time codes from everywhere, andperAddresstheir wrong codes at one client network -- one-time codes and recovery codes apart.Give
perAddressthe smaller limit. What is left ofperUserwhen one network has used its share up is what everybody else, the user included, still has.- Parameters:
perUser- the limiter for a user's total, or null to count noneperAddress- the limiter for a user at one network, or null to count none
-
defaultSuccessUrl
Where a user goes after the code when no page asked for the sign-in. -
successHandler
Answers a completed sign-in itself, instead of the redirect. -
clock
The clock the pending sign-in's lifetime is read from; for tests. -
init
Description copied from class:SecurityConfigurerShares what other parts need to know; nothing by default.- Overrides:
initin classSecurityConfigurer
-
configure
Description copied from class:SecurityConfigurerAdds this part's filters; nothing by default.- Overrides:
configurein classSecurityConfigurer
-