Class MFAUser

java.lang.Object
com.iizix.server.user.mfa.MFAUser

public class MFAUser extends Object
The class used to provide an MFA authentication using a TOTP based QR code (RFC 6238). The authentication software will use this QR code to set-up the users' account. At sign in, the user provides a code (six digits by default) that is valid for one time period, 30 seconds by default. To allow for a small clock drift between the server and the authenticator app, and for the time it takes to type the code, the code of the period immediately before and immediately after the current one is also accepted (ALLOWED_DISCREPANCY = 1), i.e. a 90 second window for the default period.

Enrolment: generateSecret() then generateQRCodeData(ErrorCorrectionLevel). Sign in: validateCode(String). The digits, period and algorithm must be configured BEFORE the secret is generated; the authenticator app is enrolled with them and they cannot change afterwards.

The time source is the system clock. The server host is expected to be NTP-disciplined at operating system level.

Since SignIn L-6 an instance is also made from a secret that was saved (MFAUser(String,String,String,Hashing,int,int)), and a validation tells the time step of the code it accepted (validateCodeStep(String)), which is what a caller needs to refuse a code that was used before. This class keeps nothing: the sealed storage of the secret, the last accepted time step, the attempt limits and the recovery codes are UserMFA's.

  • Nested Class Summary

    Nested Classes
    Modifier and Type
    Class
    Description
    static enum 
    Enum to define valid hashing algorithms.
  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    static final int
    The number of time periods either side of the current one for which a code is still accepted: 1 (RFC 6238 section 5.2).
    static final int
    The default number of digits: 6.
    static final int
    The default time period in seconds during which a code is valid: 30.
    static final int
    The default number of characters in the secret: 32.
  • Constructor Summary

    Constructors
    Constructor
    Description
    MFAUser(String appName, String account)
    Constructor of the instance for a six digit code, using the system clock as time source.
    MFAUser(String appName, String account, String secret, MFAUser.Hashing algorithm, int digits, int period)
    Constructor of the instance of a user who is enrolled already, from the secret and the parameters that were saved when the enrolment was confirmed (SignIn L-6), using the system clock as time source.
  • Method Summary

    Modifier and Type
    Method
    Description
    int
    driftOf(String code, int maxSteps)
    Looks for the time step a refused code would have matched at, outside the window validateCodeStep(String) accepts (CC-FIX-SIGNIN-TOTP-ENROL, item 9): the steps from ALLOWED_DISCREPANCY+1 to maxSteps away from the current one, on both sides, the nearest first.
    Generate the QR code data with relevant information with specified error correction level.
    Generate the secret with the secretLength specified, default SECRET_LENGTH = 32.
    Gets the QR code data the authenticator app is enrolled with: the account, the issuer, the secret and the parameters, which are what is saved when the enrolment is confirmed and what MFAUser(String,String,String,Hashing,int,int) takes them back from.
    Gets or generate the secret with the secretLength specified, default SECRET_LENGTH = 32, if not previously done.
    Sets the algorithm to use when creating the QR code.
    setDigits(int digits)
    Sets the number of digits to use for the code.
    setPeriod(int period)
    Sets the time period in seconds during which a code is valid, default is PERIOD = 30.
    setSecretLength(int secretLength)
    Sets the number of Base32 characters of the secret to generate, default is SECRET_LENGTH = 32 (160 bits, the RFC 4226 recommendation).
    boolean
    Validates a code against the generated secret, for the current time period and the period immediately before and after it (ALLOWED_DISCREPANCY = 1).
    long
    Validates a code as validateCode(String) does, and tells the time step of the code it accepted.
  • Field Details

    • SECRET_LENGTH

      public static final int SECRET_LENGTH
      The default number of characters in the secret: 32.
      See Also:
    • DIGITS

      public static final int DIGITS
      The default number of digits: 6.
      See Also:
    • PERIOD

      public static final int PERIOD
      The default time period in seconds during which a code is valid: 30.
      See Also:
    • ALLOWED_DISCREPANCY

      public static final int ALLOWED_DISCREPANCY
      The number of time periods either side of the current one for which a code is still accepted: 1 (RFC 6238 section 5.2). The acceptance window is (2*ALLOWED_DISCREPANCY+1)*period seconds, 90 seconds for the default period.
      See Also:
  • Constructor Details

    • MFAUser

      public MFAUser(String appName, String account)
      Constructor of the instance for a six digit code, using the system clock as time source.
      Parameters:
      appName - The application name as the issuer, in localized form (should never change).
      account - The account name is generally something to identify uniquely the user, generally an email address.
      Throws:
      NullPointerException - If appName or account is null.
    • MFAUser

      public MFAUser(String appName, String account, String secret, MFAUser.Hashing algorithm, int digits, int period)
      Constructor of the instance of a user who is enrolled already, from the secret and the parameters that were saved when the enrolment was confirmed (SignIn L-6), using the system clock as time source. Until SignIn L-6 a secret lived in the instance that generated it and nowhere else, so no code could be validated by another instance: not after a restart, and not by the sign-in of another session.
      Parameters:
      appName - The application name as the issuer.
      account - The account name.
      secret - The Base32 shared secret.
      algorithm - The hashing algorithm the authenticator app was enrolled with.
      digits - The number of digits it was enrolled with (range 4 to 8).
      period - The period in seconds it was enrolled with (range 10 to 60).
      Throws:
      NullPointerException - If appName, account, secret or algorithm is null.
      IllegalArgumentException - If the secret is empty, or digits or period is not within its range.
  • Method Details

    • setDigits

      public MFAUser setDigits(int digits)
      Sets the number of digits to use for the code. Must be called before generateSecret().

      Setting how many digits long the generated codes are:

      Most TOTP implementations generate codes that are 6 digits long, but codes can have a length of any positive non-zero integer.

      Parameters:
      digits - The requested number of digits (default 6, minimum 4 and maximum 8 is recommended).
      Returns:
      "this" instance for concatenation.
      Throws:
      IllegalArgumentException - If digits range is not within 4 to 8.
      IllegalStateException - If the secret has already been generated.
    • setPeriod

      public MFAUser setPeriod(int period)
      Sets the time period in seconds during which a code is valid, default is PERIOD = 30. Must be called before generateSecret().

      Setting the time period:

      The one time password codes generated in the authenticator apps only last for a certain time period before they are re-generated. Independently of the period, the code of the period immediately before and immediately after the current one is also accepted (ALLOWED_DISCREPANCY = 1), to allow for a small time drift between the server and the authenticator app and for the time it takes to type the code.

      Parameters:
      period - The requested period in seconds (range is 10 to 60).
      Returns:
      "this" instance for concatenation.
      Throws:
      IllegalArgumentException - If period range is not within 10 to 60 seconds.
      IllegalStateException - If the secret has already been generated.
    • setAlgorithm

      public MFAUser setAlgorithm(MFAUser.Hashing alg)
      Sets the algorithm to use when creating the QR code. Must be called before generateSecret().
      Parameters:
      alg - The hashing algorithm.
      Returns:
      "this" instance for concatenation.
      Throws:
      NullPointerException - If alg is null.
      IllegalStateException - If the secret has already been generated.
    • setSecretLength

      public MFAUser setSecretLength(int secretLength)
      Sets the number of Base32 characters of the secret to generate, default is SECRET_LENGTH = 32 (160 bits, the RFC 4226 recommendation). Must be called before generateSecret().
      Parameters:
      secretLength - The requested secret length in characters (range is 32 to 128).
      Returns:
      "this" instance for concatenation.
      Throws:
      IllegalArgumentException - If secret length is not within 32 to 128 characters.
      IllegalStateException - If the secret has already been generated.
    • generateSecret

      public MFAUser generateSecret()
      Generate the secret with the secretLength specified, default SECRET_LENGTH = 32. The secret is only generated once, so there is no point in calling this method many times, nothing will happen.
      Returns:
      "this" instance for concatenation.
    • getSecret

      public String getSecret()
      Gets or generate the secret with the secretLength specified, default SECRET_LENGTH = 32, if not previously done.
      Returns:
      The secret.
    • generateQRCodeData

      public BarcodeURN generateQRCodeData(ErrorCorrectionLevel errorCorrectionLevel) throws BarcodeException
      Generate the QR code data with relevant information with specified error correction level.
      Parameters:
      errorCorrectionLevel - null for default L = Low = ~7% error correction level.
      Returns:
      The generated barcode URN from where e.g. an SVG can be extracted.
      Throws:
      IllegalStateException - If the secret is missing (has not been generated).
      BarcodeException - If there are missing or invalid parameters assigned in this builder encountered during barcode encoding.
    • getQRData

      public QrData getQRData()
      Gets the QR code data the authenticator app is enrolled with: the account, the issuer, the secret and the parameters, which are what is saved when the enrolment is confirmed and what MFAUser(String,String,String,Hashing,int,int) takes them back from.
      Returns:
      The immutable QR code data.
      Throws:
      IllegalStateException - If the secret is missing (has not been generated).
    • validateCode

      public boolean validateCode(String code)
      Validates a code against the generated secret, for the current time period and the period immediately before and after it (ALLOWED_DISCREPANCY = 1).

      The verification parameters (algorithm, digits, period) are taken from the QR code data the authenticator app was enrolled with, so they can never disagree with the app. Every candidate period is computed and compared in constant time; a null, empty or whitespace-only code is never valid and never throws. Safe to call concurrently.

      Parameters:
      code - The code as typed by the user; whitespace is ignored.
      Returns:
      true if the code is valid, false otherwise.
      Throws:
      IllegalStateException - If the secret is missing (has not been generated): validating a user who was never enrolled is a caller error, not a wrong code.
    • validateCodeStep

      public long validateCodeStep(String code)
      Validates a code as validateCode(String) does, and tells the time step of the code it accepted. A code is valid for its own time step only, so the caller that keeps the step of the last code it accepted, and refuses every step that is not later, accepts each code once (RFC 6238 section 5.2).
      Parameters:
      code - The code as typed by the user; whitespace is ignored.
      Returns:
      The time step of the code - the count of periods since the epoch - or DefaultCodeVerifier.NO_STEP if the code is not valid.
      Throws:
      IllegalStateException - If the secret is missing (has not been generated).
    • driftOf

      public int driftOf(String code, int maxSteps)
      Looks for the time step a refused code would have matched at, outside the window validateCodeStep(String) accepts (CC-FIX-SIGNIN-TOTP-ENROL, item 9): the steps from ALLOWED_DISCREPANCY+1 to maxSteps away from the current one, on both sides, the nearest first. Nothing is accepted here: the caller names the drift in its refusal, so that a user whose phone's clock is off is told so instead of guessing.
      Parameters:
      code - The code as typed by the user; whitespace is ignored.
      maxSteps - How many time steps away to look, at most.
      Returns:
      The offset in time steps of the step the code matches at: positive when the clock of the code is ahead of this server's, negative when behind; zero when the code matches at none of them, and for no code.
      Throws:
      IllegalStateException - If the secret is missing (has not been generated).