Merge pull request #4896 from Infisical/feat/webauth-and-session-mfa

feat: webauthn and PAM session mfa
This commit is contained in:
Sheen
2026-01-06 21:46:10 +08:00
committed by GitHub
63 changed files with 2846 additions and 322 deletions

View File

@@ -56,6 +56,7 @@
"@peculiar/x509": "^1.12.1", "@peculiar/x509": "^1.12.1",
"@react-email/components": "^1.0.1", "@react-email/components": "^1.0.1",
"@serdnam/pino-cloudwatch-transport": "^1.0.4", "@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@simplewebauthn/server": "^13.2.2",
"@sindresorhus/slugify": "1.1.0", "@sindresorhus/slugify": "1.1.0",
"@slack/oauth": "^3.0.2", "@slack/oauth": "^3.0.2",
"@slack/web-api": "^7.8.0", "@slack/web-api": "^7.8.0",
@@ -663,7 +664,6 @@
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.637.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.637.0.tgz",
"integrity": "sha512-xUi7x4qDubtA8QREtlblPuAcn91GS/09YVEY/RwU7xCY0aqGuFwgszAANlha4OUIqva8oVj2WO4gJuG+iaSnhw==", "integrity": "sha512-xUi7x4qDubtA8QREtlblPuAcn91GS/09YVEY/RwU7xCY0aqGuFwgszAANlha4OUIqva8oVj2WO4gJuG+iaSnhw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0", "@aws-crypto/sha256-js": "5.2.0",
@@ -2110,7 +2110,6 @@
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.682.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.682.0.tgz",
"integrity": "sha512-ZPZ7Y/r/w3nx/xpPzGSqSQsB090Xk5aZZOH+WBhTDn/pBEuim09BYXCLzvvxb7R7NnuoQdrTJiwimdJAhHl7ZQ==", "integrity": "sha512-ZPZ7Y/r/w3nx/xpPzGSqSQsB090Xk5aZZOH+WBhTDn/pBEuim09BYXCLzvvxb7R7NnuoQdrTJiwimdJAhHl7ZQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0", "@aws-crypto/sha256-js": "5.2.0",
@@ -2164,7 +2163,6 @@
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.682.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.682.0.tgz",
"integrity": "sha512-xKuo4HksZ+F8m9DOfx/ZuWNhaPuqZFPwwy0xqcBT6sWH7OAuBjv/fnpOTzyQhpVTWddlf+ECtMAMrxjxuOExGQ==", "integrity": "sha512-xKuo4HksZ+F8m9DOfx/ZuWNhaPuqZFPwwy0xqcBT6sWH7OAuBjv/fnpOTzyQhpVTWddlf+ECtMAMrxjxuOExGQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0", "@aws-crypto/sha256-js": "5.2.0",
@@ -2664,7 +2662,6 @@
"version": "3.632.0", "version": "3.632.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.632.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.632.0.tgz",
"integrity": "sha512-Oh1fIWaoZluihOCb/zDEpRTi+6an82fgJz7fyRBugyLhEtDjmvpCQ3oKjzaOhoN+4EvXAm1ZS/ZgpvXBlIRTgw==", "integrity": "sha512-Oh1fIWaoZluihOCb/zDEpRTi+6an82fgJz7fyRBugyLhEtDjmvpCQ3oKjzaOhoN+4EvXAm1ZS/ZgpvXBlIRTgw==",
"peer": true,
"dependencies": { "dependencies": {
"@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0", "@aws-crypto/sha256-js": "5.2.0",
@@ -2741,7 +2738,6 @@
"version": "3.632.0", "version": "3.632.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.632.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.632.0.tgz",
"integrity": "sha512-Ss5cBH09icpTvT+jtGGuQlRdwtO7RyE9BF4ZV/CEPATdd9whtJt4Qxdya8BUnkWR7h5HHTrQHqai3YVYjku41A==", "integrity": "sha512-Ss5cBH09icpTvT+jtGGuQlRdwtO7RyE9BF4ZV/CEPATdd9whtJt4Qxdya8BUnkWR7h5HHTrQHqai3YVYjku41A==",
"peer": true,
"dependencies": { "dependencies": {
"@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0", "@aws-crypto/sha256-js": "5.2.0",
@@ -5179,7 +5175,6 @@
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.2", "@babel/code-frame": "^7.26.2",
@@ -7211,7 +7206,6 @@
"url": "https://opencollective.com/csstools" "url": "https://opencollective.com/csstools"
} }
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
@@ -7233,7 +7227,6 @@
"url": "https://opencollective.com/csstools" "url": "https://opencollective.com/csstools"
} }
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -8561,6 +8554,12 @@
"resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-2.1.0.tgz", "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-2.1.0.tgz",
"integrity": "sha512-i1BpaNDVLJdRBEKeJWkVO6tYX6DMFBuwMhSuWqLsY4ufeTKGVuV5rBsUhxPayXqnnWHgXUAmWK16H/ykO5Wj4Q==" "integrity": "sha512-i1BpaNDVLJdRBEKeJWkVO6tYX6DMFBuwMhSuWqLsY4ufeTKGVuV5rBsUhxPayXqnnWHgXUAmWK16H/ykO5Wj4Q=="
}, },
"node_modules/@hexagon/base64": {
"version": "1.1.28",
"resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz",
"integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==",
"license": "MIT"
},
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.11.13", "version": "0.11.13",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
@@ -9214,9 +9213,6 @@
"win32" "win32"
] ]
}, },
"node_modules/@infisical/quic/node_modules/@infisical/quic-linux-arm": {
"optional": true
},
"node_modules/@ioredis/commands": { "node_modules/@ioredis/commands": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
@@ -9428,6 +9424,12 @@
"resolved": "https://registry.npmjs.org/@ldapjs/protocol/-/protocol-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@ldapjs/protocol/-/protocol-1.2.1.tgz",
"integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ==" "integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ=="
}, },
"node_modules/@levischuck/tiny-cbor": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz",
"integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==",
"license": "MIT"
},
"node_modules/@lukeed/ms": { "node_modules/@lukeed/ms": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz",
@@ -10667,7 +10669,6 @@
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz",
"integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@octokit/auth-token": "^4.0.0", "@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.1.0", "@octokit/graphql": "^7.1.0",
@@ -11111,7 +11112,6 @@
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"peer": true,
"engines": { "engines": {
"node": ">=8.0.0" "node": ">=8.0.0"
} }
@@ -11430,147 +11430,160 @@
"@otplib/plugin-thirty-two": "^12.0.1" "@otplib/plugin-thirty-two": "^12.0.1"
} }
}, },
"node_modules/@peculiar/asn1-cms": { "node_modules/@peculiar/asn1-android": {
"version": "2.3.8", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.3.8.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.6.0.tgz",
"integrity": "sha512-Wtk9R7yQxGaIaawHorWKP2OOOm/RZzamOmSWwaqGphIuU6TcKYih0slL6asZlSSZtVoYTrBfrddSOD/jTu9vuQ==", "integrity": "sha512-cBRCKtYPF7vJGN76/yG8VbxRcHLPF3HnkoHhKOZeHpoVtbMYfY9ROKtH3DtYUY9m8uI1Mh47PRhHf2hSK3xcSQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.6.0",
"@peculiar/asn1-x509": "^2.3.8", "asn1js": "^3.0.6",
"@peculiar/asn1-x509-attr": "^2.3.8", "tslib": "^2.8.1"
"asn1js": "^3.0.5", }
"tslib": "^2.6.2" },
"node_modules/@peculiar/asn1-cms": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.0.tgz",
"integrity": "sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.6.0",
"@peculiar/asn1-x509": "^2.6.0",
"@peculiar/asn1-x509-attr": "^2.6.0",
"asn1js": "^3.0.6",
"tslib": "^2.8.1"
} }
}, },
"node_modules/@peculiar/asn1-csr": { "node_modules/@peculiar/asn1-csr": {
"version": "2.3.8", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.3.8.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.0.tgz",
"integrity": "sha512-ZmAaP2hfzgIGdMLcot8gHTykzoI+X/S53x1xoGbTmratETIaAbSWMiPGvZmXRA0SNEIydpMkzYtq4fQBxN1u1w==", "integrity": "sha512-BeWIu5VpTIhfRysfEp73SGbwjjoLL/JWXhJ/9mo4vXnz3tRGm+NGm3KNcRzQ9VMVqwYS2RHlolz21svzRXIHPQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.6.0",
"@peculiar/asn1-x509": "^2.3.8", "@peculiar/asn1-x509": "^2.6.0",
"asn1js": "^3.0.5", "asn1js": "^3.0.6",
"tslib": "^2.6.2" "tslib": "^2.8.1"
} }
}, },
"node_modules/@peculiar/asn1-ecc": { "node_modules/@peculiar/asn1-ecc": {
"version": "2.3.8", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.8.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.0.tgz",
"integrity": "sha512-Ah/Q15y3A/CtxbPibiLM/LKcMbnLTdUdLHUgdpB5f60sSvGkXzxJCu5ezGTFHogZXWNX3KSmYqilCrfdmBc6pQ==", "integrity": "sha512-FF3LMGq6SfAOwUG2sKpPXblibn6XnEIKa+SryvUl5Pik+WR9rmRA3OCiwz8R3lVXnYnyRkSZsSLdml8H3UiOcw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.6.0",
"@peculiar/asn1-x509": "^2.3.8", "@peculiar/asn1-x509": "^2.6.0",
"asn1js": "^3.0.5", "asn1js": "^3.0.6",
"tslib": "^2.6.2" "tslib": "^2.8.1"
} }
}, },
"node_modules/@peculiar/asn1-pfx": { "node_modules/@peculiar/asn1-pfx": {
"version": "2.3.8", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.3.8.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.0.tgz",
"integrity": "sha512-XhdnCVznMmSmgy68B9pVxiZ1XkKoE1BjO4Hv+eUGiY1pM14msLsFZ3N7K46SoITIVZLq92kKkXpGiTfRjlNLyg==", "integrity": "sha512-rtUvtf+tyKGgokHHmZzeUojRZJYPxoD/jaN1+VAB4kKR7tXrnDCA/RAWXAIhMJJC+7W27IIRGe9djvxKgsldCQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-cms": "^2.3.8", "@peculiar/asn1-cms": "^2.6.0",
"@peculiar/asn1-pkcs8": "^2.3.8", "@peculiar/asn1-pkcs8": "^2.6.0",
"@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-rsa": "^2.6.0",
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.6.0",
"asn1js": "^3.0.5", "asn1js": "^3.0.6",
"tslib": "^2.6.2" "tslib": "^2.8.1"
} }
}, },
"node_modules/@peculiar/asn1-pkcs8": { "node_modules/@peculiar/asn1-pkcs8": {
"version": "2.3.8", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.3.8.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.0.tgz",
"integrity": "sha512-rL8k2x59v8lZiwLRqdMMmOJ30GHt6yuHISFIuuWivWjAJjnxzZBVzMTQ72sknX5MeTSSvGwPmEFk2/N8+UztFQ==", "integrity": "sha512-KyQ4D8G/NrS7Fw3XCJrngxmjwO/3htnA0lL9gDICvEQ+GJ+EPFqldcJQTwPIdvx98Tua+WjkdKHSC0/Km7T+lA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.6.0",
"@peculiar/asn1-x509": "^2.3.8", "@peculiar/asn1-x509": "^2.6.0",
"asn1js": "^3.0.5", "asn1js": "^3.0.6",
"tslib": "^2.6.2" "tslib": "^2.8.1"
} }
}, },
"node_modules/@peculiar/asn1-pkcs9": { "node_modules/@peculiar/asn1-pkcs9": {
"version": "2.3.8", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.3.8.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.0.tgz",
"integrity": "sha512-+nONq5tcK7vm3qdY7ZKoSQGQjhJYMJbwJGbXLFOhmqsFIxEWyQPHyV99+wshOjpOjg0wUSSkEEzX2hx5P6EKeQ==", "integrity": "sha512-b78OQ6OciW0aqZxdzliXGYHASeCvvw5caqidbpQRYW2mBtXIX2WhofNXTEe7NyxTb0P6J62kAAWLwn0HuMF1Fw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-cms": "^2.3.8", "@peculiar/asn1-cms": "^2.6.0",
"@peculiar/asn1-pfx": "^2.3.8", "@peculiar/asn1-pfx": "^2.6.0",
"@peculiar/asn1-pkcs8": "^2.3.8", "@peculiar/asn1-pkcs8": "^2.6.0",
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.6.0",
"@peculiar/asn1-x509": "^2.3.8", "@peculiar/asn1-x509": "^2.6.0",
"@peculiar/asn1-x509-attr": "^2.3.8", "@peculiar/asn1-x509-attr": "^2.6.0",
"asn1js": "^3.0.5", "asn1js": "^3.0.6",
"tslib": "^2.6.2" "tslib": "^2.8.1"
} }
}, },
"node_modules/@peculiar/asn1-rsa": { "node_modules/@peculiar/asn1-rsa": {
"version": "2.3.8", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.8.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.0.tgz",
"integrity": "sha512-ES/RVEHu8VMYXgrg3gjb1m/XG0KJWnV4qyZZ7mAg7rrF3VTmRbLxO8mk+uy0Hme7geSMebp+Wvi2U6RLLEs12Q==", "integrity": "sha512-Nu4C19tsrTsCp9fDrH+sdcOKoVfdfoQQ7S3VqjJU6vedR7tY3RLkQ5oguOIB3zFW33USDUuYZnPEQYySlgha4w==",
"license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.6.0",
"@peculiar/asn1-x509": "^2.3.8", "@peculiar/asn1-x509": "^2.6.0",
"asn1js": "^3.0.5", "asn1js": "^3.0.6",
"tslib": "^2.6.2" "tslib": "^2.8.1"
} }
}, },
"node_modules/@peculiar/asn1-schema": { "node_modules/@peculiar/asn1-schema": {
"version": "2.3.8", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.8.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz",
"integrity": "sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==", "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==",
"license": "MIT",
"dependencies": { "dependencies": {
"asn1js": "^3.0.5", "asn1js": "^3.0.6",
"pvtsutils": "^1.3.5", "pvtsutils": "^1.3.6",
"tslib": "^2.6.2" "tslib": "^2.8.1"
} }
}, },
"node_modules/@peculiar/asn1-x509": { "node_modules/@peculiar/asn1-x509": {
"version": "2.3.8", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.8.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.0.tgz",
"integrity": "sha512-voKxGfDU1c6r9mKiN5ZUsZWh3Dy1BABvTM3cimf0tztNwyMJPhiXY94eRTgsMQe6ViLfT6EoXxkWVzcm3mFAFw==", "integrity": "sha512-uzYbPEpoQiBoTq0/+jZtpM6Gq6zADBx+JNFP3yqRgziWBxQ/Dt/HcuvRfm9zJTPdRcBqPNdaRHTVwpyiq6iNMA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.6.0",
"asn1js": "^3.0.5", "asn1js": "^3.0.6",
"ipaddr.js": "^2.1.0", "pvtsutils": "^1.3.6",
"pvtsutils": "^1.3.5", "tslib": "^2.8.1"
"tslib": "^2.6.2"
} }
}, },
"node_modules/@peculiar/asn1-x509-attr": { "node_modules/@peculiar/asn1-x509-attr": {
"version": "2.3.8", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.3.8.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.0.tgz",
"integrity": "sha512-4Z8mSN95MOuX04Aku9BUyMdsMKtVQUqWnr627IheiWnwFoheUhX3R4Y2zh23M7m80r4/WG8MOAckRKc77IRv6g==", "integrity": "sha512-MuIAXFX3/dc8gmoZBkwJWxUWOSvG4MMDntXhrOZpJVMkYX+MYc/rUAU2uJOved9iJEoiUx7//3D8oG83a78UJA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.6.0",
"@peculiar/asn1-x509": "^2.3.8", "@peculiar/asn1-x509": "^2.6.0",
"asn1js": "^3.0.5", "asn1js": "^3.0.6",
"tslib": "^2.6.2" "tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-x509/node_modules/ipaddr.js": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
"integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
"engines": {
"node": ">= 10"
} }
}, },
"node_modules/@peculiar/x509": { "node_modules/@peculiar/x509": {
"version": "1.12.1", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.12.1.tgz", "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.0.tgz",
"integrity": "sha512-2T9t2viNP9m20mky50igPTpn2ByhHl5NlT6wW4Tp4BejQaQ5XDNZgfsabYwYysLXhChABlgtTCpp2gM3JBZRKA==", "integrity": "sha512-Yc4PDxN3OrxUPiXgU63c+ZRXKGE8YKF2McTciYhUHFtHVB0KMnjeFSU0qpztGhsp4P0uKix4+J2xEpIEDu8oXg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-cms": "^2.3.8", "@peculiar/asn1-cms": "^2.5.0",
"@peculiar/asn1-csr": "^2.3.8", "@peculiar/asn1-csr": "^2.5.0",
"@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-ecc": "^2.5.0",
"@peculiar/asn1-pkcs9": "^2.3.8", "@peculiar/asn1-pkcs9": "^2.5.0",
"@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-rsa": "^2.5.0",
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.5.0",
"@peculiar/asn1-x509": "^2.3.8", "@peculiar/asn1-x509": "^2.5.0",
"pvtsutils": "^1.3.5", "pvtsutils": "^1.3.6",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"tslib": "^2.6.2", "tslib": "^2.8.1",
"tsyringe": "^4.8.0" "tsyringe": "^4.10.0"
} }
}, },
"node_modules/@phc/format": { "node_modules/@phc/format": {
@@ -11752,7 +11765,6 @@
"resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.2.0.tgz",
"integrity": "sha512-9GCWmVmKUAoRfloboCd+RKm6X17xn7eGL7HnpAZUnjBXBilWCxsKnLMTC/ixSHDKS/A/057M1Tx6ZUXd89sVBw==", "integrity": "sha512-9GCWmVmKUAoRfloboCd+RKm6X17xn7eGL7HnpAZUnjBXBilWCxsKnLMTC/ixSHDKS/A/057M1Tx6ZUXd89sVBw==",
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"react": "^18.0 || ^19.0 || ^19.0.0-rc" "react": "^18.0 || ^19.0 || ^19.0.0-rc"
} }
@@ -11762,7 +11774,6 @@
"resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.2.0.tgz",
"integrity": "sha512-8i+v6cMxr2emz4ihCrRiYJPp2/sdYsNNsBzXStlcA+/B9Umpm5Jj3WJKYpgTPM+aeyiqlG/MMI1AucnBm4f1oQ==", "integrity": "sha512-8i+v6cMxr2emz4ihCrRiYJPp2/sdYsNNsBzXStlcA+/B9Umpm5Jj3WJKYpgTPM+aeyiqlG/MMI1AucnBm4f1oQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@@ -11775,7 +11786,6 @@
"resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.2.0.tgz",
"integrity": "sha512-eIrPW9PIFgDopQU0e/OPpwCW2QWQDtNZDSsiN4sJO8KdMnWWnXJicnRfzrit5rHwFo+Y98i+w/Y5ScnBAFr1dQ==", "integrity": "sha512-eIrPW9PIFgDopQU0e/OPpwCW2QWQDtNZDSsiN4sJO8KdMnWWnXJicnRfzrit5rHwFo+Y98i+w/Y5ScnBAFr1dQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"prismjs": "^1.30.0" "prismjs": "^1.30.0"
}, },
@@ -11791,7 +11801,6 @@
"resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.5.tgz", "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.5.tgz",
"integrity": "sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==", "integrity": "sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@@ -11867,7 +11876,6 @@
"resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.15.tgz", "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.15.tgz",
"integrity": "sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==", "integrity": "sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@@ -11901,7 +11909,6 @@
"resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.15.tgz", "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.15.tgz",
"integrity": "sha512-xF2GqsvBrp/HbRHWEfOgSfRFX+Q8I5KBEIG5+Lv3Vb2R/NYr0s8A5JhHHGf2pWBMJdbP4B2WHgj/VUrhy8dkIg==", "integrity": "sha512-xF2GqsvBrp/HbRHWEfOgSfRFX+Q8I5KBEIG5+Lv3Vb2R/NYr0s8A5JhHHGf2pWBMJdbP4B2WHgj/VUrhy8dkIg==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@@ -11914,7 +11921,6 @@
"resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.11.tgz", "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.11.tgz",
"integrity": "sha512-S1gZHVhwOsd1Iad5IFhpfICwNPMGPJidG/Uysy1AwmspyoAP5a4Iw3OWEpINFdgh9MHladbxcLKO2AJO+cA9Lw==", "integrity": "sha512-S1gZHVhwOsd1Iad5IFhpfICwNPMGPJidG/Uysy1AwmspyoAP5a4Iw3OWEpINFdgh9MHladbxcLKO2AJO+cA9Lw==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@@ -11939,7 +11945,6 @@
"resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.11.tgz", "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.11.tgz",
"integrity": "sha512-aGc8Y6U5C3igoMaqAJKsCpkbm1XjguQ09Acd+YcTKwjnC2+0w3yGUJkjWB2vTx4tN8dCqQCXO8FmdJpMfOA9EQ==", "integrity": "sha512-aGc8Y6U5C3igoMaqAJKsCpkbm1XjguQ09Acd+YcTKwjnC2+0w3yGUJkjWB2vTx4tN8dCqQCXO8FmdJpMfOA9EQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@@ -11952,7 +11957,6 @@
"resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.12.tgz", "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.12.tgz",
"integrity": "sha512-vF+xxQk2fGS1CN7UPQDbzvcBGfffr+GjTPNiWM38fhBfsLv6A/YUfaqxWlmL7zLzVmo0K2cvvV9wxlSyNba1aQ==", "integrity": "sha512-vF+xxQk2fGS1CN7UPQDbzvcBGfffr+GjTPNiWM38fhBfsLv6A/YUfaqxWlmL7zLzVmo0K2cvvV9wxlSyNba1aQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@@ -11980,7 +11984,6 @@
"resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.13.tgz", "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.13.tgz",
"integrity": "sha512-F7j9FJ0JN/A4d7yr+aw28p4uX7VLWs7hTHtLo7WRyw4G+Lit6Zucq4UWKRxJC8lpsUdzVmG7aBJnKOT+urqs/w==", "integrity": "sha512-F7j9FJ0JN/A4d7yr+aw28p4uX7VLWs7hTHtLo7WRyw4G+Lit6Zucq4UWKRxJC8lpsUdzVmG7aBJnKOT+urqs/w==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@@ -12085,7 +12088,6 @@
"resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.5.tgz", "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.5.tgz",
"integrity": "sha512-o5PNHFSE085VMXayxH+SJ1LSOtGsTv+RpNKnTiJDrJUwoBu77G3PlKOsZZQHCNyD28WsQpl9v2WcJLbQudqwPg==", "integrity": "sha512-o5PNHFSE085VMXayxH+SJ1LSOtGsTv+RpNKnTiJDrJUwoBu77G3PlKOsZZQHCNyD28WsQpl9v2WcJLbQudqwPg==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@@ -12515,6 +12517,25 @@
"split2": "^4.0.0" "split2": "^4.0.0"
} }
}, },
"node_modules/@simplewebauthn/server": {
"version": "13.2.2",
"resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.2.2.tgz",
"integrity": "sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA==",
"license": "MIT",
"dependencies": {
"@hexagon/base64": "^1.1.27",
"@levischuck/tiny-cbor": "^0.2.2",
"@peculiar/asn1-android": "^2.3.10",
"@peculiar/asn1-ecc": "^2.3.8",
"@peculiar/asn1-rsa": "^2.3.8",
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/asn1-x509": "^2.3.8",
"@peculiar/x509": "^1.13.0"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@sindresorhus/slugify": { "node_modules/@sindresorhus/slugify": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-1.1.0.tgz",
@@ -14074,7 +14095,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz",
"integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==", "integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
@@ -14563,7 +14583,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.20.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.20.0.tgz",
"integrity": "sha512-bYerPDF/H5v6V76MdMYhjwmwgMA+jlPVqjSDq2cRqMi8bP5sR3Z+RLOiOMad3nsnmDVmn2gAFCyNgh/dIrfP/w==", "integrity": "sha512-bYerPDF/H5v6V76MdMYhjwmwgMA+jlPVqjSDq2cRqMi8bP5sR3Z+RLOiOMad3nsnmDVmn2gAFCyNgh/dIrfP/w==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "6.20.0", "@typescript-eslint/scope-manager": "6.20.0",
"@typescript-eslint/types": "6.20.0", "@typescript-eslint/types": "6.20.0",
@@ -15228,7 +15247,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -15717,7 +15735,6 @@
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"bn.js": "^4.0.0", "bn.js": "^4.0.0",
"inherits": "^2.0.1", "inherits": "^2.0.1",
@@ -15747,13 +15764,14 @@
} }
}, },
"node_modules/asn1js": { "node_modules/asn1js": {
"version": "3.0.5", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz",
"integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", "integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==",
"license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"pvtsutils": "^1.3.2", "pvtsutils": "^1.3.6",
"pvutils": "^1.1.3", "pvutils": "^1.1.3",
"tslib": "^2.4.0" "tslib": "^2.8.1"
}, },
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=12.0.0"
@@ -15942,7 +15960,6 @@
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.4", "form-data": "^4.0.4",
@@ -16544,7 +16561,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.9", "baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746", "caniuse-lite": "^1.0.30001746",
@@ -18099,7 +18115,6 @@
"version": "16.4.1", "version": "16.4.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz",
"integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==", "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -18485,7 +18500,6 @@
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
}, },
@@ -18568,7 +18582,6 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
"integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1", "@eslint-community/regexpp": "^4.6.1",
@@ -18667,7 +18680,6 @@
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
"dev": true, "dev": true,
"peer": true,
"bin": { "bin": {
"eslint-config-prettier": "bin/cli.js" "eslint-config-prettier": "bin/cli.js"
}, },
@@ -18756,7 +18768,6 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
"integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"array-includes": "^3.1.7", "array-includes": "^3.1.7",
"array.prototype.findlastindex": "^1.2.3", "array.prototype.findlastindex": "^1.2.3",
@@ -19239,6 +19250,7 @@
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz",
"integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"cookie": "0.7.2", "cookie": "0.7.2",
"cookie-signature": "1.0.7", "cookie-signature": "1.0.7",
@@ -19256,12 +19268,14 @@
"node_modules/express-session/node_modules/cookie-signature": { "node_modules/express-session/node_modules/cookie-signature": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"peer": true
}, },
"node_modules/express-session/node_modules/debug": { "node_modules/express-session/node_modules/debug": {
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"peer": true,
"dependencies": { "dependencies": {
"ms": "2.0.0" "ms": "2.0.0"
} }
@@ -19269,7 +19283,8 @@
"node_modules/express-session/node_modules/ms": { "node_modules/express-session/node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"peer": true
}, },
"node_modules/express/node_modules/cookie": { "node_modules/express/node_modules/cookie": {
"version": "0.7.1", "version": "0.7.1",
@@ -25926,6 +25941,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"peer": true,
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@@ -26625,7 +26641,6 @@
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz",
"integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==",
"peer": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
}, },
@@ -26968,7 +26983,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -27084,7 +27098,6 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
@@ -27456,11 +27469,12 @@
} }
}, },
"node_modules/pvtsutils": { "node_modules/pvtsutils": {
"version": "1.3.5", "version": "1.3.6",
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz", "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
"integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==", "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
"license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.6.1" "tslib": "^2.8.1"
} }
}, },
"node_modules/pvutils": { "node_modules/pvutils": {
@@ -27548,6 +27562,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
"peer": true,
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@@ -27643,7 +27658,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -27653,7 +27667,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.26.0" "scheduler": "^0.26.0"
}, },
@@ -29478,7 +29491,6 @@
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz",
"integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"ip-address": "^9.0.5", "ip-address": "^9.0.5",
"smart-buffer": "^4.2.0" "smart-buffer": "^4.2.0"
@@ -30977,7 +30989,6 @@
"integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "~0.25.0", "esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5" "get-tsconfig": "^4.7.5"
@@ -30993,9 +31004,10 @@
} }
}, },
"node_modules/tsyringe": { "node_modules/tsyringe": {
"version": "4.8.0", "version": "4.10.0",
"resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.8.0.tgz", "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz",
"integrity": "sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==", "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==",
"license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^1.9.3" "tslib": "^1.9.3"
}, },
@@ -31006,7 +31018,8 @@
"node_modules/tsyringe/node_modules/tslib": { "node_modules/tsyringe/node_modules/tslib": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"license": "0BSD"
}, },
"node_modules/ttl-set": { "node_modules/ttl-set": {
"version": "1.0.0", "version": "1.0.0",
@@ -31155,7 +31168,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
"integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
"dev": true, "dev": true,
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -31196,6 +31208,7 @@
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"peer": true,
"dependencies": { "dependencies": {
"random-bytes": "~1.0.0" "random-bytes": "~1.0.0"
}, },
@@ -31783,7 +31796,6 @@
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -32465,7 +32477,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz",
"integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }

View File

@@ -188,6 +188,7 @@
"@peculiar/x509": "^1.12.1", "@peculiar/x509": "^1.12.1",
"@react-email/components": "^1.0.1", "@react-email/components": "^1.0.1",
"@serdnam/pino-cloudwatch-transport": "^1.0.4", "@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@simplewebauthn/server": "^13.2.2",
"@sindresorhus/slugify": "1.1.0", "@sindresorhus/slugify": "1.1.0",
"@slack/oauth": "^3.0.2", "@slack/oauth": "^3.0.2",
"@slack/web-api": "^7.8.0", "@slack/web-api": "^7.8.0",

View File

@@ -102,6 +102,7 @@ import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/i
import { TMembershipGroupServiceFactory } from "@app/services/membership-group/membership-group-service"; import { TMembershipGroupServiceFactory } from "@app/services/membership-group/membership-group-service";
import { TMembershipIdentityServiceFactory } from "@app/services/membership-identity/membership-identity-service"; import { TMembershipIdentityServiceFactory } from "@app/services/membership-identity/membership-identity-service";
import { TMembershipUserServiceFactory } from "@app/services/membership-user/membership-user-service"; import { TMembershipUserServiceFactory } from "@app/services/membership-user/membership-user-service";
import { TMfaSessionServiceFactory } from "@app/services/mfa-session/mfa-session-service";
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
import { TNotificationServiceFactory } from "@app/services/notification/notification-service"; import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
import { TOfflineUsageReportServiceFactory } from "@app/services/offline-usage-report/offline-usage-report-service"; import { TOfflineUsageReportServiceFactory } from "@app/services/offline-usage-report/offline-usage-report-service";
@@ -137,6 +138,7 @@ import { TUpgradePathService } from "@app/services/upgrade-path/upgrade-path-ser
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
import { TUserServiceFactory } from "@app/services/user/user-service"; import { TUserServiceFactory } from "@app/services/user/user-service";
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
import { TWebAuthnServiceFactory } from "@app/services/webauthn/webauthn-service";
import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service"; import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
@@ -329,6 +331,7 @@ declare module "fastify" {
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory; externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
projectTemplate: TProjectTemplateServiceFactory; projectTemplate: TProjectTemplateServiceFactory;
totp: TTotpServiceFactory; totp: TTotpServiceFactory;
webAuthn: TWebAuthnServiceFactory;
appConnection: TAppConnectionServiceFactory; appConnection: TAppConnectionServiceFactory;
secretSync: TSecretSyncServiceFactory; secretSync: TSecretSyncServiceFactory;
kmip: TKmipServiceFactory; kmip: TKmipServiceFactory;
@@ -355,6 +358,7 @@ declare module "fastify" {
pamResource: TPamResourceServiceFactory; pamResource: TPamResourceServiceFactory;
pamAccount: TPamAccountServiceFactory; pamAccount: TPamAccountServiceFactory;
pamSession: TPamSessionServiceFactory; pamSession: TPamSessionServiceFactory;
mfaSession: TMfaSessionServiceFactory;
upgradePath: TUpgradePathService; upgradePath: TUpgradePathService;
membershipUser: TMembershipUserServiceFactory; membershipUser: TMembershipUserServiceFactory;

View File

@@ -614,6 +614,9 @@ import {
TVaultExternalMigrationConfigs, TVaultExternalMigrationConfigs,
TVaultExternalMigrationConfigsInsert, TVaultExternalMigrationConfigsInsert,
TVaultExternalMigrationConfigsUpdate, TVaultExternalMigrationConfigsUpdate,
TWebauthnCredentials,
TWebauthnCredentialsInsert,
TWebauthnCredentialsUpdate,
TWebhooks, TWebhooks,
TWebhooksInsert, TWebhooksInsert,
TWebhooksUpdate, TWebhooksUpdate,
@@ -1528,6 +1531,11 @@ declare module "knex/types/tables" {
TVaultExternalMigrationConfigsInsert, TVaultExternalMigrationConfigsInsert,
TVaultExternalMigrationConfigsUpdate TVaultExternalMigrationConfigsUpdate
>; >;
[TableName.WebAuthnCredential]: KnexOriginal.CompositeTableType<
TWebauthnCredentials,
TWebauthnCredentialsInsert,
TWebauthnCredentialsUpdate
>;
[TableName.AiMcpServer]: KnexOriginal.CompositeTableType<TAiMcpServers, TAiMcpServersInsert, TAiMcpServersUpdate>; [TableName.AiMcpServer]: KnexOriginal.CompositeTableType<TAiMcpServers, TAiMcpServersInsert, TAiMcpServersUpdate>;
[TableName.AiMcpServerTool]: KnexOriginal.CompositeTableType< [TableName.AiMcpServerTool]: KnexOriginal.CompositeTableType<
TAiMcpServerTools, TAiMcpServerTools,

View File

@@ -0,0 +1,30 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.WebAuthnCredential))) {
await knex.schema.createTable(TableName.WebAuthnCredential, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("userId").notNullable();
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.text("credentialId").notNullable(); // base64url encoded credential ID
t.text("publicKey").notNullable(); // base64url encoded public key
t.bigInteger("counter").defaultTo(0).notNullable(); // signature counter for replay protection
t.specificType("transports", "text[]").nullable(); // transport methods
t.string("name").nullable(); // user-friendly name
t.timestamp("lastUsedAt").nullable();
t.timestamps(true, true, true);
t.unique("credentialId"); // credential IDs must be unique across all users
t.index("userId"); // index for fast lookups by user
});
await createOnUpdateTrigger(knex, TableName.WebAuthnCredential);
}
}
export async function down(knex: Knex): Promise<void> {
await dropOnUpdateTrigger(knex, TableName.WebAuthnCredential);
await knex.schema.dropTableIfExists(TableName.WebAuthnCredential);
}

View File

@@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.PamAccount, "requireMfa"))) {
await knex.schema.alterTable(TableName.PamAccount, (t) => {
t.boolean("requireMfa").defaultTo(false);
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.PamAccount, "requireMfa")) {
await knex.schema.alterTable(TableName.PamAccount, (t) => {
t.dropColumn("requireMfa");
});
}
}

View File

@@ -209,5 +209,6 @@ export * from "./user-encryption-keys";
export * from "./user-group-membership"; export * from "./user-group-membership";
export * from "./users"; export * from "./users";
export * from "./vault-external-migration-configs"; export * from "./vault-external-migration-configs";
export * from "./webauthn-credentials";
export * from "./webhooks"; export * from "./webhooks";
export * from "./workflow-integrations"; export * from "./workflow-integrations";

View File

@@ -160,6 +160,7 @@ export enum TableName {
InternalKms = "internal_kms", InternalKms = "internal_kms",
InternalKmsKeyVersion = "internal_kms_key_version", InternalKmsKeyVersion = "internal_kms_key_version",
TotpConfig = "totp_configs", TotpConfig = "totp_configs",
WebAuthnCredential = "webauthn_credentials",
// @depreciated // @depreciated
KmsKeyVersion = "kms_key_versions", KmsKeyVersion = "kms_key_versions",
WorkflowIntegrations = "workflow_integrations", WorkflowIntegrations = "workflow_integrations",

View File

@@ -23,7 +23,8 @@ export const PamAccountsSchema = z.object({
rotationIntervalSeconds: z.number().nullable().optional(), rotationIntervalSeconds: z.number().nullable().optional(),
lastRotatedAt: z.date().nullable().optional(), lastRotatedAt: z.date().nullable().optional(),
rotationStatus: z.string().nullable().optional(), rotationStatus: z.string().nullable().optional(),
encryptedLastRotationMessage: zodBuffer.nullable().optional() encryptedLastRotationMessage: zodBuffer.nullable().optional(),
requireMfa: z.boolean().default(false).nullable().optional()
}); });
export type TPamAccounts = z.infer<typeof PamAccountsSchema>; export type TPamAccounts = z.infer<typeof PamAccountsSchema>;

View File

@@ -0,0 +1,25 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const WebauthnCredentialsSchema = z.object({
id: z.string().uuid(),
userId: z.string().uuid(),
credentialId: z.string(),
publicKey: z.string(),
counter: z.coerce.number().default(0),
transports: z.string().array().nullable().optional(),
name: z.string().nullable().optional(),
lastUsedAt: z.date().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TWebauthnCredentials = z.infer<typeof WebauthnCredentialsSchema>;
export type TWebauthnCredentialsInsert = Omit<z.input<typeof WebauthnCredentialsSchema>, TImmutableDBKeys>;
export type TWebauthnCredentialsUpdate = Partial<Omit<z.input<typeof WebauthnCredentialsSchema>, TImmutableDBKeys>>;

View File

@@ -24,6 +24,7 @@ export const registerPamAccountEndpoints = <C extends TPamAccount>({
description?: C["description"]; description?: C["description"];
rotationEnabled?: C["rotationEnabled"]; rotationEnabled?: C["rotationEnabled"];
rotationIntervalSeconds?: C["rotationIntervalSeconds"]; rotationIntervalSeconds?: C["rotationIntervalSeconds"];
requireMfa?: C["requireMfa"];
}>; }>;
updateAccountSchema: z.ZodType<{ updateAccountSchema: z.ZodType<{
credentials?: C["credentials"]; credentials?: C["credentials"];
@@ -31,6 +32,7 @@ export const registerPamAccountEndpoints = <C extends TPamAccount>({
description?: C["description"]; description?: C["description"];
rotationEnabled?: C["rotationEnabled"]; rotationEnabled?: C["rotationEnabled"];
rotationIntervalSeconds?: C["rotationIntervalSeconds"]; rotationIntervalSeconds?: C["rotationIntervalSeconds"];
requireMfa?: C["requireMfa"];
}>; }>;
accountResponseSchema: z.ZodTypeAny; accountResponseSchema: z.ZodTypeAny;
}) => { }) => {
@@ -66,7 +68,8 @@ export const registerPamAccountEndpoints = <C extends TPamAccount>({
name: req.body.name, name: req.body.name,
description: req.body.description, description: req.body.description,
rotationEnabled: req.body.rotationEnabled ?? false, rotationEnabled: req.body.rotationEnabled ?? false,
rotationIntervalSeconds: req.body.rotationIntervalSeconds rotationIntervalSeconds: req.body.rotationIntervalSeconds,
requireMfa: req.body.requireMfa
} }
} }
}); });
@@ -116,7 +119,8 @@ export const registerPamAccountEndpoints = <C extends TPamAccount>({
name: req.body.name, name: req.body.name,
description: req.body.description, description: req.body.description,
rotationEnabled: req.body.rotationEnabled, rotationEnabled: req.body.rotationEnabled,
rotationIntervalSeconds: req.body.rotationIntervalSeconds rotationIntervalSeconds: req.body.rotationIntervalSeconds,
requireMfa: req.body.requireMfa
} }
} }
}); });

View File

@@ -115,6 +115,7 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
body: z.object({ body: z.object({
accountPath: z.string().trim(), accountPath: z.string().trim(),
projectId: z.string().uuid(), projectId: z.string().uuid(),
mfaSessionId: z.string().optional(),
duration: z duration: z
.string() .string()
.min(1) .min(1)
@@ -163,7 +164,8 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
actorUserAgent: req.auditLogInfo.userAgent ?? "", actorUserAgent: req.auditLogInfo.userAgent ?? "",
accountPath: req.body.accountPath, accountPath: req.body.accountPath,
projectId: req.body.projectId, projectId: req.body.projectId,
duration: req.body.duration duration: req.body.duration,
mfaSessionId: req.body.mfaSessionId
}, },
req.permission req.permission
); );

View File

@@ -4199,6 +4199,7 @@ interface PamAccountCreateEvent {
description?: string | null; description?: string | null;
rotationEnabled: boolean; rotationEnabled: boolean;
rotationIntervalSeconds?: number | null; rotationIntervalSeconds?: number | null;
requireMfa?: boolean | null;
}; };
} }
@@ -4212,6 +4213,7 @@ interface PamAccountUpdateEvent {
description?: string | null; description?: string | null;
rotationEnabled?: boolean; rotationEnabled?: boolean;
rotationIntervalSeconds?: number | null; rotationIntervalSeconds?: number | null;
requireMfa?: boolean | null;
}; };
} }

View File

@@ -38,11 +38,16 @@ import { TApprovalPolicyDALFactory } from "@app/services/approval-policy/approva
import { ApprovalPolicyType } from "@app/services/approval-policy/approval-policy-enums"; import { ApprovalPolicyType } from "@app/services/approval-policy/approval-policy-enums";
import { APPROVAL_POLICY_FACTORY_MAP } from "@app/services/approval-policy/approval-policy-factory"; import { APPROVAL_POLICY_FACTORY_MAP } from "@app/services/approval-policy/approval-policy-factory";
import { TApprovalRequestGrantsDALFactory } from "@app/services/approval-policy/approval-request-dal"; import { TApprovalRequestGrantsDALFactory } from "@app/services/approval-policy/approval-request-dal";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType, MfaMethod } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { TMfaSessionServiceFactory } from "@app/services/mfa-session/mfa-session-service";
import { MfaSessionStatus } from "@app/services/mfa-session/mfa-session-types";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TPamSessionExpirationServiceFactory } from "@app/services/pam-session-expiration/pam-session-expiration-queue"; import { TPamSessionExpirationServiceFactory } from "@app/services/pam-session-expiration/pam-session-expiration-queue";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
import { EventType, TAuditLogServiceFactory } from "../audit-log/audit-log-types"; import { EventType, TAuditLogServiceFactory } from "../audit-log/audit-log-types";
@@ -68,7 +73,9 @@ type TPamAccountServiceFactoryDep = {
pamSessionDAL: TPamSessionDALFactory; pamSessionDAL: TPamSessionDALFactory;
pamAccountDAL: TPamAccountDALFactory; pamAccountDAL: TPamAccountDALFactory;
pamFolderDAL: TPamFolderDALFactory; pamFolderDAL: TPamFolderDALFactory;
mfaSessionService: TMfaSessionServiceFactory;
projectDAL: TProjectDALFactory; projectDAL: TProjectDALFactory;
orgDAL: TOrgDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
@@ -78,10 +85,13 @@ type TPamAccountServiceFactoryDep = {
>; >;
userDAL: TUserDALFactory; userDAL: TUserDALFactory;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">; auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser" | "validateTokenForUser">;
smtpService: Pick<TSmtpService, "sendMail">;
approvalPolicyDAL: TApprovalPolicyDALFactory; approvalPolicyDAL: TApprovalPolicyDALFactory;
approvalRequestGrantsDAL: TApprovalRequestGrantsDALFactory; approvalRequestGrantsDAL: TApprovalRequestGrantsDALFactory;
pamSessionExpirationService: Pick<TPamSessionExpirationServiceFactory, "scheduleSessionExpiration">; pamSessionExpirationService: Pick<TPamSessionExpirationServiceFactory, "scheduleSessionExpiration">;
}; };
export type TPamAccountServiceFactory = ReturnType<typeof pamAccountServiceFactory>; export type TPamAccountServiceFactory = ReturnType<typeof pamAccountServiceFactory>;
const ROTATION_CONCURRENCY_LIMIT = 10; const ROTATION_CONCURRENCY_LIMIT = 10;
@@ -90,8 +100,10 @@ export const pamAccountServiceFactory = ({
pamResourceDAL, pamResourceDAL,
pamSessionDAL, pamSessionDAL,
pamAccountDAL, pamAccountDAL,
mfaSessionService,
pamFolderDAL, pamFolderDAL,
projectDAL, projectDAL,
orgDAL,
userDAL, userDAL,
permissionService, permissionService,
licenseService, licenseService,
@@ -110,7 +122,8 @@ export const pamAccountServiceFactory = ({
description, description,
folderId, folderId,
rotationEnabled, rotationEnabled,
rotationIntervalSeconds rotationIntervalSeconds,
requireMfa
}: TCreateAccountDTO, }: TCreateAccountDTO,
actor: OrgServiceActor actor: OrgServiceActor
) => { ) => {
@@ -198,7 +211,8 @@ export const pamAccountServiceFactory = ({
description, description,
folderId, folderId,
rotationEnabled, rotationEnabled,
rotationIntervalSeconds rotationIntervalSeconds,
requireMfa
}); });
return { return {
@@ -222,7 +236,15 @@ export const pamAccountServiceFactory = ({
}; };
const updateById = async ( const updateById = async (
{ accountId, credentials, description, name, rotationEnabled, rotationIntervalSeconds }: TUpdateAccountDTO, {
accountId,
credentials,
description,
name,
rotationEnabled,
rotationIntervalSeconds,
requireMfa
}: TUpdateAccountDTO,
actor: OrgServiceActor actor: OrgServiceActor
) => { ) => {
const orgLicensePlan = await licenseService.getPlan(actor.orgId); const orgLicensePlan = await licenseService.getPlan(actor.orgId);
@@ -268,6 +290,10 @@ export const pamAccountServiceFactory = ({
updateDoc.name = name; updateDoc.name = name;
} }
if (requireMfa !== undefined) {
updateDoc.requireMfa = requireMfa;
}
if (description !== undefined) { if (description !== undefined) {
updateDoc.description = description; updateDoc.description = description;
} }
@@ -552,7 +578,16 @@ export const pamAccountServiceFactory = ({
}; };
const access = async ( const access = async (
{ accountPath, projectId, actorEmail, actorIp, actorName, actorUserAgent, duration }: TAccessAccountDTO, {
accountPath,
projectId,
actorEmail,
actorIp,
actorName,
actorUserAgent,
duration,
mfaSessionId
}: TAccessAccountDTO,
actor: OrgServiceActor actor: OrgServiceActor
) => { ) => {
const orgLicensePlan = await licenseService.getPlan(actor.orgId); const orgLicensePlan = await licenseService.getPlan(actor.orgId);
@@ -640,6 +675,76 @@ export const pamAccountServiceFactory = ({
); );
} }
const project = await projectDAL.findById(account.projectId);
if (!project) throw new NotFoundError({ message: `Project with ID '${account.projectId}' not found` });
const actorUser = await userDAL.findById(actor.id);
if (!actorUser) throw new NotFoundError({ message: `User with ID '${actor.id}' not found` });
// If no mfaSessionId is provided, create a new MFA session
if (!mfaSessionId && account.requireMfa) {
// Get organization to check if MFA is enforced at org level
const org = await orgDAL.findOrgById(project.orgId);
if (!org) throw new NotFoundError({ message: `Organization with ID '${project.orgId}' not found` });
// Determine which MFA method to use
// Priority: org-enforced > user-selected > email as fallback
const orgMfaMethod = org.enforceMfa ? (org.selectedMfaMethod as MfaMethod | null) : undefined;
const userMfaMethod = actorUser.isMfaEnabled ? (actorUser.selectedMfaMethod as MfaMethod | null) : undefined;
const mfaMethod = (orgMfaMethod ?? userMfaMethod ?? MfaMethod.EMAIL) as MfaMethod;
// Create MFA session
const newMfaSessionId = await mfaSessionService.createMfaSession(actorUser.id, account.id, mfaMethod);
// If MFA method is email, send the code immediately
if (mfaMethod === MfaMethod.EMAIL && actorUser.email) {
await mfaSessionService.sendMfaCode(actorUser.id, actorUser.email);
}
// Throw an error with the mfaSessionId to signal that MFA is required
throw new BadRequestError({
message: "MFA verification required to access PAM account",
name: "SESSION_MFA_REQUIRED",
details: {
mfaSessionId: newMfaSessionId,
mfaMethod
}
});
}
if (mfaSessionId && account.requireMfa) {
const mfaSession = await mfaSessionService.getMfaSession(mfaSessionId);
if (!mfaSession) {
throw new BadRequestError({
message: "MFA session not found or expired"
});
}
// Verify the session belongs to the current user
if (mfaSession.userId !== actor.id) {
throw new BadRequestError({
message: "MFA session does not belong to current user"
});
}
// Verify the session is for the same account
if (mfaSession.resourceId !== account.id) {
throw new BadRequestError({
message: "MFA session is for a different account"
});
}
// Check if MFA session is active
if (mfaSession.status !== MfaSessionStatus.ACTIVE) {
throw new BadRequestError({
message: "MFA session is not active. Please complete MFA verification first."
});
}
// MFA verified successfully, delete the session and proceed with access
await mfaSessionService.deleteMfaSession(mfaSessionId);
}
const { connectionDetails, gatewayId, resourceType } = await decryptResource( const { connectionDetails, gatewayId, resourceType } = await decryptResource(
resource, resource,
account.projectId, account.projectId,

View File

@@ -6,7 +6,7 @@ import { PamAccountOrderBy, PamAccountView } from "./pam-account-enums";
// DTOs // DTOs
export type TCreateAccountDTO = Pick< export type TCreateAccountDTO = Pick<
TPamAccount, TPamAccount,
"name" | "description" | "credentials" | "folderId" | "resourceId" | "rotationIntervalSeconds" "name" | "description" | "credentials" | "folderId" | "resourceId" | "rotationIntervalSeconds" | "requireMfa"
> & { > & {
rotationEnabled?: boolean; rotationEnabled?: boolean;
}; };
@@ -23,6 +23,7 @@ export type TAccessAccountDTO = {
actorName: string; actorName: string;
actorUserAgent: string; actorUserAgent: string;
duration: number; duration: number;
mfaSessionId?: string;
}; };
export type TListAccountsDTO = { export type TListAccountsDTO = {

View File

@@ -66,12 +66,14 @@ export const BaseCreatePamAccountSchema = z.object({
name: slugSchema({ field: "name" }), name: slugSchema({ field: "name" }),
description: z.string().max(512).nullable().optional(), description: z.string().max(512).nullable().optional(),
rotationEnabled: z.boolean(), rotationEnabled: z.boolean(),
rotationIntervalSeconds: z.number().min(3600).nullable().optional() rotationIntervalSeconds: z.number().min(3600).nullable().optional(),
requireMfa: z.boolean().optional().default(false)
}); });
export const BaseUpdatePamAccountSchema = z.object({ export const BaseUpdatePamAccountSchema = z.object({
name: slugSchema({ field: "name" }).optional(), name: slugSchema({ field: "name" }).optional(),
description: z.string().max(512).nullable().optional(), description: z.string().max(512).nullable().optional(),
rotationEnabled: z.boolean().optional(), rotationEnabled: z.boolean().optional(),
rotationIntervalSeconds: z.number().min(3600).nullable().optional() rotationIntervalSeconds: z.number().min(3600).nullable().optional(),
requireMfa: z.boolean().optional()
}); });

View File

@@ -81,6 +81,8 @@ export const KeyStorePrefixes = {
`group-member-project-permission:${projectId}:${groupId}:*` as const, `group-member-project-permission:${projectId}:${groupId}:*` as const,
PkiAcmeNonce: (nonce: string) => `pki-acme-nonce:${nonce}` as const, PkiAcmeNonce: (nonce: string) => `pki-acme-nonce:${nonce}` as const,
MfaSession: (mfaSessionId: string) => `mfa-session:${mfaSessionId}` as const,
WebAuthnChallenge: (userId: string) => `webauthn-challenge:${userId}` as const,
AiMcpServerOAuth: (sessionId: string) => `ai-mcp-server-oauth:${sessionId}` as const, AiMcpServerOAuth: (sessionId: string) => `ai-mcp-server-oauth:${sessionId}` as const,
@@ -94,7 +96,9 @@ export const KeyStoreTtls = {
SetSecretSyncLastRunTimestampInSeconds: 60, SetSecretSyncLastRunTimestampInSeconds: 60,
AccessTokenStatusUpdateInSeconds: 120, AccessTokenStatusUpdateInSeconds: 120,
ProjectPermissionCacheInSeconds: 300, // 5 minutes ProjectPermissionCacheInSeconds: 300, // 5 minutes
ProjectPermissionDalVersionTtl: "15m" // Project permission DAL version TTL ProjectPermissionDalVersionTtl: "15m", // Project permission DAL version TTL
MfaSessionInSeconds: 300, // 5 minutes
WebAuthnChallengeInSeconds: 300 // 5 minutes
}; };
type TDeleteItems = { type TDeleteItems = {

View File

@@ -90,10 +90,23 @@ export class BadRequestError extends Error {
error: unknown; error: unknown;
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) { details?: unknown;
constructor({
name,
error,
message,
details
}: {
message?: string;
name?: string;
error?: unknown;
details?: unknown;
}) {
super(message ?? "The request is invalid"); super(message ?? "The request is invalid");
this.name = name || "BadRequest"; this.name = name || "BadRequest";
this.error = error; this.error = error;
this.details = details;
} }
} }

View File

@@ -134,9 +134,13 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
} }
if (error instanceof BadRequestError) { if (error instanceof BadRequestError) {
void res void res.status(HttpStatusCodes.BadRequest).send({
.status(HttpStatusCodes.BadRequest) reqId: req.id,
.send({ reqId: req.id, statusCode: HttpStatusCodes.BadRequest, message: error.message, error: error.name }); statusCode: HttpStatusCodes.BadRequest,
message: error.message,
error: error.name,
details: error.details
});
} else if (error instanceof NotFoundError) { } else if (error instanceof NotFoundError) {
void res void res
.status(HttpStatusCodes.NotFound) .status(HttpStatusCodes.NotFound)

View File

@@ -293,6 +293,7 @@ import { membershipIdentityDALFactory } from "@app/services/membership-identity/
import { membershipIdentityServiceFactory } from "@app/services/membership-identity/membership-identity-service"; import { membershipIdentityServiceFactory } from "@app/services/membership-identity/membership-identity-service";
import { membershipUserDALFactory } from "@app/services/membership-user/membership-user-dal"; import { membershipUserDALFactory } from "@app/services/membership-user/membership-user-dal";
import { membershipUserServiceFactory } from "@app/services/membership-user/membership-user-service"; import { membershipUserServiceFactory } from "@app/services/membership-user/membership-user-service";
import { mfaSessionServiceFactory } from "@app/services/mfa-session/mfa-session-service";
import { microsoftTeamsIntegrationDALFactory } from "@app/services/microsoft-teams/microsoft-teams-integration-dal"; import { microsoftTeamsIntegrationDALFactory } from "@app/services/microsoft-teams/microsoft-teams-integration-dal";
import { microsoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; import { microsoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
import { projectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal"; import { projectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
@@ -391,6 +392,8 @@ import { userDALFactory } from "@app/services/user/user-dal";
import { userServiceFactory } from "@app/services/user/user-service"; import { userServiceFactory } from "@app/services/user/user-service";
import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { userEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { userEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
import { webAuthnCredentialDALFactory } from "@app/services/webauthn/webauthn-credential-dal";
import { webAuthnServiceFactory } from "@app/services/webauthn/webauthn-service";
import { webhookDALFactory } from "@app/services/webhook/webhook-dal"; import { webhookDALFactory } from "@app/services/webhook/webhook-dal";
import { webhookServiceFactory } from "@app/services/webhook/webhook-service"; import { webhookServiceFactory } from "@app/services/webhook/webhook-service";
import { workflowIntegrationDALFactory } from "@app/services/workflow-integration/workflow-integration-dal"; import { workflowIntegrationDALFactory } from "@app/services/workflow-integration/workflow-integration-dal";
@@ -570,6 +573,7 @@ export const registerRoutes = async (
const projectSlackConfigDAL = projectSlackConfigDALFactory(db); const projectSlackConfigDAL = projectSlackConfigDALFactory(db);
const workflowIntegrationDAL = workflowIntegrationDALFactory(db); const workflowIntegrationDAL = workflowIntegrationDALFactory(db);
const totpConfigDAL = totpConfigDALFactory(db); const totpConfigDAL = totpConfigDALFactory(db);
const webAuthnCredentialDAL = webAuthnCredentialDALFactory(db);
const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db); const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db);
@@ -914,6 +918,13 @@ export const registerRoutes = async (
kmsService kmsService
}); });
const webAuthnService = webAuthnServiceFactory({
webAuthnCredentialDAL,
userDAL,
tokenService,
keyStore
});
const loginService = authLoginServiceFactory({ const loginService = authLoginServiceFactory({
userDAL, userDAL,
smtpService, smtpService,
@@ -2451,6 +2462,13 @@ export const registerRoutes = async (
gatewayV2Service gatewayV2Service
}); });
const mfaSessionService = mfaSessionServiceFactory({
keyStore,
tokenService,
smtpService,
totpService
});
const approvalPolicyDAL = approvalPolicyDALFactory(db); const approvalPolicyDAL = approvalPolicyDALFactory(db);
const pamSessionExpirationService = pamSessionExpirationServiceFactory({ const pamSessionExpirationService = pamSessionExpirationServiceFactory({
queueService, queueService,
@@ -2467,8 +2485,12 @@ export const registerRoutes = async (
pamSessionDAL, pamSessionDAL,
permissionService, permissionService,
projectDAL, projectDAL,
orgDAL,
userDAL, userDAL,
auditLogService, auditLogService,
mfaSessionService,
tokenService,
smtpService,
approvalRequestGrantsDAL, approvalRequestGrantsDAL,
approvalPolicyDAL, approvalPolicyDAL,
pamSessionExpirationService pamSessionExpirationService
@@ -2702,6 +2724,7 @@ export const registerRoutes = async (
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService, externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService,
projectTemplate: projectTemplateService, projectTemplate: projectTemplateService,
totp: totpService, totp: totpService,
webAuthn: webAuthnService,
appConnection: appConnectionService, appConnection: appConnectionService,
secretSync: secretSyncService, secretSync: secretSyncService,
kmip: kmipService, kmip: kmipService,
@@ -2723,6 +2746,7 @@ export const registerRoutes = async (
pamResource: pamResourceService, pamResource: pamResourceService,
pamAccount: pamAccountService, pamAccount: pamAccountService,
pamSession: pamSessionService, pamSession: pamSessionService,
mfaSession: mfaSessionService,
upgradePath: upgradePathService, upgradePath: upgradePathService,
membershipUser: membershipUserService, membershipUser: membershipUserService,

View File

@@ -37,7 +37,8 @@ export const DefaultResponseErrorsSchema = {
reqId: z.string(), reqId: z.string(),
statusCode: z.literal(400), statusCode: z.literal(400),
message: z.string(), message: z.string(),
error: z.string() error: z.string(),
details: z.any().optional()
}), }),
404: z.object({ 404: z.object({
reqId: z.string(), reqId: z.string(),

View File

@@ -1,3 +1,4 @@
import type { AuthenticationResponseJSON, RegistrationResponseJSON } from "@simplewebauthn/server";
import { z } from "zod"; import { z } from "zod";
import { UsersSchema } from "@app/db/schemas"; import { UsersSchema } from "@app/db/schemas";
@@ -284,4 +285,217 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
}); });
} }
}); });
// WebAuthn/Passkey Routes
server.route({
method: "GET",
url: "/me/webauthn",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
credentials: z.array(
z.object({
id: z.string(),
credentialId: z.string(),
name: z.string().nullable().optional(),
transports: z.array(z.string()).nullable().optional(),
createdAt: z.date(),
lastUsedAt: z.date().nullable().optional()
})
)
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const credentials = await server.services.webAuthn.getUserWebAuthnCredentials({
userId: req.permission.id
});
return { credentials };
}
});
server.route({
method: "POST",
url: "/me/webauthn/register",
config: {
rateLimit: writeLimit
},
schema: {
response: {
200: z.any() // Returns PublicKeyCredentialCreationOptionsJSON from @simplewebauthn/server
}
},
onRequest: verifyAuth([AuthMode.JWT], {
requireOrg: false
}),
handler: async (req) => {
return server.services.webAuthn.generateRegistrationOptions({
userId: req.permission.id
});
}
});
server.route({
method: "POST",
url: "/me/webauthn/register/verify",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
registrationResponse: z
.object({
id: z.string(),
rawId: z.string(),
response: z
.object({
clientDataJSON: z.string(),
attestationObject: z.string()
})
.passthrough(),
clientExtensionResults: z.record(z.unknown()).default({}),
type: z.literal("public-key")
})
.passthrough(),
name: z.string().optional()
}),
response: {
200: z.object({
credentialId: z.string(),
name: z.string().nullable().optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT], {
requireOrg: false
}),
handler: async (req) => {
return server.services.webAuthn.verifyRegistrationResponse({
userId: req.permission.id,
registrationResponse: req.body.registrationResponse as RegistrationResponseJSON,
name: req.body.name
});
}
});
server.route({
method: "POST",
url: "/me/webauthn/authenticate",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.any() // Returns PublicKeyCredentialRequestOptionsJSON from @simplewebauthn/server
}
},
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => {
return server.services.webAuthn.generateAuthenticationOptions({
userId: req.permission.id
});
}
});
server.route({
method: "POST",
url: "/me/webauthn/authenticate/verify",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
authenticationResponse: z
.object({
id: z.string(),
rawId: z.string(),
response: z
.object({
clientDataJSON: z.string(),
authenticatorData: z.string(),
signature: z.string()
})
.passthrough(),
clientExtensionResults: z.record(z.unknown()).optional(),
type: z.literal("public-key")
})
.passthrough()
}),
response: {
200: z.object({
verified: z.boolean(),
credentialId: z.string(),
sessionToken: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => {
return server.services.webAuthn.verifyAuthenticationResponse({
userId: req.permission.id,
authenticationResponse: req.body.authenticationResponse as AuthenticationResponseJSON
});
}
});
server.route({
method: "PATCH",
url: "/me/webauthn/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
body: z.object({
name: z.string().optional()
}),
response: {
200: z.object({
id: z.string(),
credentialId: z.string(),
name: z.string().nullable().optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
return server.services.webAuthn.updateWebAuthnCredential({
userId: req.permission.id,
id: req.params.id,
name: req.body.name
});
}
});
server.route({
method: "DELETE",
url: "/me/webauthn/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: z.object({
success: z.boolean()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
await server.services.webAuthn.deleteWebAuthnCredential({
userId: req.permission.id,
id: req.params.id
});
return { success: true };
}
});
}; };

View File

@@ -6,6 +6,7 @@ import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-
import { registerDeprecatedProjectRouter } from "./deprecated-project-router"; import { registerDeprecatedProjectRouter } from "./deprecated-project-router";
import { registerIdentityOrgRouter } from "./identity-org-router"; import { registerIdentityOrgRouter } from "./identity-org-router";
import { registerMfaRouter } from "./mfa-router"; import { registerMfaRouter } from "./mfa-router";
import { registerMfaSessionRouter } from "./mfa-session-router";
import { registerOrgRouter } from "./organization-router"; import { registerOrgRouter } from "./organization-router";
import { registerPasswordRouter } from "./password-router"; import { registerPasswordRouter } from "./password-router";
import { registerPkiAlertRouter } from "./pki-alert-router"; import { registerPkiAlertRouter } from "./pki-alert-router";
@@ -17,6 +18,7 @@ import { registerUserRouter } from "./user-router";
export const registerV2Routes = async (server: FastifyZodProvider) => { export const registerV2Routes = async (server: FastifyZodProvider) => {
await server.register(registerMfaRouter, { prefix: "/auth" }); await server.register(registerMfaRouter, { prefix: "/auth" });
await server.register(registerMfaSessionRouter, { prefix: "/mfa-sessions" });
await server.register(registerUserRouter, { prefix: "/users" }); await server.register(registerUserRouter, { prefix: "/users" });
await server.register(registerServiceTokenRouter, { prefix: "/service-token" }); await server.register(registerServiceTokenRouter, { prefix: "/service-token" });
await server.register(registerPasswordRouter, { prefix: "/password" }); await server.register(registerPasswordRouter, { prefix: "/password" });

View File

@@ -186,4 +186,37 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
return handleMfaVerification(req, res, server, req.body.recoveryCode, MfaMethod.TOTP, true); return handleMfaVerification(req, res, server, req.body.recoveryCode, MfaMethod.TOTP, true);
} }
}); });
// WebAuthn MFA routes
server.route({
method: "GET",
url: "/mfa/check/webauthn",
config: {
rateLimit: mfaRateLimit
},
schema: {
response: {
200: z.object({
hasPasskeys: z.boolean()
})
}
},
handler: async (req) => {
try {
const credentials = await server.services.webAuthn.getUserWebAuthnCredentials({
userId: req.mfa.userId
});
return {
hasPasskeys: credentials.length > 0
};
} catch (error) {
if (error instanceof NotFoundError) {
return { hasPasskeys: false };
}
throw error;
}
}
});
}; };

View File

@@ -0,0 +1,72 @@
import { z } from "zod";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode, MfaMethod } from "@app/services/auth/auth-type";
import { MfaSessionStatus } from "@app/services/mfa-session/mfa-session-types";
export const registerMfaSessionRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:mfaSessionId/verify",
config: {
rateLimit: writeLimit
},
schema: {
description: "Verify MFA session",
params: z.object({
mfaSessionId: z.string().trim()
}),
body: z.object({
mfaToken: z.string().trim(),
mfaMethod: z.nativeEnum(MfaMethod)
}),
response: {
200: z.object({
success: z.boolean(),
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const result = await server.services.mfaSession.verifyMfaSession({
mfaSessionId: req.params.mfaSessionId,
userId: req.permission.id,
mfaToken: req.body.mfaToken,
mfaMethod: req.body.mfaMethod
});
return result;
}
});
server.route({
method: "GET",
url: "/:mfaSessionId/status",
config: {
rateLimit: readLimit
},
schema: {
description: "Get MFA session status",
params: z.object({
mfaSessionId: z.string().trim()
}),
response: {
200: z.object({
status: z.nativeEnum(MfaSessionStatus),
mfaMethod: z.nativeEnum(MfaMethod)
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const result = await server.services.mfaSession.getMfaSessionStatus({
mfaSessionId: req.params.mfaSessionId,
userId: req.permission.id
});
return result;
}
});
};

View File

@@ -74,6 +74,13 @@ export const getTokenConfig = (tokenType: TokenType) => {
const expiresAt = new Date(new Date().getTime() + 259200000); const expiresAt = new Date(new Date().getTime() + 259200000);
return { token, expiresAt }; return { token, expiresAt };
} }
case TokenType.TOKEN_WEBAUTHN_SESSION: {
// generate random hex token for WebAuthn session
const token = crypto.randomBytes(32).toString("hex");
const triesLeft = 1;
const expiresAt = new Date(new Date().getTime() + 60000); // 60 seconds
return { token, triesLeft, expiresAt };
}
default: { default: {
const token = crypto.randomBytes(16).toString("hex"); const token = crypto.randomBytes(16).toString("hex");
const expiresAt = new Date(); const expiresAt = new Date();

View File

@@ -8,7 +8,8 @@ export enum TokenType {
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation", TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset", TOKEN_EMAIL_PASSWORD_RESET = "passwordReset",
TOKEN_EMAIL_PASSWORD_SETUP = "passwordSetup", TOKEN_EMAIL_PASSWORD_SETUP = "passwordSetup",
TOKEN_USER_UNLOCK = "userUnlock" TOKEN_USER_UNLOCK = "userUnlock",
TOKEN_WEBAUTHN_SESSION = "webauthnSession"
} }
export type TCreateTokenForUserDTO = { export type TCreateTokenForUserDTO = {

View File

@@ -882,6 +882,18 @@ export const authLoginServiceFactory = ({
totp: mfaToken totp: mfaToken
}); });
} }
} else if (mfaMethod === MfaMethod.WEBAUTHN) {
if (!mfaToken) {
throw new BadRequestError({
message: "WebAuthn session token is required"
});
}
// Validate the one-time WebAuthn session token (passed as mfaToken)
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_WEBAUTHN_SESSION,
userId,
code: mfaToken
});
} }
} catch (err) { } catch (err) {
const updatedUser = await processFailedMfaAttempt(userId); const updatedUser = await processFailedMfaAttempt(userId);

View File

@@ -100,5 +100,6 @@ export type AuthModeProviderSignUpTokenPayload = {
export enum MfaMethod { export enum MfaMethod {
EMAIL = "email", EMAIL = "email",
TOTP = "totp" TOTP = "totp",
WEBAUTHN = "webauthn"
} }

View File

@@ -0,0 +1,175 @@
import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/keystore";
import { crypto } from "@app/lib/crypto";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { MfaMethod } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TTotpServiceFactory } from "@app/services/totp/totp-service";
import { MfaSessionStatus, TGetMfaSessionStatusDTO, TMfaSession, TVerifyMfaSessionDTO } from "./mfa-session-types";
type TMfaSessionServiceFactoryDep = {
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser" | "validateTokenForUser">;
smtpService: Pick<TSmtpService, "sendMail">;
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
};
export type TMfaSessionServiceFactory = ReturnType<typeof mfaSessionServiceFactory>;
export const mfaSessionServiceFactory = ({
keyStore,
tokenService,
smtpService,
totpService
}: TMfaSessionServiceFactoryDep) => {
// Helper function to get MFA session from Redis
const getMfaSession = async (mfaSessionId: string): Promise<TMfaSession | null> => {
const mfaSessionKey = KeyStorePrefixes.MfaSession(mfaSessionId);
const mfaSessionData = await keyStore.getItem(mfaSessionKey);
if (!mfaSessionData) {
return null;
}
return JSON.parse(mfaSessionData) as TMfaSession;
};
// Helper function to update MFA session in Redis
const updateMfaSession = async (mfaSession: TMfaSession, ttlSeconds: number): Promise<void> => {
const mfaSessionKey = KeyStorePrefixes.MfaSession(mfaSession.sessionId);
await keyStore.setItemWithExpiry(mfaSessionKey, ttlSeconds, JSON.stringify(mfaSession));
};
// Helper function to send MFA code via email
const sendMfaCode = async (userId: string, email: string) => {
const code = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_MFA,
userId
});
await smtpService.sendMail({
template: SmtpTemplates.EmailMfa,
subjectLine: "Infisical MFA code",
recipients: [email],
substitutions: {
code
}
});
};
const verifyMfaSession = async ({ mfaSessionId, userId, mfaToken, mfaMethod }: TVerifyMfaSessionDTO) => {
const mfaSession = await getMfaSession(mfaSessionId);
if (!mfaSession) {
throw new BadRequestError({
message: "MFA session not found or expired"
});
}
if (mfaSession.mfaMethod !== mfaMethod) {
throw new BadRequestError({
message: "MFA method does not match the session"
});
}
// Verify the session belongs to the current user
if (mfaSession.userId !== userId) {
throw new ForbiddenRequestError({
message: "MFA session does not belong to current user"
});
}
try {
if (mfaMethod === MfaMethod.EMAIL) {
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_MFA,
userId,
code: mfaToken
});
} else if (mfaMethod === MfaMethod.TOTP) {
if (mfaToken.length !== 6) {
throw new BadRequestError({
message: "Please use a valid TOTP code."
});
}
await totpService.verifyUserTotp({
userId,
totp: mfaToken
});
} else if (mfaMethod === MfaMethod.WEBAUTHN) {
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_WEBAUTHN_SESSION,
userId,
code: mfaToken
});
}
} catch (error) {
throw new BadRequestError({
message: "Invalid MFA code"
});
}
mfaSession.status = MfaSessionStatus.ACTIVE;
await updateMfaSession(mfaSession, KeyStoreTtls.MfaSessionInSeconds);
return {
success: true,
message: "MFA verification successful"
};
};
const getMfaSessionStatus = async ({ mfaSessionId, userId }: TGetMfaSessionStatusDTO) => {
const mfaSession = await getMfaSession(mfaSessionId);
if (!mfaSession) {
throw new NotFoundError({
message: "MFA session not found or expired"
});
}
if (mfaSession.userId !== userId) {
throw new ForbiddenRequestError({
message: "MFA session does not belong to current user"
});
}
return {
status: mfaSession.status,
mfaMethod: mfaSession.mfaMethod
};
};
const createMfaSession = async (userId: string, resourceId: string, mfaMethod: MfaMethod): Promise<string> => {
const mfaSessionId = crypto.randomBytes(32).toString("hex");
const mfaSession: TMfaSession = {
sessionId: mfaSessionId,
userId,
resourceId,
status: MfaSessionStatus.PENDING,
mfaMethod
};
await keyStore.setItemWithExpiry(
KeyStorePrefixes.MfaSession(mfaSessionId),
KeyStoreTtls.MfaSessionInSeconds,
JSON.stringify(mfaSession)
);
return mfaSessionId;
};
const deleteMfaSession = async (mfaSessionId: string) => {
await keyStore.deleteItem(KeyStorePrefixes.MfaSession(mfaSessionId));
};
return {
createMfaSession,
verifyMfaSession,
getMfaSessionStatus,
sendMfaCode,
getMfaSession,
deleteMfaSession
};
};

View File

@@ -0,0 +1,32 @@
import { MfaMethod } from "@app/services/auth/auth-type";
export enum MfaSessionStatus {
PENDING = "PENDING",
ACTIVE = "ACTIVE"
}
export type TMfaSession = {
sessionId: string;
userId: string;
resourceId: string; // Generic - can be accountId, documentId, etc.
status: MfaSessionStatus;
mfaMethod: MfaMethod;
};
export type TCreateMfaSessionDTO = {
userId: string;
resourceId: string;
mfaMethod: MfaMethod;
};
export type TVerifyMfaSessionDTO = {
mfaSessionId: string;
userId: string;
mfaToken: string;
mfaMethod: MfaMethod;
};
export type TGetMfaSessionStatusDTO = {
mfaSessionId: string;
userId: string;
};

View File

@@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TWebAuthnCredentialDALFactory = ReturnType<typeof webAuthnCredentialDALFactory>;
export const webAuthnCredentialDALFactory = (db: TDbClient) => {
const webAuthnCredentialDal = ormify(db, TableName.WebAuthnCredential);
return webAuthnCredentialDal;
};

View File

@@ -0,0 +1,385 @@
import {
AuthenticatorTransportFuture,
generateAuthenticationOptions,
generateRegistrationOptions,
VerifiedAuthenticationResponse,
VerifiedRegistrationResponse,
verifyAuthenticationResponse,
verifyRegistrationResponse
} from "@simplewebauthn/server";
import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types";
import { TUserDALFactory } from "../user/user-dal";
import { TWebAuthnCredentialDALFactory } from "./webauthn-credential-dal";
import {
TDeleteWebAuthnCredentialDTO,
TGenerateAuthenticationOptionsDTO,
TGenerateRegistrationOptionsDTO,
TGetUserWebAuthnCredentialsDTO,
TUpdateWebAuthnCredentialDTO,
TVerifyAuthenticationResponseDTO,
TVerifyRegistrationResponseDTO
} from "./webauthn-types";
type TWebAuthnServiceFactoryDep = {
userDAL: TUserDALFactory;
webAuthnCredentialDAL: TWebAuthnCredentialDALFactory;
tokenService: TAuthTokenServiceFactory;
keyStore: TKeyStoreFactory;
};
export type TWebAuthnServiceFactory = ReturnType<typeof webAuthnServiceFactory>;
export const webAuthnServiceFactory = ({
userDAL,
webAuthnCredentialDAL,
tokenService,
keyStore
}: TWebAuthnServiceFactoryDep) => {
const storeChallenge = async (userId: string, challenge: string) => {
const challengeKey = KeyStorePrefixes.WebAuthnChallenge(userId);
await keyStore.setItemWithExpiry(challengeKey, KeyStoreTtls.WebAuthnChallengeInSeconds, challenge);
};
const getChallenge = async (userId: string): Promise<string | null> => {
const challengeKey = KeyStorePrefixes.WebAuthnChallenge(userId);
return keyStore.getItem(challengeKey);
};
const clearChallenge = async (userId: string) => {
const challengeKey = KeyStorePrefixes.WebAuthnChallenge(userId);
await keyStore.deleteItem(challengeKey);
};
const appCfg = getConfig();
// Relying Party (RP) information - extracted from SITE_URL
const RP_NAME = "Infisical";
const RP_ID = new URL(appCfg.SITE_URL || "http://localhost:8080").hostname;
const ORIGIN = appCfg.SITE_URL || "http://localhost:8080";
/**
* Generate registration options for a new passkey
* This is the first step in passkey registration
*/
const generateRegistrationOptionsForUser = async ({ userId }: TGenerateRegistrationOptionsDTO) => {
const user = await userDAL.findById(userId);
if (!user) {
throw new NotFoundError({
message: "User not found"
});
}
// Get existing credentials to exclude them from registration
const existingCredentials = await webAuthnCredentialDAL.find({ userId });
const options = await generateRegistrationOptions({
rpName: RP_NAME,
rpID: RP_ID,
userID: Buffer.from(userId, "utf-8"),
userName: user.email || "",
userDisplayName: user.email || "",
attestationType: "none",
excludeCredentials: existingCredentials.map((cred) => ({
id: cred.credentialId,
transports: cred.transports as AuthenticatorTransportFuture[]
})),
authenticatorSelection: {
requireResidentKey: true,
residentKey: "required",
userVerification: "required"
}
});
// Store challenge for verification
await storeChallenge(userId, options.challenge);
return options;
};
/**
* Verify registration response and store the credential
* This is the second step in passkey registration
*/
const verifyRegistrationResponseFromUser = async ({
userId,
registrationResponse,
name
}: TVerifyRegistrationResponseDTO) => {
const user = await userDAL.findById(userId);
if (!user) {
throw new NotFoundError({
message: "User not found"
});
}
// Retrieve the stored challenge
const expectedChallenge = await getChallenge(userId);
if (!expectedChallenge) {
throw new BadRequestError({
message: "Challenge not found or expired. Please try registering again."
});
}
let verification: VerifiedRegistrationResponse;
try {
verification = await verifyRegistrationResponse({
response: registrationResponse,
expectedChallenge,
expectedOrigin: ORIGIN,
expectedRPID: RP_ID,
requireUserVerification: true
});
} catch (error: unknown) {
await clearChallenge(userId);
throw new BadRequestError({
message: `Registration verification failed: ${error instanceof Error ? error.message : "Unknown error"}`
});
}
if (!verification.verified || !verification.registrationInfo) {
await clearChallenge(userId);
throw new BadRequestError({
message: "Registration verification failed"
});
}
const { credential: registeredCredential } = verification.registrationInfo;
// Check if credential already exists
const credentialIdBase64 = registeredCredential.id;
const existingCredential = await webAuthnCredentialDAL.findOne({
credentialId: credentialIdBase64
});
if (existingCredential) {
await clearChallenge(userId);
throw new BadRequestError({
message: "This credential has already been registered"
});
}
// Store the credential
const credential = await webAuthnCredentialDAL.create({
userId,
credentialId: credentialIdBase64,
publicKey: Buffer.from(registeredCredential.publicKey).toString("base64url"),
counter: registeredCredential.counter,
transports: registrationResponse.response.transports || null,
name: name || "Passkey"
});
// Clear the challenge
await clearChallenge(userId);
return {
credentialId: credential.credentialId,
name: credential.name
};
};
/**
* Generate authentication options for passkey verification
* This is used during login/2FA
*/
const generateAuthenticationOptionsForUser = async ({ userId }: TGenerateAuthenticationOptionsDTO) => {
const credentials = await webAuthnCredentialDAL.find({ userId });
if (credentials.length === 0) {
throw new NotFoundError({
message: "No passkeys registered for this user"
});
}
const options = await generateAuthenticationOptions({
rpID: RP_ID,
allowCredentials: credentials.map((cred) => ({
id: cred.credentialId,
transports: cred.transports as AuthenticatorTransportFuture[]
})),
userVerification: "required"
});
// Store challenge for verification
await storeChallenge(userId, options.challenge);
return options;
};
/**
* Verify authentication response
* This is used during login/2FA to verify the user's passkey
*/
const verifyAuthenticationResponseFromUser = async ({
userId,
authenticationResponse
}: TVerifyAuthenticationResponseDTO) => {
const credentialIdBase64 = authenticationResponse.id;
if (!credentialIdBase64) {
throw new BadRequestError({
message: "Invalid authentication response"
});
}
// Find the credential
const credential = await webAuthnCredentialDAL.findOne({ credentialId: credentialIdBase64 });
if (!credential) {
throw new NotFoundError({
message: "Credential not found"
});
}
// Verify the credential belongs to the user
if (userId !== credential.userId) {
throw new ForbiddenRequestError({
message: "Credential does not belong to this user"
});
}
// Retrieve the stored challenge
const expectedChallenge = await getChallenge(userId);
if (!expectedChallenge) {
throw new BadRequestError({
message: "Challenge not found or expired. Please try authenticating again."
});
}
let verification: VerifiedAuthenticationResponse;
try {
verification = await verifyAuthenticationResponse({
response: authenticationResponse,
expectedChallenge,
expectedOrigin: ORIGIN,
expectedRPID: RP_ID,
credential: {
id: credential.credentialId,
publicKey: Buffer.from(credential.publicKey, "base64url"),
counter: credential.counter
},
requireUserVerification: true
});
} catch (error: unknown) {
await clearChallenge(userId);
throw new BadRequestError({
message: `Authentication verification failed: ${error instanceof Error ? error.message : "Unknown error"}`
});
}
if (!verification.verified) {
await clearChallenge(userId);
throw new BadRequestError({
message: "Authentication verification failed"
});
}
// Update last used timestamp and counter
await webAuthnCredentialDAL.updateById(credential.id, {
lastUsedAt: new Date(),
counter: verification.authenticationInfo.newCounter
});
// Clear the challenge
await clearChallenge(userId);
// Generate one-time WebAuthn session token with 60-second expiration
const sessionToken = await tokenService.createTokenForUser({
type: TokenType.TOKEN_WEBAUTHN_SESSION,
userId
});
return {
verified: true,
credentialId: credential.credentialId,
sessionToken
};
};
/**
* Get all WebAuthn credentials for a user
*/
const getUserWebAuthnCredentials = async ({ userId }: TGetUserWebAuthnCredentialsDTO) => {
const credentials = await webAuthnCredentialDAL.find({ userId });
// Don't return sensitive data like public keys
return credentials.map((cred) => ({
id: cred.id,
credentialId: cred.credentialId,
name: cred.name,
transports: cred.transports,
createdAt: cred.createdAt,
lastUsedAt: cred.lastUsedAt
}));
};
/**
* Delete a WebAuthn credential
*/
const deleteWebAuthnCredential = async ({ userId, id }: TDeleteWebAuthnCredentialDTO) => {
const credential = await webAuthnCredentialDAL.findById(id);
if (!credential) {
throw new NotFoundError({
message: "Credential not found"
});
}
if (userId !== credential.userId) {
throw new ForbiddenRequestError({
message: "Credential does not belong to this user"
});
}
await webAuthnCredentialDAL.deleteById(credential.id);
return {
success: true
};
};
/**
* Update a WebAuthn credential (e.g., rename it)
*/
const updateWebAuthnCredential = async ({ userId, id, name }: TUpdateWebAuthnCredentialDTO) => {
const credential = await webAuthnCredentialDAL.findById(id);
if (!credential) {
throw new NotFoundError({
message: "Credential not found"
});
}
if (userId !== credential.userId) {
throw new ForbiddenRequestError({
message: "Credential does not belong to this user"
});
}
const updatedCredential = await webAuthnCredentialDAL.updateById(credential.id, {
name: name || credential.name
});
return {
id: updatedCredential.id,
credentialId: updatedCredential.credentialId,
name: updatedCredential.name
};
};
return {
generateRegistrationOptions: generateRegistrationOptionsForUser,
verifyRegistrationResponse: verifyRegistrationResponseFromUser,
generateAuthenticationOptions: generateAuthenticationOptionsForUser,
verifyAuthenticationResponse: verifyAuthenticationResponseFromUser,
getUserWebAuthnCredentials,
deleteWebAuthnCredential,
updateWebAuthnCredential
};
};

View File

@@ -0,0 +1,35 @@
import { AuthenticationResponseJSON, RegistrationResponseJSON } from "@simplewebauthn/server";
export type TGenerateRegistrationOptionsDTO = {
userId: string;
};
export type TVerifyRegistrationResponseDTO = {
userId: string;
registrationResponse: RegistrationResponseJSON;
name?: string; // User-friendly name for the credential
};
export type TGenerateAuthenticationOptionsDTO = {
userId: string;
};
export type TVerifyAuthenticationResponseDTO = {
userId: string;
authenticationResponse: AuthenticationResponseJSON;
};
export type TGetUserWebAuthnCredentialsDTO = {
userId: string;
};
export type TDeleteWebAuthnCredentialDTO = {
userId: string;
id: string;
};
export type TUpdateWebAuthnCredentialDTO = {
userId: string;
id: string;
name?: string;
};

View File

@@ -48,6 +48,7 @@
"@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.3", "@radix-ui/react-toast": "^1.2.3",
"@radix-ui/react-tooltip": "^1.1.5", "@radix-ui/react-tooltip": "^1.1.5",
"@simplewebauthn/browser": "^13.2.2",
"@sindresorhus/slugify": "^2.2.1", "@sindresorhus/slugify": "^2.2.1",
"@tanstack/react-query": "^5.62.7", "@tanstack/react-query": "^5.62.7",
"@tanstack/react-router": "^1.95.1", "@tanstack/react-router": "^1.95.1",
@@ -4141,6 +4142,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@simplewebauthn/browser": {
"version": "13.2.2",
"resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.2.2.tgz",
"integrity": "sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==",
"license": "MIT"
},
"node_modules/@sindresorhus/slugify": { "node_modules/@sindresorhus/slugify": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz",

View File

@@ -57,6 +57,7 @@
"@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.3", "@radix-ui/react-toast": "^1.2.3",
"@radix-ui/react-tooltip": "^1.1.5", "@radix-ui/react-tooltip": "^1.1.5",
"@simplewebauthn/browser": "^13.2.2",
"@sindresorhus/slugify": "^2.2.1", "@sindresorhus/slugify": "^2.2.1",
"@tanstack/react-query": "^5.62.7", "@tanstack/react-query": "^5.62.7",
"@tanstack/react-router": "^1.95.1", "@tanstack/react-router": "^1.95.1",

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import ReactCodeInput from "react-code-input"; import ReactCodeInput from "react-code-input";
import { startAuthentication, startRegistration } from "@simplewebauthn/browser";
import { Link, useNavigate } from "@tanstack/react-router"; import { Link, useNavigate } from "@tanstack/react-router";
import { t } from "i18next"; import { t } from "i18next";
@@ -7,11 +8,23 @@ import Error from "@app/components/basic/Error";
import TotpRegistration from "@app/components/mfa/TotpRegistration"; import TotpRegistration from "@app/components/mfa/TotpRegistration";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import SecurityClient from "@app/components/utilities/SecurityClient"; import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Tooltip } from "@app/components/v2"; import { Button, Input, Tooltip } from "@app/components/v2";
import { isInfisicalCloud } from "@app/helpers/platform"; import { isInfisicalCloud } from "@app/helpers/platform";
import { useLogoutUser, useSendMfaToken } from "@app/hooks/api"; import { useLogoutUser, useSendMfaToken } from "@app/hooks/api";
import { checkUserTotpMfa, verifyMfaToken, verifyRecoveryCode } from "@app/hooks/api/auth/queries"; import {
checkUserTotpMfa,
checkUserWebAuthnMfa,
verifyMfaToken,
verifyRecoveryCode
} from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types"; import { MfaMethod } from "@app/hooks/api/auth/types";
import { getMfaTempToken } from "@app/hooks/api/reactQuery";
import {
useGenerateAuthenticationOptions,
useGenerateRegistrationOptions,
useVerifyAuthentication,
useVerifyRegistration
} from "@app/hooks/api/webauthn";
// The style for the verification code input // The style for the verification code input
const codeInputProps = { const codeInputProps = {
@@ -68,9 +81,17 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Prop
const [isLoadingResend, setIsLoadingResend] = useState(false); const [isLoadingResend, setIsLoadingResend] = useState(false);
const [triesLeft, setTriesLeft] = useState<number | undefined>(undefined); const [triesLeft, setTriesLeft] = useState<number | undefined>(undefined);
const [shouldShowTotpRegistration, setShouldShowTotpRegistration] = useState(false); const [shouldShowTotpRegistration, setShouldShowTotpRegistration] = useState(false);
const [shouldShowWebAuthnRegistration, setShouldShowWebAuthnRegistration] = useState(false);
const [credentialName, setCredentialName] = useState("");
const [isRegisteringPasskey, setIsRegisteringPasskey] = useState(false);
const logout = useLogoutUser(true); const logout = useLogoutUser(true);
const { mutateAsync: generateWebAuthnAuthenticationOptions } = useGenerateAuthenticationOptions();
const { mutateAsync: verifyWebAuthnAuthentication } = useVerifyAuthentication();
const sendMfaToken = useSendMfaToken(); const sendMfaToken = useSendMfaToken();
const generateRegistrationOptions = useGenerateRegistrationOptions();
const verifyRegistration = useVerifyRegistration();
useEffect(() => { useEffect(() => {
if (method === MfaMethod.TOTP) { if (method === MfaMethod.TOTP) {
@@ -80,8 +101,14 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Prop
setShouldShowTotpRegistration(true); setShouldShowTotpRegistration(true);
} }
}); });
} else if (method === MfaMethod.WEBAUTHN) {
checkUserWebAuthnMfa().then((hasPasskeys) => {
if (!hasPasskeys) {
setShouldShowWebAuthnRegistration(true);
}
});
} }
}, []); }, [method]);
const getExpectedCodeLength = () => { const getExpectedCodeLength = () => {
if (method === MfaMethod.EMAIL) return 6; if (method === MfaMethod.EMAIL) return 6;
@@ -153,6 +180,153 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Prop
} }
}; };
const handleWebAuthnVerification = async () => {
setIsLoading(true);
const mfaToken = getMfaTempToken();
try {
SecurityClient.setMfaToken("");
// Get authentication options from server
const options = await generateWebAuthnAuthenticationOptions();
// Prompt user to authenticate with their passkey
const authenticationResponse = await startAuthentication({ optionsJSON: options });
// Verify with server
const result = await verifyWebAuthnAuthentication({ authenticationResponse });
// Use the sessionToken to verify MFA
if (result.sessionToken) {
SecurityClient.setMfaToken(mfaToken);
const mfaResult = await verifyMfaToken({
email,
mfaCode: result.sessionToken,
mfaMethod: MfaMethod.WEBAUTHN
});
SecurityClient.setMfaToken("");
SecurityClient.setToken(mfaResult.token);
await successCallback();
if (closeMfa) {
closeMfa();
}
}
} catch (error: any) {
console.error("WebAuthn verification failed:", error);
let errorMessage = "Failed to verify passkey";
if (error.name === "NotAllowedError") {
errorMessage = "Passkey verification was cancelled or timed out";
} else if (error.message) {
errorMessage = error.message;
}
SecurityClient.setMfaToken(mfaToken);
createNotification({
text: errorMessage,
type: "error"
});
if (typeof triesLeft === "number") {
const newTriesLeft = triesLeft - 1;
setTriesLeft(newTriesLeft);
if (newTriesLeft <= 0) {
createNotification({
text: "User is temporary locked due to multiple failed login attempts. Try again later.",
type: "error"
});
SecurityClient.setMfaToken("");
SecurityClient.setToken("");
SecurityClient.setSignupToken("");
await logout.mutateAsync();
navigate({ to: "/login" });
return;
}
} else {
setTriesLeft(2);
}
} finally {
setIsLoading(false);
}
};
const handleRegisterPasskey = async () => {
try {
setIsRegisteringPasskey(true);
// Check if WebAuthn is supported
if (
!window.PublicKeyCredential ||
!window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable
) {
createNotification({
text: "WebAuthn is not supported on this browser",
type: "error"
});
return;
}
// Check if platform authenticator is available
const available =
await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
if (!available) {
createNotification({
text: "No passkey-compatible authenticator found on this device",
type: "error"
});
return;
}
// Temporarily clear MFA token so the regular access token is used for registration endpoints
const mfaToken = getMfaTempToken();
SecurityClient.setMfaToken("");
try {
// Generate registration options from server (using regular user endpoint)
const options = await generateRegistrationOptions.mutateAsync();
const registrationResponse = await startRegistration({ optionsJSON: options });
// Verify registration with server (using regular user endpoint)
await verifyRegistration.mutateAsync({
registrationResponse,
name: credentialName || "Passkey"
});
createNotification({
text: "Successfully registered passkey",
type: "success"
});
setShouldShowWebAuthnRegistration(false);
await successCallback();
} finally {
// Restore MFA token
SecurityClient.setMfaToken(mfaToken);
}
} catch (error: any) {
console.error("Failed to register passkey:", error);
let errorMessage = "Failed to register passkey";
if (error.name === "NotAllowedError") {
errorMessage = "Passkey registration was cancelled or timed out";
} else if (error.name === "InvalidStateError") {
errorMessage = "This passkey has already been registered";
} else if (error.message) {
errorMessage = error.message;
}
createNotification({
text: errorMessage,
type: "error"
});
} finally {
setIsRegisteringPasskey(false);
}
};
if (shouldShowTotpRegistration) { if (shouldShowTotpRegistration) {
return ( return (
<> <>
@@ -172,6 +346,35 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Prop
); );
} }
if (shouldShowWebAuthnRegistration) {
return (
<>
<div className="mb-6 text-center text-lg font-bold text-white">
Your organization requires passkey authentication to be configured.
</div>
<div className="mx-auto w-max pt-4 pb-4 md:mb-16 md:px-8">
<div className="flex max-w-lg flex-col text-bunker-200">
<div className="mb-8">
1. Click the button below to register your passkey. You&apos;ll be prompted to use
your device&apos;s biometric authentication (Touch ID, Face ID, Windows Hello, etc.).
</div>
<div className="mb-4">2. Optionally, give your passkey a name to identify it later</div>
<div className="mb-4 flex flex-col gap-2">
<Input
onChange={(e) => setCredentialName(e.target.value)}
value={credentialName}
placeholder="Passkey name (optional)"
/>
<Button onClick={handleRegisterPasskey} isLoading={isRegisteringPasskey}>
Register Passkey
</Button>
</div>
</div>
</div>
</>
);
}
return ( return (
<div className="mx-auto w-max pt-6 pb-6 md:mb-16 md:px-8"> <div className="mx-auto w-max pt-6 pb-6 md:mb-16 md:px-8">
{!hideLogo && ( {!hideLogo && (
@@ -197,83 +400,113 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Prop
</p> </p>
</div> </div>
)} )}
<form onSubmit={verifyMfa}> {method === MfaMethod.WEBAUTHN && (
<div className="mx-auto hidden md:block" style={{ minWidth: "600px" }}> <div className="mb-8 text-center">
{method === MfaMethod.EMAIL && ( <h2 className="mb-3 text-xl font-medium text-bunker-100">Passkey Authentication</h2>
<div className="flex justify-center"> <p className="mx-auto max-w-md text-sm leading-relaxed text-bunker-300">
<ReactCodeInput Use your registered passkey to complete two-factor authentication
name="" </p>
inputMode="tel"
type="text"
fields={6}
onChange={setMfaCode}
className="mt-6 mb-2"
{...codeInputProps}
/>
</div>
)}
{method === MfaMethod.TOTP && (
<div className="mt-8 mb-6 flex justify-center">
<ReactCodeInput
key={showRecoveryCodeInput ? "recovery" : "totp"}
name=""
inputMode="tel"
type="text"
fields={showRecoveryCodeInput ? 8 : 6}
onChange={setMfaCode}
className="mb-2"
{...codeInputProps}
/>
</div>
)}
</div> </div>
<div className="mx-auto mt-4 block md:hidden" style={{ minWidth: "400px" }}> )}
{method === MfaMethod.EMAIL && ( {method === MfaMethod.WEBAUTHN ? (
<div className="flex justify-center"> <>
<ReactCodeInput {typeof triesLeft === "number" && (
name="" <Error text={`Failed authentication. You have ${triesLeft} attempt(s) remaining.`} />
inputMode="tel"
type="text"
fields={6}
onChange={setMfaCode}
className="mt-2 mb-2"
{...codeInputPropsPhone}
/>
</div>
)} )}
{method === MfaMethod.TOTP && ( <div className="mx-auto mt-6 flex w-full max-w-sm flex-col items-center justify-center text-center">
<div className="mt-4 mb-6 flex justify-center"> <Button
<ReactCodeInput size="md"
key={showRecoveryCodeInput ? "recovery-mobile" : "totp-mobile"} onClick={handleWebAuthnVerification}
name="" isFullWidth
inputMode="tel" className="h-11 rounded-lg font-medium shadow-xs transition-all duration-200 hover:shadow-md"
type="text" colorSchema="primary"
fields={showRecoveryCodeInput ? 8 : 6} variant="outline_bg"
onChange={setMfaCode} isLoading={isLoading}
className="mb-2" isDisabled={typeof triesLeft === "number" && triesLeft <= 0}
{...codeInputPropsPhone} >
/> Authenticate with Passkey
</div> </Button>
</div>
</>
) : (
<form onSubmit={verifyMfa}>
<div className="mx-auto hidden md:block" style={{ minWidth: "600px" }}>
{method === MfaMethod.EMAIL && (
<div className="flex justify-center">
<ReactCodeInput
name=""
inputMode="tel"
type="text"
fields={6}
onChange={setMfaCode}
className="mt-6 mb-2"
{...codeInputProps}
/>
</div>
)}
{method === MfaMethod.TOTP && (
<div className="mt-8 mb-6 flex justify-center">
<ReactCodeInput
key={showRecoveryCodeInput ? "recovery" : "totp"}
name=""
inputMode="tel"
type="text"
fields={showRecoveryCodeInput ? 8 : 6}
onChange={setMfaCode}
className="mb-2"
{...codeInputProps}
/>
</div>
)}
</div>
<div className="mx-auto mt-4 block md:hidden" style={{ minWidth: "400px" }}>
{method === MfaMethod.EMAIL && (
<div className="flex justify-center">
<ReactCodeInput
name=""
inputMode="tel"
type="text"
fields={6}
onChange={setMfaCode}
className="mt-2 mb-2"
{...codeInputPropsPhone}
/>
</div>
)}
{method === MfaMethod.TOTP && (
<div className="mt-4 mb-6 flex justify-center">
<ReactCodeInput
key={showRecoveryCodeInput ? "recovery-mobile" : "totp-mobile"}
name=""
inputMode="tel"
type="text"
fields={showRecoveryCodeInput ? 8 : 6}
onChange={setMfaCode}
className="mb-2"
{...codeInputPropsPhone}
/>
</div>
)}
</div>
{typeof triesLeft === "number" && (
<Error text={`Invalid code. You have ${triesLeft} attempt(s) remaining.`} />
)} )}
</div> <div className="mx-auto mt-6 flex w-full max-w-sm flex-col items-center justify-center text-center">
{typeof triesLeft === "number" && ( <Button
<Error text={`Invalid code. You have ${triesLeft} attempt(s) remaining.`} /> size="md"
)} type="submit"
<div className="mx-auto mt-6 flex w-full max-w-sm flex-col items-center justify-center text-center"> isFullWidth
<Button className="h-11 rounded-lg font-medium shadow-xs transition-all duration-200 hover:shadow-md"
size="md" colorSchema="primary"
type="submit" variant="outline_bg"
isFullWidth isLoading={isLoading}
className="h-11 rounded-lg font-medium shadow-xs transition-all duration-200 hover:shadow-md" isDisabled={!isCodeComplete || (typeof triesLeft === "number" && triesLeft <= 0)}
colorSchema="primary" >
variant="outline_bg" {String(t("mfa.verify"))}
isLoading={isLoading} </Button>
isDisabled={!isCodeComplete || (typeof triesLeft === "number" && triesLeft <= 0)} </div>
> </form>
{String(t("mfa.verify"))} )}
</Button>
</div>
</form>
{method === MfaMethod.TOTP && ( {method === MfaMethod.TOTP && (
<div className="mt-6 flex flex-col items-center gap-4 text-sm"> <div className="mt-6 flex flex-col items-center gap-4 text-sm">
<button <button

View File

@@ -0,0 +1,16 @@
import { MfaMethod } from "@app/hooks/api/auth/types";
export type TMfaSessionError = {
name: "SESSION_MFA_REQUIRED";
message: string;
error: {
mfaSessionId: string;
mfaMethod: MfaMethod;
};
};
export const isMfaSessionError = (
error: any
): error is { response: { data: TMfaSessionError } } => {
return error?.response?.data?.name === "SESSION_MFA_REQUIRED";
};

View File

@@ -7,6 +7,7 @@ import { SessionStorageKeys } from "@app/const";
import { organizationKeys } from "../organization/queries"; import { organizationKeys } from "../organization/queries";
import { projectKeys } from "../projects"; import { projectKeys } from "../projects";
import { setAuthToken } from "../reactQuery"; import { setAuthToken } from "../reactQuery";
import { TGenerateAuthenticationOptionsResponse, TVerifyAuthenticationDTO } from "../webauthn";
import { import {
CompleteAccountDTO, CompleteAccountDTO,
CompleteAccountSignupDTO, CompleteAccountSignupDTO,
@@ -333,6 +334,36 @@ export const checkUserTotpMfa = async () => {
return data.isVerified; return data.isVerified;
}; };
export const checkUserWebAuthnMfa = async () => {
const { data } = await apiRequest.get<{ hasPasskeys: boolean }>(
"/api/v2/auth/mfa/check/webauthn"
);
return data.hasPasskeys;
};
export const useMfaGenerateAuthenticationOptions = () =>
useMutation({
mutationFn: async () => {
const { data } = await apiRequest.post<TGenerateAuthenticationOptionsResponse>(
"/api/v2/auth/mfa/webauthn/authenticate"
);
return data;
}
});
export const useMfaVerifyAuthentication = () =>
useMutation({
mutationFn: async (dto: TVerifyAuthenticationDTO) => {
const { data } = await apiRequest.post<{
verified: boolean;
credentialId: string;
sessionToken: string;
}>("/api/v2/auth/mfa/webauthn/verify", dto);
return data;
}
});
export const useSendPasswordSetupEmail = () => { export const useSendPasswordSetupEmail = () => {
return useMutation({ return useMutation({
mutationFn: async () => { mutationFn: async () => {

View File

@@ -149,5 +149,6 @@ export enum UserAgentType {
export enum MfaMethod { export enum MfaMethod {
EMAIL = "email", EMAIL = "email",
TOTP = "totp" TOTP = "totp",
WEBAUTHN = "webauthn"
} }

View File

@@ -28,6 +28,7 @@ export * from "./integrationAuth";
export * from "./integrations"; export * from "./integrations";
export * from "./kms"; export * from "./kms";
export * from "./ldapConfig"; export * from "./ldapConfig";
export * from "./mfaSession";
export * from "./oidcConfig"; export * from "./oidcConfig";
export * from "./orgAdmin"; export * from "./orgAdmin";
export * from "./organization"; export * from "./organization";

View File

@@ -0,0 +1,7 @@
export { useMfaSessionStatus, useVerifyMfaSession } from "./queries";
export type {
TMfaSessionStatusResponse,
TVerifyMfaSessionRequest,
TVerifyMfaSessionResponse
} from "./types";
export { MfaSessionStatus } from "./types";

View File

@@ -0,0 +1,45 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import {
MfaSessionStatus,
TMfaSessionStatusResponse,
TVerifyMfaSessionRequest,
TVerifyMfaSessionResponse
} from "./types";
export const useMfaSessionStatus = (mfaSessionId: string, enabled = true) => {
return useQuery({
queryKey: ["mfa-session-status", mfaSessionId],
queryFn: async () => {
const { data } = await apiRequest.get<TMfaSessionStatusResponse>(
`/api/v2/mfa-sessions/${mfaSessionId}/status`
);
return data;
},
enabled,
refetchInterval: (query) => {
// Poll every 2 seconds if status is still PENDING
if (query.state.data?.status === MfaSessionStatus.PENDING) {
return 2000;
}
return false;
}
});
};
export const useVerifyMfaSession = () => {
return useMutation({
mutationFn: async ({ mfaSessionId, mfaToken, mfaMethod }: TVerifyMfaSessionRequest) => {
const { data } = await apiRequest.post<TVerifyMfaSessionResponse>(
`/api/v2/mfa-sessions/${mfaSessionId}/verify`,
{
mfaToken,
mfaMethod
}
);
return data;
}
});
};

View File

@@ -0,0 +1,22 @@
import { MfaMethod } from "../auth/types";
export enum MfaSessionStatus {
PENDING = "PENDING",
ACTIVE = "ACTIVE"
}
export type TMfaSessionStatusResponse = {
status: MfaSessionStatus;
mfaMethod: MfaMethod;
};
export type TVerifyMfaSessionRequest = {
mfaSessionId: string;
mfaToken: string;
mfaMethod: MfaMethod;
};
export type TVerifyMfaSessionResponse = {
success: boolean;
message: string;
};

View File

@@ -14,7 +14,8 @@ export interface TBasePamAccount {
name: string; name: string;
description?: string | null; description?: string | null;
rotationEnabled: boolean; rotationEnabled: boolean;
rotationIntervalSeconds?: number | null; rotationIntervalSeconds?: number;
requireMfa?: boolean | null;
lastRotatedAt?: string | null; lastRotatedAt?: string | null;
lastRotationMessage?: string | null; lastRotationMessage?: string | null;
rotationStatus?: string | null; rotationStatus?: string | null;

View File

@@ -143,13 +143,13 @@ export type TListPamAccountsDTO = {
export type TCreatePamAccountDTO = Pick< export type TCreatePamAccountDTO = Pick<
TPamAccount, TPamAccount,
"name" | "description" | "credentials" | "projectId" | "resourceId" | "folderId" "name" | "description" | "credentials" | "projectId" | "resourceId" | "folderId" | "requireMfa"
> & { > & {
resourceType: PamResourceType; resourceType: PamResourceType;
}; };
export type TUpdatePamAccountDTO = Partial< export type TUpdatePamAccountDTO = Partial<
Pick<TPamAccount, "name" | "description" | "credentials"> Pick<TPamAccount, "name" | "description" | "credentials" | "requireMfa">
> & { > & {
accountId: string; accountId: string;
resourceType: PamResourceType; resourceType: PamResourceType;

View File

@@ -0,0 +1,18 @@
export {
useDeleteWebAuthnCredential,
useGenerateAuthenticationOptions,
useGenerateRegistrationOptions,
useUpdateWebAuthnCredential,
useVerifyAuthentication,
useVerifyRegistration
} from "./mutations";
export { useGetWebAuthnCredentials } from "./queries";
export type {
TDeleteWebAuthnCredentialDTO,
TGenerateAuthenticationOptionsResponse,
TGenerateRegistrationOptionsResponse,
TUpdateWebAuthnCredentialDTO,
TVerifyAuthenticationDTO,
TVerifyRegistrationDTO,
TWebAuthnCredential
} from "./types";

View File

@@ -0,0 +1,96 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { webAuthnKeys } from "./queries";
import {
TDeleteWebAuthnCredentialDTO,
TGenerateAuthenticationOptionsResponse,
TGenerateRegistrationOptionsResponse,
TUpdateWebAuthnCredentialDTO,
TVerifyAuthenticationDTO,
TVerifyRegistrationDTO
} from "./types";
export const useGenerateRegistrationOptions = () =>
useMutation({
mutationFn: async () => {
const { data } = await apiRequest.post<TGenerateRegistrationOptionsResponse>(
"/api/v1/user/me/webauthn/register"
);
return data;
}
});
export const useVerifyRegistration = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (dto: TVerifyRegistrationDTO) => {
const { data } = await apiRequest.post<{ credentialId: string; name?: string | null }>(
"/api/v1/user/me/webauthn/register/verify",
dto
);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: webAuthnKeys.credentials });
}
});
};
export const useGenerateAuthenticationOptions = () =>
useMutation({
mutationFn: async () => {
const { data } = await apiRequest.post<TGenerateAuthenticationOptionsResponse>(
"/api/v1/user/me/webauthn/authenticate"
);
return data;
}
});
export const useVerifyAuthentication = () =>
useMutation({
mutationFn: async (dto: TVerifyAuthenticationDTO) => {
const { data } = await apiRequest.post<{
verified: boolean;
credentialId: string;
sessionToken: string;
}>("/api/v1/user/me/webauthn/authenticate/verify", dto);
return data;
}
});
export const useDeleteWebAuthnCredential = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id }: TDeleteWebAuthnCredentialDTO) => {
const { data } = await apiRequest.delete<{ success: boolean }>(
`/api/v1/user/me/webauthn/${id}`
);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: webAuthnKeys.credentials });
}
});
};
export const useUpdateWebAuthnCredential = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, name }: TUpdateWebAuthnCredentialDTO) => {
const { data } = await apiRequest.patch<{
id: string;
credentialId: string;
name?: string | null;
}>(`/api/v1/user/me/webauthn/${id}`, { name });
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: webAuthnKeys.credentials });
}
});
};

View File

@@ -0,0 +1,22 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TWebAuthnCredential } from "./types";
export const webAuthnKeys = {
credentials: ["webauthn-credentials"] as const,
registrationOptions: ["webauthn-registration-options"] as const,
authenticationOptions: ["webauthn-authentication-options"] as const
};
export const useGetWebAuthnCredentials = () =>
useQuery({
queryKey: webAuthnKeys.credentials,
queryFn: async () => {
const { data } = await apiRequest.get<{ credentials: TWebAuthnCredential[] }>(
"/api/v1/user/me/webauthn"
);
return data.credentials;
}
});

View File

@@ -0,0 +1,37 @@
import type {
AuthenticationResponseJSON,
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialRequestOptionsJSON,
RegistrationResponseJSON
} from "@simplewebauthn/browser";
export type TWebAuthnCredential = {
id: string;
credentialId: string;
name?: string | null;
transports?: string[] | null;
createdAt: Date;
lastUsedAt?: Date | null;
};
export type TGenerateRegistrationOptionsResponse = PublicKeyCredentialCreationOptionsJSON;
export type TVerifyRegistrationDTO = {
registrationResponse: RegistrationResponseJSON;
name?: string;
};
export type TGenerateAuthenticationOptionsResponse = PublicKeyCredentialRequestOptionsJSON;
export type TVerifyAuthenticationDTO = {
authenticationResponse: AuthenticationResponseJSON;
};
export type TUpdateWebAuthnCredentialDTO = {
id: string;
name?: string;
};
export type TDeleteWebAuthnCredentialDTO = {
id: string;
};

View File

@@ -0,0 +1,311 @@
import { useEffect, useState } from "react";
import ReactCodeInput from "react-code-input";
import { startAuthentication } from "@simplewebauthn/browser";
import { useParams } from "@tanstack/react-router";
import Error from "@app/components/basic/Error";
import { createNotification } from "@app/components/notifications";
import { Button } from "@app/components/v2";
import { MfaMethod } from "@app/hooks/api/auth/types";
import {
MfaSessionStatus,
useMfaSessionStatus,
useVerifyMfaSession
} from "@app/hooks/api/mfaSession";
import { useGenerateAuthenticationOptions, useVerifyAuthentication } from "@app/hooks/api/webauthn";
const codeInputProps = {
inputStyle: {
fontFamily: "monospace",
margin: "4px",
MozAppearance: "textfield",
width: "55px",
borderRadius: "5px",
fontSize: "24px",
height: "55px",
paddingLeft: "7",
backgroundColor: "#0d1117",
color: "white",
border: "1px solid #2d2f33",
textAlign: "center",
outlineColor: "#8ca542",
borderColor: "#2d2f33"
}
} as const;
const codeInputPropsPhone = {
inputStyle: {
fontFamily: "monospace",
margin: "4px",
MozAppearance: "textfield",
width: "40px",
borderRadius: "5px",
fontSize: "24px",
height: "40px",
paddingLeft: "7",
backgroundColor: "#0d1117",
color: "white",
border: "1px solid #2d2f33",
textAlign: "center",
outlineColor: "#8ca542",
borderColor: "#2d2f33"
}
} as const;
export const MfaSessionPage = () => {
const { mfaSessionId } = useParams({ strict: false }) as { mfaSessionId: string };
const [mfaCode, setMfaCode] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { data: sessionStatus, isError: isStatusError } = useMfaSessionStatus(mfaSessionId);
const verifyMfaSession = useVerifyMfaSession();
const { mutateAsync: generateWebAuthnAuthenticationOptions } = useGenerateAuthenticationOptions();
const { mutateAsync: verifyWebAuthnAuthentication } = useVerifyAuthentication();
// Show notification and auto-close when MFA is completed
useEffect(() => {
if (sessionStatus?.status === MfaSessionStatus.ACTIVE) {
createNotification({
text: "MFA verification successful! Closing window...",
type: "success"
});
// Auto-close window after showing success message
setTimeout(() => {
window.close();
}, 1000);
}
}, [sessionStatus?.status]);
// Handle status error (session not found or expired)
useEffect(() => {
if (isStatusError) {
setError("MFA session not found or expired. Please try again.");
}
}, [isStatusError]);
const getExpectedCodeLength = () => {
if (sessionStatus?.mfaMethod === MfaMethod.EMAIL) return 6;
if (sessionStatus?.mfaMethod === MfaMethod.TOTP) return 6;
return 6;
};
const isCodeComplete = mfaCode.length === getExpectedCodeLength();
const handleVerifyMfa = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!mfaCode.trim() || !isCodeComplete || !sessionStatus?.mfaMethod) return;
setIsLoading(true);
setError(null);
try {
await verifyMfaSession.mutateAsync({
mfaSessionId,
mfaToken: mfaCode.trim(),
mfaMethod: sessionStatus.mfaMethod
});
createNotification({
text: "MFA verification successful! Closing window...",
type: "success"
});
// Auto-close window after showing success message
setTimeout(() => {
window.close();
}, 1000);
} catch (err: any) {
setError(err?.response?.data?.message || "Invalid MFA code. Please try again.");
setMfaCode("");
} finally {
setIsLoading(false);
}
};
const handleWebAuthnVerification = async () => {
if (!sessionStatus?.mfaMethod) return;
setIsLoading(true);
setError(null);
try {
// Get authentication options from server
const options = await generateWebAuthnAuthenticationOptions();
// Prompt user to authenticate with their passkey
const authenticationResponse = await startAuthentication({ optionsJSON: options });
// Verify with server to get session token
const result = await verifyWebAuthnAuthentication({ authenticationResponse });
// Use the sessionToken to verify MFA session
if (result.sessionToken) {
await verifyMfaSession.mutateAsync({
mfaSessionId,
mfaToken: result.sessionToken,
mfaMethod: MfaMethod.WEBAUTHN
});
createNotification({
text: "MFA verification successful! Closing window...",
type: "success"
});
// Auto-close window after showing success message
setTimeout(() => {
window.close();
}, 1000);
}
} catch (err: any) {
console.error("WebAuthn verification failed:", err);
let errorMessage = "Failed to verify passkey";
if (err.name === "NotAllowedError") {
errorMessage = "Passkey verification was cancelled or timed out";
} else if (err?.response?.data?.message) {
errorMessage = err.response.data.message;
} else if (err.message) {
errorMessage = err.message;
}
setError(errorMessage);
} finally {
setIsLoading(false);
}
};
if (isStatusError) {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-bunker-800 bg-linear-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<div className="mx-auto w-max pt-6 pb-6 md:mb-16 md:px-8">
<div className="mb-4 flex justify-center">
<img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
</div>
<div className="mb-6 text-center">
<h2 className="mb-3 text-xl font-medium text-red-400">Session Expired</h2>
<p className="text-bunker-300">
This MFA session has expired or is invalid. Please try your action again.
</p>
</div>
</div>
</div>
);
}
if (!sessionStatus) {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-bunker-800 bg-linear-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<div className="text-center">
<div className="mb-4 text-bunker-300">Loading...</div>
</div>
</div>
);
}
if (sessionStatus.status === MfaSessionStatus.ACTIVE) {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-bunker-800 bg-linear-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<div className="mx-auto w-max pt-6 pb-6 md:mb-16 md:px-8">
<div className="mb-4 flex justify-center">
<img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
</div>
<div className="mb-6 text-center">
<h2 className="mb-3 text-xl font-medium text-bunker-50">Verification Complete</h2>
<p className="text-bunker-300">This window will close automatically...</p>
</div>
</div>
</div>
);
}
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-bunker-800 bg-linear-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<div className="mx-auto w-max pt-6 pb-6 md:mb-16 md:px-8">
<div className="mb-4 flex justify-center">
<img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
</div>
<div className="mb-8 text-center">
<h2 className="mb-3 text-xl font-medium text-bunker-100">Two-Factor Authentication</h2>
<p className="mx-auto max-w-md text-sm leading-relaxed text-bunker-300">
{sessionStatus.mfaMethod === MfaMethod.EMAIL &&
"Enter the verification code sent to your email"}
{sessionStatus.mfaMethod === MfaMethod.TOTP &&
"Enter the verification code from your authenticator app"}
{sessionStatus.mfaMethod === MfaMethod.WEBAUTHN &&
"Use your registered passkey to complete two-factor authentication"}
</p>
</div>
{sessionStatus.mfaMethod === MfaMethod.WEBAUTHN ? (
<>
{error && <Error text={error} />}
<div className="mx-auto mt-6 flex w-full max-w-sm flex-col items-center justify-center text-center">
<Button
size="md"
onClick={handleWebAuthnVerification}
isFullWidth
className="h-11 rounded-lg font-medium shadow-xs transition-all duration-200 hover:shadow-md"
colorSchema="primary"
variant="outline_bg"
isLoading={isLoading}
>
Authenticate with Passkey
</Button>
</div>
</>
) : (
<form onSubmit={handleVerifyMfa}>
<div className="mx-auto hidden md:block" style={{ minWidth: "600px" }}>
<div className="mt-8 mb-6 flex justify-center">
<ReactCodeInput
name=""
inputMode="tel"
type="text"
fields={getExpectedCodeLength()}
onChange={setMfaCode}
value={mfaCode}
className="mb-2"
{...codeInputProps}
/>
</div>
</div>
<div className="mx-auto mt-4 block md:hidden" style={{ minWidth: "400px" }}>
<div className="mt-4 mb-6 flex justify-center">
<ReactCodeInput
name=""
inputMode="tel"
type="text"
fields={getExpectedCodeLength()}
onChange={setMfaCode}
value={mfaCode}
className="mb-2"
{...codeInputPropsPhone}
/>
</div>
</div>
{error && <Error text={error} />}
<div className="mx-auto mt-6 flex w-full max-w-sm flex-col items-center justify-center text-center">
<Button
size="md"
type="submit"
isFullWidth
className="h-11 rounded-lg font-medium shadow-xs transition-all duration-200 hover:shadow-md"
colorSchema="primary"
variant="outline_bg"
isLoading={isLoading}
isDisabled={!isCodeComplete}
>
Verify
</Button>
</div>
</form>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1 @@
export { MfaSessionPage } from "./MfaSessionPage";

View File

@@ -0,0 +1,7 @@
import { createFileRoute } from "@tanstack/react-router";
import { MfaSessionPage } from "./MfaSessionPage";
export const Route = createFileRoute("/_authenticate/mfa-session/$mfaSessionId")({
component: MfaSessionPage
});

View File

@@ -87,6 +87,9 @@ export const OrgGenericAuthSection = () => {
<SelectItem value={MfaMethod.TOTP} key="mfa-method-totp"> <SelectItem value={MfaMethod.TOTP} key="mfa-method-totp">
Mobile Authenticator Mobile Authenticator
</SelectItem> </SelectItem>
<SelectItem value={MfaMethod.WEBAUTHN} key="mfa-method-webauthn">
Passkey (WebAuthn)
</SelectItem>
</Select> </Select>
</FormControl> </FormControl>
)} )}

View File

@@ -9,6 +9,7 @@ import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas"; import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
import { SqlAccountFields } from "./shared/SqlAccountFields"; import { SqlAccountFields } from "./shared/SqlAccountFields";
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields"; import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
import { RequireMfaField } from "./RequireMfaField";
type Props = { type Props = {
account?: TMySQLAccount; account?: TMySQLAccount;
@@ -21,7 +22,8 @@ const formSchema = genericAccountFieldsSchema.extend({
credentials: BaseSqlAccountSchema, credentials: BaseSqlAccountSchema,
// We don't support rotation for now, just feed a false value to // We don't support rotation for now, just feed a false value to
// make the schema happy // make the schema happy
rotationEnabled: z.boolean().default(false) rotationEnabled: z.boolean().default(false),
requireMfa: z.boolean().nullable().optional()
}); });
type FormData = z.infer<typeof formSchema>; type FormData = z.infer<typeof formSchema>;
@@ -49,13 +51,10 @@ export const MySQLAccountForm = ({ account, onSubmit }: Props) => {
return ( return (
<FormProvider {...form}> <FormProvider {...form}>
<form <form onSubmit={handleSubmit(onSubmit)}>
onSubmit={(e) => {
handleSubmit(onSubmit)(e);
}}
>
<GenericAccountFields /> <GenericAccountFields />
<SqlAccountFields isUpdate={isUpdate} /> <SqlAccountFields isUpdate={isUpdate} />
<RequireMfaField />
<div className="mt-6 flex items-center"> <div className="mt-6 flex items-center">
<Button <Button
className="mr-4" className="mr-4"

View File

@@ -39,7 +39,7 @@ const CreateForm = ({
const createPamAccount = useCreatePamAccount(); const createPamAccount = useCreatePamAccount();
const onSubmit = async ( const onSubmit = async (
formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials"> formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials" | "requireMfa">
) => { ) => {
const account = await createPamAccount.mutateAsync({ const account = await createPamAccount.mutateAsync({
...formData, ...formData,
@@ -97,7 +97,7 @@ const UpdateForm = ({ account, onComplete }: UpdateFormProps) => {
const updatePamAccount = useUpdatePamAccount(); const updatePamAccount = useUpdatePamAccount();
const onSubmit = async ( const onSubmit = async (
formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials"> formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials" | "requireMfa">
) => { ) => {
const updatedAccount = await updatePamAccount.mutateAsync({ const updatedAccount = await updatePamAccount.mutateAsync({
accountId: account.id, accountId: account.id,

View File

@@ -15,6 +15,7 @@ import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas"; import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
import { SqlAccountFields } from "./shared/SqlAccountFields"; import { SqlAccountFields } from "./shared/SqlAccountFields";
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields"; import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
import { RequireMfaField } from "./RequireMfaField";
import { RotateAccountFields, rotateAccountFieldsSchema } from "./RotateAccountFields"; import { RotateAccountFields, rotateAccountFieldsSchema } from "./RotateAccountFields";
type Props = { type Props = {
@@ -25,7 +26,8 @@ type Props = {
}; };
const formSchema = genericAccountFieldsSchema.extend(rotateAccountFieldsSchema.shape).extend({ const formSchema = genericAccountFieldsSchema.extend(rotateAccountFieldsSchema.shape).extend({
credentials: BaseSqlAccountSchema credentials: BaseSqlAccountSchema,
requireMfa: z.boolean().nullable().optional()
}); });
type FormData = z.infer<typeof formSchema>; type FormData = z.infer<typeof formSchema>;
@@ -77,6 +79,7 @@ export const PostgresAccountForm = ({ account, resourceId, resourceType, onSubmi
<GenericAccountFields /> <GenericAccountFields />
<SqlAccountFields isUpdate={isUpdate} /> <SqlAccountFields isUpdate={isUpdate} />
<RotateAccountFields rotationCredentialsConfigured={rotationCredentialsConfigured} /> <RotateAccountFields rotationCredentialsConfigured={rotationCredentialsConfigured} />
<RequireMfaField />
<div className="mt-6 flex items-center"> <div className="mt-6 flex items-center">
<Button <Button
className="mr-4" className="mr-4"

View File

@@ -0,0 +1,31 @@
import { Controller, useFormContext } from "react-hook-form";
import { FormControl, Switch } from "@app/components/v2";
export const RequireMfaField = () => {
const { control } = useFormContext<{
requireMfa?: boolean | null;
}>();
return (
<div className="flex h-9 w-fit items-center gap-3">
<Controller
control={control}
name="requireMfa"
defaultValue={false}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} className="mb-0">
<Switch
className="ml-0 bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
id="require-mfa"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value ?? false}
/>
</FormControl>
)}
/>
<span className="text-sm">Require MFA for Access</span>
</div>
);
};

View File

@@ -6,7 +6,7 @@ import { FormControl, Select, SelectItem, Switch, Tooltip } from "@app/component
export const rotateAccountFieldsSchema = z.object({ export const rotateAccountFieldsSchema = z.object({
rotationEnabled: z.boolean(), rotationEnabled: z.boolean(),
rotationIntervalSeconds: z.number().nullable().optional() rotationIntervalSeconds: z.number()
}); });
export const RotateAccountFields = ({ export const RotateAccountFields = ({
@@ -16,15 +16,15 @@ export const RotateAccountFields = ({
}) => { }) => {
const { control, watch } = useFormContext<{ const { control, watch } = useFormContext<{
rotationEnabled: boolean; rotationEnabled: boolean;
rotationIntervalSeconds?: number | null; rotationIntervalSeconds: number;
}>(); }>();
const rotationEnabled = watch("rotationEnabled"); const rotationEnabled = watch("rotationEnabled");
return ( return (
<Tooltip <Tooltip
isDisabled={rotationCredentialsConfigured}
content="The resource which owns this account does not have rotation credentials configured." content="The resource which owns this account does not have rotation credentials configured."
isDisabled={rotationCredentialsConfigured}
> >
<div <div
className={twMerge( className={twMerge(
@@ -62,7 +62,7 @@ export const RotateAccountFields = ({
className="mb-0" className="mb-0"
> >
<Select <Select
value={(value || 2592000).toString()} value={value.toString()}
onValueChange={(val) => onChange(parseInt(val, 10))} onValueChange={(val) => onChange(parseInt(val, 10))}
className="w-full border border-mineshaft-500 capitalize" className="w-full border border-mineshaft-500 capitalize"
position="popper" position="popper"

View File

@@ -18,6 +18,7 @@ import { SSHAuthMethod } from "@app/hooks/api/pam/types/ssh-resource";
import { SshCaSetupSection } from "../../../components/SshCaSetupSection"; import { SshCaSetupSection } from "../../../components/SshCaSetupSection";
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields"; import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
import { RequireMfaField } from "./RequireMfaField";
type Props = { type Props = {
account?: TSSHAccount; account?: TSSHAccount;
@@ -53,7 +54,8 @@ const formSchema = genericAccountFieldsSchema.extend({
credentials: BaseSshAccountSchema, credentials: BaseSshAccountSchema,
// We don't support rotation for now, just feed a false value to // We don't support rotation for now, just feed a false value to
// make the schema happy // make the schema happy
rotationEnabled: z.boolean().default(false) rotationEnabled: z.boolean().default(false),
requireMfa: z.boolean().nullable().optional()
}); });
type FormData = z.infer<typeof formSchema>; type FormData = z.infer<typeof formSchema>;
@@ -225,6 +227,8 @@ export const SshAccountForm = ({ account, resourceId, onSubmit }: Props) => {
: { : {
name: "", name: "",
description: "", description: "",
requireMfa: false,
rotationEnabled: false,
credentials: { credentials: {
authMethod: SSHAuthMethod.Password, authMethod: SSHAuthMethod.Password,
username: "", username: "",
@@ -247,6 +251,7 @@ export const SshAccountForm = ({ account, resourceId, onSubmit }: Props) => {
> >
<GenericAccountFields /> <GenericAccountFields />
<SshAccountFields isUpdate={isUpdate} resourceId={effectiveResourceId} /> <SshAccountFields isUpdate={isUpdate} resourceId={effectiveResourceId} />
<RequireMfaField />
<div className="mt-6 flex items-center"> <div className="mt-6 flex items-center">
<Button <Button
className="mr-4" className="mr-4"

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { startRegistration } from "@simplewebauthn/browser";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import QRCode from "qrcode"; import QRCode from "qrcode";
@@ -11,6 +12,8 @@ import {
EmailServiceSetupModal, EmailServiceSetupModal,
FormControl, FormControl,
Input, Input,
Modal,
ModalContent,
Select, Select,
SelectItem SelectItem
} from "@app/components/v2"; } from "@app/components/v2";
@@ -28,6 +31,13 @@ import {
useGetUserTotpRegistration useGetUserTotpRegistration
} from "@app/hooks/api/users/queries"; } from "@app/hooks/api/users/queries";
import { AuthMethod } from "@app/hooks/api/users/types"; import { AuthMethod } from "@app/hooks/api/users/types";
import {
useDeleteWebAuthnCredential,
useGenerateRegistrationOptions,
useGetWebAuthnCredentials,
useUpdateWebAuthnCredential,
useVerifyRegistration
} from "@app/hooks/api/webauthn";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
export const MFASection = () => { export const MFASection = () => {
@@ -46,7 +56,10 @@ export const MFASection = () => {
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"setUpEmail", "setUpEmail",
"deleteTotpConfig", "deleteTotpConfig",
"downloadRecoveryCodes" "downloadRecoveryCodes",
"deleteWebAuthnCredential",
"renameWebAuthnCredential",
"registerPasskey"
] as const); ] as const);
const [shouldShowRecoveryCodes, setShouldShowRecoveryCodes] = useToggle(); const [shouldShowRecoveryCodes, setShouldShowRecoveryCodes] = useToggle();
const { data: totpConfiguration } = useGetUserTotpConfiguration(); const { data: totpConfiguration } = useGetUserTotpConfiguration();
@@ -60,6 +73,20 @@ export const MFASection = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: serverDetails } = useFetchServerStatus(); const { data: serverDetails } = useFetchServerStatus();
// WebAuthn/Passkey hooks
const { data: webAuthnCredentials, isPending: isWebAuthnCredentialsLoading } =
useGetWebAuthnCredentials();
const { mutateAsync: generateRegistrationOptions, isPending: isGeneratingOptions } =
useGenerateRegistrationOptions();
const { mutateAsync: verifyRegistration, isPending: isVerifyingRegistration } =
useVerifyRegistration();
const { mutateAsync: deleteWebAuthnCredential } = useDeleteWebAuthnCredential();
const { mutateAsync: updateWebAuthnCredential } = useUpdateWebAuthnCredential();
const [selectedCredentialId, setSelectedCredentialId] = useState<string>("");
const [credentialName, setCredentialName] = useState<string>("");
const [isRegisteringPasskey, setIsRegisteringPasskey] = useState(false);
// Update form data when user data changes // Update form data when user data changes
useEffect(() => { useEffect(() => {
if (user) { if (user) {
@@ -107,6 +134,114 @@ export const MFASection = () => {
}); });
}; };
const handleRegisterPasskey = async () => {
try {
setIsRegisteringPasskey(true);
// Check if WebAuthn is supported
if (
!window.PublicKeyCredential ||
!window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable
) {
createNotification({
text: "WebAuthn is not supported on this browser",
type: "error"
});
return;
}
// Check if platform authenticator is available
const available =
await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
if (!available) {
createNotification({
text: "No passkey-compatible authenticator found on this device",
type: "error"
});
return;
}
// Generate registration options from server
const options = await generateRegistrationOptions();
const registrationResponse = await startRegistration({ optionsJSON: options });
// Verify registration with server
await verifyRegistration({
registrationResponse,
name: credentialName || "Passkey"
});
createNotification({
text: "Successfully registered passkey",
type: "success"
});
handlePopUpClose("registerPasskey");
setCredentialName("");
} catch (error: any) {
console.error("Failed to register passkey:", error);
// Better error messages
let errorMessage = "Failed to register passkey";
if (error.name === "NotAllowedError") {
errorMessage = "Passkey registration was cancelled or timed out";
} else if (error.name === "InvalidStateError") {
errorMessage = "This passkey has already been registered";
} else if (error.message) {
errorMessage = error.message;
}
createNotification({
text: errorMessage,
type: "error"
});
} finally {
setIsRegisteringPasskey(false);
}
};
const handleDeleteWebAuthnCredential = async () => {
try {
await deleteWebAuthnCredential({ id: selectedCredentialId });
createNotification({
text: "Successfully deleted passkey",
type: "success"
});
handlePopUpClose("deleteWebAuthnCredential");
setSelectedCredentialId("");
} catch (error: any) {
createNotification({
text: error.message || "Failed to delete passkey",
type: "error"
});
}
};
const handleRenameWebAuthnCredential = async () => {
try {
await updateWebAuthnCredential({
id: selectedCredentialId,
name: credentialName
});
createNotification({
text: "Successfully renamed passkey",
type: "success"
});
handlePopUpClose("renameWebAuthnCredential");
setSelectedCredentialId("");
setCredentialName("");
} catch (error: any) {
createNotification({
text: error.message || "Failed to rename passkey",
type: "error"
});
}
};
const handleFormDataChange = async (field: string, value: any) => { const handleFormDataChange = async (field: string, value: any) => {
setFormData((prev) => ({ setFormData((prev) => ({
...prev, ...prev,
@@ -149,6 +284,19 @@ export const MFASection = () => {
return; return;
} }
// If selecting passkey as 2FA method but no passkeys registered
if (
formData.isMfaEnabled &&
formData.selectedMfaMethod === MfaMethod.WEBAUTHN &&
(!webAuthnCredentials || webAuthnCredentials.length === 0)
) {
createNotification({
text: "Please register at least one passkey before selecting it as your two-factor authentication method",
type: "error"
});
return;
}
setIsLoading(true); setIsLoading(true);
// If enabling 2FA with mobile authenticator, verify TOTP first // If enabling 2FA with mobile authenticator, verify TOTP first
@@ -240,9 +388,97 @@ export const MFASection = () => {
if (totpConfiguration?.isVerified) return true; if (totpConfiguration?.isVerified) return true;
return totpCode.trim().length > 0; return totpCode.trim().length > 0;
} }
if (formData.selectedMfaMethod === MfaMethod.WEBAUTHN) return true;
return false; return false;
}; };
const registerPasskeyModal = (
<Modal
isOpen={popUp.registerPasskey?.isOpen || false}
onOpenChange={(isOpen) => {
if (!isOpen) {
handlePopUpClose("registerPasskey");
setCredentialName("");
}
}}
>
<ModalContent title="Register New Passkey">
<div className="space-y-4">
<p className="text-sm text-mineshaft-300">
Give your passkey a name to help you identify it later. After clicking
&quot;Register&quot;, you&apos;ll be prompted to use your device&apos;s biometric
authentication (Touch ID, Face ID, Windows Hello, etc.).
</p>
<FormControl label="Passkey Name">
<Input
value={credentialName}
onChange={(e) => setCredentialName(e.target.value)}
placeholder="e.g., My MacBook Pro"
/>
</FormControl>
<div className="flex gap-2">
<Button
onClick={handleRegisterPasskey}
isLoading={isRegisteringPasskey || isVerifyingRegistration}
disabled={!credentialName.trim()}
colorSchema="primary"
>
Register
</Button>
<Button
variant="outline_bg"
onClick={() => handlePopUpClose("registerPasskey")}
disabled={isRegisteringPasskey || isVerifyingRegistration}
>
Cancel
</Button>
</div>
</div>
</ModalContent>
</Modal>
);
const renamePasskeyModal = (
<Modal
isOpen={popUp.renameWebAuthnCredential?.isOpen || false}
onOpenChange={(isOpen) => {
if (!isOpen) {
handlePopUpClose("renameWebAuthnCredential");
setCredentialName("");
setSelectedCredentialId("");
}
}}
>
<ModalContent title="Rename Passkey">
<div className="space-y-4">
<FormControl label="Passkey Name">
<Input
value={credentialName}
onChange={(e) => setCredentialName(e.target.value)}
placeholder="e.g., My MacBook Pro"
/>
</FormControl>
<div className="flex gap-2">
<Button
onClick={handleRenameWebAuthnCredential}
disabled={!credentialName.trim()}
colorSchema="primary"
>
Save
</Button>
<Button
variant="outline_bg"
onClick={() => handlePopUpClose("renameWebAuthnCredential")}
>
Cancel
</Button>
</div>
</div>
</ModalContent>
</Modal>
);
return ( return (
<> <>
<form <form
@@ -293,6 +529,7 @@ export const MFASection = () => {
> >
<SelectItem value={MfaMethod.EMAIL}>Email</SelectItem> <SelectItem value={MfaMethod.EMAIL}>Email</SelectItem>
<SelectItem value={MfaMethod.TOTP}>Mobile Authenticator</SelectItem> <SelectItem value={MfaMethod.TOTP}>Mobile Authenticator</SelectItem>
<SelectItem value={MfaMethod.WEBAUTHN}>Passkey (WebAuthn)</SelectItem>
</Select> </Select>
</FormControl> </FormControl>
</div> </div>
@@ -411,57 +648,162 @@ export const MFASection = () => {
</Button> </Button>
</div> </div>
)} )}
{user?.isMfaEnabled && totpConfiguration?.isVerified && (
<div className="mt-8 border-t border-mineshaft-600 pt-6">
<h3 className="mb-4 text-lg font-medium text-mineshaft-100">
Mobile Authenticator Management
</h3>
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
<Button
colorSchema="secondary"
variant="outline_bg"
onClick={setShouldShowRecoveryCodes.toggle}
>
{shouldShowRecoveryCodes ? "Hide recovery codes" : "Show recovery codes"}
</Button>
<Button
colorSchema="secondary"
variant="outline_bg"
onClick={handleGenerateMoreRecoveryCodes}
>
Generate more codes
</Button>
<Button
colorSchema="danger"
variant="outline_bg"
onClick={() => handlePopUpOpen("deleteTotpConfig")}
>
Remove Authenticator
</Button>
</div>
{shouldShowRecoveryCodes && (
<div className="w-fit rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4 pr-8">
<div className="grid grid-cols-2 gap-x-6 gap-y-2 font-mono text-sm">
{totpConfiguration.recoveryCodes.map((code, index) => (
<div key={code} className="flex items-center text-mineshaft-200">
<span className="w-8 text-right text-mineshaft-400">{index + 1}.</span>
<span className="pl-2">{code}</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
)}
</div> </div>
)} )}
</form> </form>
{/* Management Sections - Separate from configuration form */}
{user && (
<div className="mt-6 mb-6 space-y-6">
{/* Mobile Authenticator Management - Show if configured, regardless of active method */}
{totpConfiguration?.isVerified && (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<h3 className="mb-4 text-lg font-medium text-mineshaft-100">
Mobile Authenticator Management
</h3>
<p className="mb-4 text-sm text-mineshaft-400">
Manage your mobile authenticator configuration and recovery codes.
</p>
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
<Button
colorSchema="secondary"
variant="outline_bg"
onClick={setShouldShowRecoveryCodes.toggle}
>
{shouldShowRecoveryCodes ? "Hide recovery codes" : "Show recovery codes"}
</Button>
<Button
colorSchema="secondary"
variant="outline_bg"
onClick={handleGenerateMoreRecoveryCodes}
>
Generate more codes
</Button>
<Button
colorSchema="danger"
variant="outline_bg"
onClick={() => handlePopUpOpen("deleteTotpConfig")}
>
Remove Authenticator
</Button>
</div>
{shouldShowRecoveryCodes && (
<div className="w-fit rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4 pr-8">
<div className="grid grid-cols-2 gap-x-6 gap-y-2 font-mono text-sm">
{totpConfiguration.recoveryCodes.map((code, index) => (
<div key={code} className="flex items-center text-mineshaft-200">
<span className="w-8 text-right text-mineshaft-400">{index + 1}.</span>
<span className="pl-2">{code}</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
)}
{/* Passkey Management - Always show */}
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<h3 className="text-lg font-medium text-mineshaft-100">Passkey Management</h3>
<p className="mt-1 text-sm text-mineshaft-400">
Manage your passkeys. Passkeys can be used for two-factor authentication.
</p>
</div>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setCredentialName("");
handlePopUpOpen("registerPasskey");
}}
isLoading={isGeneratingOptions}
>
Add Passkey
</Button>
</div>
{(() => {
if (isWebAuthnCredentialsLoading) {
return <ContentLoader />;
}
if (webAuthnCredentials && webAuthnCredentials.length > 0) {
return (
<div className="space-y-3">
{webAuthnCredentials.map((credential) => (
<div
key={credential.id}
className="flex items-center justify-between rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-mineshaft-100">
{credential.name || "Unnamed Passkey"}
</span>
{credential.transports && credential.transports.length > 0 && (
<span className="text-xs text-mineshaft-400">
({credential.transports.join(", ")})
</span>
)}
</div>
<div className="mt-1 text-xs text-mineshaft-400">
Added {new Date(credential.createdAt).toLocaleDateString()}
{credential.lastUsedAt && (
<>
{" "}
· Last used {new Date(credential.lastUsedAt).toLocaleDateString()}
</>
)}
</div>
</div>
<div className="flex gap-2">
<Button
size="xs"
colorSchema="secondary"
variant="outline_bg"
onClick={() => {
setSelectedCredentialId(credential.id);
setCredentialName(credential.name || "");
handlePopUpOpen("renameWebAuthnCredential");
}}
>
Rename
</Button>
<Button
size="xs"
colorSchema="danger"
variant="outline_bg"
onClick={() => {
setSelectedCredentialId(credential.id);
handlePopUpOpen("deleteWebAuthnCredential");
}}
>
Remove
</Button>
</div>
</div>
))}
</div>
);
}
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-6 text-center">
<p className="text-sm text-mineshaft-300">
No passkeys registered yet. Add a passkey to use it for two-factor
authentication.
</p>
</div>
);
})()}
</div>
</div>
)}
<EmailServiceSetupModal <EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen} isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
@@ -481,6 +823,19 @@ export const MFASection = () => {
recoveryCodes={totpRegistration?.recoveryCodes || []} recoveryCodes={totpRegistration?.recoveryCodes || []}
onDownloadComplete={() => handlePopUpClose("downloadRecoveryCodes")} onDownloadComplete={() => handlePopUpClose("downloadRecoveryCodes")}
/> />
{registerPasskeyModal}
{renamePasskeyModal}
{/* Delete Passkey Modal */}
<DeleteActionModal
isOpen={popUp.deleteWebAuthnCredential?.isOpen || false}
title="Remove passkey?"
subTitle="This action is irreversible. You'll need to register this passkey again if you want to use it in the future."
onChange={(isOpen) => handlePopUpToggle("deleteWebAuthnCredential", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleDeleteWebAuthnCredential}
/>
</> </>
); );
}; };

View File

@@ -38,6 +38,7 @@ import { Route as authAdminLoginPageRouteImport } from './pages/auth/AdminLoginP
import { Route as adminSignUpPageRouteImport } from './pages/admin/SignUpPage/route' import { Route as adminSignUpPageRouteImport } from './pages/admin/SignUpPage/route'
import { Route as organizationNoOrgPageRouteImport } from './pages/organization/NoOrgPage/route' import { Route as organizationNoOrgPageRouteImport } from './pages/organization/NoOrgPage/route'
import { Route as organizationMcpEndpointFinalizePageRouteImport } from './pages/organization/McpEndpointFinalizePage/route' import { Route as organizationMcpEndpointFinalizePageRouteImport } from './pages/organization/McpEndpointFinalizePage/route'
import { Route as MfaSessionPageRouteImport } from './pages/MfaSessionPage/route'
import { Route as authSignUpPageRouteImport } from './pages/auth/SignUpPage/route' import { Route as authSignUpPageRouteImport } from './pages/auth/SignUpPage/route'
import { Route as authLoginPageRouteImport } from './pages/auth/LoginPage/route' import { Route as authLoginPageRouteImport } from './pages/auth/LoginPage/route'
import { Route as redirectsProjectRedirectImport } from './pages/redirects/project-redirect' import { Route as redirectsProjectRedirectImport } from './pages/redirects/project-redirect'
@@ -549,6 +550,12 @@ const organizationMcpEndpointFinalizePageRouteRoute =
getParentRoute: () => middlewaresAuthenticateRoute, getParentRoute: () => middlewaresAuthenticateRoute,
} as any) } as any)
const MfaSessionPageRouteRoute = MfaSessionPageRouteImport.update({
id: '/mfa-session/$mfaSessionId',
path: '/mfa-session/$mfaSessionId',
getParentRoute: () => middlewaresAuthenticateRoute,
} as any)
const authSignUpPageRouteRoute = authSignUpPageRouteImport.update({ const authSignUpPageRouteRoute = authSignUpPageRouteImport.update({
id: '/', id: '/',
path: '/', path: '/',
@@ -2478,6 +2485,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof authSignUpPageRouteImport preLoaderRoute: typeof authSignUpPageRouteImport
parentRoute: typeof RestrictLoginSignupSignupImport parentRoute: typeof RestrictLoginSignupSignupImport
} }
'/_authenticate/mfa-session/$mfaSessionId': {
id: '/_authenticate/mfa-session/$mfaSessionId'
path: '/mfa-session/$mfaSessionId'
fullPath: '/mfa-session/$mfaSessionId'
preLoaderRoute: typeof MfaSessionPageRouteImport
parentRoute: typeof middlewaresAuthenticateImport
}
'/_authenticate/organization/mcp-endpoint-finalize': { '/_authenticate/organization/mcp-endpoint-finalize': {
id: '/_authenticate/organization/mcp-endpoint-finalize' id: '/_authenticate/organization/mcp-endpoint-finalize'
path: '/organization/mcp-endpoint-finalize' path: '/organization/mcp-endpoint-finalize'
@@ -5268,6 +5282,7 @@ interface middlewaresAuthenticateRouteChildren {
authPasswordSetupPageRouteRoute: typeof authPasswordSetupPageRouteRoute authPasswordSetupPageRouteRoute: typeof authPasswordSetupPageRouteRoute
middlewaresInjectOrgDetailsRoute: typeof middlewaresInjectOrgDetailsRouteWithChildren middlewaresInjectOrgDetailsRoute: typeof middlewaresInjectOrgDetailsRouteWithChildren
AuthenticatePersonalSettingsRoute: typeof AuthenticatePersonalSettingsRouteWithChildren AuthenticatePersonalSettingsRoute: typeof AuthenticatePersonalSettingsRouteWithChildren
MfaSessionPageRouteRoute: typeof MfaSessionPageRouteRoute
organizationMcpEndpointFinalizePageRouteRoute: typeof organizationMcpEndpointFinalizePageRouteRoute organizationMcpEndpointFinalizePageRouteRoute: typeof organizationMcpEndpointFinalizePageRouteRoute
organizationNoOrgPageRouteRoute: typeof organizationNoOrgPageRouteRoute organizationNoOrgPageRouteRoute: typeof organizationNoOrgPageRouteRoute
} }
@@ -5279,6 +5294,7 @@ const middlewaresAuthenticateRouteChildren: middlewaresAuthenticateRouteChildren
middlewaresInjectOrgDetailsRouteWithChildren, middlewaresInjectOrgDetailsRouteWithChildren,
AuthenticatePersonalSettingsRoute: AuthenticatePersonalSettingsRoute:
AuthenticatePersonalSettingsRouteWithChildren, AuthenticatePersonalSettingsRouteWithChildren,
MfaSessionPageRouteRoute: MfaSessionPageRouteRoute,
organizationMcpEndpointFinalizePageRouteRoute: organizationMcpEndpointFinalizePageRouteRoute:
organizationMcpEndpointFinalizePageRouteRoute, organizationMcpEndpointFinalizePageRouteRoute,
organizationNoOrgPageRouteRoute: organizationNoOrgPageRouteRoute, organizationNoOrgPageRouteRoute: organizationNoOrgPageRouteRoute,
@@ -5376,6 +5392,7 @@ export interface FileRoutesByFullPath {
'/signup': typeof RestrictLoginSignupSignupRouteWithChildren '/signup': typeof RestrictLoginSignupSignupRouteWithChildren
'/login/': typeof authLoginPageRouteRoute '/login/': typeof authLoginPageRouteRoute
'/signup/': typeof authSignUpPageRouteRoute '/signup/': typeof authSignUpPageRouteRoute
'/mfa-session/$mfaSessionId': typeof MfaSessionPageRouteRoute
'/organization/mcp-endpoint-finalize': typeof organizationMcpEndpointFinalizePageRouteRoute '/organization/mcp-endpoint-finalize': typeof organizationMcpEndpointFinalizePageRouteRoute
'/organizations/none': typeof organizationNoOrgPageRouteRoute '/organizations/none': typeof organizationNoOrgPageRouteRoute
'/admin/signup': typeof adminSignUpPageRouteRoute '/admin/signup': typeof adminSignUpPageRouteRoute
@@ -5631,6 +5648,7 @@ export interface FileRoutesByTo {
'/personal-settings': typeof userPersonalSettingsPageRouteRoute '/personal-settings': typeof userPersonalSettingsPageRouteRoute
'/login': typeof authLoginPageRouteRoute '/login': typeof authLoginPageRouteRoute
'/signup': typeof authSignUpPageRouteRoute '/signup': typeof authSignUpPageRouteRoute
'/mfa-session/$mfaSessionId': typeof MfaSessionPageRouteRoute
'/organization/mcp-endpoint-finalize': typeof organizationMcpEndpointFinalizePageRouteRoute '/organization/mcp-endpoint-finalize': typeof organizationMcpEndpointFinalizePageRouteRoute
'/organizations/none': typeof organizationNoOrgPageRouteRoute '/organizations/none': typeof organizationNoOrgPageRouteRoute
'/admin/signup': typeof adminSignUpPageRouteRoute '/admin/signup': typeof adminSignUpPageRouteRoute
@@ -5878,6 +5896,7 @@ export interface FileRoutesById {
'/_restrict-login-signup/signup': typeof RestrictLoginSignupSignupRouteWithChildren '/_restrict-login-signup/signup': typeof RestrictLoginSignupSignupRouteWithChildren
'/_restrict-login-signup/login/': typeof authLoginPageRouteRoute '/_restrict-login-signup/login/': typeof authLoginPageRouteRoute
'/_restrict-login-signup/signup/': typeof authSignUpPageRouteRoute '/_restrict-login-signup/signup/': typeof authSignUpPageRouteRoute
'/_authenticate/mfa-session/$mfaSessionId': typeof MfaSessionPageRouteRoute
'/_authenticate/organization/mcp-endpoint-finalize': typeof organizationMcpEndpointFinalizePageRouteRoute '/_authenticate/organization/mcp-endpoint-finalize': typeof organizationMcpEndpointFinalizePageRouteRoute
'/_authenticate/organizations/none': typeof organizationNoOrgPageRouteRoute '/_authenticate/organizations/none': typeof organizationNoOrgPageRouteRoute
'/_restrict-login-signup/admin/signup': typeof adminSignUpPageRouteRoute '/_restrict-login-signup/admin/signup': typeof adminSignUpPageRouteRoute
@@ -6147,6 +6166,7 @@ export interface FileRouteTypes {
| '/signup' | '/signup'
| '/login/' | '/login/'
| '/signup/' | '/signup/'
| '/mfa-session/$mfaSessionId'
| '/organization/mcp-endpoint-finalize' | '/organization/mcp-endpoint-finalize'
| '/organizations/none' | '/organizations/none'
| '/admin/signup' | '/admin/signup'
@@ -6401,6 +6421,7 @@ export interface FileRouteTypes {
| '/personal-settings' | '/personal-settings'
| '/login' | '/login'
| '/signup' | '/signup'
| '/mfa-session/$mfaSessionId'
| '/organization/mcp-endpoint-finalize' | '/organization/mcp-endpoint-finalize'
| '/organizations/none' | '/organizations/none'
| '/admin/signup' | '/admin/signup'
@@ -6646,6 +6667,7 @@ export interface FileRouteTypes {
| '/_restrict-login-signup/signup' | '/_restrict-login-signup/signup'
| '/_restrict-login-signup/login/' | '/_restrict-login-signup/login/'
| '/_restrict-login-signup/signup/' | '/_restrict-login-signup/signup/'
| '/_authenticate/mfa-session/$mfaSessionId'
| '/_authenticate/organization/mcp-endpoint-finalize' | '/_authenticate/organization/mcp-endpoint-finalize'
| '/_authenticate/organizations/none' | '/_authenticate/organizations/none'
| '/_restrict-login-signup/admin/signup' | '/_restrict-login-signup/admin/signup'
@@ -6960,6 +6982,7 @@ export const routeTree = rootRoute
"/_authenticate/password-setup", "/_authenticate/password-setup",
"/_authenticate/_inject-org-details", "/_authenticate/_inject-org-details",
"/_authenticate/personal-settings", "/_authenticate/personal-settings",
"/_authenticate/mfa-session/$mfaSessionId",
"/_authenticate/organization/mcp-endpoint-finalize", "/_authenticate/organization/mcp-endpoint-finalize",
"/_authenticate/organizations/none" "/_authenticate/organizations/none"
] ]
@@ -7047,6 +7070,10 @@ export const routeTree = rootRoute
"filePath": "auth/SignUpPage/route.tsx", "filePath": "auth/SignUpPage/route.tsx",
"parent": "/_restrict-login-signup/signup" "parent": "/_restrict-login-signup/signup"
}, },
"/_authenticate/mfa-session/$mfaSessionId": {
"filePath": "MfaSessionPage/route.tsx",
"parent": "/_authenticate"
},
"/_authenticate/organization/mcp-endpoint-finalize": { "/_authenticate/organization/mcp-endpoint-finalize": {
"filePath": "organization/McpEndpointFinalizePage/route.tsx", "filePath": "organization/McpEndpointFinalizePage/route.tsx",
"parent": "/_authenticate" "parent": "/_authenticate"

View File

@@ -443,6 +443,7 @@ export const routes = rootRoute("root.tsx", [
]), ]),
middleware("authenticate.tsx", [ middleware("authenticate.tsx", [
route("/password-setup", "auth/PasswordSetupPage/route.tsx"), route("/password-setup", "auth/PasswordSetupPage/route.tsx"),
route("/mfa-session/$mfaSessionId", "MfaSessionPage/route.tsx"),
route("/personal-settings", [ route("/personal-settings", [
layout("user/layout.tsx", [index("user/PersonalSettingsPage/route.tsx")]) layout("user/layout.tsx", [index("user/PersonalSettingsPage/route.tsx")])
]), ]),