Class DeviceTokenStore
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 ClassesModifier and TypeClassDescriptionstatic final recordA token as issued.static final recordWhat became of a presented token.static enumWhat a presented token is.Field Summary
FieldsModifier and TypeFieldDescriptionstatic final longThe time in milliseconds a token is still accepted, and rotated again, after the token that replaced it was issued: 30 seconds.static final longThe lifetime of a token from its last use in milliseconds, unless the store is given another: 30 days (ruled T-3).static final intThe most devices a user is remembered on; one more forgets the least recently used one.static final longThe longest a device can be trusted for MFA in milliseconds: 90 days, the most the server setting$signIn.trustDeviceDayscan be.Constructor Summary
ConstructorsConstructorDescriptionDeviceTokenStore(ConnectionSource connections) Constructs the store on the system clock.DeviceTokenStore(ConnectionSource connections, LongSupplier clock) Constructs the store on a clock.DeviceTokenStore(ConnectionSource connections, LongSupplier clock, LongSupplier lifetime) Constructs the store on a clock, with the lifetime of its tokens.Method Summary
Modifier and TypeMethodDescriptionstatic Stringabbreviate(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.intendMfaTrust(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).booleanCreates the table when it is absent.booleanisMfaTrusted(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.Issues a token for a user on a device in an application.Lists the devices a user is remembered on, the most recently used first; the expired ones are left out.static StringMakes a device identifier for a device that has none of its own: a Browser.Looks at a token a device presents without using it: verified asredeem(String, String, String, boolean)verifies it, not rotated, its row not touched - except that an expired or reused one is deleted.intDeletes the expired rows: the daily cleanup.Redeems a token a device presents: verifies it and replaces it by a new one, which the device is to be given.booleanRevokes a device of a user: deletes its row.intrevokeAll(long uid) Revokes every device of a user.intrevokeAll(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.intrevokeOthers(long uid, String appID, String deviceID) Revokes every device of a user but one.booleansetMfaTrust(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_MSThe 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_MSThe 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_USERThe 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_MSThe longest a device can be trusted for MFA in milliseconds: 90 days, the most the server setting$signIn.trustDeviceDayscan be.- See Also:
Constructor Details
DeviceTokenStore
Constructs the store on the system clock.- Parameters:
connections- The connection source, in the serverDBUtilities::getPooledConnection.
DeviceTokenStore
Constructs the store on a clock.- Parameters:
connections- The connection source.clock- The clock: epoch milliseconds.
DeviceTokenStore
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 isLIFETIME_MS.
Method Details
ensureSchema
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
Makes a device identifier for a device that has none of its own: a Browser.- Returns:
- The identifier:
Band 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 onMAX_DEVICES_PER_USERdevices 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- IfappIDis null.IllegalArgumentException- IfappIDordeviceIDis 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 asredeem(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
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
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 isrevokeAll(long).- Returns:
- The number of devices revoked.
- Throws:
SQLException- For SQL errors.
revokeAll
Revokes every device of a user.- Parameters:
uid- The user.- Returns:
- The number of devices revoked.
- Throws:
SQLException- For SQL errors.
revokeAll
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
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
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 mostMAX_MFA_TRUST_MSfrom 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
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
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"...".