add webauthn routes

This commit is contained in:
2025-02-17 14:48:55 +05:30
parent 167e89b464
commit 453c3ef995
7 changed files with 589 additions and 1 deletions

View File

@@ -20,6 +20,8 @@
"@fastify/multipart": "^9.0.1",
"@fastify/oauth2": "^8.1.0",
"@paralleldrive/cuid2": "^2.2.2",
"@simplewebauthn/server": "^13.1.1",
"axios": "^1.7.9",
"bcryptjs": "^3.0.0",
"fastify": "^5.2.0",
"fastify-type-provider-zod": "^4.0.2",

287
pnpm-lock.yaml generated
View File

@@ -29,6 +29,12 @@ importers:
'@paralleldrive/cuid2':
specifier: ^2.2.2
version: 2.2.2
'@simplewebauthn/server':
specifier: ^13.1.1
version: 13.1.1
axios:
specifier: ^1.7.9
version: 1.7.9
bcryptjs:
specifier: ^3.0.0
version: 3.0.0
@@ -301,6 +307,12 @@ packages:
'@hapi/wreck@18.1.0':
resolution: {integrity: sha512-0z6ZRCmFEfV/MQqkQomJ7sl/hyxvcZM7LtuVqN3vdAO4vM9eBbowl0kaqQj9EJJQab+3Uuh1GxbGIBFy4NfJ4w==}
'@hexagon/base64@1.1.28':
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
'@levischuck/tiny-cbor@0.2.11':
resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==}
'@lukeed/ms@2.0.2':
resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
engines: {node: '>=8'}
@@ -315,6 +327,21 @@ packages:
'@paralleldrive/cuid2@2.2.2':
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
'@peculiar/asn1-android@2.3.15':
resolution: {integrity: sha512-8U2TIj59cRlSXTX2d0mzUKP7whfWGFMzTeC3qPgAbccXFrPNZLaDhpNEdG5U2QZ/tBv/IHlCJ8s+KYXpJeop6w==}
'@peculiar/asn1-ecc@2.3.15':
resolution: {integrity: sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA==}
'@peculiar/asn1-rsa@2.3.15':
resolution: {integrity: sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==}
'@peculiar/asn1-schema@2.3.15':
resolution: {integrity: sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==}
'@peculiar/asn1-x509@2.3.15':
resolution: {integrity: sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==}
'@sideway/address@4.1.5':
resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==}
@@ -324,6 +351,10 @@ packages:
'@sideway/pinpoint@2.0.0':
resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==}
'@simplewebauthn/server@13.1.1':
resolution: {integrity: sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA==}
engines: {node: '>=20.0.0'}
'@smithy/abort-controller@3.1.9':
resolution: {integrity: sha512-yiW0WI30zj8ZKoSYNx90no7ugVn3khlyH/z5W8qtKBtVE6awRALbhSG+2SAHA1r6bO/6M9utxYKVZ3PCJ1rWxw==}
engines: {node: '>=16.0.0'}
@@ -559,6 +590,13 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
asn1js@3.0.5:
resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==}
engines: {node: '>=12.0.0'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
atomic-sleep@1.0.0:
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
engines: {node: '>=8.0.0'}
@@ -566,6 +604,9 @@ packages:
avvio@9.1.0:
resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==}
axios@1.7.9:
resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -583,6 +624,10 @@ packages:
resolution: {integrity: sha512-P92xmHDQjSKPLHqFxefqMxASNq/aWJMEZugpCjf+AF/pgcUpMMQCg7t7+ewko0/u8AapvF3luf/FoehddEK+sA==}
engines: {node: '>=16.20.1'}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
camel-case@4.1.2:
resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
@@ -592,6 +637,10 @@ packages:
change-case@4.1.2:
resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
constant-case@3.0.4:
resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==}
@@ -612,6 +661,10 @@ packages:
supports-color:
optional: true
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@@ -619,6 +672,26 @@ packages:
dot-case@3.0.4:
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
es-set-tostringtag@2.1.0:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
@@ -675,6 +748,19 @@ packages:
resolution: {integrity: sha512-Y5jIsuYR4BwWDYYQ2A/RWWE6gD8a0FMgtU+HOq1WKku+Cwdz8M1v8wcAmRXXM1/iqtoqg06v+LjAxMYbCjViMw==}
engines: {node: '>=14'}
follow-redirects@1.15.9:
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
form-data@4.0.2:
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
engines: {node: '>= 6'}
forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
@@ -682,11 +768,38 @@ packages:
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
get-intrinsic@1.2.7:
resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==}
engines: {node: '>= 0.4'}
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
glob@8.1.0:
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
engines: {node: '>=12'}
deprecated: Glob versions prior to v9 are no longer supported
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
has-tostringtag@1.0.2:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
header-case@2.0.4:
resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==}
@@ -739,9 +852,21 @@ packages:
resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==}
engines: {node: 20 || >=22}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
memory-pager@1.5.0:
resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
mime@3.0.0:
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
engines: {node: '>=10.0.0'}
@@ -845,10 +970,20 @@ packages:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
pvtsutils@1.3.6:
resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==}
pvutils@1.1.3:
resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==}
engines: {node: '>=6.0.0'}
quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
@@ -1621,6 +1756,10 @@ snapshots:
'@hapi/bourne': 3.0.0
'@hapi/hoek': 11.0.7
'@hexagon/base64@1.1.28': {}
'@levischuck/tiny-cbor@0.2.11': {}
'@lukeed/ms@2.0.2': {}
'@mongodb-js/saslprep@1.1.9':
@@ -1633,6 +1772,39 @@ snapshots:
dependencies:
'@noble/hashes': 1.6.1
'@peculiar/asn1-android@2.3.15':
dependencies:
'@peculiar/asn1-schema': 2.3.15
asn1js: 3.0.5
tslib: 2.8.1
'@peculiar/asn1-ecc@2.3.15':
dependencies:
'@peculiar/asn1-schema': 2.3.15
'@peculiar/asn1-x509': 2.3.15
asn1js: 3.0.5
tslib: 2.8.1
'@peculiar/asn1-rsa@2.3.15':
dependencies:
'@peculiar/asn1-schema': 2.3.15
'@peculiar/asn1-x509': 2.3.15
asn1js: 3.0.5
tslib: 2.8.1
'@peculiar/asn1-schema@2.3.15':
dependencies:
asn1js: 3.0.5
pvtsutils: 1.3.6
tslib: 2.8.1
'@peculiar/asn1-x509@2.3.15':
dependencies:
'@peculiar/asn1-schema': 2.3.15
asn1js: 3.0.5
pvtsutils: 1.3.6
tslib: 2.8.1
'@sideway/address@4.1.5':
dependencies:
'@hapi/hoek': 9.3.0
@@ -1641,6 +1813,16 @@ snapshots:
'@sideway/pinpoint@2.0.0': {}
'@simplewebauthn/server@13.1.1':
dependencies:
'@hexagon/base64': 1.1.28
'@levischuck/tiny-cbor': 0.2.11
'@peculiar/asn1-android': 2.3.15
'@peculiar/asn1-ecc': 2.3.15
'@peculiar/asn1-rsa': 2.3.15
'@peculiar/asn1-schema': 2.3.15
'@peculiar/asn1-x509': 2.3.15
'@smithy/abort-controller@3.1.9':
dependencies:
'@smithy/types': 3.7.2
@@ -2003,6 +2185,14 @@ snapshots:
argparse@2.0.1: {}
asn1js@3.0.5:
dependencies:
pvtsutils: 1.3.6
pvutils: 1.1.3
tslib: 2.8.1
asynckit@0.4.0: {}
atomic-sleep@1.0.0: {}
avvio@9.1.0:
@@ -2010,6 +2200,14 @@ snapshots:
'@fastify/error': 4.0.0
fastq: 1.17.1
axios@1.7.9:
dependencies:
follow-redirects: 1.15.9
form-data: 4.0.2
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
balanced-match@1.0.2: {}
bcryptjs@3.0.0: {}
@@ -2022,6 +2220,11 @@ snapshots:
bson@6.10.1: {}
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
camel-case@4.1.2:
dependencies:
pascal-case: 3.1.2
@@ -2048,6 +2251,10 @@ snapshots:
snake-case: 3.0.4
tslib: 2.8.1
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
constant-case@3.0.4:
dependencies:
no-case: 3.0.4
@@ -2064,6 +2271,8 @@ snapshots:
dependencies:
ms: 2.1.3
delayed-stream@1.0.0: {}
depd@2.0.0: {}
dot-case@3.0.4:
@@ -2071,6 +2280,27 @@ snapshots:
no-case: 3.0.4
tslib: 2.8.1
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
es-set-tostringtag@2.1.0:
dependencies:
es-errors: 1.3.0
get-intrinsic: 1.2.7
has-tostringtag: 1.0.2
hasown: 2.0.2
escape-html@1.0.3: {}
fast-decode-uri-component@1.0.1: {}
@@ -2155,10 +2385,39 @@ snapshots:
fast-querystring: 1.1.2
safe-regex2: 4.0.0
follow-redirects@1.15.9: {}
form-data@4.0.2:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
mime-types: 2.1.35
forwarded@0.2.0: {}
fs.realpath@1.0.0: {}
function-bind@1.1.2: {}
get-intrinsic@1.2.7:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.2
math-intrinsics: 1.1.0
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
glob@8.1.0:
dependencies:
fs.realpath: 1.0.0
@@ -2167,6 +2426,18 @@ snapshots:
minimatch: 5.1.6
once: 1.4.0
gopd@1.2.0: {}
has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
dependencies:
has-symbols: 1.1.0
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
header-case@2.0.4:
dependencies:
capital-case: 1.0.4
@@ -2231,8 +2502,16 @@ snapshots:
lru-cache@11.0.2: {}
math-intrinsics@1.1.0: {}
memory-pager@1.5.0: {}
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
mime@3.0.0: {}
minimatch@5.1.6:
@@ -2344,8 +2623,16 @@ snapshots:
forwarded: 0.2.0
ipaddr.js: 1.9.1
proxy-from-env@1.1.0: {}
punycode@2.3.1: {}
pvtsutils@1.3.6:
dependencies:
tslib: 2.8.1
pvutils@1.1.3: {}
quick-format-unescaped@4.0.4: {}
real-require@0.2.0: {}

View File

@@ -16,8 +16,9 @@ import { rtsSchemas } from "./rts/rts.schema";
import { taskSchemas } from "./task/task.schema";
import { notificationSchemas } from "./notification/notification.schema";
import { noteSchemas } from "./note/note.schema";
import { webAuthnRoutes } from "./webauthn/webauthn.route";
const app = fastify({ logger: true });
const app = fastify({ logger: true, trustProxy: true });
app.get("/health", (req, res) => {
return { status: "OK" };
@@ -33,6 +34,7 @@ app.register(cors, {
});
app.register(multipart, { limits: { fileSize: 50000000 } });
app.register(authRoutes, { prefix: "/auth" });
app.register(webAuthnRoutes, { prefix: "/webauthn" });
app.register(routes, { prefix: "/api/v1" });
for (const schema of [

View File

@@ -28,6 +28,21 @@ const userSchema = new mongoose.Schema({
type: String,
required: true,
},
passKeys: [],
challenge: new mongoose.Schema(
{
value: String,
expiry: Date,
},
{ _id: false }
),
otp: new mongoose.Schema(
{
value: String,
expiry: Date,
},
{ _id: false }
),
createdAt: Date,
createdBy: mongoose.Types.ObjectId,
lastLogin: Date,

View File

@@ -17,3 +17,13 @@ export async function generateToken(): Promise<string> {
});
});
}
export function generateOTP() {
let otp = "";
for (let i = 0; i < 6; i++) {
otp += crypto.randomInt(10);
}
return parseInt(otp);
}

59
src/utils/mail.ts Normal file
View File

@@ -0,0 +1,59 @@
import axios from "axios";
let token = {
value: "",
expiry: new Date(),
};
export async function getToken() {
if (token.value != "" && new Date() < token.expiry) {
return token.value;
}
const res = await axios({
url: `https://accounts.zoho.com/oauth/v2/token?refresh_token=${process.env.ZOHO_REFRESH_TOKEN}&grant_type=refresh_token&client_id=${process.env.ZOHO_CLIENT_ID}&client_secret=${process.env.ZOHO_CLIENT_SECRET}`,
method: "POST",
});
if (res.data.status && res.data.status.code != 200) {
console.dir(res.data, { depth: null });
throw "error fetching token";
}
token.value = res.data.access_token;
token.expiry = new Date(Date.now() + res.data.expires_in * 1000);
return token.value;
}
export async function sendMail(
to: string,
subject: string,
body: string
): Promise<Boolean> {
try {
const token = await getToken();
const res = await axios({
url: `https://mail.zoho.com/api/accounts/${process.env.ZOHO_ACCOUNT_ID}/messages`,
method: "POST",
headers: {
Authorization: `Zoho-oauthtoken ${token}`,
},
data: {
fromAddress: "akhil.reddy@qualyval.com",
toAddress: to,
subject,
content: body,
mailFormat: "plaintext",
},
});
} catch (err) {
if (err.response) console.log(err.response.data);
else console.log(err);
return false;
}
return true;
}

View File

@@ -0,0 +1,213 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import {
generateAuthenticationOptions,
generateRegistrationOptions,
RegistrationResponseJSON,
VerifiedAuthenticationResponse,
verifyAuthenticationResponse,
verifyRegistrationResponse,
} from "@simplewebauthn/server";
import { isoUint8Array } from "@simplewebauthn/server/helpers";
import { getUserByEmail } from "../user/user.service";
import { createSession } from "../auth/auth.service";
import { generateOTP } from "../utils/id";
import { sendMail } from "../utils/mail";
const rpID = process.env.SERVER_DOMAIN;
const rpName = "Quicker Permits";
const origin = `http://${rpID}:3000`;
export async function webAuthnRoutes(fastify: FastifyInstance) {
fastify.post<{ Body: { email: string } }>(
"/register/request",
{
schema: {
body: {
type: "object",
properties: {
email: { type: "string" },
},
},
},
},
async (req, res: FastifyReply) => {
const { email } = req.body;
if (!email) {
return res.code(400).send({ error: "Email is required" });
}
const userInDB = await getUserByEmail(email);
if (!userInDB) return res.code(400).send({ error: "not allowed" });
const otp = generateOTP();
const userId = userInDB.id;
const options = await generateRegistrationOptions({
rpName,
rpID,
userID: isoUint8Array.fromUTF8String(userId),
userName: email,
attestationType: "none",
excludeCredentials: userInDB.passKeys.map((cred) => ({
id: cred.credentialID,
type: "public-key",
transports: cred.transports,
})),
});
userInDB.challenge = {
value: options.challenge,
expiry: new Date(Date.now() + 60 * 5 * 1000),
};
userInDB.otp = {
value: otp,
expiry: new Date(Date.now() + 60 * 5 * 1000),
};
await userInDB.save();
const sent = await sendMail(
email,
"Code for Quicker Permits Authentication",
`Your code is ${otp}`
);
if (!sent) return res.code(500).send({ error: "server error" });
return res.send(options);
}
);
fastify.post<{
Body: {
email: string;
code: string;
attestationResponse: RegistrationResponseJSON;
};
}>("/register/verify", async (req, res: FastifyReply) => {
const { email, code, attestationResponse } = req.body;
const userInDB = await getUserByEmail(email);
if (!userInDB) return res.code(400).send({ error: "not allowed" });
const challenge = userInDB.challenge;
const requiredOTP = userInDB.otp;
if (!challenge || !requiredOTP)
return res.code(400).send({ error: "not allowed" });
if (new Date() > challenge.expiry || new Date() > requiredOTP.expiry)
return res.code(400).send({ error: "challenge expired" });
if (code != requiredOTP.value)
return res.code(400).send({ error: "invalid code" });
try {
const verification = await verifyRegistrationResponse({
response: attestationResponse,
expectedChallenge: challenge.value as string,
expectedRPID: rpID,
expectedOrigin: origin,
});
if (!verification.verified) {
return res.code(400).send({ error: "registration failed" });
}
userInDB.passKeys.push({
credentialID: verification.registrationInfo.credential.id,
credentialPublicKey: verification.registrationInfo.credential.publicKey,
counter: verification.registrationInfo.credential.counter,
transports: attestationResponse.response.transports,
});
await userInDB.save();
return res.code(200).send({ success: "registration complete" });
} catch (error) {
return res.code(400).send({ error: error.message });
}
});
fastify.post<{ Body: { email: string } }>(
"/login/request",
async (req, res) => {
const { email } = req.body;
const userInDB = await getUserByEmail(email);
if (!userInDB || userInDB.passKeys.length === 0) {
return res.code(400).send({ error: "user not registered" });
}
const options: PublicKeyCredentialRequestOptionsJSON =
await generateAuthenticationOptions({
rpID,
allowCredentials: userInDB.passKeys.map((cred) => ({
id: cred.credentialID,
type: "public-key",
transports: cred.transports,
})),
userVerification: "preferred",
});
userInDB.challenge.value = options.challenge;
userInDB.challenge.expiry = new Date(Date.now() + 60 * 5 * 1000);
await userInDB.save();
return res.send(options);
}
);
// Authentication Verification (Step 4)
fastify.post<{ Body: { email: string; assertionResponse: any } }>(
"/login/verify",
async (req, res) => {
const { email, assertionResponse } = req.body;
const userInDB = await getUserByEmail(email);
if (!userInDB) return res.code(400).send({ error: "not allowed" });
const challenge = userInDB.challenge;
if (!challenge) return res.code(400).send({ error: "not allowed" });
if (new Date() > challenge.expiry)
return res.code(400).send({ error: "challenge expired" });
try {
const credential = userInDB.passKeys.find(
(cred) => cred.credentialID === assertionResponse.id
);
if (!credential)
return res.code(400).send({ error: "credential not found" });
const verification: VerifiedAuthenticationResponse =
await verifyAuthenticationResponse({
response: assertionResponse,
expectedChallenge: userInDB.challenge.value as string,
expectedRPID: rpID,
expectedOrigin: origin,
credential: credential,
});
if (!verification.verified)
return res.code(400).send({ error: "Authentication failed" });
credential.counter = verification.authenticationInfo.newCounter;
const newSession = await createSession(
userInDB.id,
req.ip,
req.headers["user-agent"]
);
res.send({ session_token: newSession.sid });
} catch (error) {
res.code(400).send({ error: (error as Error).message });
}
}
);
}