Skip to content

Commit 1ff8f81

Browse files
authored
feat: adds migration api and queries (#503)
* feat: adds migration api and queries * fixes * fixes * removes unnecessary query * refactor, starts adding tests * fixes * fixes * adds remaining tests for passwordhashes * fixes * fixes comment * udpates CHANGELOG and dependencies * minor fixes * implements some feedback * comment fixes * fixes * fix * implements feedback * updates CHANGELOG.md and versio * fixes * adds hashingAlgorithm field param * fixes * implements feedback * fixes * test fixes * marks function as testonly * adds test for checking hahsing algorithm with lowercase and upper case names * remvoes unnecessary enum * feat: Adds support to migrate users from Firebase (#507) * progress on adding firebase scrypt * implements feedback * removes unnecessary spacing * adds poolsize for firebase scrypt hashing * removes unnecessary file * removes unnecessary statements * adds some feedback * fixes configs * adds comment * implements feedback * fixs comment * adds more tests; test fixs * adds test to check that not setting signer keys throws error * adds more tests and fixes * adds test for signing in when wrong signer key is set * adds test for firebase scrypt validation with different config values * adds tests for firebase scrypt pool checking * fixs for test * feedback changes * test fixs * adds additional test check * updates CHANGELOG.md and fixs typo * updates CHANGELOG.md * updates CHANGELOG.md * test fixs * fixs * updates query * updates CHANGELOG.md
1 parent 42fba64 commit 1ff8f81

23 files changed

+1936
-31
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,26 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [unreleased]
99

10+
## [4.0.0] - 2022-09-19
11+
1012
### Added
1113

14+
- EmailPassword User migration API which allows you to import users with their email and password hashes.
15+
- Support to import users with password hashes from Firebase
16+
- Support with CDI version `2.16`
1217
- Hello API on `/` route.
1318

19+
### Database Changes
20+
- Updates the `password_hash` column in the `emailpassword_users` table from `VARCHAR(128)` to `VARCHAR(256)` to support more password hash lengths.
21+
- Updates the `third_party_user_id` column in the `thirdparty_users` table from `VARCHAR(128)` to `VARCHAR(256)` to resolve https://github.com/supertokens/supertokens-core/issues/306
22+
23+
- For legacy users who are self hosting the SuperTokens core run the following command to update your database with the changes:
24+
- With MySql:
25+
`ALTER TABLE thirdparty_users MODIFY third_party_user_id VARCHAR(256); ALTER TABLE emailpassword_users MODIFY password_hash VARCHAR(256);`
26+
- With PostgreSQL:
27+
`ALTER TABLE thirdparty_users MODIFY third_party_user_id VARCHAR(256); ALTER TABLE emailpassword_users MODIFY password_hash VARCHAR(256);`
28+
29+
1430
## [3.16.2] - 2022-09-02
1531

1632
### Bug fixes

build.gradle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ compileTestJava { options.encoding = "UTF-8" }
1919
// }
2020
//}
2121

22-
version = "3.16.2"
22+
version = "4.0.0"
2323

2424

2525
repositories {
@@ -61,6 +61,9 @@ dependencies {
6161
// https://mvnrepository.com/artifact/com.auth0/java-jwt
6262
implementation 'com.auth0:java-jwt:4.0.0'
6363

64+
// https://mvnrepository.com/artifact/com.lambdaworks/scrypt
65+
implementation group: 'com.lambdaworks', name: 'scrypt', version: '1.4.0'
66+
6467
compileOnly project(":supertokens-plugin-interface")
6568
testImplementation project(":supertokens-plugin-interface")
6669

config.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,10 @@ core_config_version: 0
105105
# argon2_hashing_pool_size:
106106

107107
# (OPTIONAL | Default: "INFO"). Logging level for the core. Values are "DEBUG" | "INFO" | "WARN" | "ERROR" | "NONE"
108-
# log_level:
108+
# log_level:
109+
110+
# (OPTIONAL | Default: null). The signer key used for firebase scrypt password hashing
111+
# firebase_password_hashing_signer_key:
112+
113+
# (OPTIONAL | Default: 1). Number of concurrent firebase scrypt hashes that can happen at the same time for sign in requests.
114+
# firebase_password_hashing_pool_size:

coreDriverInterfaceSupported.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"2.12",
1010
"2.13",
1111
"2.14",
12-
"2.15"
12+
"2.15",
13+
"2.16"
1314
]
1415
}

devConfig.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,10 @@ disable_telemetry: true
105105
# argon2_hashing_pool_size:
106106

107107
# (OPTIONAL | Default: "INFO"). Logging level for the core. Values are "DEBUG" | "INFO" | "WARN" | "ERROR" | "NONE"
108-
# log_level:
108+
# log_level:
109+
110+
# (OPTIONAL | Default: null). The signer key used for firebase scrypt password hashing
111+
# firebase_password_hashing_signer_key:
112+
113+
# (OPTIONAL | Default: 1). Number of concurrent firebase scrypt hashes that can happen at the same time for sign in requests.
114+
# firebase_password_hashing_pool_size:

implementationDependencies.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@
9595
"jar": "https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.8.0/jna-5.8.0.jar",
9696
"name": "JNA 5.8.0",
9797
"src": "https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.8.0/jna-5.8.0-sources.jar"
98+
},
99+
{
100+
"jar": "https://repo1.maven.org/maven2/com/lambdaworks/scrypt/1.4.0/scrypt-1.4.0-javadoc.jar",
101+
"name": "Scrypt 1.4.0",
102+
"src": "https://repo1.maven.org/maven2/com/lambdaworks/scrypt/1.4.0/scrypt-1.4.0-sources.jar"
98103
}
99104
]
100105
}

src/main/java/io/supertokens/ProcessState.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ public enum PROCESS_STATE {
7777
INIT, INIT_FAILURE, STARTED, SHUTTING_DOWN, STOPPED, RETRYING_ACCESS_TOKEN_JWT_VERIFICATION,
7878
CRON_TASK_ERROR_LOGGING, WAITING_TO_INIT_STORAGE_MODULE, GET_SESSION_NEW_TOKENS, DEADLOCK_FOUND,
7979
CREATING_NEW_TABLE, SENDING_TELEMETRY, SENT_TELEMETRY, SETTING_ACCESS_TOKEN_SIGNING_KEY_TO_NULL,
80-
PASSWORD_HASH_BCRYPT, PASSWORD_HASH_ARGON, PASSWORD_VERIFY_BCRYPT, PASSWORD_VERIFY_ARGON
80+
PASSWORD_HASH_BCRYPT, PASSWORD_HASH_ARGON, PASSWORD_VERIFY_BCRYPT, PASSWORD_VERIFY_ARGON,
81+
PASSWORD_VERIFY_FIREBASE_SCRYPT
8182
}
8283

8384
public static class EventAndException {

src/main/java/io/supertokens/config/CoreConfig.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ public class CoreConfig {
9999
@JsonProperty
100100
private int argon2_hashing_pool_size = 1;
101101

102+
@JsonProperty
103+
private int firebase_password_hashing_pool_size = 1;
104+
102105
@JsonProperty
103106
private int bcrypt_log_rounds = 11;
104107

@@ -116,6 +119,9 @@ public class CoreConfig {
116119
@JsonProperty
117120
private String log_level = "INFO";
118121

122+
@JsonProperty
123+
private String firebase_password_hashing_signer_key = null;
124+
119125
private Set<LOG_LEVEL> allowedLogLevels = null;
120126

121127
public Set<LOG_LEVEL> getLogLevels(Main main) {
@@ -161,7 +167,7 @@ public String getBasePath() {
161167
}
162168

163169
public enum PASSWORD_HASHING_ALG {
164-
ARGON2, BCRYPT
170+
ARGON2, BCRYPT, FIREBASE_SCRYPT
165171
}
166172

167173
public int getArgon2HashingPoolSize() {
@@ -172,6 +178,10 @@ public int getArgon2HashingPoolSize() {
172178
return Math.max(1, argon2_hashing_pool_size);
173179
}
174180

181+
public int getFirebaseSCryptPasswordHashingPoolSize() {
182+
return Math.max(1, firebase_password_hashing_pool_size);
183+
}
184+
175185
public int getArgon2Iterations() {
176186
return argon2_iterations;
177187
}
@@ -188,6 +198,13 @@ public int getArgon2Parallelism() {
188198
return argon2_parallelism;
189199
}
190200

201+
public String getFirebase_password_hashing_signer_key() {
202+
if (firebase_password_hashing_signer_key == null) {
203+
throw new IllegalStateException("'firebase_password_hashing_signer_key' cannot be null");
204+
}
205+
return firebase_password_hashing_signer_key;
206+
}
207+
191208
public PASSWORD_HASHING_ALG getPasswordHashingAlg() {
192209
return PASSWORD_HASHING_ALG.valueOf(password_hashing_alg.toUpperCase());
193210
}

src/main/java/io/supertokens/emailpassword/EmailPassword.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
import io.supertokens.Main;
2020
import io.supertokens.authRecipe.UserPaginationToken;
2121
import io.supertokens.config.Config;
22+
import io.supertokens.config.CoreConfig;
2223
import io.supertokens.emailpassword.exceptions.ResetPasswordInvalidTokenException;
24+
import io.supertokens.emailpassword.exceptions.UnsupportedPasswordHashingFormatException;
2325
import io.supertokens.emailpassword.exceptions.WrongCredentialsException;
2426
import io.supertokens.pluginInterface.emailpassword.PasswordResetTokenInfo;
2527
import io.supertokens.pluginInterface.emailpassword.UserInfo;
@@ -36,12 +38,23 @@
3638

3739
import javax.annotation.Nonnull;
3840
import javax.annotation.Nullable;
41+
import javax.servlet.ServletException;
3942
import java.security.NoSuchAlgorithmException;
4043
import java.security.SecureRandom;
4144
import java.security.spec.InvalidKeySpecException;
4245

4346
public class EmailPassword {
4447

48+
public static class ImportUserResponse {
49+
public boolean didUserAlreadyExist;
50+
public UserInfo user;
51+
52+
public ImportUserResponse(boolean didUserAlreadyExist, UserInfo user) {
53+
this.didUserAlreadyExist = didUserAlreadyExist;
54+
this.user = user;
55+
}
56+
}
57+
4558
@TestOnly
4659
public static long getPasswordResetTokenLifetimeForTests(Main main) {
4760
return getPasswordResetTokenLifetime(main);
@@ -76,6 +89,46 @@ public static UserInfo signUp(Main main, @Nonnull String email, @Nonnull String
7689
}
7790
}
7891

92+
public static ImportUserResponse importUserWithPasswordHash(Main main, @Nonnull String email,
93+
@Nonnull String passwordHash, @Nullable CoreConfig.PASSWORD_HASHING_ALG hashingAlgorithm)
94+
throws StorageQueryException, StorageTransactionLogicException, UnsupportedPasswordHashingFormatException {
95+
96+
PasswordHashingUtils.assertSuperTokensSupportInputPasswordHashFormat(main, passwordHash, hashingAlgorithm);
97+
98+
while (true) {
99+
String userId = Utils.getUUID();
100+
long timeJoined = System.currentTimeMillis();
101+
102+
UserInfo userInfo = new UserInfo(userId, email, passwordHash, timeJoined);
103+
EmailPasswordSQLStorage storage = StorageLayer.getEmailPasswordStorage(main);
104+
105+
try {
106+
StorageLayer.getEmailPasswordStorage(main).signUp(userInfo);
107+
return new ImportUserResponse(false, userInfo);
108+
} catch (DuplicateUserIdException e) {
109+
// we retry with a new userId
110+
} catch (DuplicateEmailException e) {
111+
UserInfo userInfoToBeUpdated = StorageLayer.getEmailPasswordStorage(main).getUserInfoUsingEmail(email);
112+
// if user does not exist we retry signup
113+
if (userInfoToBeUpdated != null) {
114+
String finalPasswordHash = passwordHash;
115+
storage.startTransaction(con -> {
116+
storage.updateUsersPassword_Transaction(con, userInfoToBeUpdated.id, finalPasswordHash);
117+
return null;
118+
});
119+
return new ImportUserResponse(true, userInfoToBeUpdated);
120+
}
121+
}
122+
}
123+
}
124+
125+
@TestOnly
126+
public static ImportUserResponse importUserWithPasswordHash(Main main, @Nonnull String email,
127+
@Nonnull String passwordHash)
128+
throws StorageQueryException, StorageTransactionLogicException, UnsupportedPasswordHashingFormatException {
129+
return importUserWithPasswordHash(main, email, passwordHash, null);
130+
}
131+
79132
public static UserInfo signIn(Main main, @Nonnull String email, @Nonnull String password)
80133
throws StorageQueryException, WrongCredentialsException {
81134

@@ -91,6 +144,12 @@ public static UserInfo signIn(Main main, @Nonnull String email, @Nonnull String
91144
}
92145
} catch (WrongCredentialsException e) {
93146
throw e;
147+
} catch (IllegalStateException e) {
148+
if (e.getMessage().equals("'firebase_password_hashing_signer_key' cannot be null")) {
149+
throw e;
150+
}
151+
throw new WrongCredentialsException();
152+
94153
} catch (Exception ignored) {
95154
throw new WrongCredentialsException();
96155
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved.
3+
*
4+
* This software is licensed under the Apache License, Version 2.0 (the
5+
* "License") as published by the Apache Software Foundation.
6+
*
7+
* You may not use this file except in compliance with the License. You may
8+
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package io.supertokens.emailpassword;
18+
19+
public class ParsedFirebaseSCryptResponse {
20+
public String passwordHash;
21+
public String salt;
22+
public String saltSeparator;
23+
public int rounds;
24+
public int memCost;
25+
26+
public static final String FIREBASE_SCRYPT_PREFIX = "f_scrypt";
27+
private static final String FIREBASE_SCRYPT_SEPARATOR = "\\$";
28+
private static final String FIREBASE_SCRYPT_MEM_COST_SEPARATOR = "m=";
29+
private static final String FIREBASE_SCRYPT_ROUNDS_SEPARATOR = "r=";
30+
private static final String FIREBASE_SCRYPT_SALT_SEPARATOR = "s=";
31+
32+
public ParsedFirebaseSCryptResponse(String passwordHash, String salt, String saltSeparator, int rounds,
33+
int memCost) {
34+
this.passwordHash = passwordHash;
35+
this.salt = salt;
36+
this.saltSeparator = saltSeparator;
37+
this.rounds = rounds;
38+
this.memCost = memCost;
39+
}
40+
41+
public static ParsedFirebaseSCryptResponse fromHashString(String hash) {
42+
try {
43+
String[] separatedPasswordHash = hash.split(FIREBASE_SCRYPT_SEPARATOR);
44+
45+
// check that stored password hash contains 7 fields and after splitting, the first field is empty and the
46+
// second field has the firebase scrypt prefix
47+
if (!(separatedPasswordHash.length == 7 && separatedPasswordHash[0].equals("")
48+
&& separatedPasswordHash[1].equals(FIREBASE_SCRYPT_PREFIX))) {
49+
return null;
50+
}
51+
52+
String passwordHash = separatedPasswordHash[2];
53+
String salt = separatedPasswordHash[3];
54+
String saltSeparator = null;
55+
Integer memCost = null;
56+
Integer rounds = null;
57+
58+
for (int i = 4; i < separatedPasswordHash.length; i++) {
59+
if (separatedPasswordHash[i].startsWith(FIREBASE_SCRYPT_MEM_COST_SEPARATOR)) {
60+
memCost = Integer.parseInt(separatedPasswordHash[i].split(FIREBASE_SCRYPT_MEM_COST_SEPARATOR)[1]);
61+
continue;
62+
}
63+
if (separatedPasswordHash[i].startsWith(FIREBASE_SCRYPT_ROUNDS_SEPARATOR)) {
64+
rounds = Integer.parseInt(separatedPasswordHash[i].split(FIREBASE_SCRYPT_ROUNDS_SEPARATOR)[1]);
65+
continue;
66+
}
67+
if (separatedPasswordHash[i].startsWith(FIREBASE_SCRYPT_SALT_SEPARATOR)) {
68+
saltSeparator = separatedPasswordHash[i].split(FIREBASE_SCRYPT_SALT_SEPARATOR)[1];
69+
}
70+
}
71+
72+
if (passwordHash == null || salt == null || saltSeparator == null || memCost == null || rounds == null) {
73+
return null;
74+
}
75+
return new ParsedFirebaseSCryptResponse(passwordHash, salt, saltSeparator, rounds, memCost);
76+
} catch (Throwable e) {
77+
return null;
78+
}
79+
}
80+
}

0 commit comments

Comments
 (0)