Skip to content

Commit 623a051

Browse files
authored
Merge pull request #172 from supertokens/feat/webauthn-1
feat: webauthn support
2 parents 22f2008 + 8c35eb3 commit 623a051

18 files changed

+438
-3
lines changed

src/main/java/io/supertokens/pluginInterface/RECIPE_ID.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public enum RECIPE_ID {
2222
EMAIL_PASSWORD("emailpassword"), THIRD_PARTY("thirdparty"), SESSION("session"),
2323
EMAIL_VERIFICATION("emailverification"), JWT("jwt"), PASSWORDLESS("passwordless"), USER_METADATA("usermetadata"),
2424
USER_ROLES("userroles"), USER_ID_MAPPING("useridmapping"), DASHBOARD("dashboard"), TOTP("totp"),
25-
MULTITENANCY("multitenancy"), ACCOUNT_LINKING("accountlinking"), MFA("mfa"), OAUTH("oauth");
25+
MULTITENANCY("multitenancy"), ACCOUNT_LINKING("accountlinking"), MFA("mfa"), OAUTH("oauth"), WEBAUTHN("webauthn");
2626

2727
private final String name;
2828

src/main/java/io/supertokens/pluginInterface/StorageUtils.java

+8
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import io.supertokens.pluginInterface.useridmapping.UserIdMappingStorage;
3131
import io.supertokens.pluginInterface.usermetadata.sqlStorage.UserMetadataSQLStorage;
3232
import io.supertokens.pluginInterface.userroles.sqlStorage.UserRolesSQLStorage;
33+
import io.supertokens.pluginInterface.webauthn.slqStorage.WebAuthNSQLStorage;
3334

3435
public class StorageUtils {
3536
public static AuthRecipeSQLStorage getAuthRecipeStorage(Storage storage) {
@@ -151,4 +152,11 @@ public static OAuthStorage getOAuthStorage(Storage storage) {
151152
}
152153
return (OAuthStorage) storage;
153154
}
155+
156+
public static WebAuthNSQLStorage getWebAuthNStorage(Storage storage) {
157+
if (storage.getType() != STORAGE_TYPE.SQL) {
158+
throw new UnsupportedOperationException("");
159+
}
160+
return (WebAuthNSQLStorage) storage;
161+
}
154162
}

src/main/java/io/supertokens/pluginInterface/authRecipe/AuthRecipeStorage.java

+3
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ AuthRecipeUserInfo[] listPrimaryUsersByEmail(TenantIdentifier tenantIdentifier,
5858
AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber(TenantIdentifier tenantIdentifier, String phoneNumber)
5959
throws StorageQueryException;
6060

61+
AuthRecipeUserInfo getPrimaryUserByWebauthNCredentialId(TenantIdentifier tenantIdentifier, String webauthNCredentialId)
62+
throws StorageQueryException;
63+
6164
AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo(AppIdentifier appIdentifier, String thirdPartyId,
6265
String thirdPartyUserId)
6366
throws StorageQueryException;

src/main/java/io/supertokens/pluginInterface/authRecipe/AuthRecipeUserInfo.java

+29-2
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@
1616

1717
package io.supertokens.pluginInterface.authRecipe;
1818

19+
import com.google.gson.Gson;
1920
import com.google.gson.JsonArray;
2021
import com.google.gson.JsonObject;
2122
import com.google.gson.JsonPrimitive;
2223
import io.supertokens.pluginInterface.RECIPE_ID;
2324

24-
import java.util.*;
25+
import java.util.Arrays;
26+
import java.util.HashSet;
27+
import java.util.Set;
2528

2629
public class AuthRecipeUserInfo {
2730

@@ -124,7 +127,7 @@ public int hashCode() {
124127
return hashCode;
125128
}
126129

127-
public JsonObject toJson() {
130+
public JsonObject toJson(boolean includeWebauthn) {
128131
if (!didCallSetExternalUserId) {
129132
throw new RuntimeException("Found a bug: Did you forget to call setExternalUserId?");
130133
}
@@ -142,7 +145,13 @@ public JsonObject toJson() {
142145
Set<String> emails = new HashSet<>();
143146
Set<String> phoneNumbers = new HashSet<>();
144147
Set<LoginMethod.ThirdParty> thirdParty = new HashSet<>();
148+
Set<String> webauthn = new HashSet<>();
145149
for (LoginMethod loginMethod : this.loginMethods) {
150+
if (!includeWebauthn) {
151+
if (loginMethod.recipeId == RECIPE_ID.WEBAUTHN) {
152+
continue;
153+
}
154+
}
146155
if (loginMethod.email != null) {
147156
emails.add(loginMethod.email);
148157
}
@@ -152,6 +161,9 @@ public JsonObject toJson() {
152161
if (loginMethod.thirdParty != null) {
153162
thirdParty.add(loginMethod.thirdParty);
154163
}
164+
if(loginMethod.webauthN != null) {
165+
webauthn.addAll(loginMethod.webauthN.credentialIds);
166+
}
155167
}
156168
JsonArray emailsJson = new JsonArray();
157169
for (String email : emails) {
@@ -172,6 +184,14 @@ public JsonObject toJson() {
172184
}
173185
jsonObject.add("thirdParty", thirdPartyJson);
174186

187+
if (includeWebauthn) {
188+
JsonObject webauthnJson = new JsonObject();
189+
JsonArray j = new JsonArray();
190+
j.addAll(new Gson().toJsonTree(webauthn).getAsJsonArray());
191+
webauthnJson.add("credentialIds", j);
192+
jsonObject.add("webauthn", webauthnJson);
193+
}
194+
175195
// now we add login methods..
176196
JsonArray loginMethodsArr = new JsonArray();
177197
for (LoginMethod lM : this.loginMethods) {
@@ -197,6 +217,13 @@ public JsonObject toJson() {
197217
thirdPartyJsonObject.addProperty("userId", lM.thirdParty.userId);
198218
lMJsonObject.add("thirdParty", thirdPartyJsonObject);
199219
}
220+
if (includeWebauthn) {
221+
if(lM.webauthN != null) {
222+
JsonObject webauthNJson = new JsonObject();
223+
webauthNJson.add("credentialIds", new Gson().toJsonTree(lM.webauthN.credentialIds));
224+
lMJsonObject.add("webauthn", webauthNJson);
225+
}
226+
}
200227
loginMethodsArr.add(lMJsonObject);
201228
}
202229
jsonObject.add("loginMethods", loginMethodsArr);

src/main/java/io/supertokens/pluginInterface/authRecipe/LoginMethod.java

+49
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import java.util.Collections;
2222
import java.util.HashSet;
23+
import java.util.List;
2324
import java.util.Set;
2425

2526
public class LoginMethod {
@@ -42,6 +43,8 @@ public class LoginMethod {
4243

4344
public final Set<String> tenantIds;
4445

46+
public final WebAuthN webauthN;
47+
4548
public transient final String passwordHash;
4649

4750
private boolean didCallSetExternalUserId = false;
@@ -55,6 +58,7 @@ public LoginMethod(String recipeUserId, long timeJoined, boolean verified, Strin
5558
this.email = email;
5659
this.phoneNumber = null;
5760
this.thirdParty = null;
61+
this.webauthN = null;
5862
this.tenantIds = new HashSet<>();
5963
Collections.addAll(this.tenantIds, tenantIds);
6064
this.passwordHash = passwordHash;
@@ -71,6 +75,7 @@ public LoginMethod(String recipeUserId, long timeJoined, boolean verified, Passw
7175
this.tenantIds = new HashSet<>();
7276
Collections.addAll(this.tenantIds, tenantIds);
7377
this.thirdParty = null;
78+
this.webauthN = null;
7479
this.passwordHash = null;
7580
}
7681

@@ -84,6 +89,22 @@ public LoginMethod(String recipeUserId, long timeJoined, boolean verified, Strin
8489
this.tenantIds = new HashSet<>();
8590
Collections.addAll(this.tenantIds, tenantIds);
8691
this.thirdParty = thirdPartyInfo;
92+
this.webauthN = null;
93+
this.phoneNumber = null;
94+
this.passwordHash = null;
95+
}
96+
97+
public LoginMethod(String recipeUserId, long timeJoined, boolean verified, String email, WebAuthN webauthN,
98+
String[] tenantIds) {
99+
this.verified = verified;
100+
this.timeJoined = timeJoined;
101+
this.recipeUserId = recipeUserId;
102+
this.recipeId = RECIPE_ID.WEBAUTHN;
103+
this.email = email;
104+
this.tenantIds = new HashSet<>();
105+
Collections.addAll(this.tenantIds, tenantIds);
106+
this.webauthN = webauthN;
107+
this.thirdParty = null;
87108
this.phoneNumber = null;
88109
this.passwordHash = null;
89110
}
@@ -147,6 +168,32 @@ public int hashCode() {
147168
}
148169
}
149170

171+
public static class WebAuthN {
172+
public List<String> credentialIds;
173+
174+
public WebAuthN(List<String> credentialIds) {
175+
this.credentialIds = credentialIds;
176+
}
177+
178+
public void addCredentialId(String credentialId) {
179+
this.credentialIds.add(credentialId);
180+
}
181+
182+
@Override
183+
public boolean equals(Object other) {
184+
if (!(other instanceof WebAuthN)) {
185+
return false;
186+
}
187+
WebAuthN webauthN = (WebAuthN) other;
188+
return this.credentialIds.equals(webauthN.credentialIds);
189+
}
190+
191+
@Override
192+
public int hashCode() {
193+
return credentialIds.hashCode();
194+
}
195+
}
196+
150197
@Override
151198
public boolean equals(Object other) {
152199
if (!(other instanceof LoginMethod)) {
@@ -159,6 +206,7 @@ public boolean equals(Object other) {
159206
&& java.util.Objects.equals(this.phoneNumber, otherLoginMethod.phoneNumber)
160207
&& java.util.Objects.equals(this.passwordHash, otherLoginMethod.passwordHash)
161208
&& java.util.Objects.equals(this.thirdParty, otherLoginMethod.thirdParty)
209+
&& java.util.Objects.equals(this.webauthN, otherLoginMethod.webauthN)
162210
&& this.tenantIds.equals(otherLoginMethod.tenantIds);
163211
}
164212

@@ -176,6 +224,7 @@ public int hashCode() {
176224
result = 31 * result + tenantIds.hashCode();
177225
result = 31 * result + (passwordHash != null ? passwordHash.hashCode() : 0);
178226
result = 31 * result + (thirdParty != null ? thirdParty.hashCode() : 0);
227+
result = 31 * result + (webauthN != null ? webauthN.hashCode() : 0);
179228
return result;
180229
}
181230
}

src/main/java/io/supertokens/pluginInterface/authRecipe/sqlStorage/AuthRecipeSQLStorage.java

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
2121
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
2222
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
23+
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
2324
import io.supertokens.pluginInterface.sqlStorage.SQLStorage;
2425
import io.supertokens.pluginInterface.sqlStorage.TransactionConnection;
2526

@@ -32,6 +33,10 @@ AuthRecipeUserInfo getPrimaryUserById_Transaction(AppIdentifier appIdentifier, T
3233
String userId)
3334
throws StorageQueryException;
3435

36+
AuthRecipeUserInfo getPrimaryUserByWebauthNCredentialId_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con,
37+
String credentialId)
38+
throws StorageQueryException;
39+
3540
List<AuthRecipeUserInfo> getPrimaryUsersByIds_Transaction(AppIdentifier appIdentifier, TransactionConnection con,
3641
List<String> userIds)
3742
throws StorageQueryException;

src/main/java/io/supertokens/pluginInterface/dashboard/DashboardSearchTags.java

+5
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ public boolean shouldPasswordlessTableBeSearched() {
5555
return false;
5656
}
5757

58+
public boolean shouldWebauthnTableBeSearched() {
59+
List<SUPPORTED_SEARCH_TAGS> nonNullSearchTags = getNonNullSearchFields();
60+
return nonNullSearchTags.contains(SUPPORTED_SEARCH_TAGS.EMAIL) && nonNullSearchTags.size() == 1;
61+
}
62+
5863
private List<SUPPORTED_SEARCH_TAGS> getNonNullSearchFields() {
5964
List<SUPPORTED_SEARCH_TAGS> nonNullSearchTags = new ArrayList<>();
6065

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright (c) 2020, 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.pluginInterface.webauthn;
18+
19+
public class AccountRecoveryTokenInfo {
20+
21+
public final String userId;
22+
23+
public final String token;
24+
25+
public final long expiresAt;
26+
27+
public final String email;
28+
29+
public AccountRecoveryTokenInfo(String userId, String email, String token, long expiresAt) {
30+
this.userId = userId;
31+
this.email = email;
32+
this.token = token;
33+
this.expiresAt = expiresAt;
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright (c) 2024, 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.pluginInterface.webauthn;
18+
19+
public class WebAuthNOptions {
20+
21+
public String generatedOptionsId;
22+
public String relyingPartyId;
23+
public String relyingPartyName;
24+
public String userEmail;
25+
public Long timeout;
26+
public String challenge; //base64 url encoded
27+
public String origin;
28+
public Long expiresAt;
29+
public Long createdAt;
30+
public boolean userPresenceRequired;
31+
public String userVerification;
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright (c) 2024, 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.pluginInterface.webauthn;
18+
19+
import io.supertokens.pluginInterface.authRecipe.AuthRecipeStorage;
20+
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
21+
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
22+
import io.supertokens.pluginInterface.webauthn.exceptions.*;
23+
24+
import java.util.List;
25+
26+
public interface WebAuthNStorage extends AuthRecipeStorage {
27+
28+
WebAuthNStoredCredential saveCredentials(TenantIdentifier tenantIdentifier, WebAuthNStoredCredential credential) throws StorageQueryException;
29+
30+
WebAuthNOptions saveGeneratedOptions(TenantIdentifier tenantIdentifier, WebAuthNOptions optionsToSave) throws StorageQueryException;
31+
32+
WebAuthNOptions loadOptionsById(TenantIdentifier tenantIdentifier, String optionsId) throws StorageQueryException;
33+
34+
WebAuthNStoredCredential loadCredentialByIdForUser(TenantIdentifier tenantIdentifier, String credentialId, String recipeUserId) throws StorageQueryException;
35+
36+
void addRecoverAccountToken(TenantIdentifier tenantIdentifier, AccountRecoveryTokenInfo accountRecoveryTokenInfo) throws
37+
DuplicateRecoverAccountTokenException, StorageQueryException;
38+
39+
void removeCredential(TenantIdentifier tenantIdentifier, String userId, String credentialId)
40+
throws StorageQueryException, WebauthNCredentialNotExistsException;
41+
42+
void removeOptions(TenantIdentifier tenantIdentifier, String optionsId)
43+
throws StorageQueryException, WebauthNOptionsNotExistsException;
44+
45+
List<WebAuthNStoredCredential> listCredentialsForUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException;
46+
47+
void updateUserEmail(TenantIdentifier tenantIdentifier, String userId, String newEmail)
48+
throws StorageQueryException, UserIdNotFoundException, DuplicateUserEmailException;
49+
50+
void deleteExpiredAccountRecoveryTokens() throws StorageQueryException;
51+
52+
void deleteExpiredGeneratedOptions() throws StorageQueryException;
53+
}

0 commit comments

Comments
 (0)