Class UserMFA
java.lang.Object
com.iizix.server.user.mfa.UserMFA
The second factor of the users of the server (SignIn L-6, the survey's section 9.3 item 7, and sections 3.5 and 6 of the SignIn flow design): what
MFAUser and com.iizix.totp could not do on their own. They validate a code against a secret; this class is what makes that a factor of a sign-in:- the secret is stored, sealed (
MFAStore,MFASecretSeal): until now it lived in the memory of the instance that generated it; - an enrolment is confirmed by a first valid code before anything is stored (
beginEnrolment(Object, long, String, String),confirmEnrolment(Object, long, String)), so that an authenticator app that was enrolled wrongly locks nobody out; - a code is accepted once: the time step of the last accepted code is kept, and the code of that step or of an earlier one is refused (RFC 6238 section 5.2);
- invalid codes are limited per account, by waits that grow with the count (
waitAfter(int)), and per waiting sign-in (MAX_PENDING_FAILURES); the limit per client source is the caller'sSourceThrottle; - recovery codes are made when the enrolment is confirmed, returned once, stored hashed as passwords are, and each is used once;
- a passkey may be the second factor (CC-FIX-SIGNIN-2FA-CHOICE): the user who chose it, and registered one in the session a short time ago, has a second factor from
enrolPasskey(Object, long, String)on - a row of the methodpasskey, without a secret, and the recovery codes, which are made at the first second factor of any kind. The passkeys themselves are the passkey store's; a passkey registered to sign in faster, by a user who has no second factor, is none; - a sign-in waits for its second factor, held for the client session it came from (
holdSignIn(Object, long, String, boolean),completeSignIn(Object, String, String)): nothing is signed in before; - the proof of HUB-DESIGN 7.4 (RB-11) is kept for the session whose sign-in verified the factor (
getProof(Object, long)). A passkey that was asserted with user verification is that proof too (methodOfPasskey(boolean), CC-LOGIN-M11-PASSKEY-PROOF): the passkey itself is not this class's, the proof is kept here, in the one place there is.
What is not here (CC-SIGNIN-MERGE-L5-L6-L8). The devices that are trusted not to be asked again are the rows of the device tokens (DeviceTokenStore, CC-LOGIN-L5-DEVICE-TOKEN): the trust is given and asked for by IUserAuthenticationUI, under the rules of this order - bound to the password, never by a recovery code, for the days of the policy. The recent sign-in a change of the second factor requires is the authentication time of the AppSession (CC-LOGIN-L8-PASSKEYS); what is kept here is whether the sign-in of a session verified a second factor, which has no time of its own.
The sessions are the keys of what is held, weakly: what is held for a session goes with it, and with the server. Nothing here logs a code, a secret or a recovery code.
- Author:
- Christopher Mindus
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic final recordThe answer to a code.static final recordA confirmed enrolment.static final recordA sign-in that waits for its second factor.static enumWhat became of a request.static final classA refusal of a request that answers with data.static final recordThe names of the entry an authenticator app shows for a user's enrolment, as the enrolment's QR code gave them (CC-FIX-SIGNIN-RECOVERY): what the code page names, so that the user picks the right entry among several.Field Summary
FieldsModifier and TypeFieldDescriptionstatic final intHow many time steps away from this server's clock a refused code is looked for, to name the drift of a clock in the refusal (CC-FIX-SIGNIN-TOTP-ENROL, item 9): 10 steps of 30 seconds, 5 minutes.static final MFAUser.HashingThe hashing algorithm an authenticator app is enrolled with: SHA1, the algorithm of RFC 6238 that every authenticator app implements.static final intThe invalid codes after which a waiting sign-in or enrolment ends: 5.The methods of a second factor this class does: the authenticator app and the recovery codes.static final longHow long an enrolment waits for its first code, in milliseconds: 10 minutes.static final longHow long a sign-in waits for its second factor, in milliseconds: 5 minutes.static final intThe quiet zone of the enrolment QR code, in modules: 4, the light margin around the code that ISO/IEC 18004 requires and that a scanner needs to find the code (QR-D-1).static final intThe recovery codes made for a user: 10.static final StringThe method of the row of a user who chose a passkey as the second factor and enrolled an authenticator app beside it (CC-FIX-SIGNIN-2FA-CHOICE): "totp+passkey".Constructor Summary
ConstructorsConstructorDescriptionUserMFA(Supplier<? extends ISensitiveParameterHandler> keys, LongSupplier clock, LongSupplier monotonic) Constructs an instance on its keys and clocks; the tests drive them.Method Summary
Modifier and TypeMethodDescriptionbeginEnrolment(Object session, long uid, String userName, String issuer) Begins the enrolment of an authenticator app for a user: the secret is generated and held for the client session, and nothing is stored.completeSignIn(Object session, String method, String code) Completes the sign-in the client session holds, with a verification code or a recovery code.confirmEnrolment(Object session, long uid, String code) Confirms the enrolment the client session holds with the first code of the authenticator app: the secret is sealed and stored, the time step of the code is the last accepted one (the code that confirmed is spent), the recovery codes are made and their hashes stored.enrolPasskey(Object session, long uid, String userName) Makes a passkey the second factor of a user (CC-FIX-SIGNIN-2FA-CHOICE): the user chose a passkey, and the client session registered one for the user a short time ago (onPasskeyRegistered(Object, long)).voidForgets what is known of the sign-in of a session.getHeldSignIn(Object session) Gets the sign-in that waits for a client session.static UserMFAGets the second factor of the server's users.longgetLastPasswordChange(long uid) Gets the time the password of a user was last changed: the time the newest replaced password of the password history was replaced.getMethods(long uid) Gets the second factors of a user as the user's own pages show them: the authenticator app and the recovery codes, each when the user has it.Gets the proof that the sign-in of a session verified the second factor of a user.getSecondFactorMethod(long uid) Gets the method of the second factor a user has (CC-FIX-SIGNIN-2FA-CHOICE).getTotpLabel(long uid) Gets the names of the entry of a user's authenticator app, as the enrolment's QR code gave them (CC-FIX-SIGNIN-RECOVERY).booleanhasSecondFactor(long uid) Checks if a user has a second factor: the question every password sign-in asks.voidholdSignIn(Object session, long uid, String userName, boolean enrolmentRequired) Holds a user whose first factor passed for the client session the sign-in came from, untilcompleteSignIn(Object, String, String)has a code for it orconfirmEnrolment(Object, long, String)an enrolment.static StringmethodOfPasskey(boolean userVerified) Gets the method of the proof a passkey gives (CC-LOGIN-M11-PASSKEY-PROOF, ruling M-11 (b) of CC-SIGNIN-MERGE-L5-L6-L8).voidonPasskeyRegistered(Object session, long uid) Tells that a client session has registered a passkey for a user, now: whatenrolPasskey(Object, long, String)makes the second factor of the user, when the user chose a passkey, forPENDING_ENROLMENT_MS.voidonSignedIn(Object session, long uid, String method) Tells that a session has signed a user in, now - or that the user it has signed in has verified a second factor in it now: an enrolment that was confirmed, a passkey that confirmed the user.String[]regenerateRecoveryCodes(long uid, String userName) Replaces the recovery codes of a user.voidLets go of what is held for a client session: the sign-in and the enrolment that wait.Removes the second factor of a user: the secret and every recovery code.Removes the authenticator app of a user (CC-FIX-SIGNIN-2FA-CHOICE): of a user who chose a passkey as the second factor and has one still (TOTP_AND_PASSKEY), the passkey is the second factor that is left, with the recovery codes; of every other user the second factor goes, asremove(long,String)removes it - also of the user whose passkeys were registered to sign in faster, as before.removePasskeyFactor(long uid, String userName) Tells that the last passkey a user chose as the second factor was removed (CC-FIX-SIGNIN-2FA-CHOICE): the app beside it is the second factor that is left (TOTP_AND_PASSKEYbecomestotp); of a user who has no app, the second factor goes with its recovery codes, asremove(long,String)removes it.restartEnrolment(Object session, long uid, String userName, String issuer) Begins the enrolment of an authenticator app for a user ANEW ("Start over", CC-FIX-SIGNIN-TOTP-ENROL): the enrolment the session held is dropped, whatever was shown before, and a new secret is generated asbeginEnrolment(Object, long, String, String)generates it the first time.static booleantrustsDevice(MFAProof proof) Checks if a proof trusts a device for MFA: a device is trusted by a verification code, or by a passkey that was asserted with user verification (CC-LOGIN-M11-PASSKEY-PROOF) - never by a recovery code, which is what is left when the device is lost, and not by a sign-in that verified nothing.Verifies a code of a user: the account's wait after invalid codes first, then the code, which is accepted once.static longwaitAfter(int failCount) Gets the wait after a count of invalid codes.
Field Details
METHODS
RECOVERY_CODES
public static final int RECOVERY_CODESThe recovery codes made for a user: 10.- See Also:
PENDING_SIGN_IN_MS
public static final long PENDING_SIGN_IN_MSHow long a sign-in waits for its second factor, in milliseconds: 5 minutes.- See Also:
PENDING_ENROLMENT_MS
public static final long PENDING_ENROLMENT_MSHow long an enrolment waits for its first code, in milliseconds: 10 minutes. A sign-in that waits for the enrolment is kept waiting as long.- See Also:
MAX_PENDING_FAILURES
public static final int MAX_PENDING_FAILURESThe invalid codes after which a waiting sign-in or enrolment ends: 5. The user begins again, with the first factor.- See Also:
DRIFT_STEPS
public static final int DRIFT_STEPSHow many time steps away from this server's clock a refused code is looked for, to name the drift of a clock in the refusal (CC-FIX-SIGNIN-TOTP-ENROL, item 9): 10 steps of 30 seconds, 5 minutes. Nothing is accepted outside the verifier's own window (MFAUser.ALLOWED_DISCREPANCY).- See Also:
ENROLMENT_ALGORITHM
The hashing algorithm an authenticator app is enrolled with: SHA1, the algorithm of RFC 6238 that every authenticator app implements. Apps are known to ignore the algorithm of the QR code and to compute SHA1 whatever it says; enrolled with another algorithm, such an app shows codes that are never valid.QR_QUIET_ZONE
public static final int QR_QUIET_ZONEThe quiet zone of the enrolment QR code, in modules: 4, the light margin around the code that ISO/IEC 18004 requires and that a scanner needs to find the code (QR-D-1).- See Also:
TOTP_AND_PASSKEY
The method of the row of a user who chose a passkey as the second factor and enrolled an authenticator app beside it (CC-FIX-SIGNIN-2FA-CHOICE): "totp+passkey". The row is the app's; the passkey is the second factor that is left when the app is removed. An app enrolled by a user whose passkeys are none (methodtotp) leaves nothing when it is removed, as before.- See Also:
Constructor Details
UserMFA
public UserMFA(Supplier<? extends ISensitiveParameterHandler> keys, LongSupplier clock, LongSupplier monotonic) Constructs an instance on its keys and clocks; the tests drive them.- Parameters:
keys- The sealing keys, asked for when a secret is sealed or opened.clock- The wall clock, milliseconds since the epoch.monotonic- The monotonic clock in milliseconds, any origin.- Throws:
NullPointerException- If a parameter is null.
Method Details
getInstance
Gets the second factor of the server's users.- Returns:
- The instance: the server's keys, the system clock.
waitAfter
public static long waitAfter(int failCount) Gets the wait after a count of invalid codes.- Parameters:
failCount- The invalid codes since the last accepted one.- Returns:
- The milliseconds to wait after the last invalid code, zero for none.
hasSecondFactor
Checks if a user has a second factor: the question every password sign-in asks.- Parameters:
uid- The user ID.- Returns:
- true when the user has one; false when the user has none, and when the database has no tables of the second factor and none can be created (nobody has one then).
- Throws:
SQLException- For SQL errors: whether the user has one is not known.
getSecondFactorMethod
Gets the method of the second factor a user has (CC-FIX-SIGNIN-2FA-CHOICE).- Parameters:
uid- The user ID.- Returns:
SignInPolicy.METHOD_TOTPfor an authenticator app,SignInPolicy.METHOD_PASSKEYfor a user whose second factor is a passkey and no app,TOTP_AND_PASSKEYfor an app beside the passkey the user chose; null when the user has none - and when the database has no tables of the second factor and none can be created.- Throws:
SQLException- For SQL errors.
getMethods
Gets the second factors of a user as the user's own pages show them: the authenticator app and the recovery codes, each when the user has it.- Parameters:
uid- The user ID.- Returns:
- The second factors, strongest first; empty when the user has none and when they cannot be read (logged).
getTotpLabel
Gets the names of the entry of a user's authenticator app, as the enrolment's QR code gave them (CC-FIX-SIGNIN-RECOVERY).- Parameters:
uid- The user ID.- Returns:
- The names, each null when the enrolment stored none (one made before this order); null when the user has no authenticator app, and when they cannot be read (logged).
getLastPasswordChange
public long getLastPasswordChange(long uid) Gets the time the password of a user was last changed: the time the newest replaced password of the password history was replaced.- Parameters:
uid- The user ID.- Returns:
- The time, milliseconds since the epoch; zero when it is not known - the password was never changed, or the policy keeps no history.
holdSignIn
Holds a user whose first factor passed for the client session the sign-in came from, untilcompleteSignIn(Object, String, String)has a code for it orconfirmEnrolment(Object, long, String)an enrolment. A sign-in and an enrolment the session held before are let go.- Parameters:
session- The client session, the key of what is held.uid- The user ID.userName- The user name.enrolmentRequired- Whether the sign-in waits for an enrolment: the user has no second factor and the policy requires one.- Throws:
NullPointerException- IfsessionoruserNameis null.
getHeldSignIn
Gets the sign-in that waits for a client session.- Parameters:
session- The client session, null for none.- Returns:
- The sign-in, null when none waits or the one that did has expired.
release
Lets go of what is held for a client session: the sign-in and the enrolment that wait. The session has signed a user in without them, or the user went back to the start.- Parameters:
session- The client session, null for none.
completeSignIn
Completes the sign-in the client session holds, with a verification code or a recovery code. When the code is accepted the sign-in is no longer held; the caller then signs the user in and tells so (onSignedIn(Object, long, String)). AfterMAX_PENDING_FAILURESinvalid codes the sign-in is no longer held either.- Parameters:
session- The client session.method- The method the code is of.code- The code as typed.- Returns:
- The answer:
ACCEPTEDwith the user and the method,NO_PENDINGwhen nothing waits,NOT_ENROLLEDwhen the sign-in waits for an enrolment, or the refusal.
verify
Verifies a code of a user: the account's wait after invalid codes first, then the code, which is accepted once. An accepted code forgets the invalid ones; an invalid one is counted. A code that is not of the method given is invalid: a recovery code is no verification code and the reverse.- Parameters:
uid- The user ID.userName- The user name, for the log.method- The method the code is of,SignInPolicy.METHOD_TOTPorSignInPolicy.METHOD_RECOVERY.code- The code as typed, null for none.- Returns:
- The answer, never null.
beginEnrolment
public TotpEnrolment beginEnrolment(Object session, long uid, String userName, String issuer) throws UserMFA.Refusal Begins the enrolment of an authenticator app for a user: the secret is generated and held for the client session, and nothing is stored. An enrolment of this user the session holds already, and that waits, is shown AGAIN - the same secret, the same QR code - and waits anotherPENDING_ENROLMENT_MS: going back and forth on the set-up page mints no new secret silently (SIGNIN-TOTP-ENROL-RESTART-SILENT, CC-FIX-SIGNIN-TOTP-ENROL item 8);restartEnrolment(Object, long, String, String)is the one way to a new secret. An enrolment of another user the session held is replaced. A sign-in of this user that waits in the session is kept waiting as long as the enrolment.- Parameters:
session- The client session, the key of what is held.uid- The user ID.userName- The user name: the account the authenticator app shows.issuer- The issuer the authenticator app shows.- Returns:
- What the user needs to enrol the app.
- Throws:
UserMFA.Refusal-ALREADY_ENROLLED, orFAILEDwhen the database failed or has no tables of the second factor (logged).NullPointerException- If a reference parameter is null.
restartEnrolment
public TotpEnrolment restartEnrolment(Object session, long uid, String userName, String issuer) throws UserMFA.Refusal Begins the enrolment of an authenticator app for a user ANEW ("Start over", CC-FIX-SIGNIN-TOTP-ENROL): the enrolment the session held is dropped, whatever was shown before, and a new secret is generated asbeginEnrolment(Object, long, String, String)generates it the first time. The entry an authenticator app made of the old QR code is dead from now on.- Parameters:
session- The client session, the key of what is held.uid- The user ID.userName- The user name: the account the authenticator app shows.issuer- The issuer the authenticator app shows.- Returns:
- What the user needs to enrol the app.
- Throws:
UserMFA.Refusal-ALREADY_ENROLLED, orFAILEDwhen the database failed or has no tables of the second factor (logged).NullPointerException- If a reference parameter is null.
confirmEnrolment
public UserMFA.Confirmed confirmEnrolment(Object session, long uid, String code) throws UserMFA.Refusal Confirms the enrolment the client session holds with the first code of the authenticator app: the secret is sealed and stored, the time step of the code is the last accepted one (the code that confirmed is spent), the recovery codes are made and their hashes stored. A sign-in of the user that waited in the session is no longer held: the caller signs the user in and tells so (onSignedIn(Object, long, String)).- Parameters:
session- The client session.uid- The user ID of the user the enrolment is confirmed for: the user whose sign-in waits, or the user the session has signed in. The enrolment that waits is confirmed for the user who began it, and for no other.code- The code as typed.- Returns:
- The recovery codes, and whether a sign-in waited for the enrolment.
- Throws:
UserMFA.Refusal-NOT_ENROLLEDwhen no enrolment of this user waits,INVALIDfor an invalid code - with the drift of a clock the code would be right for (UserMFA.Refusal.driftSeconds, item 9), andUserMFA.Refusal.endedfor the fifth invalid code, which drops the enrolment (CC-FIX-SIGNIN-TOTP-ENROL) -,ALREADY_ENROLLEDwhen the user has got a second factor since,FAILEDwhen the database or the server's keys failed (logged).
onPasskeyRegistered
Tells that a client session has registered a passkey for a user, now: whatenrolPasskey(Object, long, String)makes the second factor of the user, when the user chose a passkey, forPENDING_ENROLMENT_MS. A passkey is registered with user verification only.- Parameters:
session- The client session, null for none.uid- The user ID.
enrolPasskey
public UserMFA.Confirmed enrolPasskey(Object session, long uid, String userName) throws UserMFA.Refusal Makes a passkey the second factor of a user (CC-FIX-SIGNIN-2FA-CHOICE): the user chose a passkey, and the client session registered one for the user a short time ago (onPasskeyRegistered(Object, long)). A user without a second factor has one now: the row of the methodpasskey, and the recovery codes, made here asconfirmEnrolment(Object, long, String)makes them. A user who has one keeps it, and its recovery codes. A sign-in of the user that waited in the session is no longer held: the caller signs the user in and tells so (onSignedIn(Object, long, String)).- Parameters:
session- The client session.uid- The user ID: the user whose sign-in waits, or the user the session has signed in. The passkey that was registered is one of this user, or nothing is done.userName- The user name, for the log.- Returns:
- The recovery codes, empty when the user had a second factor already, and whether a sign-in waited for this enrolment.
- Throws:
UserMFA.Refusal-NOT_ENROLLEDwhen the session registered no passkey of this user a short time ago,FAILEDwhen the database failed or has no tables of the second factor (logged).
remove
Removes the second factor of a user: the secret and every recovery code. Whether the user may is the caller's check, and the trust of the user's devices is the caller's to end.- Parameters:
uid- The user ID.userName- The user name, for the log.- Returns:
ACCEPTEDwhen it is removed,NOT_ENROLLEDwhen the user had none,FAILEDwhen the database failed (logged).
remove
Removes the authenticator app of a user (CC-FIX-SIGNIN-2FA-CHOICE): of a user who chose a passkey as the second factor and has one still (TOTP_AND_PASSKEY), the passkey is the second factor that is left, with the recovery codes; of every other user the second factor goes, asremove(long,String)removes it - also of the user whose passkeys were registered to sign in faster, as before. Whether the user may is the caller's check, and the trust of the user's devices is the caller's to end.- Parameters:
uid- The user ID.userName- The user name, for the log.passkeyLeft- Whether the user has a passkey that is the proof of a second factor.- Returns:
ACCEPTEDwhen it is removed,NOT_ENROLLEDwhen the user had no app,FAILEDwhen the database failed (logged).
removePasskeyFactor
Tells that the last passkey a user chose as the second factor was removed (CC-FIX-SIGNIN-2FA-CHOICE): the app beside it is the second factor that is left (TOTP_AND_PASSKEYbecomestotp); of a user who has no app, the second factor goes with its recovery codes, asremove(long,String)removes it. Whether the user may is the caller's check, and the trust of the user's devices is the caller's to end.- Parameters:
uid- The user ID.userName- The user name, for the log.- Returns:
ACCEPTEDwhen the second factor went,ALREADY_ENROLLEDwhen the app is left,NOT_ENROLLEDwhen no passkey was the user's second factor,FAILEDwhen the database failed (logged).
regenerateRecoveryCodes
Replaces the recovery codes of a user. Whether the user may is the caller's check.- Parameters:
uid- The user ID.userName- The user name, for the log.- Returns:
- The new recovery codes, in clear, this once.
- Throws:
UserMFA.Refusal-NOT_ENROLLEDwhen the user has no second factor,FAILEDwhen the database failed (logged).
onSignedIn
Tells that a session has signed a user in, now - or that the user it has signed in has verified a second factor in it now: an enrolment that was confirmed, a passkey that confirmed the user.- Parameters:
session- The session, null for none.uid- The user ID.method- The method of the second factor that was verified,SignInPolicy.METHOD_TOTP,SignInPolicy.METHOD_RECOVERY, or whatmethodOfPasskey(boolean)gives for a passkey; null when the sign-in verified none: the user has none, the device is trusted or stays signed in.
methodOfPasskey
Gets the method of the proof a passkey gives (CC-LOGIN-M11-PASSKEY-PROOF, ruling M-11 (b) of CC-SIGNIN-MERGE-L5-L6-L8). A passkey that was asserted with user verification is the proof of a second factor, as a verified code is: the passkey is what the user has, the verification what the user is or knows. An answer without user verification proves the device and no user: it is no proof. The server accepts no such answer; this is where the rule is, should one ever arrive.- Parameters:
userVerified- Whether the authenticator verified the user: the UV flag of the answer that was verified, not what the request asked for.- Returns:
SignInPolicy.METHOD_PASSKEY, the method to tellonSignedIn(Object, long, String); null for an answer that is no proof.
trustsDevice
Checks if a proof trusts a device for MFA: a device is trusted by a verification code, or by a passkey that was asserted with user verification (CC-LOGIN-M11-PASSKEY-PROOF) - never by a recovery code, which is what is left when the device is lost, and not by a sign-in that verified nothing.- Parameters:
proof- The proof of the session, null for none.- Returns:
- true when the session that has the proof may trust its device.
getProof
Gets the proof that the sign-in of a session verified the second factor of a user.- Parameters:
session- The session, null for none.uid- The user ID of the user the session has signed in.- Returns:
- The proof,
MFAProof.NONEwhen the session verified no second factor of this user.
forget
Forgets what is known of the sign-in of a session.- Parameters:
session- The session, null for none.