Skip to content

Commit 175a3a3

Browse files
coolbuebniftyvictorporcellus
authored
feat: Dashboard WebAuthn support (#984)
* added support for webauthn user editing * bumped version * chore: update version and rebuild --------- Co-authored-by: Victor Bojica <[email protected]> Co-authored-by: Mihaly Lengyel <[email protected]>
1 parent d547593 commit 175a3a3

File tree

17 files changed

+122
-19
lines changed

17 files changed

+122
-19
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
77

8+
## [22.0.1] - 2025-03-26
9+
10+
- Added Dashboard support for WebAuthn
11+
812
## [22.0.0] - 2025-03-19
913

1014
### Breaking changes

lib/build/recipe/dashboard/api/multitenancy/utils.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ function factorIdToRecipe(factorId) {
6666
"link-email": "Passwordless",
6767
"link-phone": "Passwordless",
6868
totp: "Totp",
69+
webauthn: "WebAuthn",
6970
};
7071
return factorIdToRecipe[factorId];
7172
}

lib/build/recipe/dashboard/api/userdetails/userPut.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ exports.userPut = void 0;
99
const error_1 = __importDefault(require("../../../../error"));
1010
const recipe_1 = __importDefault(require("../../../emailpassword/recipe"));
1111
const recipe_2 = __importDefault(require("../../../passwordless/recipe"));
12+
const recipe_3 = __importDefault(require("../../../webauthn/recipe"));
1213
const emailpassword_1 = __importDefault(require("../../../emailpassword"));
1314
const passwordless_1 = __importDefault(require("../../../passwordless"));
15+
const webauthn_1 = __importDefault(require("../../../webauthn"));
1416
const utils_1 = require("../../utils");
15-
const recipe_3 = __importDefault(require("../../../usermetadata/recipe"));
17+
const recipe_4 = __importDefault(require("../../../usermetadata/recipe"));
1618
const usermetadata_1 = __importDefault(require("../../../usermetadata"));
1719
const constants_1 = require("../../../emailpassword/constants");
1820
const utils_2 = require("../../../passwordless/utils");
@@ -99,6 +101,33 @@ const updateEmailForRecipeId = async (recipeId, recipeUserId, email, tenantId, u
99101
status: "OK",
100102
};
101103
}
104+
if (recipeId === "webauthn") {
105+
let validationError = await recipe_3.default
106+
.getInstanceOrThrowError()
107+
.config.validateEmailAddress(email, tenantId, userContext);
108+
if (validationError !== undefined) {
109+
return {
110+
status: "INVALID_EMAIL_ERROR",
111+
error: validationError,
112+
};
113+
}
114+
const emailUpdateResponse = await webauthn_1.default.updateUserEmail({
115+
email,
116+
recipeUserId: recipeUserId.getAsString(),
117+
tenantId,
118+
userContext,
119+
});
120+
if (emailUpdateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR") {
121+
return {
122+
status: "EMAIL_ALREADY_EXISTS_ERROR",
123+
};
124+
} else if (emailUpdateResponse.status === "UNKNOWN_USER_ID_ERROR") {
125+
throw new Error("Should never come here");
126+
}
127+
return {
128+
status: "OK",
129+
};
130+
}
102131
/**
103132
* If it comes here then the user is a third party user in which case the UI should not have allowed this
104133
*/
@@ -211,7 +240,7 @@ const userPut = async (_, tenantId, options, userContext) => {
211240
if (firstName.trim() !== "" || lastName.trim() !== "") {
212241
let isRecipeInitialised = false;
213242
try {
214-
recipe_3.default.getInstanceOrThrowError();
243+
recipe_4.default.getInstanceOrThrowError();
215244
isRecipeInitialised = true;
216245
} catch (_) {
217246
// no op

lib/build/recipe/dashboard/types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export declare type APIFunction = (
5151
options: APIOptions,
5252
userContext: UserContext
5353
) => Promise<any>;
54-
export declare type RecipeIdForUser = "emailpassword" | "thirdparty" | "passwordless";
54+
export declare type RecipeIdForUser = "emailpassword" | "thirdparty" | "passwordless" | "webauthn";
5555
export declare type AuthMode = "api-key" | "email-password";
5656
export declare type UserWithFirstAndLastName = User & {
5757
firstName?: string;

lib/build/recipe/dashboard/utils.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export declare function getUserForRecipeId(
1212
userContext: UserContext
1313
): Promise<{
1414
user: UserWithFirstAndLastName | undefined;
15-
recipe: "emailpassword" | "thirdparty" | "passwordless" | undefined;
15+
recipe: "emailpassword" | "thirdparty" | "passwordless" | "webauthn" | undefined;
1616
}>;
1717
export declare function validateApiKey(input: {
1818
req: BaseRequest;

lib/build/recipe/dashboard/utils.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const recipe_1 = __importDefault(require("../accountlinking/recipe"));
2626
const recipe_2 = __importDefault(require("../emailpassword/recipe"));
2727
const recipe_3 = __importDefault(require("../thirdparty/recipe"));
2828
const recipe_4 = __importDefault(require("../passwordless/recipe"));
29+
const recipe_5 = __importDefault(require("../webauthn/recipe"));
2930
const logger_1 = require("../../logger");
3031
function validateAndNormaliseUserInput(config) {
3132
let override = Object.assign(
@@ -57,7 +58,12 @@ function sendUnauthorisedAccess(res) {
5758
}
5859
exports.sendUnauthorisedAccess = sendUnauthorisedAccess;
5960
function isValidRecipeId(recipeId) {
60-
return recipeId === "emailpassword" || recipeId === "thirdparty" || recipeId === "passwordless";
61+
return (
62+
recipeId === "emailpassword" ||
63+
recipeId === "thirdparty" ||
64+
recipeId === "passwordless" ||
65+
recipeId === "webauthn"
66+
);
6167
}
6268
exports.isValidRecipeId = isValidRecipeId;
6369
async function getUserForRecipeId(recipeUserId, recipeId, userContext) {
@@ -115,6 +121,13 @@ async function _getUserForRecipeId(recipeUserId, recipeId, userContext) {
115121
} catch (e) {
116122
// No - op
117123
}
124+
} else if (recipeId === recipe_5.default.RECIPE_ID) {
125+
try {
126+
recipe_5.default.getInstanceOrThrowError();
127+
recipe = "webauthn";
128+
} catch (e) {
129+
// No - op
130+
}
118131
}
119132
return {
120133
user,

lib/build/recipe/multitenancy/api/implementation.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ function getAPIInterface() {
7676
enabled: validFirstFactors.includes("thirdparty"),
7777
providers: finalProviderList,
7878
},
79+
webauthn: {
80+
enabled: validFirstFactors.includes("webauthn"),
81+
},
7982
passwordless: {
8083
enabled:
8184
validFirstFactors.includes("otp-email") ||

lib/build/recipe/webauthn/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,9 +223,9 @@ export default class Wrapper {
223223
status:
224224
| "OK"
225225
| "UNKNOWN_USER_ID_ERROR"
226-
| "INVALID_CREDENTIALS_ERROR"
227226
| "INVALID_OPTIONS_ERROR"
228227
| "OPTIONS_NOT_FOUND_ERROR"
228+
| "INVALID_CREDENTIALS_ERROR"
229229
| "INVALID_AUTHENTICATOR_ERROR"
230230
| "CREDENTIAL_NOT_FOUND_ERROR";
231231
}>;

lib/build/version.d.ts

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/build/version.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/ts/recipe/dashboard/api/multitenancy/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export function factorIdToRecipe(factorId: string): string {
6767
"link-email": "Passwordless",
6868
"link-phone": "Passwordless",
6969
totp: "Totp",
70+
webauthn: "WebAuthn",
7071
};
7172

7273
return factorIdToRecipe[factorId];

lib/ts/recipe/dashboard/api/userdetails/userPut.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { APIInterface, APIOptions } from "../../types";
22
import STError from "../../../../error";
33
import EmailPasswordRecipe from "../../../emailpassword/recipe";
44
import PasswordlessRecipe from "../../../passwordless/recipe";
5+
import WebAuthnRecipe from "../../../webauthn/recipe";
56
import EmailPassword from "../../../emailpassword";
67
import Passwordless from "../../../passwordless";
8+
import WebAuthn from "../../../webauthn";
79
import { isValidRecipeId, getUserForRecipeId } from "../../utils";
810
import UserMetadataRecipe from "../../../usermetadata/recipe";
911
import UserMetadata from "../../../usermetadata";
@@ -40,7 +42,7 @@ type Response =
4042
};
4143

4244
const updateEmailForRecipeId = async (
43-
recipeId: "emailpassword" | "passwordless" | "thirdparty",
45+
recipeId: "emailpassword" | "passwordless" | "thirdparty" | "webauthn",
4446
recipeUserId: RecipeUserId,
4547
email: string,
4648
tenantId: string,
@@ -159,6 +161,40 @@ const updateEmailForRecipeId = async (
159161
};
160162
}
161163

164+
if (recipeId === "webauthn") {
165+
let validationError = await WebAuthnRecipe.getInstanceOrThrowError().config.validateEmailAddress(
166+
email,
167+
tenantId,
168+
userContext
169+
);
170+
171+
if (validationError !== undefined) {
172+
return {
173+
status: "INVALID_EMAIL_ERROR",
174+
error: validationError,
175+
};
176+
}
177+
178+
const emailUpdateResponse = await WebAuthn.updateUserEmail({
179+
email,
180+
recipeUserId: recipeUserId.getAsString(),
181+
tenantId,
182+
userContext,
183+
});
184+
185+
if (emailUpdateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR") {
186+
return {
187+
status: "EMAIL_ALREADY_EXISTS_ERROR",
188+
};
189+
} else if (emailUpdateResponse.status === "UNKNOWN_USER_ID_ERROR") {
190+
throw new Error("Should never come here");
191+
}
192+
193+
return {
194+
status: "OK",
195+
};
196+
}
197+
162198
/**
163199
* If it comes here then the user is a third party user in which case the UI should not have allowed this
164200
*/

lib/ts/recipe/dashboard/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export type APIFunction = (
7272
userContext: UserContext
7373
) => Promise<any>;
7474

75-
export type RecipeIdForUser = "emailpassword" | "thirdparty" | "passwordless";
75+
export type RecipeIdForUser = "emailpassword" | "thirdparty" | "passwordless" | "webauthn";
7676

7777
export type AuthMode = "api-key" | "email-password";
7878

lib/ts/recipe/dashboard/utils.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import AccountLinking from "../accountlinking/recipe";
2828
import EmailPasswordRecipe from "../emailpassword/recipe";
2929
import ThirdPartyRecipe from "../thirdparty/recipe";
3030
import PasswordlessRecipe from "../passwordless/recipe";
31+
import WebAuthnRecipe from "../webauthn/recipe";
3132
import RecipeUserId from "../../recipeUserId";
3233
import { User, UserContext } from "../../types";
3334
import { logDebugMessage } from "../../logger";
@@ -62,7 +63,12 @@ export function sendUnauthorisedAccess(res: BaseResponse) {
6263
}
6364

6465
export function isValidRecipeId(recipeId: string): recipeId is RecipeIdForUser {
65-
return recipeId === "emailpassword" || recipeId === "thirdparty" || recipeId === "passwordless";
66+
return (
67+
recipeId === "emailpassword" ||
68+
recipeId === "thirdparty" ||
69+
recipeId === "passwordless" ||
70+
recipeId === "webauthn"
71+
);
6672
}
6773

6874
export async function getUserForRecipeId(
@@ -71,7 +77,7 @@ export async function getUserForRecipeId(
7177
userContext: UserContext
7278
): Promise<{
7379
user: UserWithFirstAndLastName | undefined;
74-
recipe: "emailpassword" | "thirdparty" | "passwordless" | undefined;
80+
recipe: "emailpassword" | "thirdparty" | "passwordless" | "webauthn" | undefined;
7581
}> {
7682
let userResponse = await _getUserForRecipeId(recipeUserId, recipeId, userContext);
7783
let user: UserWithFirstAndLastName | undefined = undefined;
@@ -94,9 +100,9 @@ async function _getUserForRecipeId(
94100
userContext: UserContext
95101
): Promise<{
96102
user: User | undefined;
97-
recipe: "emailpassword" | "thirdparty" | "passwordless" | undefined;
103+
recipe: "emailpassword" | "thirdparty" | "passwordless" | "webauthn" | undefined;
98104
}> {
99-
let recipe: "emailpassword" | "thirdparty" | "passwordless" | undefined;
105+
let recipe: "emailpassword" | "thirdparty" | "passwordless" | "webauthn" | undefined;
100106

101107
const user = await AccountLinking.getInstance().recipeInterfaceImpl.getUser({
102108
userId: recipeUserId.getAsString(),
@@ -143,6 +149,13 @@ async function _getUserForRecipeId(
143149
} catch (e) {
144150
// No - op
145151
}
152+
} else if (recipeId === WebAuthnRecipe.RECIPE_ID) {
153+
try {
154+
WebAuthnRecipe.getInstanceOrThrowError();
155+
recipe = "webauthn";
156+
} catch (e) {
157+
// No - op
158+
}
146159
}
147160
return {
148161
user,

lib/ts/recipe/multitenancy/api/implementation.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ export default function getAPIInterface(): APIInterface {
9090
enabled: validFirstFactors.includes("thirdparty"),
9191
providers: finalProviderList,
9292
},
93+
webauthn: {
94+
enabled: validFirstFactors.includes("webauthn"),
95+
},
9396
passwordless: {
9497
enabled:
9598
validFirstFactors.includes("otp-email") ||

lib/ts/version.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
* License for the specific language governing permissions and limitations
1313
* under the License.
1414
*/
15-
export const version = "22.0.0";
15+
export const version = "22.0.1";
1616

1717
export const cdiSupported = ["5.3"];
1818

1919
// Note: The actual script import for dashboard uses v{DASHBOARD_VERSION}
20-
export const dashboardVersion = "0.13";
20+
export const dashboardVersion = "0.15";

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "supertokens-node",
3-
"version": "22.0.0",
3+
"version": "22.0.1",
44
"description": "NodeJS driver for SuperTokens core",
55
"main": "index.js",
66
"scripts": {

0 commit comments

Comments
 (0)