Implement WebAuthn (#967)

* implement webauthn

* code review

---------

Co-authored-by: Madeline <46743919+MaddyUnderStars@users.noreply.github.com>
This commit is contained in:
Puyodead1 2023-01-29 21:30:42 -05:00 committed by GitHub
parent e98cdfbce0
commit 709dc7280e
20 changed files with 676 additions and 47 deletions

Binary file not shown.

Binary file not shown.

BIN
package-lock.json generated

Binary file not shown.

View File

@ -78,6 +78,7 @@
"dotenv": "^16.0.2",
"exif-be-gone": "^1.3.1",
"fast-zlib": "^2.0.1",
"fido2-lib": "^3.3.5",
"file-type": "16.5",
"form-data": "^4.0.0",
"i18next": "^21.9.2",

View File

@ -19,7 +19,13 @@
import "missing-native-js-functions";
import { Server, ServerOptions } from "lambert-server";
import { Authentication, CORS } from "./middlewares/";
import { Config, initDatabase, initEvent, Sentry } from "@fosscord/util";
import {
Config,
initDatabase,
initEvent,
Sentry,
WebAuthn,
} from "@fosscord/util";
import { ErrorHandler } from "./middlewares/ErrorHandler";
import { BodyParser } from "./middlewares/BodyParser";
import { Router, Request, Response } from "express";
@ -58,6 +64,7 @@ export class FosscordServer extends Server {
await initEvent();
await initInstance();
await Sentry.init(this.app);
WebAuthn.init();
const logRequests = process.env["LOG_REQUESTS"] != undefined;
if (logRequests) {

View File

@ -27,6 +27,7 @@ export const NO_AUTHORIZATION_ROUTES = [
"/auth/register",
"/auth/location-metadata",
"/auth/mfa/totp",
"/auth/mfa/webauthn",
// Routes with a seperate auth system
"/webhooks/",
// Public information endpoints

View File

@ -16,18 +16,20 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Request, Response, Router } from "express";
import { route, getIpAdress, verifyCaptcha } from "@fosscord/api";
import bcrypt from "bcrypt";
import { getIpAdress, route, verifyCaptcha } from "@fosscord/api";
import {
Config,
User,
generateToken,
adjustEmail,
Config,
FieldErrors,
generateToken,
generateWebAuthnTicket,
LoginSchema,
User,
WebAuthn,
} from "@fosscord/util";
import bcrypt from "bcrypt";
import crypto from "crypto";
import { Request, Response, Router } from "express";
const router: Router = Router();
export default router;
@ -73,7 +75,10 @@ router.post(
"settings",
"totp_secret",
"mfa_enabled",
"webauthn_enabled",
"security_keys",
],
relations: ["security_keys"],
}).catch(() => {
throw FieldErrors({
login: {
@ -116,7 +121,7 @@ router.post(
});
}
if (user.mfa_enabled) {
if (user.mfa_enabled && !user.webauthn_enabled) {
// TODO: This is not a discord.com ticket. I'm not sure what it is but I'm lazy
const ticket = crypto.randomBytes(40).toString("hex");
@ -130,6 +135,40 @@ router.post(
});
}
if (user.mfa_enabled && user.webauthn_enabled) {
if (!WebAuthn.fido2) {
// TODO: I did this for typescript and I can't use !
throw new Error("WebAuthn not enabled");
}
const options = await WebAuthn.fido2.assertionOptions();
const challenge = JSON.stringify({
publicKey: {
...options,
challenge: Buffer.from(options.challenge).toString(
"base64",
),
allowCredentials: user.security_keys.map((x) => ({
id: x.key_id,
type: "public-key",
})),
transports: ["usb", "ble", "nfc"],
timeout: 60000,
},
});
const ticket = await generateWebAuthnTicket(challenge);
await User.update({ id: user.id }, { totp_last_ticket: ticket });
return res.json({
ticket: ticket,
mfa: true,
sms: false, // TODO
token: null,
webauthn: challenge,
});
}
const token = await generateToken(user.id);
// Notice this will have a different token structure, than discord
@ -147,6 +186,9 @@ router.post(
* MFA required:
* @returns {"token": null, "mfa": true, "sms": true, "ticket": "SOME TICKET JWT TOKEN"}
* WebAuthn MFA required:
* @returns {"token": null, "mfa": true, "webauthn": true, "sms": true, "ticket": "SOME TICKET JWT TOKEN"}
* Captcha required:
* @returns {"captcha_key": ["captcha-required"], "captcha_sitekey": null, "captcha_service": "recaptcha"}

View File

@ -0,0 +1,112 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { route } from "@fosscord/api";
import {
generateToken,
SecurityKey,
User,
verifyWebAuthnToken,
WebAuthn,
WebAuthnTotpSchema,
} from "@fosscord/util";
import { Request, Response, Router } from "express";
import { ExpectedAssertionResult } from "fido2-lib";
import { HTTPError } from "lambert-server";
const router = Router();
function toArrayBuffer(buf: Buffer) {
const ab = new ArrayBuffer(buf.length);
const view = new Uint8Array(ab);
for (let i = 0; i < buf.length; ++i) {
view[i] = buf[i];
}
return ab;
}
router.post(
"/",
route({ body: "WebAuthnTotpSchema" }),
async (req: Request, res: Response) => {
if (!WebAuthn.fido2) {
// TODO: I did this for typescript and I can't use !
throw new Error("WebAuthn not enabled");
}
const { code, ticket } = req.body as WebAuthnTotpSchema;
const user = await User.findOneOrFail({
where: {
totp_last_ticket: ticket,
},
select: ["id", "settings"],
});
const ret = await verifyWebAuthnToken(ticket);
if (!ret)
throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
await User.update({ id: user.id }, { totp_last_ticket: "" });
const clientAttestationResponse = JSON.parse(code);
const securityKey = await SecurityKey.findOneOrFail({
where: {
user_id: req.user_id,
key_id: clientAttestationResponse.rawId,
},
});
if (!clientAttestationResponse.rawId)
throw new HTTPError("Missing rawId", 400);
clientAttestationResponse.rawId = toArrayBuffer(
Buffer.from(clientAttestationResponse.rawId, "base64"),
);
const assertionExpectations: ExpectedAssertionResult = JSON.parse(
Buffer.from(
clientAttestationResponse.response.clientDataJSON,
"base64",
).toString(),
);
const authnResult = await WebAuthn.fido2.assertionResult(
clientAttestationResponse,
{
...assertionExpectations,
factor: "second",
publicKey: securityKey.public_key,
prevCounter: securityKey.counter,
userHandle: securityKey.key_id,
},
);
const counter = authnResult.authnrData.get("counter");
securityKey.counter = counter;
await securityKey.save();
return res.json({
token: await generateToken(user.id),
user_settings: user.settings,
});
},
);
export default router;

View File

@ -0,0 +1,35 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { route } from "@fosscord/api";
import { SecurityKey } from "@fosscord/util";
import { Request, Response, Router } from "express";
const router = Router();
router.delete("/", route({}), async (req: Request, res: Response) => {
const { key_id } = req.params;
await SecurityKey.delete({
id: key_id,
user_id: req.user_id,
});
res.sendStatus(204);
});
export default router;

View File

@ -0,0 +1,196 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { route } from "@fosscord/api";
import {
CreateWebAuthnCredentialSchema,
DiscordApiErrors,
FieldErrors,
GenerateWebAuthnCredentialsSchema,
generateWebAuthnTicket,
SecurityKey,
User,
verifyWebAuthnToken,
WebAuthn,
WebAuthnPostSchema,
} from "@fosscord/util";
import bcrypt from "bcrypt";
import { Request, Response, Router } from "express";
import { ExpectedAttestationResult } from "fido2-lib";
import { HTTPError } from "lambert-server";
const router = Router();
const isGenerateSchema = (
body: WebAuthnPostSchema,
): body is GenerateWebAuthnCredentialsSchema => {
return "password" in body;
};
const isCreateSchema = (
body: WebAuthnPostSchema,
): body is CreateWebAuthnCredentialSchema => {
return "credential" in body;
};
function toArrayBuffer(buf: Buffer) {
const ab = new ArrayBuffer(buf.length);
const view = new Uint8Array(ab);
for (let i = 0; i < buf.length; ++i) {
view[i] = buf[i];
}
return ab;
}
router.get("/", route({}), async (req: Request, res: Response) => {
const securityKeys = await SecurityKey.find({
where: {
user_id: req.user_id,
},
});
return res.json(
securityKeys.map((key) => ({
id: key.id,
name: key.name,
})),
);
});
router.post(
"/",
route({ body: "WebAuthnPostSchema" }),
async (req: Request, res: Response) => {
if (!WebAuthn.fido2) {
// TODO: I did this for typescript and I can't use !
throw new Error("WebAuthn not enabled");
}
const user = await User.findOneOrFail({
where: {
id: req.user_id,
},
select: [
"data",
"id",
"disabled",
"deleted",
"settings",
"totp_secret",
"mfa_enabled",
"username",
],
});
if (isGenerateSchema(req.body)) {
const { password } = req.body;
const same_password = await bcrypt.compare(
password,
user.data.hash || "",
);
if (!same_password) {
throw FieldErrors({
password: {
message: req.t("auth:login.INVALID_PASSWORD"),
code: "INVALID_PASSWORD",
},
});
}
const registrationOptions =
await WebAuthn.fido2.attestationOptions();
const challenge = JSON.stringify({
publicKey: {
...registrationOptions,
challenge: Buffer.from(
registrationOptions.challenge,
).toString("base64"),
user: {
id: user.id,
name: user.username,
displayName: user.username,
},
},
});
const ticket = await generateWebAuthnTicket(challenge);
return res.json({
ticket: ticket,
challenge,
});
} else if (isCreateSchema(req.body)) {
const { credential, name, ticket } = req.body;
const verified = await verifyWebAuthnToken(ticket);
if (!verified) throw new HTTPError("Invalid ticket", 400);
const clientAttestationResponse = JSON.parse(credential);
if (!clientAttestationResponse.rawId)
throw new HTTPError("Missing rawId", 400);
const rawIdBuffer = Buffer.from(
clientAttestationResponse.rawId,
"base64",
);
clientAttestationResponse.rawId = toArrayBuffer(rawIdBuffer);
const attestationExpectations: ExpectedAttestationResult =
JSON.parse(
Buffer.from(
clientAttestationResponse.response.clientDataJSON,
"base64",
).toString(),
);
const regResult = await WebAuthn.fido2.attestationResult(
clientAttestationResponse,
{
...attestationExpectations,
factor: "second",
},
);
const authnrData = regResult.authnrData;
const keyId = Buffer.from(authnrData.get("credId")).toString(
"base64",
);
const counter = authnrData.get("counter");
const publicKey = authnrData.get("credentialPublicKeyPem");
const securityKey = SecurityKey.create({
name,
counter,
public_key: publicKey,
user_id: req.user_id,
key_id: keyId,
});
await securityKey.save();
return res.json({
name,
id: securityKey.id,
});
} else {
throw DiscordApiErrors.INVALID_AUTHENTICATION_TOKEN;
}
},
);
export default router;

View File

@ -0,0 +1,46 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { User } from "./User";
@Entity("security_keys")
export class SecurityKey extends BaseClass {
@Column({ nullable: true })
@RelationId((key: SecurityKey) => key.user)
user_id: string;
@JoinColumn({ name: "user_id" })
@ManyToOne(() => User, {
onDelete: "CASCADE",
})
user: User;
@Column()
key_id: string;
@Column()
public_key: string;
@Column()
counter: number;
@Column()
name: string;
}

View File

@ -33,6 +33,7 @@ import { UserSettings } from "./UserSettings";
import { Session } from "./Session";
import { Config, FieldErrors, Snowflake, trimSpecial, adjustEmail } from "..";
import { Request } from "express";
import { SecurityKey } from "./SecurityKey";
export enum PublicUserEnum {
username,
@ -138,6 +139,9 @@ export class User extends BaseClass {
@Column({ select: false })
mfa_enabled: boolean = false; // if multi factor authentication is enabled
@Column({ select: false, default: false })
webauthn_enabled: boolean = false; // if webauthn multi factor authentication is enabled
@Column({ select: false, nullable: true })
totp_secret?: string = "";
@ -223,6 +227,9 @@ export class User extends BaseClass {
@Column({ type: "simple-json", select: false })
extended_settings: string = "{}";
@OneToMany(() => SecurityKey, (key: SecurityKey) => key.user)
security_keys: SecurityKey[];
// TODO: I don't like this method?
validate() {
if (this.email) {

View File

@ -23,8 +23,8 @@ export * from "./BackupCodes";
export * from "./Ban";
export * from "./BaseClass";
export * from "./Categories";
export * from "./ClientRelease";
export * from "./Channel";
export * from "./ClientRelease";
export * from "./Config";
export * from "./ConnectedAccount";
export * from "./EmbedCache";
@ -41,6 +41,7 @@ export * from "./ReadState";
export * from "./Recipient";
export * from "./Relationship";
export * from "./Role";
export * from "./SecurityKey";
export * from "./Session";
export * from "./Sticker";
export * from "./StickerPack";

View File

@ -0,0 +1,27 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class webauthn1675045120206 implements MigrationInterface {
name = "webauthn1675045120206";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE \`security_keys\` (\`id\` varchar(255) NOT NULL, \`user_id\` varchar(255) NULL, \`key_id\` varchar(255) NOT NULL, \`public_key\` varchar(255) NOT NULL, \`counter\` int NOT NULL, \`name\` varchar(255) NOT NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`,
);
await queryRunner.query(
`ALTER TABLE \`users\` ADD \`webauthn_enabled\` tinyint NOT NULL DEFAULT 0`,
);
await queryRunner.query(
`ALTER TABLE \`security_keys\` ADD CONSTRAINT \`FK_24c97d0771cafedce6d7163eaad\` FOREIGN KEY (\`user_id\`) REFERENCES \`users\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`security_keys\` DROP FOREIGN KEY \`FK_24c97d0771cafedce6d7163eaad\``,
);
await queryRunner.query(
`ALTER TABLE \`users\` DROP COLUMN \`webauthn_enabled\``,
);
await queryRunner.query(`DROP TABLE \`security_keys\``);
}
}

View File

@ -0,0 +1,27 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class webauthn1675045120206 implements MigrationInterface {
name = "webauthn1675045120206";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE \`security_keys\` (\`id\` varchar(255) NOT NULL, \`user_id\` varchar(255) NULL, \`key_id\` varchar(255) NOT NULL, \`public_key\` varchar(255) NOT NULL, \`counter\` int NOT NULL, \`name\` varchar(255) NOT NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`,
);
await queryRunner.query(
`ALTER TABLE \`users\` ADD \`webauthn_enabled\` tinyint NOT NULL DEFAULT 0`,
);
await queryRunner.query(
`ALTER TABLE \`security_keys\` ADD CONSTRAINT \`FK_24c97d0771cafedce6d7163eaad\` FOREIGN KEY (\`user_id\`) REFERENCES \`users\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`security_keys\` DROP FOREIGN KEY \`FK_24c97d0771cafedce6d7163eaad\``,
);
await queryRunner.query(
`ALTER TABLE \`users\` DROP COLUMN \`webauthn_enabled\``,
);
await queryRunner.query(`DROP TABLE \`security_keys\``);
}
}

View File

@ -0,0 +1,27 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class webauthn1675044825710 implements MigrationInterface {
name = "webauthn1675044825710";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "security_keys" ("id" character varying NOT NULL, "user_id" character varying, "key_id" character varying NOT NULL, "public_key" character varying NOT NULL, "counter" integer NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_6e95cdd91779e7cca06d1fff89c" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "users" ADD "webauthn_enabled" boolean NOT NULL DEFAULT false`,
);
await queryRunner.query(
`ALTER TABLE "security_keys" ADD CONSTRAINT "FK_24c97d0771cafedce6d7163eaad" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "security_keys" DROP CONSTRAINT "FK_24c97d0771cafedce6d7163eaad"`,
);
await queryRunner.query(
`ALTER TABLE "users" DROP COLUMN "webauthn_enabled"`,
);
await queryRunner.query(`DROP TABLE "security_keys"`);
}
}

View File

@ -0,0 +1,38 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// FIXME: better naming
export interface GenerateWebAuthnCredentialsSchema {
password: string;
}
// FIXME: better naming
export interface CreateWebAuthnCredentialSchema {
credential: string;
name: string;
ticket: string;
}
export type WebAuthnPostSchema = Partial<
GenerateWebAuthnCredentialsSchema | CreateWebAuthnCredentialSchema
>;
export interface WebAuthnTotpSchema {
code: string;
ticket: string;
}

View File

@ -16,66 +16,59 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./Validator";
export * from "./SelectProtocolSchema";
export * from "./LoginSchema";
export * from "./RegisterSchema";
export * from "./TotpSchema";
export * from "./ActivitySchema";
export * from "./ApplicationAuthorizeSchema";
export * from "./ApplicationCreateSchema";
export * from "./ApplicationModifySchema";
export * from "./BackupCodesChallengeSchema";
export * from "./ChannelModifySchema";
export * from "./InviteCreateSchema";
export * from "./PurgeSchema";
export * from "./WebhookCreateSchema";
export * from "./MessageCreateSchema";
export * from "./MessageAcknowledgeSchema";
export * from "./GuildCreateSchema";
export * from "./BanCreateSchema";
export * from "./BanModeratorSchema";
export * from "./BanRegistrySchema";
export * from "./BotModifySchema";
export * from "./ChannelModifySchema";
export * from "./ChannelPermissionOverwriteSchema";
export * from "./ChannelReorderSchema";
export * from "./CodesVerificationSchema";
export * from "./DmChannelCreateSchema";
export * from "./EmojiCreateSchema";
export * from "./EmojiModifySchema";
export * from "./ModifyGuildStickerSchema";
export * from "./TemplateCreateSchema";
export * from "./TemplateModifySchema";
export * from "./VanityUrlSchema";
export * from "./GatewayPayloadSchema";
export * from "./GuildCreateSchema";
export * from "./GuildTemplateCreateSchema";
export * from "./GuildUpdateSchema";
export * from "./GuildUpdateWelcomeScreenSchema";
export * from "./WidgetModifySchema";
export * from "./IdentifySchema";
export * from "./InviteCreateSchema";
export * from "./LazyRequestSchema";
export * from "./LoginSchema";
export * from "./MemberChangeProfileSchema";
export * from "./MemberChangeSchema";
export * from "./RoleModifySchema";
export * from "./GuildTemplateCreateSchema";
export * from "./DmChannelCreateSchema";
export * from "./UserModifySchema";
export * from "./MessageAcknowledgeSchema";
export * from "./MessageCreateSchema";
export * from "./MfaCodesSchema";
export * from "./ModifyGuildStickerSchema";
export * from "./PurgeSchema";
export * from "./RegisterSchema";
export * from "./RelationshipPostSchema";
export * from "./RelationshipPutSchema";
export * from "./CodesVerificationSchema";
export * from "./MfaCodesSchema";
export * from "./RoleModifySchema";
export * from "./RolePositionUpdateSchema";
export * from "./SelectProtocolSchema";
export * from "./TemplateCreateSchema";
export * from "./TemplateModifySchema";
export * from "./TotpDisableSchema";
export * from "./TotpEnableSchema";
export * from "./VoiceIdentifySchema";
export * from "./TotpSchema";
export * from "./UserDeleteSchema";
export * from "./UserGuildSettingsSchema";
export * from "./UserModifySchema";
export * from "./UserProfileModifySchema";
export * from "./UserSettingsSchema";
export * from "./Validator";
export * from "./VanityUrlSchema";
export * from "./VoiceIdentifySchema";
export * from "./VoiceStateUpdateSchema";
export * from "./VoiceVideoSchema";
export * from "./IdentifySchema";
export * from "./ActivitySchema";
export * from "./LazyRequestSchema";
export * from "./GuildUpdateSchema";
export * from "./ChannelPermissionOverwriteSchema";
export * from "./UserGuildSettingsSchema";
export * from "./GatewayPayloadSchema";
export * from "./RolePositionUpdateSchema";
export * from "./ChannelReorderSchema";
export * from "./UserSettingsSchema";
export * from "./BotModifySchema";
export * from "./ApplicationModifySchema";
export * from "./ApplicationCreateSchema";
export * from "./ApplicationAuthorizeSchema";
export * from "./WebAuthnSchema";
export * from "./WebhookCreateSchema";
export * from "./WidgetModifySchema";

68
src/util/util/WebAuthn.ts Normal file
View File

@ -0,0 +1,68 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Fido2Lib } from "fido2-lib";
import jwt from "jsonwebtoken";
import { Config } from "./Config";
const JWTOptions: jwt.SignOptions = {
algorithm: "HS256",
expiresIn: "5m",
};
export const WebAuthn: {
fido2: Fido2Lib | null;
init: () => void;
} = {
fido2: null,
init: function () {
this.fido2 = new Fido2Lib({
challengeSize: 128,
});
},
};
export async function generateWebAuthnTicket(
challenge: string,
): Promise<string> {
return new Promise((res, rej) => {
jwt.sign(
{ challenge },
Config.get().security.jwtSecret,
JWTOptions,
(err, token) => {
if (err || !token) return rej(err || "no token");
return res(token);
},
);
});
}
export async function verifyWebAuthnToken(token: string) {
return new Promise((res, rej) => {
jwt.verify(
token,
Config.get().security.jwtSecret,
JWTOptions,
async (err, decoded) => {
if (err) return rej(err);
return res(decoded);
},
);
});
}

View File

@ -39,3 +39,4 @@ export * from "./Array";
export * from "./TraverseDirectory";
export * from "./InvisibleCharacters";
export * from "./Sentry";
export * from "./WebAuthn";