Class DeviceTokenStore

java.lang.Object
com.iizix.server.user.DeviceTokenStore

public final class DeviceTokenStore extends Object
The device tokens: what a device holds instead of a password to sign its user in again, and to be trusted for MFA (CC-LOGIN-L5-DEVICE-TOKEN, SignIn L-5 = HUB-DESIGN 7.7 and 14 row B-6e, R-22; the owner's rulings T-3, RB-4 (a) and RB-5 (a)). It replaces the password that DeviceAutoSignIn stored on the device and the password hash that was sent to the client at every sign-in.

The token. 32 bytes of SecureRandom, base64url (43 characters), known to the device alone: IZS.DEVICE_TOKENS holds its SHA-256, never the token, so that a copy of the table signs nobody in. A row is one user on one device in one application. 30 days from its last use, or the days the server is set to ($signIn.staySignedInDays); rotated at every use (redeem(String, String, String, boolean)): the device is given a new token and the one it presented is good for nothing more, except for a moment in which it is rotated again - two pages of one Browser start with the same stored token, and a device may lose the connection before it has stored the new one. A token that comes back after that moment is held by two parties: the row is deleted and both are signed out (DeviceTokenStore.Status.REUSED). Deleted at sign-out (revoke(long, String, String)), at every change of the user's password (revokeAll(Connection,long), called by BasicUserInfo inside the transaction of the change), with the user (the foreign key), and when expired (purgeExpired(), daily).

The device. An iiziRun device is its device UUID. Every Browser reports the same one, so a Browser is given an identifier of its own when it is first remembered (newDeviceID()), which it keeps, sealed, beside the token (RB-5 (a)). A token is accepted only for the application and the device it was issued to.

The table is created here when it is absent (ensureSchema()), not by createBaseTables.sql: that script runs on a database without tables only, so a database that exists would never get it. The members of a hub share the database; a table another server created at the same moment is used as found, as SubscriptionStore does it. Where the table cannot be created the store is unavailable: nothing is issued, nothing is accepted, and a sign-in with the password is not affected.

The SQL is SQL-92 and runs unchanged on Derby, PostgreSQL and MySQL. Nothing here logs a token or its hash. Thread-safe: every method takes its own connection and commits or rolls back it, except revokeAll(Connection,long), which works in its caller's transaction. The clock is injected, so that the tests drive time.

Author:
Christopher Mindus
  • Nested Class Summary

    Nested Classes
    Modifier and Type
    Class
    Description
    static final record 
    A token as issued.
    static final record 
    What became of a presented token.
    static enum 
    What a presented token is.
  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    static final long
    The time in milliseconds a token is still accepted, and rotated again, after the token that replaced it was issued: 30 seconds.
    static final long
    The lifetime of a token from its last use in milliseconds, unless the store is given another: 30 days (ruled T-3).
    static final int
    The most devices a user is remembered on; one more forgets the least recently used one.
    static final long
    The longest a device can be trusted for MFA in milliseconds: 90 days, the most the server setting $signIn.trustDeviceDays can be.
  • Constructor Summary

    Constructors
    Constructor
    Description
    Constructs the store on the system clock.
    Constructs the store on a clock.
    Constructs the store on a clock, with the lifetime of its tokens.
  • Method Summary

    Modifier and Type
    Method
    Description
    static String
    Abbreviates an identifier for a log line or a message: its first eight characters and "...", never the whole of a longer one - enough to tell two apart.
    int
    endMfaTrust(long uid)
    Ends the trust for MFA of every device of a user, in every application: what the removal of the user's second factor does, and its enrolment (CC-SIGNIN-MERGE-L5-L6-L8).
    boolean
    Creates the table when it is absent.
    boolean
    isMfaTrusted(long uid, String appID, String deviceID, String token)
    Checks if a device is trusted for MFA for a user now: the token it presents is a good one of that user for the application and the device - looked at, not used (peek(String, String, String)) - and the trust of its row has not ended.
    issue(long uid, String appID, String deviceID, String deviceName, boolean staySignedIn)
    Issues a token for a user on a device in an application.
    list(long uid, String currentAppID, String currentDeviceID)
    Lists the devices a user is remembered on, the most recently used first; the expired ones are left out.
    static String
    Makes a device identifier for a device that has none of its own: a Browser.
    peek(String appID, String deviceID, String token)
    Looks at a token a device presents without using it: verified as redeem(String, String, String, boolean) verifies it, not rotated, its row not touched - except that an expired or reused one is deleted.
    int
    Deletes the expired rows: the daily cleanup.
    redeem(String appID, String deviceID, String token, boolean forSignIn)
    Redeems a token a device presents: verifies it and replaces it by a new one, which the device is to be given.
    boolean
    revoke(long uid, String appID, String deviceID)
    Revokes a device of a user: deletes its row.
    int
    revokeAll(long uid)
    Revokes every device of a user.
    int
    revokeAll(Connection conn, long uid)
    Revokes every device of a user inside the caller's transaction: what a change of the user's password does, so that the devices are forgotten if and only if the password is changed.
    int
    revokeOthers(long uid, String appID, String deviceID)
    Revokes every device of a user but one.
    boolean
    setMfaTrust(long uid, String appID, String deviceID, long until)
    Trusts a device of a user for MFA until a time, or ends the trust.
  • Field Details

    • LIFETIME_MS

      public static final long LIFETIME_MS
      The lifetime of a token from its last use in milliseconds, unless the store is given another: 30 days (ruled T-3).
      See Also:
    • GRACE_MS

      public static final long GRACE_MS
      The time in milliseconds a token is still accepted, and rotated again, after the token that replaced it was issued: 30 seconds.
      See Also:
    • MAX_DEVICES_PER_USER

      public static final int MAX_DEVICES_PER_USER
      The most devices a user is remembered on; one more forgets the least recently used one.
      See Also:
    • MAX_MFA_TRUST_MS

      public static final long MAX_MFA_TRUST_MS
      The longest a device can be trusted for MFA in milliseconds: 90 days, the most the server setting $signIn.trustDeviceDays can be.
      See Also:
  • Constructor Details

    • DeviceTokenStore

      public DeviceTokenStore(ConnectionSource connections)
      Constructs the store on the system clock.
      Parameters:
      connections - The connection source, in the server DBUtilities::getPooledConnection.
    • DeviceTokenStore

      public DeviceTokenStore(ConnectionSource connections, LongSupplier clock)
      Constructs the store on a clock.
      Parameters:
      connections - The connection source.
      clock - The clock: epoch milliseconds.
    • DeviceTokenStore

      public DeviceTokenStore(ConnectionSource connections, LongSupplier clock, LongSupplier lifetime)
      Constructs the store on a clock, with the lifetime of its tokens.
      Parameters:
      connections - The connection source.
      clock - The clock: epoch milliseconds.
      lifetime - The lifetime of a token from its last use in milliseconds, asked at every issue and use; in the server the days of its setting $signIn.staySignedInDays. A lifetime that is not positive is LIFETIME_MS.
  • Method Details

    • ensureSchema

      public boolean ensureSchema() throws SQLException
      Creates the table when it is absent. Another server on the same database may do so at the same moment: a table it created first is used as found.
      Returns:
      true if the table was created here, false if it existed or another server made it.
      Throws:
      SQLException - For SQL errors: the table is absent and cannot be created.
    • newDeviceID

      public static String newDeviceID()
      Makes a device identifier for a device that has none of its own: a Browser.
      Returns:
      The identifier: B and 22 base64url characters of 16 random bytes.
    • issue

      public DeviceTokenStore.Issued issue(long uid, String appID, String deviceID, String deviceName, boolean staySignedIn) throws SQLException
      Issues a token for a user on a device in an application. A row the device had for the user and the application is replaced: its old token is good for nothing from now on, its first creation and its MFA trust are kept. A user remembered on MAX_DEVICES_PER_USER devices loses the least recently used one.
      Parameters:
      uid - The user.
      appID - The application.
      deviceID - The device; null or empty for a device without an identifier of its own, which is given one (newDeviceID()).
      deviceName - What the device says it is, for the devices list; cut to the column.
      staySignedIn - Whether the token is one to sign in with.
      Returns:
      The token and the device identifier of its row; null when the store is unavailable.
      Throws:
      NullPointerException - If appID is null.
      IllegalArgumentException - If appID or deviceID is longer than its column.
      SQLException - For SQL errors.
    • redeem

      public DeviceTokenStore.Redeemed redeem(String appID, String deviceID, String token, boolean forSignIn) throws SQLException
      Redeems a token a device presents: verifies it and replaces it by a new one, which the device is to be given. See the description of this class for what is accepted.
      Parameters:
      appID - The application of the session that presents it.
      deviceID - The device that presents it: the device UUID of an iiziRun device, the identifier the Browser kept beside the token.
      token - The token.
      forSignIn - true to sign in with it: a token that is not one to sign in with is then refused, and not rotated.
      Returns:
      What became of it; never null.
      Throws:
      NullPointerException - If a parameter is null.
      SQLException - For SQL errors.
    • peek

      public DeviceTokenStore.Redeemed peek(String appID, String deviceID, String token) throws SQLException
      Looks at a token a device presents without using it: verified as redeem(String, String, String, boolean) verifies it, not rotated, its row not touched - except that an expired or reused one is deleted.
      Parameters:
      appID - The application of the session that presents it.
      deviceID - The device that presents it.
      token - The token.
      Returns:
      What it is, with no token to give the device; never null.
      Throws:
      NullPointerException - If a parameter is null.
      SQLException - For SQL errors.
    • revoke

      public boolean revoke(long uid, String appID, String deviceID) throws SQLException
      Revokes a device of a user: deletes its row.
      Parameters:
      uid - The user.
      appID - The application.
      deviceID - The device.
      Returns:
      true if there was one.
      Throws:
      SQLException - For SQL errors.
    • revokeOthers

      public int revokeOthers(long uid, String appID, String deviceID) throws SQLException
      Revokes every device of a user but one.
      Parameters:
      uid - The user.
      appID - The application of the device kept.
      deviceID - The device kept; null for none, which is revokeAll(long).
      Returns:
      The number of devices revoked.
      Throws:
      SQLException - For SQL errors.
    • revokeAll

      public int revokeAll(long uid) throws SQLException
      Revokes every device of a user.
      Parameters:
      uid - The user.
      Returns:
      The number of devices revoked.
      Throws:
      SQLException - For SQL errors.
    • revokeAll

      public int revokeAll(Connection conn, long uid) throws SQLException
      Revokes every device of a user inside the caller's transaction: what a change of the user's password does, so that the devices are forgotten if and only if the password is changed. Nothing is committed here. A database without the table has no device to revoke: the table is looked for, never created, and its absence is not an error.
      Parameters:
      conn - The connection of the caller's transaction.
      uid - The user.
      Returns:
      The number of devices revoked.
      Throws:
      SQLException - For SQL errors.
    • purgeExpired

      public int purgeExpired() throws SQLException
      Deletes the expired rows: the daily cleanup.
      Returns:
      The number of rows deleted.
      Throws:
      SQLException - For SQL errors.
    • list

      public List<SignedInDevice> list(long uid, String currentAppID, String currentDeviceID) throws SQLException
      Lists the devices a user is remembered on, the most recently used first; the expired ones are left out.
      Parameters:
      uid - The user.
      currentAppID - The application of the device to mark as the current one, null for none.
      currentDeviceID - The device to mark as the current one, null for none.
      Returns:
      The devices, possibly none.
      Throws:
      SQLException - For SQL errors.
    • setMfaTrust

      public boolean setMfaTrust(long uid, String appID, String deviceID, long until) throws SQLException
      Trusts a device of a user for MFA until a time, or ends the trust.
      Parameters:
      uid - The user.
      appID - The application.
      deviceID - The device.
      until - Until when, in epoch milliseconds; zero to end the trust. At most MAX_MFA_TRUST_MS from now: a later time is cut to it.
      Returns:
      true if the device has a row, which is updated.
      Throws:
      SQLException - For SQL errors.
    • isMfaTrusted

      public boolean isMfaTrusted(long uid, String appID, String deviceID, String token) throws SQLException
      Checks if a device is trusted for MFA for a user now: the token it presents is a good one of that user for the application and the device - looked at, not used (peek(String, String, String)) - and the trust of its row has not ended. The trust is bound to the password the device was trusted under by what a token is: every change of the password deletes the rows of the user (revokeAll(Connection,long)) (CC-SIGNIN-MERGE-L5-L6-L8).
      Parameters:
      uid - The user whose second factor would be asked for.
      appID - The application of the session that presents the token.
      deviceID - The device that presents it.
      token - The token.
      Returns:
      true when the second factor of the user may be skipped on the device.
      Throws:
      NullPointerException - If a reference parameter is null.
      SQLException - For SQL errors.
    • endMfaTrust

      public int endMfaTrust(long uid) throws SQLException
      Ends the trust for MFA of every device of a user, in every application: what the removal of the user's second factor does, and its enrolment (CC-SIGNIN-MERGE-L5-L6-L8). The rows stay: a device that stays signed in stays signed in.
      Parameters:
      uid - The user.
      Returns:
      The number of devices whose trust was ended.
      Throws:
      SQLException - For SQL errors.
    • abbreviate

      public static String abbreviate(String id)
      Abbreviates an identifier for a log line or a message: its first eight characters and "...", never the whole of a longer one - enough to tell two apart. The server's one abbreviation of a device identifier, a VAPID key fingerprint, or anything else a line must name without printing it whole (I-D-13, CC-FIX-PUSH-KEYID-LOG).
      Parameters:
      id - The identifier, possibly null.
      Returns:
      "-" for null, the identifier itself when it has eight characters or fewer, otherwise its first eight characters followed by "...".