Skip to content

Commit 155b97e

Browse files
feat: webauthn (#173)
* feat: webauthn options * feat: register credentials wip * feat: recipe user sign up * fix: temp * feat: webauthn support wip * feat: webauthn loginmethod * feat: webauthn sign in * fix: for account recovery * fix: account recovery impl * fix: integration fix for signup * feat: crud apis addition * feat: remove options api * fix: reworked error handling * feat: clean up expired data cron * feat: extending user listing with webauthn * feat: saving userVerification and userPresence values * feat: get credentials * fix: refactor exceptions * fix: user json and dashboard search * fix: fixing table locked issue with in-memory db * fix: add tests and fixes for email update * chore: changelog, version number * fix: review fix: additional possible errors while saving a credential * fix: remove unused method * fix: potential error handling when saving options --------- Co-authored-by: Sattvik Chakravarthy <[email protected]>
1 parent 22f2008 commit 155b97e

22 files changed

+489
-4
lines changed

CHANGELOG.md

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

88
## [Unreleased]
99

10+
11+
## [7.1.0]
12+
13+
- Adds support for Webauthn (passkeys)
14+
1015
## [7.0.0]
1116

1217
- Adds support for Bulk Import

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ plugins {
22
id 'java-library'
33
}
44

5-
version = "7.0.0"
5+
version = "7.1.0"
66

77
repositories {
88
mavenCentral()

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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 8 additions & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 29 additions & 2 deletions
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

Lines changed: 49 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 35 additions & 0 deletions
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+
}

0 commit comments

Comments
 (0)