diff --git a/CHANGELOG.md b/CHANGELOG.md index d389d895..568e70ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- Introduce TOTP Recipe plugin interface +- Introduce Active users storage plugin interface + ## [2.20.0] - 2023-02-21 - Dashboard Recipe Interface diff --git a/src/main/java/io/supertokens/pluginInterface/ActiveUsersStorage.java b/src/main/java/io/supertokens/pluginInterface/ActiveUsersStorage.java new file mode 100644 index 00000000..ed979ef8 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/ActiveUsersStorage.java @@ -0,0 +1,17 @@ +package io.supertokens.pluginInterface; + +import io.supertokens.pluginInterface.exceptions.StorageQueryException; + +public interface ActiveUsersStorage extends Storage { + /* Update the last active time of a user to now */ + void updateLastActive(String userId) throws StorageQueryException; + + /* Count the number of users who did some activity after given timestamp */ + int countUsersActiveSince(long time) throws StorageQueryException; + + /* Count the number of users who have enabled TOTP */ + int countUsersEnabledTotp() throws StorageQueryException; + + /* Count the number of users who have enabled TOTP and are active */ + int countUsersEnabledTotpAndActiveSince(long time) throws StorageQueryException; +} diff --git a/src/main/java/io/supertokens/pluginInterface/RECIPE_ID.java b/src/main/java/io/supertokens/pluginInterface/RECIPE_ID.java index 2e4aa263..7456f85f 100644 --- a/src/main/java/io/supertokens/pluginInterface/RECIPE_ID.java +++ b/src/main/java/io/supertokens/pluginInterface/RECIPE_ID.java @@ -21,7 +21,7 @@ public enum RECIPE_ID { EMAIL_PASSWORD("emailpassword"), THIRD_PARTY("thirdparty"), SESSION("session"), EMAIL_VERIFICATION("emailverification"), JWT("jwt"), PASSWORDLESS("passwordless"), USER_METADATA("usermetadata"), - USER_ROLES("userroles"), USER_ID_MAPPING("useridmapping"), DASHBOARD("dashboard"); + USER_ROLES("userroles"), USER_ID_MAPPING("useridmapping"), DASHBOARD("dashboard"), TOTP("totp"); private final String name; diff --git a/src/main/java/io/supertokens/pluginInterface/totp/TOTPDevice.java b/src/main/java/io/supertokens/pluginInterface/totp/TOTPDevice.java new file mode 100644 index 00000000..5709efc4 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/totp/TOTPDevice.java @@ -0,0 +1,36 @@ +package io.supertokens.pluginInterface.totp; + +public class TOTPDevice { + public final String deviceName; + public final String userId; + public final String secretKey; + public final int period; + public final int skew; + public final boolean verified; + + public TOTPDevice(String userId, String deviceName, String secretKey, int period, int skew, boolean verified) { + this.userId = userId; + this.deviceName = deviceName; + this.secretKey = secretKey; + this.period = period; + this.skew = skew; + this.verified = verified; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (!(obj instanceof TOTPDevice)) { + return false; + } + TOTPDevice other = (TOTPDevice) obj; + return this.userId.equals(other.userId) && this.deviceName.equals(other.deviceName) + && this.secretKey.equals(other.secretKey) && this.period == other.period && this.skew == other.skew + && this.verified == other.verified; + } +} diff --git a/src/main/java/io/supertokens/pluginInterface/totp/TOTPStorage.java b/src/main/java/io/supertokens/pluginInterface/totp/TOTPStorage.java new file mode 100644 index 00000000..3618de21 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/totp/TOTPStorage.java @@ -0,0 +1,29 @@ +package io.supertokens.pluginInterface.totp; + +import io.supertokens.pluginInterface.nonAuthRecipe.NonAuthRecipeStorage; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.totp.exception.DeviceAlreadyExistsException; +import io.supertokens.pluginInterface.totp.exception.UnknownDeviceException; + +public interface TOTPStorage extends NonAuthRecipeStorage { + /** Create a new device and a new user if the user does not exist: */ + void createDevice(TOTPDevice device) + throws StorageQueryException, DeviceAlreadyExistsException; + + /** Verify a user's device with the given name: */ + void markDeviceAsVerified(String userId, String deviceName) + throws StorageQueryException, UnknownDeviceException; + + /** Update device name of a device: */ + void updateDeviceName(String userId, String oldDeviceName, String newDeviceName) + throws StorageQueryException, DeviceAlreadyExistsException, + UnknownDeviceException; + + /** Get the devices for a user */ + TOTPDevice[] getDevices(String userId) + throws StorageQueryException; + + /** Remove expired codes from totp used codes for all users: */ + int removeExpiredCodes(long expiredBefore) + throws StorageQueryException; +} diff --git a/src/main/java/io/supertokens/pluginInterface/totp/TOTPUsedCode.java b/src/main/java/io/supertokens/pluginInterface/totp/TOTPUsedCode.java new file mode 100644 index 00000000..d2fb9a74 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/totp/TOTPUsedCode.java @@ -0,0 +1,34 @@ +package io.supertokens.pluginInterface.totp; + +public class TOTPUsedCode { + public final String userId; + public final String code; + public final boolean isValid; + public final long expiryTime; + public final long createdTime; + + public TOTPUsedCode(String userId, String code, Boolean isValidCode, long expiryTime, long createdTime) { + this.userId = userId; + this.code = code; + this.isValid = isValidCode; + this.expiryTime = expiryTime; + this.createdTime = createdTime; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (!(obj instanceof TOTPUsedCode)) { + return false; + } + TOTPUsedCode other = (TOTPUsedCode) obj; + return this.userId.equals(other.userId) && this.code.equals(other.code) + && this.isValid == other.isValid && this.expiryTime == other.expiryTime + && this.createdTime == other.createdTime; + } +} diff --git a/src/main/java/io/supertokens/pluginInterface/totp/exception/DeviceAlreadyExistsException.java b/src/main/java/io/supertokens/pluginInterface/totp/exception/DeviceAlreadyExistsException.java new file mode 100644 index 00000000..edf61122 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/totp/exception/DeviceAlreadyExistsException.java @@ -0,0 +1,5 @@ +package io.supertokens.pluginInterface.totp.exception; + +public class DeviceAlreadyExistsException extends Exception { + private static final long serialVersionUID = 6848053563771647272L; +} diff --git a/src/main/java/io/supertokens/pluginInterface/totp/exception/TotpNotEnabledException.java b/src/main/java/io/supertokens/pluginInterface/totp/exception/TotpNotEnabledException.java new file mode 100644 index 00000000..bb702ef5 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/totp/exception/TotpNotEnabledException.java @@ -0,0 +1,5 @@ +package io.supertokens.pluginInterface.totp.exception; + +public class TotpNotEnabledException extends Exception { + private static final long serialVersionUID = 6848053563771647272L; +} diff --git a/src/main/java/io/supertokens/pluginInterface/totp/exception/UnknownDeviceException.java b/src/main/java/io/supertokens/pluginInterface/totp/exception/UnknownDeviceException.java new file mode 100644 index 00000000..86bf659a --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/totp/exception/UnknownDeviceException.java @@ -0,0 +1,5 @@ +package io.supertokens.pluginInterface.totp.exception; + +public class UnknownDeviceException extends Exception { + private static final long serialVersionUID = 6848053563771647272L; +} diff --git a/src/main/java/io/supertokens/pluginInterface/totp/exception/UsedCodeAlreadyExistsException.java b/src/main/java/io/supertokens/pluginInterface/totp/exception/UsedCodeAlreadyExistsException.java new file mode 100644 index 00000000..7ada2a2f --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/totp/exception/UsedCodeAlreadyExistsException.java @@ -0,0 +1,5 @@ +package io.supertokens.pluginInterface.totp.exception; + +public class UsedCodeAlreadyExistsException extends Exception { + +} diff --git a/src/main/java/io/supertokens/pluginInterface/totp/sqlStorage/TOTPSQLStorage.java b/src/main/java/io/supertokens/pluginInterface/totp/sqlStorage/TOTPSQLStorage.java new file mode 100644 index 00000000..d9261de4 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/totp/sqlStorage/TOTPSQLStorage.java @@ -0,0 +1,34 @@ +package io.supertokens.pluginInterface.totp.sqlStorage; + +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.sqlStorage.SQLStorage; +import io.supertokens.pluginInterface.sqlStorage.TransactionConnection; +import io.supertokens.pluginInterface.totp.TOTPDevice; +import io.supertokens.pluginInterface.totp.TOTPStorage; +import io.supertokens.pluginInterface.totp.TOTPUsedCode; +import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; +import io.supertokens.pluginInterface.totp.exception.UsedCodeAlreadyExistsException; + +public interface TOTPSQLStorage extends TOTPStorage, SQLStorage { + public int deleteDevice_Transaction(TransactionConnection con, String userId, String deviceName) + throws StorageQueryException; + + public TOTPDevice[] getDevices_Transaction(TransactionConnection con, String userId) + throws StorageQueryException; + + public void removeUser_Transaction(TransactionConnection con, String userId) + throws StorageQueryException; + + /** + * Get totp used codes for user (expired/non-expired) yet (sorted by descending + * order of created time): + */ + public TOTPUsedCode[] getAllUsedCodesDescOrder_Transaction(TransactionConnection con, + String userId) + throws StorageQueryException; + + /** Insert a used TOTP code for an existing user: */ + void insertUsedCode_Transaction(TransactionConnection con, TOTPUsedCode code) + throws StorageQueryException, TotpNotEnabledException, UsedCodeAlreadyExistsException; + +}