mirror of
https://github.com/certd/certd.git
synced 2026-07-16 02:37:34 +08:00
perf(passkey): passkey支持多域名rpid
This commit is contained in:
@@ -107,6 +107,10 @@
|
|||||||
<div class="passkey-info">
|
<div class="passkey-info">
|
||||||
<div class="passkey-name">{{ passkey.deviceName }}</div>
|
<div class="passkey-name">{{ passkey.deviceName }}</div>
|
||||||
<div class="passkey-meta flex items-center">
|
<div class="passkey-meta flex items-center">
|
||||||
|
<span class="meta-item flex items-center">
|
||||||
|
<fs-icon icon="ion:globe-outline" class="meta-icon" />
|
||||||
|
{{ passkey.rpId || "-" }}
|
||||||
|
</span>
|
||||||
<span class="meta-item flex items-center">
|
<span class="meta-item flex items-center">
|
||||||
<fs-icon icon="ion:calendar-outline" class="meta-icon" />
|
<fs-icon icon="ion:calendar-outline" class="meta-icon" />
|
||||||
{{ formatDate(passkey.registeredAt) }}
|
{{ formatDate(passkey.registeredAt) }}
|
||||||
@@ -454,6 +458,8 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
|
background: linear-gradient(145deg, #1e1e1e, #252525);
|
||||||
|
|
||||||
.header-bg-gradient {
|
.header-bg-gradient {
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: rgba(255, 255, 255, 0.04);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -472,6 +478,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.detail-tag {
|
.detail-tag {
|
||||||
background: #3b3b3b;
|
background: #3b3b3b;
|
||||||
|
border-color: rgba(255, 255, 255, 0.12);
|
||||||
color: #e5e5e5;
|
color: #e5e5e5;
|
||||||
|
|
||||||
.tag-icon {
|
.tag-icon {
|
||||||
@@ -480,6 +487,23 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
border-bottom-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.binding-icon {
|
||||||
|
background: linear-gradient(135deg, rgba(102, 126, 234, 0.22) 0%, rgba(160, 120, 234, 0.22) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.passkey-icon {
|
||||||
|
background: linear-gradient(135deg, rgba(17, 153, 142, 0.22) 0%, rgba(56, 239, 125, 0.22) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.binding-icon .icon,
|
||||||
|
.passkey-icon .icon {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
.bindings-list {
|
.bindings-list {
|
||||||
.binding-item {
|
.binding-item {
|
||||||
background: #2d2d2d;
|
background: #2d2d2d;
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
ALTER TABLE `sys_passkey` ADD COLUMN `rp_id` varchar(256) NULL;
|
||||||
|
|
||||||
|
DROP INDEX `index_passkey_passkey_id` ON `sys_passkey`;
|
||||||
|
|
||||||
|
ALTER TABLE `sys_passkey` ADD UNIQUE INDEX `index_passkey_passkey_id` (`passkey_id`);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
ALTER TABLE "sys_passkey" ADD COLUMN "rp_id" varchar(256) NULL;
|
||||||
|
|
||||||
|
DROP INDEX "index_passkey_passkey_id";
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "index_passkey_passkey_id" ON "sys_passkey" ("passkey_id");
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
ALTER TABLE "sys_passkey" ADD COLUMN "rp_id" varchar(256) NULL;
|
||||||
|
|
||||||
|
DROP INDEX "index_passkey_passkey_id";
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "index_passkey_passkey_id" ON "sys_passkey" ("passkey_id");
|
||||||
@@ -69,7 +69,7 @@ export class MinePasskeyController extends BaseController {
|
|||||||
public async getPasskeys() {
|
public async getPasskeys() {
|
||||||
const userId = this.getUserId();
|
const userId = this.getUserId();
|
||||||
const passkeys = await this.passkeyService.find({
|
const passkeys = await this.passkeyService.find({
|
||||||
select: ["id", "deviceName", "registeredAt", "transports", "passkeyId", "updateTime"],
|
select: ["id", "deviceName", "registeredAt", "transports", "passkeyId", "rpId", "updateTime"],
|
||||||
where: { userId },
|
where: { userId },
|
||||||
order: { registeredAt: "DESC" },
|
order: { registeredAt: "DESC" },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export class PasskeyEntity {
|
|||||||
@Column({ name: "device_name", comment: "设备名称" })
|
@Column({ name: "device_name", comment: "设备名称" })
|
||||||
deviceName: string;
|
deviceName: string;
|
||||||
|
|
||||||
@Column({ name: "passkey_id", comment: "passkey_id" })
|
@Column({ name: "passkey_id", comment: "passkey_id", unique: true })
|
||||||
passkeyId: string;
|
passkeyId: string;
|
||||||
|
|
||||||
@Column({ name: "public_key", comment: "公钥", type: "text" })
|
@Column({ name: "public_key", comment: "公钥", type: "text" })
|
||||||
@@ -23,6 +23,9 @@ export class PasskeyEntity {
|
|||||||
@Column({ name: "transports", comment: "传输方式", type: "text", nullable: true })
|
@Column({ name: "transports", comment: "传输方式", type: "text", nullable: true })
|
||||||
transports: string;
|
transports: string;
|
||||||
|
|
||||||
|
@Column({ name: "rp_id", comment: "注册时的rpId,域名可能会变", nullable: true })
|
||||||
|
rpId: string;
|
||||||
|
|
||||||
@Column({ name: "registered_at", comment: "注册时间" })
|
@Column({ name: "registered_at", comment: "注册时间" })
|
||||||
registeredAt: number;
|
registeredAt: number;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { cache, logger } from "@certd/basic";
|
import { cache, logger } from "@certd/basic";
|
||||||
import { AuthException, BaseService, SysInstallInfo, SysSettingsService, SysSiteInfo } from "@certd/lib-server";
|
import { AuthException, BaseService, SysSettingsService, SysSiteInfo } from "@certd/lib-server";
|
||||||
import { isComm } from "@certd/plus-core";
|
import { isComm } from "@certd/plus-core";
|
||||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||||
import { InjectEntityModel } from "@midwayjs/typeorm";
|
import { InjectEntityModel } from "@midwayjs/typeorm";
|
||||||
@@ -23,19 +23,15 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
|
|||||||
return this.repository;
|
return this.repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRpInfo() {
|
async getRpInfo(ctx: any) {
|
||||||
let rpName = "Certd";
|
let rpName = "Certd";
|
||||||
if (isComm()) {
|
if (isComm()) {
|
||||||
const siteInfo = await this.sysSettingsService.getSetting<SysSiteInfo>(SysSiteInfo);
|
const siteInfo = await this.sysSettingsService.getSetting<SysSiteInfo>(SysSiteInfo);
|
||||||
rpName = siteInfo.title || rpName;
|
rpName = siteInfo.title || rpName;
|
||||||
}
|
}
|
||||||
|
|
||||||
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
|
const rpId = ctx.hostname;
|
||||||
|
const origin = ctx.origin;
|
||||||
const url = installInfo.bindUrl || "http://localhost:7001";
|
|
||||||
const uri = new URL(url);
|
|
||||||
const rpId = uri.hostname;
|
|
||||||
const origin = uri.origin;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rpName,
|
rpName,
|
||||||
@@ -47,7 +43,7 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
|
|||||||
const { generateRegistrationOptions } = await import("@simplewebauthn/server");
|
const { generateRegistrationOptions } = await import("@simplewebauthn/server");
|
||||||
const user = await this.userService.info(userId);
|
const user = await this.userService.info(userId);
|
||||||
|
|
||||||
const { rpName, rpId } = await this.getRpInfo();
|
const { rpName, rpId } = await this.getRpInfo(ctx);
|
||||||
|
|
||||||
const options = await generateRegistrationOptions({
|
const options = await generateRegistrationOptions({
|
||||||
rpName: rpName,
|
rpName: rpName,
|
||||||
@@ -84,7 +80,7 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
|
|||||||
throw new AuthException("注册验证失败");
|
throw new AuthException("注册验证失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { rpId, origin } = await this.getRpInfo();
|
const { rpId, origin } = await this.getRpInfo(ctx);
|
||||||
|
|
||||||
let verification: any = null;
|
let verification: any = null;
|
||||||
const verifyReq = {
|
const verifyReq = {
|
||||||
@@ -115,7 +111,7 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async generateAuthenticationOptions(ctx: any) {
|
async generateAuthenticationOptions(ctx: any) {
|
||||||
const { rpId } = await this.getRpInfo();
|
const { rpId } = await this.getRpInfo(ctx);
|
||||||
const { generateAuthenticationOptions } = await import("@simplewebauthn/server");
|
const { generateAuthenticationOptions } = await import("@simplewebauthn/server");
|
||||||
const options = await generateAuthenticationOptions({
|
const options = await generateAuthenticationOptions({
|
||||||
rpID: rpId,
|
rpID: rpId,
|
||||||
@@ -146,13 +142,19 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
|
|||||||
throw new AuthException("Passkey不存在");
|
throw new AuthException("Passkey不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { rpId, origin } = await this.getRpInfo();
|
const { rpId, origin } = await this.getRpInfo(ctx);
|
||||||
|
|
||||||
|
if (passkey.rpId && passkey.rpId !== rpId) {
|
||||||
|
throw new AuthException(`当前站点域名(${rpId})与Passkey注册域名(${passkey.rpId})不一致,请在${passkey.rpId}域名下使用该Passkey登录`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedRPID = passkey.rpId || rpId;
|
||||||
|
|
||||||
const verification = await verifyAuthenticationResponse({
|
const verification = await verifyAuthenticationResponse({
|
||||||
response: credential,
|
response: credential,
|
||||||
expectedChallenge: challenge,
|
expectedChallenge: challenge,
|
||||||
expectedOrigin: origin,
|
expectedOrigin: origin,
|
||||||
expectedRPID: rpId,
|
expectedRPID,
|
||||||
requireUserVerification: false,
|
requireUserVerification: false,
|
||||||
credential: {
|
credential: {
|
||||||
id: passkey.passkeyId,
|
id: passkey.passkeyId,
|
||||||
@@ -166,6 +168,11 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
|
|||||||
throw new AuthException("认证验证失败");
|
throw new AuthException("认证验证失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!passkey.rpId) {
|
||||||
|
passkey.rpId = rpId;
|
||||||
|
await this.repository.save(passkey);
|
||||||
|
}
|
||||||
|
|
||||||
cache.delete(`passkey:authentication:${challenge}`);
|
cache.delete(`passkey:authentication:${challenge}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -178,12 +185,15 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
|
|||||||
async registerPasskey(userId: number, response: any, challenge: string, deviceName: string, ctx: any) {
|
async registerPasskey(userId: number, response: any, challenge: string, deviceName: string, ctx: any) {
|
||||||
const verification = await this.verifyRegistrationResponse(userId, response, challenge, ctx);
|
const verification = await this.verifyRegistrationResponse(userId, response, challenge, ctx);
|
||||||
|
|
||||||
|
const rpInfo = await this.getRpInfo(ctx);
|
||||||
|
|
||||||
await this.add({
|
await this.add({
|
||||||
userId,
|
userId,
|
||||||
passkeyId: verification.credentialId,
|
passkeyId: verification.credentialId,
|
||||||
publicKey: Buffer.from(verification.credentialPublicKey).toString("base64"),
|
publicKey: Buffer.from(verification.credentialPublicKey).toString("base64"),
|
||||||
counter: verification.counter,
|
counter: verification.counter,
|
||||||
deviceName,
|
deviceName,
|
||||||
|
rpId: rpInfo.rpId,
|
||||||
registeredAt: Date.now(),
|
registeredAt: Date.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -215,20 +225,4 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
|
|||||||
const user = await this.userService.info(passkey.userId);
|
const user = await this.userService.info(passkey.userId);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
// private getRpId(ctx: any): string {
|
|
||||||
// if (ctx && ctx.request && ctx.request.host) {
|
|
||||||
// return ctx.request.host.split(':')[0];
|
|
||||||
// }
|
|
||||||
// return 'localhost';
|
|
||||||
// }
|
|
||||||
|
|
||||||
// private getOrigin(ctx: any): string {
|
|
||||||
// if (ctx && ctx.request) {
|
|
||||||
// const protocol = ctx.request.protocol;
|
|
||||||
// const host = ctx.request.host;
|
|
||||||
// return `${protocol}://${host}`;
|
|
||||||
// }
|
|
||||||
// return 'https://localhost';
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-12
@@ -28,10 +28,7 @@ describe("AliyunDeployCertToESA", () => {
|
|||||||
plugin.deployMode = "saas";
|
plugin.deployMode = "saas";
|
||||||
plugin.siteIds = [];
|
plugin.siteIds = [];
|
||||||
|
|
||||||
await assert.rejects(
|
await assert.rejects(() => (plugin as any).executeSaaS(null, null, 1, "test"), /SaaS证书模式下请先选择站点/);
|
||||||
() => (plugin as any).executeSaaS(null, null, 1, "test"),
|
|
||||||
/SaaS证书模式下请先选择站点/
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("executeSaaS throws error when multiple sites are selected", async () => {
|
it("executeSaaS throws error when multiple sites are selected", async () => {
|
||||||
@@ -40,10 +37,7 @@ describe("AliyunDeployCertToESA", () => {
|
|||||||
plugin.deployMode = "saas";
|
plugin.deployMode = "saas";
|
||||||
plugin.siteIds = ["site1", "site2"];
|
plugin.siteIds = ["site1", "site2"];
|
||||||
|
|
||||||
await assert.rejects(
|
await assert.rejects(() => (plugin as any).executeSaaS(null, null, 1, "test"), /SaaS证书模式下站点只能单选/);
|
||||||
() => (plugin as any).executeSaaS(null, null, 1, "test"),
|
|
||||||
/SaaS证书模式下站点只能单选/
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("executeSaaS throws error when no SaaS domains selected", async () => {
|
it("executeSaaS throws error when no SaaS domains selected", async () => {
|
||||||
@@ -53,10 +47,7 @@ describe("AliyunDeployCertToESA", () => {
|
|||||||
plugin.siteIds = ["site1"];
|
plugin.siteIds = ["site1"];
|
||||||
plugin.saasDomainIds = [];
|
plugin.saasDomainIds = [];
|
||||||
|
|
||||||
await assert.rejects(
|
await assert.rejects(() => (plugin as any).executeSaaS(null, null, 1, "test"), /SaaS证书模式下请选择要部署的SaaS域名/);
|
||||||
() => (plugin as any).executeSaaS(null, null, 1, "test"),
|
|
||||||
/SaaS证书模式下请选择要部署的SaaS域名/
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("executeSaaS calls UpdateCustomHostname for each selected SaaS domain", async () => {
|
it("executeSaaS calls UpdateCustomHostname for each selected SaaS domain", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user