mirror of
https://github.com/certd/certd.git
synced 2026-07-06 03:47:34 +08:00
perf(passkey): passkey支持多域名rpid
This commit is contained in:
@@ -107,6 +107,10 @@
|
||||
<div class="passkey-info">
|
||||
<div class="passkey-name">{{ passkey.deviceName }}</div>
|
||||
<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">
|
||||
<fs-icon icon="ion:calendar-outline" class="meta-icon" />
|
||||
{{ formatDate(passkey.registeredAt) }}
|
||||
@@ -454,6 +458,8 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(145deg, #1e1e1e, #252525);
|
||||
|
||||
.header-bg-gradient {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
opacity: 1;
|
||||
@@ -472,6 +478,7 @@ onMounted(async () => {
|
||||
|
||||
.detail-tag {
|
||||
background: #3b3b3b;
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
color: #e5e5e5;
|
||||
|
||||
.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 {
|
||||
.binding-item {
|
||||
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() {
|
||||
const userId = this.getUserId();
|
||||
const passkeys = await this.passkeyService.find({
|
||||
select: ["id", "deviceName", "registeredAt", "transports", "passkeyId", "updateTime"],
|
||||
select: ["id", "deviceName", "registeredAt", "transports", "passkeyId", "rpId", "updateTime"],
|
||||
where: { userId },
|
||||
order: { registeredAt: "DESC" },
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ export class PasskeyEntity {
|
||||
@Column({ name: "device_name", comment: "设备名称" })
|
||||
deviceName: string;
|
||||
|
||||
@Column({ name: "passkey_id", comment: "passkey_id" })
|
||||
@Column({ name: "passkey_id", comment: "passkey_id", unique: true })
|
||||
passkeyId: string;
|
||||
|
||||
@Column({ name: "public_key", comment: "公钥", type: "text" })
|
||||
@@ -23,6 +23,9 @@ export class PasskeyEntity {
|
||||
@Column({ name: "transports", comment: "传输方式", type: "text", nullable: true })
|
||||
transports: string;
|
||||
|
||||
@Column({ name: "rp_id", comment: "注册时的rpId,域名可能会变", nullable: true })
|
||||
rpId: string;
|
||||
|
||||
@Column({ name: "registered_at", comment: "注册时间" })
|
||||
registeredAt: number;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { InjectEntityModel } from "@midwayjs/typeorm";
|
||||
@@ -23,19 +23,15 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
|
||||
return this.repository;
|
||||
}
|
||||
|
||||
async getRpInfo() {
|
||||
async getRpInfo(ctx: any) {
|
||||
let rpName = "Certd";
|
||||
if (isComm()) {
|
||||
const siteInfo = await this.sysSettingsService.getSetting<SysSiteInfo>(SysSiteInfo);
|
||||
rpName = siteInfo.title || rpName;
|
||||
}
|
||||
|
||||
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
|
||||
|
||||
const url = installInfo.bindUrl || "http://localhost:7001";
|
||||
const uri = new URL(url);
|
||||
const rpId = uri.hostname;
|
||||
const origin = uri.origin;
|
||||
const rpId = ctx.hostname;
|
||||
const origin = ctx.origin;
|
||||
|
||||
return {
|
||||
rpName,
|
||||
@@ -47,7 +43,7 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
|
||||
const { generateRegistrationOptions } = await import("@simplewebauthn/server");
|
||||
const user = await this.userService.info(userId);
|
||||
|
||||
const { rpName, rpId } = await this.getRpInfo();
|
||||
const { rpName, rpId } = await this.getRpInfo(ctx);
|
||||
|
||||
const options = await generateRegistrationOptions({
|
||||
rpName: rpName,
|
||||
@@ -84,7 +80,7 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
|
||||
throw new AuthException("注册验证失败");
|
||||
}
|
||||
|
||||
const { rpId, origin } = await this.getRpInfo();
|
||||
const { rpId, origin } = await this.getRpInfo(ctx);
|
||||
|
||||
let verification: any = null;
|
||||
const verifyReq = {
|
||||
@@ -115,7 +111,7 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
|
||||
}
|
||||
|
||||
async generateAuthenticationOptions(ctx: any) {
|
||||
const { rpId } = await this.getRpInfo();
|
||||
const { rpId } = await this.getRpInfo(ctx);
|
||||
const { generateAuthenticationOptions } = await import("@simplewebauthn/server");
|
||||
const options = await generateAuthenticationOptions({
|
||||
rpID: rpId,
|
||||
@@ -146,13 +142,19 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
|
||||
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({
|
||||
response: credential,
|
||||
expectedChallenge: challenge,
|
||||
expectedOrigin: origin,
|
||||
expectedRPID: rpId,
|
||||
expectedRPID,
|
||||
requireUserVerification: false,
|
||||
credential: {
|
||||
id: passkey.passkeyId,
|
||||
@@ -166,6 +168,11 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
|
||||
throw new AuthException("认证验证失败");
|
||||
}
|
||||
|
||||
if (!passkey.rpId) {
|
||||
passkey.rpId = rpId;
|
||||
await this.repository.save(passkey);
|
||||
}
|
||||
|
||||
cache.delete(`passkey:authentication:${challenge}`);
|
||||
|
||||
return {
|
||||
@@ -178,12 +185,15 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
|
||||
async registerPasskey(userId: number, response: any, challenge: string, deviceName: string, ctx: any) {
|
||||
const verification = await this.verifyRegistrationResponse(userId, response, challenge, ctx);
|
||||
|
||||
const rpInfo = await this.getRpInfo(ctx);
|
||||
|
||||
await this.add({
|
||||
userId,
|
||||
passkeyId: verification.credentialId,
|
||||
publicKey: Buffer.from(verification.credentialPublicKey).toString("base64"),
|
||||
counter: verification.counter,
|
||||
deviceName,
|
||||
rpId: rpInfo.rpId,
|
||||
registeredAt: Date.now(),
|
||||
});
|
||||
|
||||
@@ -215,20 +225,4 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
|
||||
const user = await this.userService.info(passkey.userId);
|
||||
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';
|
||||
// }
|
||||
}
|
||||
|
||||
+4
-13
@@ -28,10 +28,7 @@ describe("AliyunDeployCertToESA", () => {
|
||||
plugin.deployMode = "saas";
|
||||
plugin.siteIds = [];
|
||||
|
||||
await assert.rejects(
|
||||
() => (plugin as any).executeSaaS(null, null, 1, "test"),
|
||||
/SaaS证书模式下请先选择站点/
|
||||
);
|
||||
await assert.rejects(() => (plugin as any).executeSaaS(null, null, 1, "test"), /SaaS证书模式下请先选择站点/);
|
||||
});
|
||||
|
||||
it("executeSaaS throws error when multiple sites are selected", async () => {
|
||||
@@ -40,10 +37,7 @@ describe("AliyunDeployCertToESA", () => {
|
||||
plugin.deployMode = "saas";
|
||||
plugin.siteIds = ["site1", "site2"];
|
||||
|
||||
await assert.rejects(
|
||||
() => (plugin as any).executeSaaS(null, null, 1, "test"),
|
||||
/SaaS证书模式下站点只能单选/
|
||||
);
|
||||
await assert.rejects(() => (plugin as any).executeSaaS(null, null, 1, "test"), /SaaS证书模式下站点只能单选/);
|
||||
});
|
||||
|
||||
it("executeSaaS throws error when no SaaS domains selected", async () => {
|
||||
@@ -53,10 +47,7 @@ describe("AliyunDeployCertToESA", () => {
|
||||
plugin.siteIds = ["site1"];
|
||||
plugin.saasDomainIds = [];
|
||||
|
||||
await assert.rejects(
|
||||
() => (plugin as any).executeSaaS(null, null, 1, "test"),
|
||||
/SaaS证书模式下请选择要部署的SaaS域名/
|
||||
);
|
||||
await assert.rejects(() => (plugin as any).executeSaaS(null, null, 1, "test"), /SaaS证书模式下请选择要部署的SaaS域名/);
|
||||
});
|
||||
|
||||
it("executeSaaS calls UpdateCustomHostname for each selected SaaS domain", async () => {
|
||||
@@ -120,4 +111,4 @@ describe("AliyunDeployCertToESA", () => {
|
||||
|
||||
assert.deepEqual(calledSites, ["site1", "site2"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user