Class MfaConfigurer

java.lang.Object
com.codename1.backend.security.SecurityConfigurer
com.codename1.backend.security.MfaConfigurer

public final class MfaConfigurer extends SecurityConfigurer

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 of attempts rounded 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:

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 Details

    • ATTEMPTS

      public static final String 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

      public static final String ATTEMPTS_PER_ADDRESS
      The 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 of ATTEMPTS, rounded up, unless set.
      See Also:
    • ATTEMPTS_WINDOW

      public static final String ATTEMPTS_WINDOW
      The setting that holds the length of that window in seconds; 300 unless set.
      See Also:
  • Method Details

    • totpService

      public MfaConfigurer totpService(TotpService totpService)
      What checks one-time codes, in place of the application's bean.
    • recoveryCodeService

      public MfaConfigurer recoveryCodeService(RecoveryCodeService recoveryCodeService)
      What checks recovery codes, in place of the application's bean.
    • secondFactorPage

      public MfaConfigurer secondFactorPage(String secondFactorPage)
      The application's own page that asks for the code: a path it serves.
    • processingUrl

      public MfaConfigurer processingUrl(String processingUrl)
      Where the code is posted; the page's path unless set.
    • codeParameter

      public MfaConfigurer codeParameter(String codeParameter)
      The form field the code is in; code unless set.
    • pendingValiditySeconds

      public MfaConfigurer pendingValiditySeconds(int pendingValiditySeconds)
      How long a user has to enter their code after their password was accepted; 300 seconds unless set.
    • attemptLimiter

      public MfaConfigurer attemptLimiter(RateLimiter attemptLimiter)

      What counts attempts, in place of the application's RateLimiter bean 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

      public MfaConfigurer attemptLimiter(RateLimiter perUser, RateLimiter perAddress)

      What counts attempts, as two limits: perUser a user's wrong one-time codes from everywhere, and perAddress their wrong codes at one client network -- one-time codes and recovery codes apart.

      Give perAddress the smaller limit. What is left of perUser when 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 none
      perAddress - the limiter for a user at one network, or null to count none
    • defaultSuccessUrl

      public MfaConfigurer defaultSuccessUrl(String defaultSuccessUrl)
      Where a user goes after the code when no page asked for the sign-in.
    • successHandler

      public MfaConfigurer successHandler(AuthenticationSuccessHandler successHandler)
      Answers a completed sign-in itself, instead of the redirect.
    • clock

      public MfaConfigurer clock(Clock clock)
      The clock the pending sign-in's lifetime is read from; for tests.
    • init

      public void init(HttpSecurity http)
      Description copied from class: SecurityConfigurer
      Shares what other parts need to know; nothing by default.
      Overrides:
      init in class SecurityConfigurer
    • configure

      public void configure(HttpSecurity http)
      Description copied from class: SecurityConfigurer
      Adds this part's filters; nothing by default.
      Overrides:
      configure in class SecurityConfigurer