dauth-md-node 5.0.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/router.js +179 -0
- package/dist/router.js.map +1 -1
- package/dist/router.mjs +179 -0
- package/dist/router.mjs.map +1 -1
- package/package.json +1 -1
- package/src/api/dauth.api.ts +3 -6
- package/src/index.ts +1 -5
- package/src/router.ts +196 -0
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/api/utils/config.ts","../src/api/dauth.api.ts","../src/cache.ts","../src/session.ts"],"sourcesContent":["import { Request, NextFunction, Response as ExpressResponse } from 'express';\nimport jwt from 'jsonwebtoken';\nimport { getUser } from './api/dauth.api';\nexport {\n searchUserByEmail,\n getUserById,\n batchGetUsers,\n} from './api/dauth.api';\nexport type { TenantUser } from './api/dauth.api';\nimport { UserCache } from './cache';\nimport type { CacheOptions } from './cache';\nimport { deriveEncryptionKey, decryptSessionWithKeys } from './session';\n\nexport type AuthMethodType = 'magic-link' | 'passkey';\n\nexport interface IDauthUser {\n _id: string;\n name: string;\n lastname: string;\n nickname: string;\n email: string;\n isVerified: boolean;\n language: string;\n avatar: {\n id: string;\n url: string;\n };\n role: string;\n telPrefix: string;\n telSuffix: string;\n birthDate?: string;\n country?: string;\n metadata?: Record<string, unknown>;\n authMethods?: AuthMethodType[];\n createdAt: Date;\n updatedAt: Date;\n lastLogin: Date;\n}\n\nexport interface IRequestDauth extends Request {\n user: IDauthUser;\n dauthToken: string;\n files: {\n image: { path: string };\n avatar: { path: string };\n };\n headers: {\n authorization: string;\n };\n}\n\nexport interface SessionOptions {\n cookieName?: string;\n secure?: boolean;\n previousTsk?: string;\n sessionSalt?: string;\n}\n\nexport interface DauthOptions {\n domainName: string;\n tsk: string;\n cache?: CacheOptions;\n session?: SessionOptions;\n}\n\ninterface TCustomResponse extends ExpressResponse {\n status(code: number): this;\n send(body?: unknown): this;\n}\n\nexport { UserCache };\nexport type { CacheOptions };\n\nexport const dauth = ({ domainName, tsk, cache, session }: DauthOptions) => {\n const userCache = cache ? new UserCache(cache) : null;\n\n // Lazy-init encryption keys for session cookie mode\n let keysPromise: Promise<Buffer[]> | null = null;\n async function getEncKeys(): Promise<Buffer[]> {\n if (!keysPromise) {\n keysPromise = (async () => {\n const keys: Buffer[] = [];\n keys.push(await deriveEncryptionKey(tsk, session?.sessionSalt));\n if (session?.previousTsk) {\n keys.push(\n await deriveEncryptionKey(session.previousTsk, session.sessionSalt)\n );\n }\n return keys;\n })();\n }\n return keysPromise;\n }\n\n function getSessionCookieName(): string {\n if (session?.cookieName) return session.cookieName;\n const secure = session?.secure ?? process.env.NODE_ENV !== 'development';\n return secure ? '__Host-dauth-session' : 'dauth-session';\n }\n\n return async (\n req: IRequestDauth,\n res: TCustomResponse,\n next: NextFunction\n ) => {\n let token: string;\n\n if (session) {\n // Session cookie mode: read encrypted cookie\n const cookieName = getSessionCookieName();\n const cookie = req.cookies?.[cookieName];\n if (!cookie) {\n return res.status(401).send({\n status: 'no-session',\n message: 'Not authenticated',\n });\n }\n const keys = await getEncKeys();\n const payload = decryptSessionWithKeys(cookie, keys);\n if (!payload) {\n return res.status(401).send({\n status: 'session-invalid',\n message: 'Invalid session',\n });\n }\n token = payload.accessToken;\n } else {\n // Authorization header mode\n if (!req.headers.authorization) {\n return res.status(403).send({\n status: 'token-not-found',\n message: 'Token not found',\n });\n }\n token = req.headers.authorization.replace(/['\"]+/g, '');\n }\n\n try {\n jwt.verify(token, tsk);\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Token invalid';\n\n if (message === 'jwt expired') {\n return res\n .status(401)\n .send({ status: 'token-expired', message: 'jwt expired' });\n }\n if (message === 'invalid signature') {\n return res.status(401).send({\n status: 'tsk-not-invalid',\n message: 'The TSK variable in the backend middleware is not valid',\n });\n }\n return res.status(401).send({ status: 'token-invalid', message });\n }\n\n // Expose the verified access token for downstream API calls\n req.dauthToken = token;\n\n if (userCache) {\n const cachedUser = userCache.get(token);\n if (cachedUser) {\n req.user = cachedUser;\n return next();\n }\n }\n\n try {\n const getUserFetch = await getUser(token, domainName);\n\n if (getUserFetch.response.status === 404) {\n return res.status(404).send({\n status: 'user-not-found',\n message: getUserFetch.data.message ?? 'User does not exist',\n });\n }\n if (getUserFetch.response.status === 500) {\n return res.status(500).send({\n status: 'error',\n message: getUserFetch.data.message ?? 'Dauth server error',\n });\n }\n if (getUserFetch.response.status === 200) {\n req.user = getUserFetch.data.user;\n if (userCache) {\n userCache.set(token, req.user);\n }\n return next();\n }\n return res.status(501).send({\n status: 'request-error',\n message: getUserFetch.data.message ?? 'Dauth server error',\n });\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Dauth server error';\n return res.status(500).send({ status: 'server-error', message });\n }\n };\n};\n","export const apiVersion = 'v1';\nexport const serverDomain = 'dauth.ovh';\n\nexport function getServerBasePath(): string {\n if (process.env.DAUTH_URL) {\n const base = process.env.DAUTH_URL.replace(/\\/+$/, '');\n return `${base}/api/${apiVersion}`;\n }\n\n const isLocalhost = process.env.NODE_ENV === 'development';\n const serverPort = 4012;\n const serverLocalUrl = `http://localhost:${serverPort}/api/${apiVersion}`;\n const serverProdUrl = `https://${serverDomain}/api/${apiVersion}`;\n return isLocalhost ? serverLocalUrl : serverProdUrl;\n}\n","import { getServerBasePath } from './utils/config';\n\ninterface GetUserResponse {\n response: { status: number };\n data: { user?: any; message?: string };\n}\n\nexport interface TenantUser {\n _id: string;\n name: string;\n lastname: string;\n email: string;\n avatar: { id: string; url: string };\n}\n\ninterface TenantUserResponse {\n response: { status: number };\n data: { status?: string; data?: TenantUser | null; message?: string };\n}\n\ninterface BatchUsersResponse {\n response: { status: number };\n data: { status?: string; data?: TenantUser[]; message?: string };\n}\n\nexport async function getUser(\n token: string,\n domainName: string\n): Promise<GetUserResponse> {\n const response = await fetch(\n `${getServerBasePath()}/app/${domainName}/user`,\n {\n method: 'GET',\n headers: {\n Authorization: token,\n 'Content-Type': 'application/json',\n },\n }\n );\n const data = (await response.json()) as GetUserResponse['data'];\n return { response: { status: response.status }, data };\n}\n\nexport async function searchUserByEmail(\n token: string,\n domainName: string,\n email: string\n): Promise<TenantUserResponse> {\n const params = new URLSearchParams({ email });\n const response = await fetch(\n `${getServerBasePath()}/app/${domainName}/users/search?${params}`,\n {\n method: 'GET',\n headers: {\n Authorization: token,\n 'Content-Type': 'application/json',\n },\n }\n );\n const data =\n (await response.json()) as TenantUserResponse['data'];\n return { response: { status: response.status }, data };\n}\n\nexport async function getUserById(\n token: string,\n domainName: string,\n userId: string\n): Promise<TenantUserResponse> {\n const response = await fetch(\n `${getServerBasePath()}/app/${domainName}/users/${userId}`,\n {\n method: 'GET',\n headers: {\n Authorization: token,\n 'Content-Type': 'application/json',\n },\n }\n );\n const data =\n (await response.json()) as TenantUserResponse['data'];\n return { response: { status: response.status }, data };\n}\n\nexport async function batchGetUsers(\n token: string,\n domainName: string,\n userIds: string[]\n): Promise<BatchUsersResponse> {\n const response = await fetch(\n `${getServerBasePath()}/app/${domainName}/users/batch`,\n {\n method: 'POST',\n headers: {\n Authorization: token,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ userIds }),\n }\n );\n const data =\n (await response.json()) as BatchUsersResponse['data'];\n return { response: { status: response.status }, data };\n}\n","import { IDauthUser } from './index';\n\ninterface CacheEntry {\n user: IDauthUser;\n expiresAt: number;\n}\n\nexport interface CacheOptions {\n ttlMs: number;\n}\n\nexport class UserCache {\n private store = new Map<string, CacheEntry>();\n private ttlMs: number;\n\n constructor(options: CacheOptions) {\n this.ttlMs = options.ttlMs;\n }\n\n get(token: string): IDauthUser | undefined {\n const entry = this.store.get(token);\n if (!entry) return undefined;\n\n if (Date.now() > entry.expiresAt) {\n this.store.delete(token);\n return undefined;\n }\n\n return entry.user;\n }\n\n set(token: string, user: IDauthUser): void {\n if (this.store.size > 1000) {\n this.sweep();\n }\n this.store.set(token, { user, expiresAt: Date.now() + this.ttlMs });\n }\n\n clear(): void {\n this.store.clear();\n }\n\n private sweep(): void {\n const now = Date.now();\n for (const [key, entry] of this.store) {\n if (now > entry.expiresAt) {\n this.store.delete(key);\n }\n }\n }\n}\n","import crypto from 'crypto';\n\nexport interface SessionPayload {\n accessToken: string;\n refreshToken: string;\n}\n\nconst INFO = 'dauth-cookie-enc-v1';\nconst DEFAULT_SALT = Buffer.from(\n 'a3f8c1d7e9b24f6081c5d3a7e2f49b0653d81f7a2e94c0b6d8f3a5e1c7b09d42',\n 'hex'\n);\n\nexport async function deriveEncryptionKey(\n tsk: string,\n salt?: string\n): Promise<Buffer> {\n const saltBuf = salt ? Buffer.from(salt, 'hex') : DEFAULT_SALT;\n return new Promise((resolve, reject) => {\n crypto.hkdf(\n 'sha256',\n Buffer.from(tsk),\n saltBuf,\n INFO,\n 32,\n (err, derivedKey) => {\n if (err) return reject(err);\n resolve(Buffer.from(derivedKey));\n }\n );\n });\n}\n\nexport function encryptSession(payload: SessionPayload, key: Buffer): string {\n const nonce = crypto.randomBytes(12);\n const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);\n const plaintext = JSON.stringify(payload);\n const encrypted = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final(),\n ]);\n const authTag = cipher.getAuthTag();\n // Format: base64(nonce + ciphertext + authTag)\n return Buffer.concat([nonce, encrypted, authTag]).toString('base64');\n}\n\nexport function decryptSession(\n ciphertext: string,\n key: Buffer\n): SessionPayload | null {\n try {\n const buf = Buffer.from(ciphertext, 'base64');\n if (buf.length < 12 + 16) return null; // nonce(12) + authTag(16) minimum\n const nonce = buf.subarray(0, 12);\n const authTag = buf.subarray(buf.length - 16);\n const encrypted = buf.subarray(12, buf.length - 16);\n const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);\n decipher.setAuthTag(authTag);\n const decrypted = Buffer.concat([\n decipher.update(encrypted),\n decipher.final(),\n ]);\n return JSON.parse(decrypted.toString('utf8')) as SessionPayload;\n } catch {\n return null;\n }\n}\n\nexport function decryptSessionWithKeys(\n ciphertext: string,\n keys: Buffer[]\n): SessionPayload | null {\n for (const key of keys) {\n const result = decryptSession(ciphertext, key);\n if (result) return result;\n }\n return null;\n}\n"],"mappings":"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","names":["crypto","crypto","jwt"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/api/utils/config.ts","../src/api/dauth.api.ts","../src/cache.ts","../src/session.ts"],"sourcesContent":["import { Request, NextFunction, Response as ExpressResponse } from 'express';\nimport jwt from 'jsonwebtoken';\nimport { getUser } from './api/dauth.api';\nexport { searchUserByEmail, getUserById, batchGetUsers } from './api/dauth.api';\nexport type { TenantUser } from './api/dauth.api';\nimport { UserCache } from './cache';\nimport type { CacheOptions } from './cache';\nimport { deriveEncryptionKey, decryptSessionWithKeys } from './session';\n\nexport type AuthMethodType = 'magic-link' | 'passkey';\n\nexport interface IDauthUser {\n _id: string;\n name: string;\n lastname: string;\n nickname: string;\n email: string;\n isVerified: boolean;\n language: string;\n avatar: {\n id: string;\n url: string;\n };\n role: string;\n telPrefix: string;\n telSuffix: string;\n birthDate?: string;\n country?: string;\n metadata?: Record<string, unknown>;\n authMethods?: AuthMethodType[];\n createdAt: Date;\n updatedAt: Date;\n lastLogin: Date;\n}\n\nexport interface IRequestDauth extends Request {\n user: IDauthUser;\n dauthToken: string;\n files: {\n image: { path: string };\n avatar: { path: string };\n };\n headers: {\n authorization: string;\n };\n}\n\nexport interface SessionOptions {\n cookieName?: string;\n secure?: boolean;\n previousTsk?: string;\n sessionSalt?: string;\n}\n\nexport interface DauthOptions {\n domainName: string;\n tsk: string;\n cache?: CacheOptions;\n session?: SessionOptions;\n}\n\ninterface TCustomResponse extends ExpressResponse {\n status(code: number): this;\n send(body?: unknown): this;\n}\n\nexport { UserCache };\nexport type { CacheOptions };\n\nexport const dauth = ({ domainName, tsk, cache, session }: DauthOptions) => {\n const userCache = cache ? new UserCache(cache) : null;\n\n // Lazy-init encryption keys for session cookie mode\n let keysPromise: Promise<Buffer[]> | null = null;\n async function getEncKeys(): Promise<Buffer[]> {\n if (!keysPromise) {\n keysPromise = (async () => {\n const keys: Buffer[] = [];\n keys.push(await deriveEncryptionKey(tsk, session?.sessionSalt));\n if (session?.previousTsk) {\n keys.push(\n await deriveEncryptionKey(session.previousTsk, session.sessionSalt)\n );\n }\n return keys;\n })();\n }\n return keysPromise;\n }\n\n function getSessionCookieName(): string {\n if (session?.cookieName) return session.cookieName;\n const secure = session?.secure ?? process.env.NODE_ENV !== 'development';\n return secure ? '__Host-dauth-session' : 'dauth-session';\n }\n\n return async (\n req: IRequestDauth,\n res: TCustomResponse,\n next: NextFunction\n ) => {\n let token: string;\n\n if (session) {\n // Session cookie mode: read encrypted cookie\n const cookieName = getSessionCookieName();\n const cookie = req.cookies?.[cookieName];\n if (!cookie) {\n return res.status(401).send({\n status: 'no-session',\n message: 'Not authenticated',\n });\n }\n const keys = await getEncKeys();\n const payload = decryptSessionWithKeys(cookie, keys);\n if (!payload) {\n return res.status(401).send({\n status: 'session-invalid',\n message: 'Invalid session',\n });\n }\n token = payload.accessToken;\n } else {\n // Authorization header mode\n if (!req.headers.authorization) {\n return res.status(403).send({\n status: 'token-not-found',\n message: 'Token not found',\n });\n }\n token = req.headers.authorization.replace(/['\"]+/g, '');\n }\n\n try {\n jwt.verify(token, tsk);\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Token invalid';\n\n if (message === 'jwt expired') {\n return res\n .status(401)\n .send({ status: 'token-expired', message: 'jwt expired' });\n }\n if (message === 'invalid signature') {\n return res.status(401).send({\n status: 'tsk-not-invalid',\n message: 'The TSK variable in the backend middleware is not valid',\n });\n }\n return res.status(401).send({ status: 'token-invalid', message });\n }\n\n // Expose the verified access token for downstream API calls\n req.dauthToken = token;\n\n if (userCache) {\n const cachedUser = userCache.get(token);\n if (cachedUser) {\n req.user = cachedUser;\n return next();\n }\n }\n\n try {\n const getUserFetch = await getUser(token, domainName);\n\n if (getUserFetch.response.status === 404) {\n return res.status(404).send({\n status: 'user-not-found',\n message: getUserFetch.data.message ?? 'User does not exist',\n });\n }\n if (getUserFetch.response.status === 500) {\n return res.status(500).send({\n status: 'error',\n message: getUserFetch.data.message ?? 'Dauth server error',\n });\n }\n if (getUserFetch.response.status === 200) {\n req.user = getUserFetch.data.user;\n if (userCache) {\n userCache.set(token, req.user);\n }\n return next();\n }\n return res.status(501).send({\n status: 'request-error',\n message: getUserFetch.data.message ?? 'Dauth server error',\n });\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Dauth server error';\n return res.status(500).send({ status: 'server-error', message });\n }\n };\n};\n","export const apiVersion = 'v1';\nexport const serverDomain = 'dauth.ovh';\n\nexport function getServerBasePath(): string {\n if (process.env.DAUTH_URL) {\n const base = process.env.DAUTH_URL.replace(/\\/+$/, '');\n return `${base}/api/${apiVersion}`;\n }\n\n const isLocalhost = process.env.NODE_ENV === 'development';\n const serverPort = 4012;\n const serverLocalUrl = `http://localhost:${serverPort}/api/${apiVersion}`;\n const serverProdUrl = `https://${serverDomain}/api/${apiVersion}`;\n return isLocalhost ? serverLocalUrl : serverProdUrl;\n}\n","import { getServerBasePath } from './utils/config';\n\ninterface GetUserResponse {\n response: { status: number };\n data: { user?: any; message?: string };\n}\n\nexport interface TenantUser {\n _id: string;\n name: string;\n lastname: string;\n email: string;\n avatar: { id: string; url: string };\n}\n\ninterface TenantUserResponse {\n response: { status: number };\n data: { status?: string; data?: TenantUser | null; message?: string };\n}\n\ninterface BatchUsersResponse {\n response: { status: number };\n data: { status?: string; data?: TenantUser[]; message?: string };\n}\n\nexport async function getUser(\n token: string,\n domainName: string\n): Promise<GetUserResponse> {\n const response = await fetch(\n `${getServerBasePath()}/app/${domainName}/user`,\n {\n method: 'GET',\n headers: {\n Authorization: token,\n 'Content-Type': 'application/json',\n },\n }\n );\n const data = (await response.json()) as GetUserResponse['data'];\n return { response: { status: response.status }, data };\n}\n\nexport async function searchUserByEmail(\n token: string,\n domainName: string,\n email: string\n): Promise<TenantUserResponse> {\n const params = new URLSearchParams({ email });\n const response = await fetch(\n `${getServerBasePath()}/app/${domainName}/users/search?${params}`,\n {\n method: 'GET',\n headers: {\n Authorization: token,\n 'Content-Type': 'application/json',\n },\n }\n );\n const data = (await response.json()) as TenantUserResponse['data'];\n return { response: { status: response.status }, data };\n}\n\nexport async function getUserById(\n token: string,\n domainName: string,\n userId: string\n): Promise<TenantUserResponse> {\n const response = await fetch(\n `${getServerBasePath()}/app/${domainName}/users/${userId}`,\n {\n method: 'GET',\n headers: {\n Authorization: token,\n 'Content-Type': 'application/json',\n },\n }\n );\n const data = (await response.json()) as TenantUserResponse['data'];\n return { response: { status: response.status }, data };\n}\n\nexport async function batchGetUsers(\n token: string,\n domainName: string,\n userIds: string[]\n): Promise<BatchUsersResponse> {\n const response = await fetch(\n `${getServerBasePath()}/app/${domainName}/users/batch`,\n {\n method: 'POST',\n headers: {\n Authorization: token,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ userIds }),\n }\n );\n const data = (await response.json()) as BatchUsersResponse['data'];\n return { response: { status: response.status }, data };\n}\n","import { IDauthUser } from './index';\n\ninterface CacheEntry {\n user: IDauthUser;\n expiresAt: number;\n}\n\nexport interface CacheOptions {\n ttlMs: number;\n}\n\nexport class UserCache {\n private store = new Map<string, CacheEntry>();\n private ttlMs: number;\n\n constructor(options: CacheOptions) {\n this.ttlMs = options.ttlMs;\n }\n\n get(token: string): IDauthUser | undefined {\n const entry = this.store.get(token);\n if (!entry) return undefined;\n\n if (Date.now() > entry.expiresAt) {\n this.store.delete(token);\n return undefined;\n }\n\n return entry.user;\n }\n\n set(token: string, user: IDauthUser): void {\n if (this.store.size > 1000) {\n this.sweep();\n }\n this.store.set(token, { user, expiresAt: Date.now() + this.ttlMs });\n }\n\n clear(): void {\n this.store.clear();\n }\n\n private sweep(): void {\n const now = Date.now();\n for (const [key, entry] of this.store) {\n if (now > entry.expiresAt) {\n this.store.delete(key);\n }\n }\n }\n}\n","import crypto from 'crypto';\n\nexport interface SessionPayload {\n accessToken: string;\n refreshToken: string;\n}\n\nconst INFO = 'dauth-cookie-enc-v1';\nconst DEFAULT_SALT = Buffer.from(\n 'a3f8c1d7e9b24f6081c5d3a7e2f49b0653d81f7a2e94c0b6d8f3a5e1c7b09d42',\n 'hex'\n);\n\nexport async function deriveEncryptionKey(\n tsk: string,\n salt?: string\n): Promise<Buffer> {\n const saltBuf = salt ? Buffer.from(salt, 'hex') : DEFAULT_SALT;\n return new Promise((resolve, reject) => {\n crypto.hkdf(\n 'sha256',\n Buffer.from(tsk),\n saltBuf,\n INFO,\n 32,\n (err, derivedKey) => {\n if (err) return reject(err);\n resolve(Buffer.from(derivedKey));\n }\n );\n });\n}\n\nexport function encryptSession(payload: SessionPayload, key: Buffer): string {\n const nonce = crypto.randomBytes(12);\n const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);\n const plaintext = JSON.stringify(payload);\n const encrypted = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final(),\n ]);\n const authTag = cipher.getAuthTag();\n // Format: base64(nonce + ciphertext + authTag)\n return Buffer.concat([nonce, encrypted, authTag]).toString('base64');\n}\n\nexport function decryptSession(\n ciphertext: string,\n key: Buffer\n): SessionPayload | null {\n try {\n const buf = Buffer.from(ciphertext, 'base64');\n if (buf.length < 12 + 16) return null; // nonce(12) + authTag(16) minimum\n const nonce = buf.subarray(0, 12);\n const authTag = buf.subarray(buf.length - 16);\n const encrypted = buf.subarray(12, buf.length - 16);\n const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);\n decipher.setAuthTag(authTag);\n const decrypted = Buffer.concat([\n decipher.update(encrypted),\n decipher.final(),\n ]);\n return JSON.parse(decrypted.toString('utf8')) as SessionPayload;\n } catch {\n return null;\n }\n}\n\nexport function decryptSessionWithKeys(\n ciphertext: string,\n keys: Buffer[]\n): SessionPayload | null {\n for (const key of keys) {\n const result = decryptSession(ciphertext, key);\n if (result) return result;\n }\n return null;\n}\n"],"mappings":"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","names":["crypto","crypto","jwt"]}
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/api/dauth.api.ts","../src/cache.ts"],"sourcesContent":["import { Request, NextFunction, Response as ExpressResponse } from 'express';\nimport jwt from 'jsonwebtoken';\nimport { getUser } from './api/dauth.api';\nexport {\n searchUserByEmail,\n getUserById,\n batchGetUsers,\n} from './api/dauth.api';\nexport type { TenantUser } from './api/dauth.api';\nimport { UserCache } from './cache';\nimport type { CacheOptions } from './cache';\nimport { deriveEncryptionKey, decryptSessionWithKeys } from './session';\n\nexport type AuthMethodType = 'magic-link' | 'passkey';\n\nexport interface IDauthUser {\n _id: string;\n name: string;\n lastname: string;\n nickname: string;\n email: string;\n isVerified: boolean;\n language: string;\n avatar: {\n id: string;\n url: string;\n };\n role: string;\n telPrefix: string;\n telSuffix: string;\n birthDate?: string;\n country?: string;\n metadata?: Record<string, unknown>;\n authMethods?: AuthMethodType[];\n createdAt: Date;\n updatedAt: Date;\n lastLogin: Date;\n}\n\nexport interface IRequestDauth extends Request {\n user: IDauthUser;\n dauthToken: string;\n files: {\n image: { path: string };\n avatar: { path: string };\n };\n headers: {\n authorization: string;\n };\n}\n\nexport interface SessionOptions {\n cookieName?: string;\n secure?: boolean;\n previousTsk?: string;\n sessionSalt?: string;\n}\n\nexport interface DauthOptions {\n domainName: string;\n tsk: string;\n cache?: CacheOptions;\n session?: SessionOptions;\n}\n\ninterface TCustomResponse extends ExpressResponse {\n status(code: number): this;\n send(body?: unknown): this;\n}\n\nexport { UserCache };\nexport type { CacheOptions };\n\nexport const dauth = ({ domainName, tsk, cache, session }: DauthOptions) => {\n const userCache = cache ? new UserCache(cache) : null;\n\n // Lazy-init encryption keys for session cookie mode\n let keysPromise: Promise<Buffer[]> | null = null;\n async function getEncKeys(): Promise<Buffer[]> {\n if (!keysPromise) {\n keysPromise = (async () => {\n const keys: Buffer[] = [];\n keys.push(await deriveEncryptionKey(tsk, session?.sessionSalt));\n if (session?.previousTsk) {\n keys.push(\n await deriveEncryptionKey(session.previousTsk, session.sessionSalt)\n );\n }\n return keys;\n })();\n }\n return keysPromise;\n }\n\n function getSessionCookieName(): string {\n if (session?.cookieName) return session.cookieName;\n const secure = session?.secure ?? process.env.NODE_ENV !== 'development';\n return secure ? '__Host-dauth-session' : 'dauth-session';\n }\n\n return async (\n req: IRequestDauth,\n res: TCustomResponse,\n next: NextFunction\n ) => {\n let token: string;\n\n if (session) {\n // Session cookie mode: read encrypted cookie\n const cookieName = getSessionCookieName();\n const cookie = req.cookies?.[cookieName];\n if (!cookie) {\n return res.status(401).send({\n status: 'no-session',\n message: 'Not authenticated',\n });\n }\n const keys = await getEncKeys();\n const payload = decryptSessionWithKeys(cookie, keys);\n if (!payload) {\n return res.status(401).send({\n status: 'session-invalid',\n message: 'Invalid session',\n });\n }\n token = payload.accessToken;\n } else {\n // Authorization header mode\n if (!req.headers.authorization) {\n return res.status(403).send({\n status: 'token-not-found',\n message: 'Token not found',\n });\n }\n token = req.headers.authorization.replace(/['\"]+/g, '');\n }\n\n try {\n jwt.verify(token, tsk);\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Token invalid';\n\n if (message === 'jwt expired') {\n return res\n .status(401)\n .send({ status: 'token-expired', message: 'jwt expired' });\n }\n if (message === 'invalid signature') {\n return res.status(401).send({\n status: 'tsk-not-invalid',\n message: 'The TSK variable in the backend middleware is not valid',\n });\n }\n return res.status(401).send({ status: 'token-invalid', message });\n }\n\n // Expose the verified access token for downstream API calls\n req.dauthToken = token;\n\n if (userCache) {\n const cachedUser = userCache.get(token);\n if (cachedUser) {\n req.user = cachedUser;\n return next();\n }\n }\n\n try {\n const getUserFetch = await getUser(token, domainName);\n\n if (getUserFetch.response.status === 404) {\n return res.status(404).send({\n status: 'user-not-found',\n message: getUserFetch.data.message ?? 'User does not exist',\n });\n }\n if (getUserFetch.response.status === 500) {\n return res.status(500).send({\n status: 'error',\n message: getUserFetch.data.message ?? 'Dauth server error',\n });\n }\n if (getUserFetch.response.status === 200) {\n req.user = getUserFetch.data.user;\n if (userCache) {\n userCache.set(token, req.user);\n }\n return next();\n }\n return res.status(501).send({\n status: 'request-error',\n message: getUserFetch.data.message ?? 'Dauth server error',\n });\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Dauth server error';\n return res.status(500).send({ status: 'server-error', message });\n }\n };\n};\n","import { getServerBasePath } from './utils/config';\n\ninterface GetUserResponse {\n response: { status: number };\n data: { user?: any; message?: string };\n}\n\nexport interface TenantUser {\n _id: string;\n name: string;\n lastname: string;\n email: string;\n avatar: { id: string; url: string };\n}\n\ninterface TenantUserResponse {\n response: { status: number };\n data: { status?: string; data?: TenantUser | null; message?: string };\n}\n\ninterface BatchUsersResponse {\n response: { status: number };\n data: { status?: string; data?: TenantUser[]; message?: string };\n}\n\nexport async function getUser(\n token: string,\n domainName: string\n): Promise<GetUserResponse> {\n const response = await fetch(\n `${getServerBasePath()}/app/${domainName}/user`,\n {\n method: 'GET',\n headers: {\n Authorization: token,\n 'Content-Type': 'application/json',\n },\n }\n );\n const data = (await response.json()) as GetUserResponse['data'];\n return { response: { status: response.status }, data };\n}\n\nexport async function searchUserByEmail(\n token: string,\n domainName: string,\n email: string\n): Promise<TenantUserResponse> {\n const params = new URLSearchParams({ email });\n const response = await fetch(\n `${getServerBasePath()}/app/${domainName}/users/search?${params}`,\n {\n method: 'GET',\n headers: {\n Authorization: token,\n 'Content-Type': 'application/json',\n },\n }\n );\n const data =\n (await response.json()) as TenantUserResponse['data'];\n return { response: { status: response.status }, data };\n}\n\nexport async function getUserById(\n token: string,\n domainName: string,\n userId: string\n): Promise<TenantUserResponse> {\n const response = await fetch(\n `${getServerBasePath()}/app/${domainName}/users/${userId}`,\n {\n method: 'GET',\n headers: {\n Authorization: token,\n 'Content-Type': 'application/json',\n },\n }\n );\n const data =\n (await response.json()) as TenantUserResponse['data'];\n return { response: { status: response.status }, data };\n}\n\nexport async function batchGetUsers(\n token: string,\n domainName: string,\n userIds: string[]\n): Promise<BatchUsersResponse> {\n const response = await fetch(\n `${getServerBasePath()}/app/${domainName}/users/batch`,\n {\n method: 'POST',\n headers: {\n Authorization: token,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ userIds }),\n }\n );\n const data =\n (await response.json()) as BatchUsersResponse['data'];\n return { response: { status: response.status }, data };\n}\n","import { IDauthUser } from './index';\n\ninterface CacheEntry {\n user: IDauthUser;\n expiresAt: number;\n}\n\nexport interface CacheOptions {\n ttlMs: number;\n}\n\nexport class UserCache {\n private store = new Map<string, CacheEntry>();\n private ttlMs: number;\n\n constructor(options: CacheOptions) {\n this.ttlMs = options.ttlMs;\n }\n\n get(token: string): IDauthUser | undefined {\n const entry = this.store.get(token);\n if (!entry) return undefined;\n\n if (Date.now() > entry.expiresAt) {\n this.store.delete(token);\n return undefined;\n }\n\n return entry.user;\n }\n\n set(token: string, user: IDauthUser): void {\n if (this.store.size > 1000) {\n this.sweep();\n }\n this.store.set(token, { user, expiresAt: Date.now() + this.ttlMs });\n }\n\n clear(): void {\n this.store.clear();\n }\n\n private sweep(): void {\n const now = Date.now();\n for (const [key, entry] of this.store) {\n if (now > entry.expiresAt) {\n this.store.delete(key);\n }\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/api/dauth.api.ts","../src/cache.ts"],"sourcesContent":["import { Request, NextFunction, Response as ExpressResponse } from 'express';\nimport jwt from 'jsonwebtoken';\nimport { getUser } from './api/dauth.api';\nexport { searchUserByEmail, getUserById, batchGetUsers } from './api/dauth.api';\nexport type { TenantUser } from './api/dauth.api';\nimport { UserCache } from './cache';\nimport type { CacheOptions } from './cache';\nimport { deriveEncryptionKey, decryptSessionWithKeys } from './session';\n\nexport type AuthMethodType = 'magic-link' | 'passkey';\n\nexport interface IDauthUser {\n _id: string;\n name: string;\n lastname: string;\n nickname: string;\n email: string;\n isVerified: boolean;\n language: string;\n avatar: {\n id: string;\n url: string;\n };\n role: string;\n telPrefix: string;\n telSuffix: string;\n birthDate?: string;\n country?: string;\n metadata?: Record<string, unknown>;\n authMethods?: AuthMethodType[];\n createdAt: Date;\n updatedAt: Date;\n lastLogin: Date;\n}\n\nexport interface IRequestDauth extends Request {\n user: IDauthUser;\n dauthToken: string;\n files: {\n image: { path: string };\n avatar: { path: string };\n };\n headers: {\n authorization: string;\n };\n}\n\nexport interface SessionOptions {\n cookieName?: string;\n secure?: boolean;\n previousTsk?: string;\n sessionSalt?: string;\n}\n\nexport interface DauthOptions {\n domainName: string;\n tsk: string;\n cache?: CacheOptions;\n session?: SessionOptions;\n}\n\ninterface TCustomResponse extends ExpressResponse {\n status(code: number): this;\n send(body?: unknown): this;\n}\n\nexport { UserCache };\nexport type { CacheOptions };\n\nexport const dauth = ({ domainName, tsk, cache, session }: DauthOptions) => {\n const userCache = cache ? new UserCache(cache) : null;\n\n // Lazy-init encryption keys for session cookie mode\n let keysPromise: Promise<Buffer[]> | null = null;\n async function getEncKeys(): Promise<Buffer[]> {\n if (!keysPromise) {\n keysPromise = (async () => {\n const keys: Buffer[] = [];\n keys.push(await deriveEncryptionKey(tsk, session?.sessionSalt));\n if (session?.previousTsk) {\n keys.push(\n await deriveEncryptionKey(session.previousTsk, session.sessionSalt)\n );\n }\n return keys;\n })();\n }\n return keysPromise;\n }\n\n function getSessionCookieName(): string {\n if (session?.cookieName) return session.cookieName;\n const secure = session?.secure ?? process.env.NODE_ENV !== 'development';\n return secure ? '__Host-dauth-session' : 'dauth-session';\n }\n\n return async (\n req: IRequestDauth,\n res: TCustomResponse,\n next: NextFunction\n ) => {\n let token: string;\n\n if (session) {\n // Session cookie mode: read encrypted cookie\n const cookieName = getSessionCookieName();\n const cookie = req.cookies?.[cookieName];\n if (!cookie) {\n return res.status(401).send({\n status: 'no-session',\n message: 'Not authenticated',\n });\n }\n const keys = await getEncKeys();\n const payload = decryptSessionWithKeys(cookie, keys);\n if (!payload) {\n return res.status(401).send({\n status: 'session-invalid',\n message: 'Invalid session',\n });\n }\n token = payload.accessToken;\n } else {\n // Authorization header mode\n if (!req.headers.authorization) {\n return res.status(403).send({\n status: 'token-not-found',\n message: 'Token not found',\n });\n }\n token = req.headers.authorization.replace(/['\"]+/g, '');\n }\n\n try {\n jwt.verify(token, tsk);\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Token invalid';\n\n if (message === 'jwt expired') {\n return res\n .status(401)\n .send({ status: 'token-expired', message: 'jwt expired' });\n }\n if (message === 'invalid signature') {\n return res.status(401).send({\n status: 'tsk-not-invalid',\n message: 'The TSK variable in the backend middleware is not valid',\n });\n }\n return res.status(401).send({ status: 'token-invalid', message });\n }\n\n // Expose the verified access token for downstream API calls\n req.dauthToken = token;\n\n if (userCache) {\n const cachedUser = userCache.get(token);\n if (cachedUser) {\n req.user = cachedUser;\n return next();\n }\n }\n\n try {\n const getUserFetch = await getUser(token, domainName);\n\n if (getUserFetch.response.status === 404) {\n return res.status(404).send({\n status: 'user-not-found',\n message: getUserFetch.data.message ?? 'User does not exist',\n });\n }\n if (getUserFetch.response.status === 500) {\n return res.status(500).send({\n status: 'error',\n message: getUserFetch.data.message ?? 'Dauth server error',\n });\n }\n if (getUserFetch.response.status === 200) {\n req.user = getUserFetch.data.user;\n if (userCache) {\n userCache.set(token, req.user);\n }\n return next();\n }\n return res.status(501).send({\n status: 'request-error',\n message: getUserFetch.data.message ?? 'Dauth server error',\n });\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Dauth server error';\n return res.status(500).send({ status: 'server-error', message });\n }\n };\n};\n","import { getServerBasePath } from './utils/config';\n\ninterface GetUserResponse {\n response: { status: number };\n data: { user?: any; message?: string };\n}\n\nexport interface TenantUser {\n _id: string;\n name: string;\n lastname: string;\n email: string;\n avatar: { id: string; url: string };\n}\n\ninterface TenantUserResponse {\n response: { status: number };\n data: { status?: string; data?: TenantUser | null; message?: string };\n}\n\ninterface BatchUsersResponse {\n response: { status: number };\n data: { status?: string; data?: TenantUser[]; message?: string };\n}\n\nexport async function getUser(\n token: string,\n domainName: string\n): Promise<GetUserResponse> {\n const response = await fetch(\n `${getServerBasePath()}/app/${domainName}/user`,\n {\n method: 'GET',\n headers: {\n Authorization: token,\n 'Content-Type': 'application/json',\n },\n }\n );\n const data = (await response.json()) as GetUserResponse['data'];\n return { response: { status: response.status }, data };\n}\n\nexport async function searchUserByEmail(\n token: string,\n domainName: string,\n email: string\n): Promise<TenantUserResponse> {\n const params = new URLSearchParams({ email });\n const response = await fetch(\n `${getServerBasePath()}/app/${domainName}/users/search?${params}`,\n {\n method: 'GET',\n headers: {\n Authorization: token,\n 'Content-Type': 'application/json',\n },\n }\n );\n const data = (await response.json()) as TenantUserResponse['data'];\n return { response: { status: response.status }, data };\n}\n\nexport async function getUserById(\n token: string,\n domainName: string,\n userId: string\n): Promise<TenantUserResponse> {\n const response = await fetch(\n `${getServerBasePath()}/app/${domainName}/users/${userId}`,\n {\n method: 'GET',\n headers: {\n Authorization: token,\n 'Content-Type': 'application/json',\n },\n }\n );\n const data = (await response.json()) as TenantUserResponse['data'];\n return { response: { status: response.status }, data };\n}\n\nexport async function batchGetUsers(\n token: string,\n domainName: string,\n userIds: string[]\n): Promise<BatchUsersResponse> {\n const response = await fetch(\n `${getServerBasePath()}/app/${domainName}/users/batch`,\n {\n method: 'POST',\n headers: {\n Authorization: token,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ userIds }),\n }\n );\n const data = (await response.json()) as BatchUsersResponse['data'];\n return { response: { status: response.status }, data };\n}\n","import { IDauthUser } from './index';\n\ninterface CacheEntry {\n user: IDauthUser;\n expiresAt: number;\n}\n\nexport interface CacheOptions {\n ttlMs: number;\n}\n\nexport class UserCache {\n private store = new Map<string, CacheEntry>();\n private ttlMs: number;\n\n constructor(options: CacheOptions) {\n this.ttlMs = options.ttlMs;\n }\n\n get(token: string): IDauthUser | undefined {\n const entry = this.store.get(token);\n if (!entry) return undefined;\n\n if (Date.now() > entry.expiresAt) {\n this.store.delete(token);\n return undefined;\n }\n\n return entry.user;\n }\n\n set(token: string, user: IDauthUser): void {\n if (this.store.size > 1000) {\n this.sweep();\n }\n this.store.set(token, { user, expiresAt: Date.now() + this.ttlMs });\n }\n\n clear(): void {\n this.store.clear();\n }\n\n private sweep(): void {\n const now = Date.now();\n for (const [key, entry] of this.store) {\n if (now > entry.expiresAt) {\n this.store.delete(key);\n }\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/router.js
CHANGED
|
@@ -381,6 +381,185 @@ function dauthRouter(opts) {
|
|
|
381
381
|
clearCookies(res, config);
|
|
382
382
|
return res.status(response.status).send(data);
|
|
383
383
|
});
|
|
384
|
+
router.get(
|
|
385
|
+
"/passkey/credentials",
|
|
386
|
+
async (req, res) => {
|
|
387
|
+
const config = await getConfig();
|
|
388
|
+
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
389
|
+
return res.status(403).send({
|
|
390
|
+
status: "csrf-invalid",
|
|
391
|
+
message: "CSRF token invalid"
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const session = readSession(req, config);
|
|
395
|
+
if (!session) {
|
|
396
|
+
return res.status(401).send({
|
|
397
|
+
status: "no-session",
|
|
398
|
+
message: "Not authenticated"
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
const refreshed = await maybeRefreshTokens(
|
|
402
|
+
session,
|
|
403
|
+
config,
|
|
404
|
+
res
|
|
405
|
+
);
|
|
406
|
+
const response = await fetch(
|
|
407
|
+
`${config.dauthBasePath}/app/${config.domainName}/passkey/credentials`,
|
|
408
|
+
{
|
|
409
|
+
method: "GET",
|
|
410
|
+
headers: { Authorization: refreshed.accessToken }
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
const data = await response.json();
|
|
414
|
+
return res.status(response.status).send(data);
|
|
415
|
+
}
|
|
416
|
+
);
|
|
417
|
+
router.post(
|
|
418
|
+
"/passkey/register/start",
|
|
419
|
+
async (req, res) => {
|
|
420
|
+
const config = await getConfig();
|
|
421
|
+
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
422
|
+
return res.status(403).send({
|
|
423
|
+
status: "csrf-invalid",
|
|
424
|
+
message: "CSRF token invalid"
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
const session = readSession(req, config);
|
|
428
|
+
if (!session) {
|
|
429
|
+
return res.status(401).send({
|
|
430
|
+
status: "no-session",
|
|
431
|
+
message: "Not authenticated"
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
const refreshed = await maybeRefreshTokens(
|
|
435
|
+
session,
|
|
436
|
+
config,
|
|
437
|
+
res
|
|
438
|
+
);
|
|
439
|
+
const response = await fetch(
|
|
440
|
+
`${config.dauthBasePath}/app/${config.domainName}/passkey/register/start`,
|
|
441
|
+
{
|
|
442
|
+
method: "POST",
|
|
443
|
+
headers: {
|
|
444
|
+
"Content-Type": "application/json",
|
|
445
|
+
Authorization: refreshed.accessToken
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
const data = await response.json();
|
|
450
|
+
return res.status(response.status).send(data);
|
|
451
|
+
}
|
|
452
|
+
);
|
|
453
|
+
router.post(
|
|
454
|
+
"/passkey/register/finish",
|
|
455
|
+
async (req, res) => {
|
|
456
|
+
const config = await getConfig();
|
|
457
|
+
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
458
|
+
return res.status(403).send({
|
|
459
|
+
status: "csrf-invalid",
|
|
460
|
+
message: "CSRF token invalid"
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
const session = readSession(req, config);
|
|
464
|
+
if (!session) {
|
|
465
|
+
return res.status(401).send({
|
|
466
|
+
status: "no-session",
|
|
467
|
+
message: "Not authenticated"
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
const refreshed = await maybeRefreshTokens(
|
|
471
|
+
session,
|
|
472
|
+
config,
|
|
473
|
+
res
|
|
474
|
+
);
|
|
475
|
+
const response = await fetch(
|
|
476
|
+
`${config.dauthBasePath}/app/${config.domainName}/passkey/register/finish`,
|
|
477
|
+
{
|
|
478
|
+
method: "POST",
|
|
479
|
+
headers: {
|
|
480
|
+
"Content-Type": "application/json",
|
|
481
|
+
Authorization: refreshed.accessToken
|
|
482
|
+
},
|
|
483
|
+
body: JSON.stringify(req.body)
|
|
484
|
+
}
|
|
485
|
+
);
|
|
486
|
+
const data = await response.json();
|
|
487
|
+
return res.status(response.status).send(data);
|
|
488
|
+
}
|
|
489
|
+
);
|
|
490
|
+
router.delete(
|
|
491
|
+
"/passkey/credentials/:credentialId",
|
|
492
|
+
async (req, res) => {
|
|
493
|
+
const config = await getConfig();
|
|
494
|
+
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
495
|
+
return res.status(403).send({
|
|
496
|
+
status: "csrf-invalid",
|
|
497
|
+
message: "CSRF token invalid"
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
const session = readSession(req, config);
|
|
501
|
+
if (!session) {
|
|
502
|
+
return res.status(401).send({
|
|
503
|
+
status: "no-session",
|
|
504
|
+
message: "Not authenticated"
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
const refreshed = await maybeRefreshTokens(
|
|
508
|
+
session,
|
|
509
|
+
config,
|
|
510
|
+
res
|
|
511
|
+
);
|
|
512
|
+
const { credentialId } = req.params;
|
|
513
|
+
const response = await fetch(
|
|
514
|
+
`${config.dauthBasePath}/app/${config.domainName}/passkey/credentials/${credentialId}`,
|
|
515
|
+
{
|
|
516
|
+
method: "DELETE",
|
|
517
|
+
headers: { Authorization: refreshed.accessToken }
|
|
518
|
+
}
|
|
519
|
+
);
|
|
520
|
+
const data = await response.json();
|
|
521
|
+
return res.status(response.status).send(data);
|
|
522
|
+
}
|
|
523
|
+
);
|
|
524
|
+
router.post("/avatar", async (req, res) => {
|
|
525
|
+
const config = await getConfig();
|
|
526
|
+
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
527
|
+
return res.status(403).send({
|
|
528
|
+
status: "csrf-invalid",
|
|
529
|
+
message: "CSRF token invalid"
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
const session = readSession(req, config);
|
|
533
|
+
if (!session) {
|
|
534
|
+
return res.status(401).send({
|
|
535
|
+
status: "no-session",
|
|
536
|
+
message: "Not authenticated"
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
const refreshed = await maybeRefreshTokens(
|
|
540
|
+
session,
|
|
541
|
+
config,
|
|
542
|
+
res
|
|
543
|
+
);
|
|
544
|
+
const chunks = [];
|
|
545
|
+
for await (const chunk of req) {
|
|
546
|
+
chunks.push(Buffer.from(chunk));
|
|
547
|
+
}
|
|
548
|
+
const body = Buffer.concat(chunks);
|
|
549
|
+
const response = await fetch(
|
|
550
|
+
`${config.dauthBasePath}/app/${config.domainName}/user/avatar`,
|
|
551
|
+
{
|
|
552
|
+
method: "POST",
|
|
553
|
+
headers: {
|
|
554
|
+
"Content-Type": req.headers["content-type"] || "",
|
|
555
|
+
Authorization: refreshed.accessToken
|
|
556
|
+
},
|
|
557
|
+
body
|
|
558
|
+
}
|
|
559
|
+
);
|
|
560
|
+
const data = await response.json();
|
|
561
|
+
return res.status(response.status).send(data);
|
|
562
|
+
});
|
|
384
563
|
return router;
|
|
385
564
|
}
|
|
386
565
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/router.ts","../src/api/utils/config.ts","../src/session.ts","../src/csrf.ts"],"sourcesContent":["import { Router, Request, Response } from 'express';\nimport jwt from 'jsonwebtoken';\nimport { getServerBasePath } from './api/utils/config';\nimport {\n deriveEncryptionKey,\n encryptSession,\n decryptSessionWithKeys,\n SessionPayload,\n} from './session';\nimport { generateCsrfToken, verifyCsrf } from './csrf';\n\nexport interface DauthRouterOptions {\n domainName: string;\n tsk: string;\n dauthUrl?: string;\n cookieName?: string;\n csrfCookieName?: string;\n maxAge?: number;\n secure?: boolean;\n previousTsk?: string;\n sessionSalt?: string;\n}\n\ninterface ResolvedConfig {\n domainName: string;\n dauthBasePath: string;\n cookieName: string;\n csrfCookieName: string;\n maxAgeMs: number;\n secure: boolean;\n encKeys: Buffer[];\n}\n\n// Refresh lock to prevent race conditions on concurrent token rotation\nconst refreshLocks = new Map<string, Promise<SessionPayload | null>>();\n\nfunction lockKey(refreshToken: string): string {\n return refreshToken.substring(0, 16);\n}\n\nfunction clearStaleLocks(): void {\n if (refreshLocks.size > 100) refreshLocks.clear();\n}\n\nasync function resolveConfig(\n opts: DauthRouterOptions\n): Promise<ResolvedConfig> {\n const secure = opts.secure ?? process.env.NODE_ENV !== 'development';\n const cookieName =\n opts.cookieName ?? (secure ? '__Host-dauth-session' : 'dauth-session');\n const csrfCookieName =\n opts.csrfCookieName ?? (secure ? '__Host-csrf' : 'csrf-token');\n const maxAgeMs = (opts.maxAge ?? 30 * 24 * 3600) * 1000;\n\n const keys: Buffer[] = [];\n keys.push(await deriveEncryptionKey(opts.tsk, opts.sessionSalt));\n if (opts.previousTsk) {\n keys.push(await deriveEncryptionKey(opts.previousTsk, opts.sessionSalt));\n }\n\n let dauthBasePath: string;\n if (opts.dauthUrl) {\n dauthBasePath = `${opts.dauthUrl.replace(/\\/+$/, '')}/api/v1`;\n } else {\n dauthBasePath = getServerBasePath();\n }\n\n return {\n domainName: opts.domainName,\n dauthBasePath,\n cookieName,\n csrfCookieName,\n maxAgeMs,\n secure,\n encKeys: keys,\n };\n}\n\nfunction setSessionCookie(\n res: Response,\n payload: SessionPayload,\n config: ResolvedConfig\n): void {\n const encrypted = encryptSession(payload, config.encKeys[0]);\n const cookieOpts: Record<string, unknown> = {\n httpOnly: true,\n secure: config.secure,\n sameSite: 'lax',\n maxAge: config.maxAgeMs,\n path: '/',\n };\n // __Host- prefix requires no domain attribute\n if (!config.secure) {\n // Dev mode: no __Host- prefix, no domain restriction needed\n }\n res.cookie(config.cookieName, encrypted, cookieOpts);\n}\n\nfunction setCsrfCookie(res: Response, config: ResolvedConfig): void {\n const csrfToken = generateCsrfToken();\n res.cookie(config.csrfCookieName, csrfToken, {\n httpOnly: false,\n secure: config.secure,\n sameSite: 'lax',\n maxAge: config.maxAgeMs,\n path: '/',\n });\n}\n\nfunction clearCookies(res: Response, config: ResolvedConfig): void {\n const baseOpts = { path: '/', secure: config.secure };\n res.clearCookie(config.cookieName, baseOpts);\n res.clearCookie(config.csrfCookieName, baseOpts);\n}\n\nfunction readSession(\n req: Request,\n config: ResolvedConfig\n): SessionPayload | null {\n const cookie = req.cookies?.[config.cookieName];\n if (!cookie) return null;\n return decryptSessionWithKeys(cookie, config.encKeys);\n}\n\nfunction isTokenExpiringSoon(token: string, thresholdMs = 300_000): boolean {\n try {\n const decoded = jwt.decode(token) as { exp?: number } | null;\n if (!decoded?.exp) return true;\n return decoded.exp * 1000 - Date.now() < thresholdMs;\n } catch {\n return true;\n }\n}\n\nasync function maybeRefreshTokens(\n session: SessionPayload,\n config: ResolvedConfig,\n res: Response\n): Promise<SessionPayload> {\n if (!isTokenExpiringSoon(session.accessToken)) return session;\n\n const key = lockKey(session.refreshToken);\n clearStaleLocks();\n\n const existingLock = refreshLocks.get(key);\n if (existingLock) {\n const result = await existingLock;\n return result ?? session;\n }\n\n const refreshPromise = (async (): Promise<SessionPayload | null> => {\n try {\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/refresh-token`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n refreshToken: session.refreshToken,\n }),\n }\n );\n if (!response.ok) return null;\n const data = (await response.json()) as {\n accessToken?: string;\n refreshToken?: string;\n };\n if (!data.accessToken || !data.refreshToken) return null;\n const newSession: SessionPayload = {\n accessToken: data.accessToken,\n refreshToken: data.refreshToken,\n };\n setSessionCookie(res, newSession, config);\n return newSession;\n } catch {\n return null;\n }\n })();\n\n refreshLocks.set(key, refreshPromise);\n\n // Timeout safety net: clean lock after 10s\n const timeout = setTimeout(() => refreshLocks.delete(key), 10_000);\n refreshPromise.finally(() => {\n clearTimeout(timeout);\n refreshLocks.delete(key);\n });\n\n const result = await refreshPromise;\n return result ?? session;\n}\n\nexport function dauthRouter(opts: DauthRouterOptions): Router {\n const router = Router();\n let configPromise: Promise<ResolvedConfig> | null = null;\n\n async function getConfig(): Promise<ResolvedConfig> {\n if (!configPromise) configPromise = resolveConfig(opts);\n return configPromise;\n }\n\n // POST /exchange-code — no CSRF (no prior session)\n router.post('/exchange-code', async (req: Request, res: Response) => {\n const config = await getConfig();\n const { code } = req.body;\n if (!code) {\n return res\n .status(400)\n .send({ status: 'code-required', message: 'Code required' });\n }\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/exchange-code`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ code }),\n }\n );\n if (!response.ok) {\n return res\n .status(response.status)\n .send({ status: 'code-invalid', message: 'Code invalid' });\n }\n const data = (await response.json()) as {\n accessToken: string;\n refreshToken: string;\n isNewUser: boolean;\n };\n\n setSessionCookie(\n res,\n {\n accessToken: data.accessToken,\n refreshToken: data.refreshToken,\n },\n config\n );\n setCsrfCookie(res, config);\n\n // Fetch user data to return\n const userResponse = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'GET',\n headers: { Authorization: data.accessToken },\n }\n );\n const userData = (await userResponse.json()) as {\n user?: unknown;\n domain?: unknown;\n };\n\n return res.status(200).send({\n user: userData.user,\n domain: userData.domain,\n isNewUser: data.isNewUser,\n });\n });\n\n // GET /session — no CSRF (read-only)\n router.get('/session', async (req: Request, res: Response) => {\n const config = await getConfig();\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n\n const refreshed = await maybeRefreshTokens(session, config, res);\n\n const userResponse = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'GET',\n headers: { Authorization: refreshed.accessToken },\n }\n );\n if (!userResponse.ok) {\n clearCookies(res, config);\n return res\n .status(401)\n .send({ status: 'session-invalid', message: 'Session expired' });\n }\n const userData = (await userResponse.json()) as {\n user?: unknown;\n domain?: unknown;\n };\n return res.status(200).send({\n user: userData.user,\n domain: userData.domain,\n });\n });\n\n // POST /logout — CSRF required\n router.post('/logout', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (session) {\n // Revoke refresh token server-to-server (fire-and-forget)\n fetch(`${config.dauthBasePath}/app/${config.domainName}/logout`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n refreshToken: session.refreshToken,\n }),\n }).catch(() => {});\n }\n clearCookies(res, config);\n return res.status(200).send({ status: 'success', message: 'Logged out' });\n });\n\n // PATCH /user — CSRF required\n router.patch('/user', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n const refreshed = await maybeRefreshTokens(session, config, res);\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'PATCH',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: refreshed.accessToken,\n },\n body: JSON.stringify(req.body),\n }\n );\n const data = await response.json();\n return res.status(response.status).send(data);\n });\n\n // DELETE /user — CSRF required\n router.delete('/user', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'DELETE',\n headers: { Authorization: session.accessToken },\n }\n );\n const data = await response.json();\n clearCookies(res, config);\n return res.status(response.status).send(data);\n });\n\n return router;\n}\n","export const apiVersion = 'v1';\nexport const serverDomain = 'dauth.ovh';\n\nexport function getServerBasePath(): string {\n if (process.env.DAUTH_URL) {\n const base = process.env.DAUTH_URL.replace(/\\/+$/, '');\n return `${base}/api/${apiVersion}`;\n }\n\n const isLocalhost = process.env.NODE_ENV === 'development';\n const serverPort = 4012;\n const serverLocalUrl = `http://localhost:${serverPort}/api/${apiVersion}`;\n const serverProdUrl = `https://${serverDomain}/api/${apiVersion}`;\n return isLocalhost ? serverLocalUrl : serverProdUrl;\n}\n","import crypto from 'crypto';\n\nexport interface SessionPayload {\n accessToken: string;\n refreshToken: string;\n}\n\nconst INFO = 'dauth-cookie-enc-v1';\nconst DEFAULT_SALT = Buffer.from(\n 'a3f8c1d7e9b24f6081c5d3a7e2f49b0653d81f7a2e94c0b6d8f3a5e1c7b09d42',\n 'hex'\n);\n\nexport async function deriveEncryptionKey(\n tsk: string,\n salt?: string\n): Promise<Buffer> {\n const saltBuf = salt ? Buffer.from(salt, 'hex') : DEFAULT_SALT;\n return new Promise((resolve, reject) => {\n crypto.hkdf(\n 'sha256',\n Buffer.from(tsk),\n saltBuf,\n INFO,\n 32,\n (err, derivedKey) => {\n if (err) return reject(err);\n resolve(Buffer.from(derivedKey));\n }\n );\n });\n}\n\nexport function encryptSession(payload: SessionPayload, key: Buffer): string {\n const nonce = crypto.randomBytes(12);\n const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);\n const plaintext = JSON.stringify(payload);\n const encrypted = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final(),\n ]);\n const authTag = cipher.getAuthTag();\n // Format: base64(nonce + ciphertext + authTag)\n return Buffer.concat([nonce, encrypted, authTag]).toString('base64');\n}\n\nexport function decryptSession(\n ciphertext: string,\n key: Buffer\n): SessionPayload | null {\n try {\n const buf = Buffer.from(ciphertext, 'base64');\n if (buf.length < 12 + 16) return null; // nonce(12) + authTag(16) minimum\n const nonce = buf.subarray(0, 12);\n const authTag = buf.subarray(buf.length - 16);\n const encrypted = buf.subarray(12, buf.length - 16);\n const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);\n decipher.setAuthTag(authTag);\n const decrypted = Buffer.concat([\n decipher.update(encrypted),\n decipher.final(),\n ]);\n return JSON.parse(decrypted.toString('utf8')) as SessionPayload;\n } catch {\n return null;\n }\n}\n\nexport function decryptSessionWithKeys(\n ciphertext: string,\n keys: Buffer[]\n): SessionPayload | null {\n for (const key of keys) {\n const result = decryptSession(ciphertext, key);\n if (result) return result;\n }\n return null;\n}\n","import crypto from 'crypto';\nimport type { Request } from 'express';\n\nexport function generateCsrfToken(): string {\n return crypto.randomBytes(32).toString('hex');\n}\n\nexport function verifyCsrf(req: Request, csrfCookieName: string): boolean {\n const headerToken = req.headers['x-csrf-token'] as string | undefined;\n const cookieToken = req.cookies?.[csrfCookieName] as string | undefined;\n if (!headerToken || !cookieToken) return false;\n if (headerToken.length !== cookieToken.length) return false;\n return crypto.timingSafeEqual(\n Buffer.from(headerToken),\n Buffer.from(cookieToken)\n );\n}\n"],"mappings":"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","names":["crypto","import_crypto","crypto","jwt","result"]}
|
|
1
|
+
{"version":3,"sources":["../src/router.ts","../src/api/utils/config.ts","../src/session.ts","../src/csrf.ts"],"sourcesContent":["import { Router, Request, Response } from 'express';\nimport jwt from 'jsonwebtoken';\nimport { getServerBasePath } from './api/utils/config';\nimport {\n deriveEncryptionKey,\n encryptSession,\n decryptSessionWithKeys,\n SessionPayload,\n} from './session';\nimport { generateCsrfToken, verifyCsrf } from './csrf';\n\nexport interface DauthRouterOptions {\n domainName: string;\n tsk: string;\n dauthUrl?: string;\n cookieName?: string;\n csrfCookieName?: string;\n maxAge?: number;\n secure?: boolean;\n previousTsk?: string;\n sessionSalt?: string;\n}\n\ninterface ResolvedConfig {\n domainName: string;\n dauthBasePath: string;\n cookieName: string;\n csrfCookieName: string;\n maxAgeMs: number;\n secure: boolean;\n encKeys: Buffer[];\n}\n\n// Refresh lock to prevent race conditions on concurrent token rotation\nconst refreshLocks = new Map<string, Promise<SessionPayload | null>>();\n\nfunction lockKey(refreshToken: string): string {\n return refreshToken.substring(0, 16);\n}\n\nfunction clearStaleLocks(): void {\n if (refreshLocks.size > 100) refreshLocks.clear();\n}\n\nasync function resolveConfig(\n opts: DauthRouterOptions\n): Promise<ResolvedConfig> {\n const secure = opts.secure ?? process.env.NODE_ENV !== 'development';\n const cookieName =\n opts.cookieName ?? (secure ? '__Host-dauth-session' : 'dauth-session');\n const csrfCookieName =\n opts.csrfCookieName ?? (secure ? '__Host-csrf' : 'csrf-token');\n const maxAgeMs = (opts.maxAge ?? 30 * 24 * 3600) * 1000;\n\n const keys: Buffer[] = [];\n keys.push(await deriveEncryptionKey(opts.tsk, opts.sessionSalt));\n if (opts.previousTsk) {\n keys.push(await deriveEncryptionKey(opts.previousTsk, opts.sessionSalt));\n }\n\n let dauthBasePath: string;\n if (opts.dauthUrl) {\n dauthBasePath = `${opts.dauthUrl.replace(/\\/+$/, '')}/api/v1`;\n } else {\n dauthBasePath = getServerBasePath();\n }\n\n return {\n domainName: opts.domainName,\n dauthBasePath,\n cookieName,\n csrfCookieName,\n maxAgeMs,\n secure,\n encKeys: keys,\n };\n}\n\nfunction setSessionCookie(\n res: Response,\n payload: SessionPayload,\n config: ResolvedConfig\n): void {\n const encrypted = encryptSession(payload, config.encKeys[0]);\n const cookieOpts: Record<string, unknown> = {\n httpOnly: true,\n secure: config.secure,\n sameSite: 'lax',\n maxAge: config.maxAgeMs,\n path: '/',\n };\n // __Host- prefix requires no domain attribute\n if (!config.secure) {\n // Dev mode: no __Host- prefix, no domain restriction needed\n }\n res.cookie(config.cookieName, encrypted, cookieOpts);\n}\n\nfunction setCsrfCookie(res: Response, config: ResolvedConfig): void {\n const csrfToken = generateCsrfToken();\n res.cookie(config.csrfCookieName, csrfToken, {\n httpOnly: false,\n secure: config.secure,\n sameSite: 'lax',\n maxAge: config.maxAgeMs,\n path: '/',\n });\n}\n\nfunction clearCookies(res: Response, config: ResolvedConfig): void {\n const baseOpts = { path: '/', secure: config.secure };\n res.clearCookie(config.cookieName, baseOpts);\n res.clearCookie(config.csrfCookieName, baseOpts);\n}\n\nfunction readSession(\n req: Request,\n config: ResolvedConfig\n): SessionPayload | null {\n const cookie = req.cookies?.[config.cookieName];\n if (!cookie) return null;\n return decryptSessionWithKeys(cookie, config.encKeys);\n}\n\nfunction isTokenExpiringSoon(token: string, thresholdMs = 300_000): boolean {\n try {\n const decoded = jwt.decode(token) as { exp?: number } | null;\n if (!decoded?.exp) return true;\n return decoded.exp * 1000 - Date.now() < thresholdMs;\n } catch {\n return true;\n }\n}\n\nasync function maybeRefreshTokens(\n session: SessionPayload,\n config: ResolvedConfig,\n res: Response\n): Promise<SessionPayload> {\n if (!isTokenExpiringSoon(session.accessToken)) return session;\n\n const key = lockKey(session.refreshToken);\n clearStaleLocks();\n\n const existingLock = refreshLocks.get(key);\n if (existingLock) {\n const result = await existingLock;\n return result ?? session;\n }\n\n const refreshPromise = (async (): Promise<SessionPayload | null> => {\n try {\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/refresh-token`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n refreshToken: session.refreshToken,\n }),\n }\n );\n if (!response.ok) return null;\n const data = (await response.json()) as {\n accessToken?: string;\n refreshToken?: string;\n };\n if (!data.accessToken || !data.refreshToken) return null;\n const newSession: SessionPayload = {\n accessToken: data.accessToken,\n refreshToken: data.refreshToken,\n };\n setSessionCookie(res, newSession, config);\n return newSession;\n } catch {\n return null;\n }\n })();\n\n refreshLocks.set(key, refreshPromise);\n\n // Timeout safety net: clean lock after 10s\n const timeout = setTimeout(() => refreshLocks.delete(key), 10_000);\n refreshPromise.finally(() => {\n clearTimeout(timeout);\n refreshLocks.delete(key);\n });\n\n const result = await refreshPromise;\n return result ?? session;\n}\n\nexport function dauthRouter(opts: DauthRouterOptions): Router {\n const router = Router();\n let configPromise: Promise<ResolvedConfig> | null = null;\n\n async function getConfig(): Promise<ResolvedConfig> {\n if (!configPromise) configPromise = resolveConfig(opts);\n return configPromise;\n }\n\n // POST /exchange-code — no CSRF (no prior session)\n router.post('/exchange-code', async (req: Request, res: Response) => {\n const config = await getConfig();\n const { code } = req.body;\n if (!code) {\n return res\n .status(400)\n .send({ status: 'code-required', message: 'Code required' });\n }\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/exchange-code`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ code }),\n }\n );\n if (!response.ok) {\n return res\n .status(response.status)\n .send({ status: 'code-invalid', message: 'Code invalid' });\n }\n const data = (await response.json()) as {\n accessToken: string;\n refreshToken: string;\n isNewUser: boolean;\n };\n\n setSessionCookie(\n res,\n {\n accessToken: data.accessToken,\n refreshToken: data.refreshToken,\n },\n config\n );\n setCsrfCookie(res, config);\n\n // Fetch user data to return\n const userResponse = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'GET',\n headers: { Authorization: data.accessToken },\n }\n );\n const userData = (await userResponse.json()) as {\n user?: unknown;\n domain?: unknown;\n };\n\n return res.status(200).send({\n user: userData.user,\n domain: userData.domain,\n isNewUser: data.isNewUser,\n });\n });\n\n // GET /session — no CSRF (read-only)\n router.get('/session', async (req: Request, res: Response) => {\n const config = await getConfig();\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n\n const refreshed = await maybeRefreshTokens(session, config, res);\n\n const userResponse = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'GET',\n headers: { Authorization: refreshed.accessToken },\n }\n );\n if (!userResponse.ok) {\n clearCookies(res, config);\n return res\n .status(401)\n .send({ status: 'session-invalid', message: 'Session expired' });\n }\n const userData = (await userResponse.json()) as {\n user?: unknown;\n domain?: unknown;\n };\n return res.status(200).send({\n user: userData.user,\n domain: userData.domain,\n });\n });\n\n // POST /logout — CSRF required\n router.post('/logout', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (session) {\n // Revoke refresh token server-to-server (fire-and-forget)\n fetch(`${config.dauthBasePath}/app/${config.domainName}/logout`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n refreshToken: session.refreshToken,\n }),\n }).catch(() => {});\n }\n clearCookies(res, config);\n return res.status(200).send({ status: 'success', message: 'Logged out' });\n });\n\n // PATCH /user — CSRF required\n router.patch('/user', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n const refreshed = await maybeRefreshTokens(session, config, res);\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'PATCH',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: refreshed.accessToken,\n },\n body: JSON.stringify(req.body),\n }\n );\n const data = await response.json();\n return res.status(response.status).send(data);\n });\n\n // DELETE /user — CSRF required\n router.delete('/user', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'DELETE',\n headers: { Authorization: session.accessToken },\n }\n );\n const data = await response.json();\n clearCookies(res, config);\n return res.status(response.status).send(data);\n });\n\n // GET /passkey/credentials — CSRF required\n router.get(\n '/passkey/credentials',\n async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res.status(403).send({\n status: 'csrf-invalid',\n message: 'CSRF token invalid',\n });\n }\n const session = readSession(req, config);\n if (!session) {\n return res.status(401).send({\n status: 'no-session',\n message: 'Not authenticated',\n });\n }\n const refreshed = await maybeRefreshTokens(\n session,\n config,\n res\n );\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/passkey/credentials`,\n {\n method: 'GET',\n headers: { Authorization: refreshed.accessToken },\n }\n );\n const data = await response.json();\n return res.status(response.status).send(data);\n }\n );\n\n // POST /passkey/register/start — CSRF required\n router.post(\n '/passkey/register/start',\n async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res.status(403).send({\n status: 'csrf-invalid',\n message: 'CSRF token invalid',\n });\n }\n const session = readSession(req, config);\n if (!session) {\n return res.status(401).send({\n status: 'no-session',\n message: 'Not authenticated',\n });\n }\n const refreshed = await maybeRefreshTokens(\n session,\n config,\n res\n );\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/passkey/register/start`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: refreshed.accessToken,\n },\n }\n );\n const data = await response.json();\n return res.status(response.status).send(data);\n }\n );\n\n // POST /passkey/register/finish — CSRF required\n router.post(\n '/passkey/register/finish',\n async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res.status(403).send({\n status: 'csrf-invalid',\n message: 'CSRF token invalid',\n });\n }\n const session = readSession(req, config);\n if (!session) {\n return res.status(401).send({\n status: 'no-session',\n message: 'Not authenticated',\n });\n }\n const refreshed = await maybeRefreshTokens(\n session,\n config,\n res\n );\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/passkey/register/finish`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: refreshed.accessToken,\n },\n body: JSON.stringify(req.body),\n }\n );\n const data = await response.json();\n return res.status(response.status).send(data);\n }\n );\n\n // DELETE /passkey/credentials/:credentialId — CSRF required\n router.delete(\n '/passkey/credentials/:credentialId',\n async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res.status(403).send({\n status: 'csrf-invalid',\n message: 'CSRF token invalid',\n });\n }\n const session = readSession(req, config);\n if (!session) {\n return res.status(401).send({\n status: 'no-session',\n message: 'Not authenticated',\n });\n }\n const refreshed = await maybeRefreshTokens(\n session,\n config,\n res\n );\n\n const { credentialId } = req.params;\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/passkey/credentials/${credentialId}`,\n {\n method: 'DELETE',\n headers: { Authorization: refreshed.accessToken },\n }\n );\n const data = await response.json();\n return res.status(response.status).send(data);\n }\n );\n\n // POST /avatar — CSRF required, multipart forwarding\n router.post('/avatar', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res.status(403).send({\n status: 'csrf-invalid',\n message: 'CSRF token invalid',\n });\n }\n const session = readSession(req, config);\n if (!session) {\n return res.status(401).send({\n status: 'no-session',\n message: 'Not authenticated',\n });\n }\n const refreshed = await maybeRefreshTokens(\n session,\n config,\n res\n );\n\n // Collect raw multipart body (express.json() does not consume it)\n const chunks: Buffer[] = [];\n for await (const chunk of req) {\n chunks.push(Buffer.from(chunk));\n }\n const body = Buffer.concat(chunks);\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user/avatar`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': req.headers['content-type'] || '',\n Authorization: refreshed.accessToken,\n },\n body,\n }\n );\n const data = await response.json();\n return res.status(response.status).send(data);\n });\n\n return router;\n}\n","export const apiVersion = 'v1';\nexport const serverDomain = 'dauth.ovh';\n\nexport function getServerBasePath(): string {\n if (process.env.DAUTH_URL) {\n const base = process.env.DAUTH_URL.replace(/\\/+$/, '');\n return `${base}/api/${apiVersion}`;\n }\n\n const isLocalhost = process.env.NODE_ENV === 'development';\n const serverPort = 4012;\n const serverLocalUrl = `http://localhost:${serverPort}/api/${apiVersion}`;\n const serverProdUrl = `https://${serverDomain}/api/${apiVersion}`;\n return isLocalhost ? serverLocalUrl : serverProdUrl;\n}\n","import crypto from 'crypto';\n\nexport interface SessionPayload {\n accessToken: string;\n refreshToken: string;\n}\n\nconst INFO = 'dauth-cookie-enc-v1';\nconst DEFAULT_SALT = Buffer.from(\n 'a3f8c1d7e9b24f6081c5d3a7e2f49b0653d81f7a2e94c0b6d8f3a5e1c7b09d42',\n 'hex'\n);\n\nexport async function deriveEncryptionKey(\n tsk: string,\n salt?: string\n): Promise<Buffer> {\n const saltBuf = salt ? Buffer.from(salt, 'hex') : DEFAULT_SALT;\n return new Promise((resolve, reject) => {\n crypto.hkdf(\n 'sha256',\n Buffer.from(tsk),\n saltBuf,\n INFO,\n 32,\n (err, derivedKey) => {\n if (err) return reject(err);\n resolve(Buffer.from(derivedKey));\n }\n );\n });\n}\n\nexport function encryptSession(payload: SessionPayload, key: Buffer): string {\n const nonce = crypto.randomBytes(12);\n const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);\n const plaintext = JSON.stringify(payload);\n const encrypted = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final(),\n ]);\n const authTag = cipher.getAuthTag();\n // Format: base64(nonce + ciphertext + authTag)\n return Buffer.concat([nonce, encrypted, authTag]).toString('base64');\n}\n\nexport function decryptSession(\n ciphertext: string,\n key: Buffer\n): SessionPayload | null {\n try {\n const buf = Buffer.from(ciphertext, 'base64');\n if (buf.length < 12 + 16) return null; // nonce(12) + authTag(16) minimum\n const nonce = buf.subarray(0, 12);\n const authTag = buf.subarray(buf.length - 16);\n const encrypted = buf.subarray(12, buf.length - 16);\n const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);\n decipher.setAuthTag(authTag);\n const decrypted = Buffer.concat([\n decipher.update(encrypted),\n decipher.final(),\n ]);\n return JSON.parse(decrypted.toString('utf8')) as SessionPayload;\n } catch {\n return null;\n }\n}\n\nexport function decryptSessionWithKeys(\n ciphertext: string,\n keys: Buffer[]\n): SessionPayload | null {\n for (const key of keys) {\n const result = decryptSession(ciphertext, key);\n if (result) return result;\n }\n return null;\n}\n","import crypto from 'crypto';\nimport type { Request } from 'express';\n\nexport function generateCsrfToken(): string {\n return crypto.randomBytes(32).toString('hex');\n}\n\nexport function verifyCsrf(req: Request, csrfCookieName: string): boolean {\n const headerToken = req.headers['x-csrf-token'] as string | undefined;\n const cookieToken = req.cookies?.[csrfCookieName] as string | undefined;\n if (!headerToken || !cookieToken) return false;\n if (headerToken.length !== cookieToken.length) return false;\n return crypto.timingSafeEqual(\n Buffer.from(headerToken),\n Buffer.from(cookieToken)\n );\n}\n"],"mappings":"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","names":["crypto","import_crypto","crypto","jwt","result"]}
|
package/dist/router.mjs
CHANGED
|
@@ -279,6 +279,185 @@ function dauthRouter(opts) {
|
|
|
279
279
|
clearCookies(res, config);
|
|
280
280
|
return res.status(response.status).send(data);
|
|
281
281
|
});
|
|
282
|
+
router.get(
|
|
283
|
+
"/passkey/credentials",
|
|
284
|
+
async (req, res) => {
|
|
285
|
+
const config = await getConfig();
|
|
286
|
+
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
287
|
+
return res.status(403).send({
|
|
288
|
+
status: "csrf-invalid",
|
|
289
|
+
message: "CSRF token invalid"
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
const session = readSession(req, config);
|
|
293
|
+
if (!session) {
|
|
294
|
+
return res.status(401).send({
|
|
295
|
+
status: "no-session",
|
|
296
|
+
message: "Not authenticated"
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const refreshed = await maybeRefreshTokens(
|
|
300
|
+
session,
|
|
301
|
+
config,
|
|
302
|
+
res
|
|
303
|
+
);
|
|
304
|
+
const response = await fetch(
|
|
305
|
+
`${config.dauthBasePath}/app/${config.domainName}/passkey/credentials`,
|
|
306
|
+
{
|
|
307
|
+
method: "GET",
|
|
308
|
+
headers: { Authorization: refreshed.accessToken }
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
const data = await response.json();
|
|
312
|
+
return res.status(response.status).send(data);
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
router.post(
|
|
316
|
+
"/passkey/register/start",
|
|
317
|
+
async (req, res) => {
|
|
318
|
+
const config = await getConfig();
|
|
319
|
+
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
320
|
+
return res.status(403).send({
|
|
321
|
+
status: "csrf-invalid",
|
|
322
|
+
message: "CSRF token invalid"
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
const session = readSession(req, config);
|
|
326
|
+
if (!session) {
|
|
327
|
+
return res.status(401).send({
|
|
328
|
+
status: "no-session",
|
|
329
|
+
message: "Not authenticated"
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
const refreshed = await maybeRefreshTokens(
|
|
333
|
+
session,
|
|
334
|
+
config,
|
|
335
|
+
res
|
|
336
|
+
);
|
|
337
|
+
const response = await fetch(
|
|
338
|
+
`${config.dauthBasePath}/app/${config.domainName}/passkey/register/start`,
|
|
339
|
+
{
|
|
340
|
+
method: "POST",
|
|
341
|
+
headers: {
|
|
342
|
+
"Content-Type": "application/json",
|
|
343
|
+
Authorization: refreshed.accessToken
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
);
|
|
347
|
+
const data = await response.json();
|
|
348
|
+
return res.status(response.status).send(data);
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
router.post(
|
|
352
|
+
"/passkey/register/finish",
|
|
353
|
+
async (req, res) => {
|
|
354
|
+
const config = await getConfig();
|
|
355
|
+
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
356
|
+
return res.status(403).send({
|
|
357
|
+
status: "csrf-invalid",
|
|
358
|
+
message: "CSRF token invalid"
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
const session = readSession(req, config);
|
|
362
|
+
if (!session) {
|
|
363
|
+
return res.status(401).send({
|
|
364
|
+
status: "no-session",
|
|
365
|
+
message: "Not authenticated"
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
const refreshed = await maybeRefreshTokens(
|
|
369
|
+
session,
|
|
370
|
+
config,
|
|
371
|
+
res
|
|
372
|
+
);
|
|
373
|
+
const response = await fetch(
|
|
374
|
+
`${config.dauthBasePath}/app/${config.domainName}/passkey/register/finish`,
|
|
375
|
+
{
|
|
376
|
+
method: "POST",
|
|
377
|
+
headers: {
|
|
378
|
+
"Content-Type": "application/json",
|
|
379
|
+
Authorization: refreshed.accessToken
|
|
380
|
+
},
|
|
381
|
+
body: JSON.stringify(req.body)
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
const data = await response.json();
|
|
385
|
+
return res.status(response.status).send(data);
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
router.delete(
|
|
389
|
+
"/passkey/credentials/:credentialId",
|
|
390
|
+
async (req, res) => {
|
|
391
|
+
const config = await getConfig();
|
|
392
|
+
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
393
|
+
return res.status(403).send({
|
|
394
|
+
status: "csrf-invalid",
|
|
395
|
+
message: "CSRF token invalid"
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
const session = readSession(req, config);
|
|
399
|
+
if (!session) {
|
|
400
|
+
return res.status(401).send({
|
|
401
|
+
status: "no-session",
|
|
402
|
+
message: "Not authenticated"
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
const refreshed = await maybeRefreshTokens(
|
|
406
|
+
session,
|
|
407
|
+
config,
|
|
408
|
+
res
|
|
409
|
+
);
|
|
410
|
+
const { credentialId } = req.params;
|
|
411
|
+
const response = await fetch(
|
|
412
|
+
`${config.dauthBasePath}/app/${config.domainName}/passkey/credentials/${credentialId}`,
|
|
413
|
+
{
|
|
414
|
+
method: "DELETE",
|
|
415
|
+
headers: { Authorization: refreshed.accessToken }
|
|
416
|
+
}
|
|
417
|
+
);
|
|
418
|
+
const data = await response.json();
|
|
419
|
+
return res.status(response.status).send(data);
|
|
420
|
+
}
|
|
421
|
+
);
|
|
422
|
+
router.post("/avatar", async (req, res) => {
|
|
423
|
+
const config = await getConfig();
|
|
424
|
+
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
425
|
+
return res.status(403).send({
|
|
426
|
+
status: "csrf-invalid",
|
|
427
|
+
message: "CSRF token invalid"
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
const session = readSession(req, config);
|
|
431
|
+
if (!session) {
|
|
432
|
+
return res.status(401).send({
|
|
433
|
+
status: "no-session",
|
|
434
|
+
message: "Not authenticated"
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
const refreshed = await maybeRefreshTokens(
|
|
438
|
+
session,
|
|
439
|
+
config,
|
|
440
|
+
res
|
|
441
|
+
);
|
|
442
|
+
const chunks = [];
|
|
443
|
+
for await (const chunk of req) {
|
|
444
|
+
chunks.push(Buffer.from(chunk));
|
|
445
|
+
}
|
|
446
|
+
const body = Buffer.concat(chunks);
|
|
447
|
+
const response = await fetch(
|
|
448
|
+
`${config.dauthBasePath}/app/${config.domainName}/user/avatar`,
|
|
449
|
+
{
|
|
450
|
+
method: "POST",
|
|
451
|
+
headers: {
|
|
452
|
+
"Content-Type": req.headers["content-type"] || "",
|
|
453
|
+
Authorization: refreshed.accessToken
|
|
454
|
+
},
|
|
455
|
+
body
|
|
456
|
+
}
|
|
457
|
+
);
|
|
458
|
+
const data = await response.json();
|
|
459
|
+
return res.status(response.status).send(data);
|
|
460
|
+
});
|
|
282
461
|
return router;
|
|
283
462
|
}
|
|
284
463
|
export {
|
package/dist/router.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/router.ts","../src/csrf.ts"],"sourcesContent":["import { Router, Request, Response } from 'express';\nimport jwt from 'jsonwebtoken';\nimport { getServerBasePath } from './api/utils/config';\nimport {\n deriveEncryptionKey,\n encryptSession,\n decryptSessionWithKeys,\n SessionPayload,\n} from './session';\nimport { generateCsrfToken, verifyCsrf } from './csrf';\n\nexport interface DauthRouterOptions {\n domainName: string;\n tsk: string;\n dauthUrl?: string;\n cookieName?: string;\n csrfCookieName?: string;\n maxAge?: number;\n secure?: boolean;\n previousTsk?: string;\n sessionSalt?: string;\n}\n\ninterface ResolvedConfig {\n domainName: string;\n dauthBasePath: string;\n cookieName: string;\n csrfCookieName: string;\n maxAgeMs: number;\n secure: boolean;\n encKeys: Buffer[];\n}\n\n// Refresh lock to prevent race conditions on concurrent token rotation\nconst refreshLocks = new Map<string, Promise<SessionPayload | null>>();\n\nfunction lockKey(refreshToken: string): string {\n return refreshToken.substring(0, 16);\n}\n\nfunction clearStaleLocks(): void {\n if (refreshLocks.size > 100) refreshLocks.clear();\n}\n\nasync function resolveConfig(\n opts: DauthRouterOptions\n): Promise<ResolvedConfig> {\n const secure = opts.secure ?? process.env.NODE_ENV !== 'development';\n const cookieName =\n opts.cookieName ?? (secure ? '__Host-dauth-session' : 'dauth-session');\n const csrfCookieName =\n opts.csrfCookieName ?? (secure ? '__Host-csrf' : 'csrf-token');\n const maxAgeMs = (opts.maxAge ?? 30 * 24 * 3600) * 1000;\n\n const keys: Buffer[] = [];\n keys.push(await deriveEncryptionKey(opts.tsk, opts.sessionSalt));\n if (opts.previousTsk) {\n keys.push(await deriveEncryptionKey(opts.previousTsk, opts.sessionSalt));\n }\n\n let dauthBasePath: string;\n if (opts.dauthUrl) {\n dauthBasePath = `${opts.dauthUrl.replace(/\\/+$/, '')}/api/v1`;\n } else {\n dauthBasePath = getServerBasePath();\n }\n\n return {\n domainName: opts.domainName,\n dauthBasePath,\n cookieName,\n csrfCookieName,\n maxAgeMs,\n secure,\n encKeys: keys,\n };\n}\n\nfunction setSessionCookie(\n res: Response,\n payload: SessionPayload,\n config: ResolvedConfig\n): void {\n const encrypted = encryptSession(payload, config.encKeys[0]);\n const cookieOpts: Record<string, unknown> = {\n httpOnly: true,\n secure: config.secure,\n sameSite: 'lax',\n maxAge: config.maxAgeMs,\n path: '/',\n };\n // __Host- prefix requires no domain attribute\n if (!config.secure) {\n // Dev mode: no __Host- prefix, no domain restriction needed\n }\n res.cookie(config.cookieName, encrypted, cookieOpts);\n}\n\nfunction setCsrfCookie(res: Response, config: ResolvedConfig): void {\n const csrfToken = generateCsrfToken();\n res.cookie(config.csrfCookieName, csrfToken, {\n httpOnly: false,\n secure: config.secure,\n sameSite: 'lax',\n maxAge: config.maxAgeMs,\n path: '/',\n });\n}\n\nfunction clearCookies(res: Response, config: ResolvedConfig): void {\n const baseOpts = { path: '/', secure: config.secure };\n res.clearCookie(config.cookieName, baseOpts);\n res.clearCookie(config.csrfCookieName, baseOpts);\n}\n\nfunction readSession(\n req: Request,\n config: ResolvedConfig\n): SessionPayload | null {\n const cookie = req.cookies?.[config.cookieName];\n if (!cookie) return null;\n return decryptSessionWithKeys(cookie, config.encKeys);\n}\n\nfunction isTokenExpiringSoon(token: string, thresholdMs = 300_000): boolean {\n try {\n const decoded = jwt.decode(token) as { exp?: number } | null;\n if (!decoded?.exp) return true;\n return decoded.exp * 1000 - Date.now() < thresholdMs;\n } catch {\n return true;\n }\n}\n\nasync function maybeRefreshTokens(\n session: SessionPayload,\n config: ResolvedConfig,\n res: Response\n): Promise<SessionPayload> {\n if (!isTokenExpiringSoon(session.accessToken)) return session;\n\n const key = lockKey(session.refreshToken);\n clearStaleLocks();\n\n const existingLock = refreshLocks.get(key);\n if (existingLock) {\n const result = await existingLock;\n return result ?? session;\n }\n\n const refreshPromise = (async (): Promise<SessionPayload | null> => {\n try {\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/refresh-token`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n refreshToken: session.refreshToken,\n }),\n }\n );\n if (!response.ok) return null;\n const data = (await response.json()) as {\n accessToken?: string;\n refreshToken?: string;\n };\n if (!data.accessToken || !data.refreshToken) return null;\n const newSession: SessionPayload = {\n accessToken: data.accessToken,\n refreshToken: data.refreshToken,\n };\n setSessionCookie(res, newSession, config);\n return newSession;\n } catch {\n return null;\n }\n })();\n\n refreshLocks.set(key, refreshPromise);\n\n // Timeout safety net: clean lock after 10s\n const timeout = setTimeout(() => refreshLocks.delete(key), 10_000);\n refreshPromise.finally(() => {\n clearTimeout(timeout);\n refreshLocks.delete(key);\n });\n\n const result = await refreshPromise;\n return result ?? session;\n}\n\nexport function dauthRouter(opts: DauthRouterOptions): Router {\n const router = Router();\n let configPromise: Promise<ResolvedConfig> | null = null;\n\n async function getConfig(): Promise<ResolvedConfig> {\n if (!configPromise) configPromise = resolveConfig(opts);\n return configPromise;\n }\n\n // POST /exchange-code — no CSRF (no prior session)\n router.post('/exchange-code', async (req: Request, res: Response) => {\n const config = await getConfig();\n const { code } = req.body;\n if (!code) {\n return res\n .status(400)\n .send({ status: 'code-required', message: 'Code required' });\n }\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/exchange-code`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ code }),\n }\n );\n if (!response.ok) {\n return res\n .status(response.status)\n .send({ status: 'code-invalid', message: 'Code invalid' });\n }\n const data = (await response.json()) as {\n accessToken: string;\n refreshToken: string;\n isNewUser: boolean;\n };\n\n setSessionCookie(\n res,\n {\n accessToken: data.accessToken,\n refreshToken: data.refreshToken,\n },\n config\n );\n setCsrfCookie(res, config);\n\n // Fetch user data to return\n const userResponse = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'GET',\n headers: { Authorization: data.accessToken },\n }\n );\n const userData = (await userResponse.json()) as {\n user?: unknown;\n domain?: unknown;\n };\n\n return res.status(200).send({\n user: userData.user,\n domain: userData.domain,\n isNewUser: data.isNewUser,\n });\n });\n\n // GET /session — no CSRF (read-only)\n router.get('/session', async (req: Request, res: Response) => {\n const config = await getConfig();\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n\n const refreshed = await maybeRefreshTokens(session, config, res);\n\n const userResponse = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'GET',\n headers: { Authorization: refreshed.accessToken },\n }\n );\n if (!userResponse.ok) {\n clearCookies(res, config);\n return res\n .status(401)\n .send({ status: 'session-invalid', message: 'Session expired' });\n }\n const userData = (await userResponse.json()) as {\n user?: unknown;\n domain?: unknown;\n };\n return res.status(200).send({\n user: userData.user,\n domain: userData.domain,\n });\n });\n\n // POST /logout — CSRF required\n router.post('/logout', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (session) {\n // Revoke refresh token server-to-server (fire-and-forget)\n fetch(`${config.dauthBasePath}/app/${config.domainName}/logout`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n refreshToken: session.refreshToken,\n }),\n }).catch(() => {});\n }\n clearCookies(res, config);\n return res.status(200).send({ status: 'success', message: 'Logged out' });\n });\n\n // PATCH /user — CSRF required\n router.patch('/user', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n const refreshed = await maybeRefreshTokens(session, config, res);\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'PATCH',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: refreshed.accessToken,\n },\n body: JSON.stringify(req.body),\n }\n );\n const data = await response.json();\n return res.status(response.status).send(data);\n });\n\n // DELETE /user — CSRF required\n router.delete('/user', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'DELETE',\n headers: { Authorization: session.accessToken },\n }\n );\n const data = await response.json();\n clearCookies(res, config);\n return res.status(response.status).send(data);\n });\n\n return router;\n}\n","import crypto from 'crypto';\nimport type { Request } from 'express';\n\nexport function generateCsrfToken(): string {\n return crypto.randomBytes(32).toString('hex');\n}\n\nexport function verifyCsrf(req: Request, csrfCookieName: string): boolean {\n const headerToken = req.headers['x-csrf-token'] as string | undefined;\n const cookieToken = req.cookies?.[csrfCookieName] as string | undefined;\n if (!headerToken || !cookieToken) return false;\n if (headerToken.length !== cookieToken.length) return false;\n return crypto.timingSafeEqual(\n Buffer.from(headerToken),\n Buffer.from(cookieToken)\n );\n}\n"],"mappings":"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","names":["result"]}
|
|
1
|
+
{"version":3,"sources":["../src/router.ts","../src/csrf.ts"],"sourcesContent":["import { Router, Request, Response } from 'express';\nimport jwt from 'jsonwebtoken';\nimport { getServerBasePath } from './api/utils/config';\nimport {\n deriveEncryptionKey,\n encryptSession,\n decryptSessionWithKeys,\n SessionPayload,\n} from './session';\nimport { generateCsrfToken, verifyCsrf } from './csrf';\n\nexport interface DauthRouterOptions {\n domainName: string;\n tsk: string;\n dauthUrl?: string;\n cookieName?: string;\n csrfCookieName?: string;\n maxAge?: number;\n secure?: boolean;\n previousTsk?: string;\n sessionSalt?: string;\n}\n\ninterface ResolvedConfig {\n domainName: string;\n dauthBasePath: string;\n cookieName: string;\n csrfCookieName: string;\n maxAgeMs: number;\n secure: boolean;\n encKeys: Buffer[];\n}\n\n// Refresh lock to prevent race conditions on concurrent token rotation\nconst refreshLocks = new Map<string, Promise<SessionPayload | null>>();\n\nfunction lockKey(refreshToken: string): string {\n return refreshToken.substring(0, 16);\n}\n\nfunction clearStaleLocks(): void {\n if (refreshLocks.size > 100) refreshLocks.clear();\n}\n\nasync function resolveConfig(\n opts: DauthRouterOptions\n): Promise<ResolvedConfig> {\n const secure = opts.secure ?? process.env.NODE_ENV !== 'development';\n const cookieName =\n opts.cookieName ?? (secure ? '__Host-dauth-session' : 'dauth-session');\n const csrfCookieName =\n opts.csrfCookieName ?? (secure ? '__Host-csrf' : 'csrf-token');\n const maxAgeMs = (opts.maxAge ?? 30 * 24 * 3600) * 1000;\n\n const keys: Buffer[] = [];\n keys.push(await deriveEncryptionKey(opts.tsk, opts.sessionSalt));\n if (opts.previousTsk) {\n keys.push(await deriveEncryptionKey(opts.previousTsk, opts.sessionSalt));\n }\n\n let dauthBasePath: string;\n if (opts.dauthUrl) {\n dauthBasePath = `${opts.dauthUrl.replace(/\\/+$/, '')}/api/v1`;\n } else {\n dauthBasePath = getServerBasePath();\n }\n\n return {\n domainName: opts.domainName,\n dauthBasePath,\n cookieName,\n csrfCookieName,\n maxAgeMs,\n secure,\n encKeys: keys,\n };\n}\n\nfunction setSessionCookie(\n res: Response,\n payload: SessionPayload,\n config: ResolvedConfig\n): void {\n const encrypted = encryptSession(payload, config.encKeys[0]);\n const cookieOpts: Record<string, unknown> = {\n httpOnly: true,\n secure: config.secure,\n sameSite: 'lax',\n maxAge: config.maxAgeMs,\n path: '/',\n };\n // __Host- prefix requires no domain attribute\n if (!config.secure) {\n // Dev mode: no __Host- prefix, no domain restriction needed\n }\n res.cookie(config.cookieName, encrypted, cookieOpts);\n}\n\nfunction setCsrfCookie(res: Response, config: ResolvedConfig): void {\n const csrfToken = generateCsrfToken();\n res.cookie(config.csrfCookieName, csrfToken, {\n httpOnly: false,\n secure: config.secure,\n sameSite: 'lax',\n maxAge: config.maxAgeMs,\n path: '/',\n });\n}\n\nfunction clearCookies(res: Response, config: ResolvedConfig): void {\n const baseOpts = { path: '/', secure: config.secure };\n res.clearCookie(config.cookieName, baseOpts);\n res.clearCookie(config.csrfCookieName, baseOpts);\n}\n\nfunction readSession(\n req: Request,\n config: ResolvedConfig\n): SessionPayload | null {\n const cookie = req.cookies?.[config.cookieName];\n if (!cookie) return null;\n return decryptSessionWithKeys(cookie, config.encKeys);\n}\n\nfunction isTokenExpiringSoon(token: string, thresholdMs = 300_000): boolean {\n try {\n const decoded = jwt.decode(token) as { exp?: number } | null;\n if (!decoded?.exp) return true;\n return decoded.exp * 1000 - Date.now() < thresholdMs;\n } catch {\n return true;\n }\n}\n\nasync function maybeRefreshTokens(\n session: SessionPayload,\n config: ResolvedConfig,\n res: Response\n): Promise<SessionPayload> {\n if (!isTokenExpiringSoon(session.accessToken)) return session;\n\n const key = lockKey(session.refreshToken);\n clearStaleLocks();\n\n const existingLock = refreshLocks.get(key);\n if (existingLock) {\n const result = await existingLock;\n return result ?? session;\n }\n\n const refreshPromise = (async (): Promise<SessionPayload | null> => {\n try {\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/refresh-token`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n refreshToken: session.refreshToken,\n }),\n }\n );\n if (!response.ok) return null;\n const data = (await response.json()) as {\n accessToken?: string;\n refreshToken?: string;\n };\n if (!data.accessToken || !data.refreshToken) return null;\n const newSession: SessionPayload = {\n accessToken: data.accessToken,\n refreshToken: data.refreshToken,\n };\n setSessionCookie(res, newSession, config);\n return newSession;\n } catch {\n return null;\n }\n })();\n\n refreshLocks.set(key, refreshPromise);\n\n // Timeout safety net: clean lock after 10s\n const timeout = setTimeout(() => refreshLocks.delete(key), 10_000);\n refreshPromise.finally(() => {\n clearTimeout(timeout);\n refreshLocks.delete(key);\n });\n\n const result = await refreshPromise;\n return result ?? session;\n}\n\nexport function dauthRouter(opts: DauthRouterOptions): Router {\n const router = Router();\n let configPromise: Promise<ResolvedConfig> | null = null;\n\n async function getConfig(): Promise<ResolvedConfig> {\n if (!configPromise) configPromise = resolveConfig(opts);\n return configPromise;\n }\n\n // POST /exchange-code — no CSRF (no prior session)\n router.post('/exchange-code', async (req: Request, res: Response) => {\n const config = await getConfig();\n const { code } = req.body;\n if (!code) {\n return res\n .status(400)\n .send({ status: 'code-required', message: 'Code required' });\n }\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/exchange-code`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ code }),\n }\n );\n if (!response.ok) {\n return res\n .status(response.status)\n .send({ status: 'code-invalid', message: 'Code invalid' });\n }\n const data = (await response.json()) as {\n accessToken: string;\n refreshToken: string;\n isNewUser: boolean;\n };\n\n setSessionCookie(\n res,\n {\n accessToken: data.accessToken,\n refreshToken: data.refreshToken,\n },\n config\n );\n setCsrfCookie(res, config);\n\n // Fetch user data to return\n const userResponse = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'GET',\n headers: { Authorization: data.accessToken },\n }\n );\n const userData = (await userResponse.json()) as {\n user?: unknown;\n domain?: unknown;\n };\n\n return res.status(200).send({\n user: userData.user,\n domain: userData.domain,\n isNewUser: data.isNewUser,\n });\n });\n\n // GET /session — no CSRF (read-only)\n router.get('/session', async (req: Request, res: Response) => {\n const config = await getConfig();\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n\n const refreshed = await maybeRefreshTokens(session, config, res);\n\n const userResponse = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'GET',\n headers: { Authorization: refreshed.accessToken },\n }\n );\n if (!userResponse.ok) {\n clearCookies(res, config);\n return res\n .status(401)\n .send({ status: 'session-invalid', message: 'Session expired' });\n }\n const userData = (await userResponse.json()) as {\n user?: unknown;\n domain?: unknown;\n };\n return res.status(200).send({\n user: userData.user,\n domain: userData.domain,\n });\n });\n\n // POST /logout — CSRF required\n router.post('/logout', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (session) {\n // Revoke refresh token server-to-server (fire-and-forget)\n fetch(`${config.dauthBasePath}/app/${config.domainName}/logout`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n refreshToken: session.refreshToken,\n }),\n }).catch(() => {});\n }\n clearCookies(res, config);\n return res.status(200).send({ status: 'success', message: 'Logged out' });\n });\n\n // PATCH /user — CSRF required\n router.patch('/user', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n const refreshed = await maybeRefreshTokens(session, config, res);\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'PATCH',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: refreshed.accessToken,\n },\n body: JSON.stringify(req.body),\n }\n );\n const data = await response.json();\n return res.status(response.status).send(data);\n });\n\n // DELETE /user — CSRF required\n router.delete('/user', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'DELETE',\n headers: { Authorization: session.accessToken },\n }\n );\n const data = await response.json();\n clearCookies(res, config);\n return res.status(response.status).send(data);\n });\n\n // GET /passkey/credentials — CSRF required\n router.get(\n '/passkey/credentials',\n async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res.status(403).send({\n status: 'csrf-invalid',\n message: 'CSRF token invalid',\n });\n }\n const session = readSession(req, config);\n if (!session) {\n return res.status(401).send({\n status: 'no-session',\n message: 'Not authenticated',\n });\n }\n const refreshed = await maybeRefreshTokens(\n session,\n config,\n res\n );\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/passkey/credentials`,\n {\n method: 'GET',\n headers: { Authorization: refreshed.accessToken },\n }\n );\n const data = await response.json();\n return res.status(response.status).send(data);\n }\n );\n\n // POST /passkey/register/start — CSRF required\n router.post(\n '/passkey/register/start',\n async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res.status(403).send({\n status: 'csrf-invalid',\n message: 'CSRF token invalid',\n });\n }\n const session = readSession(req, config);\n if (!session) {\n return res.status(401).send({\n status: 'no-session',\n message: 'Not authenticated',\n });\n }\n const refreshed = await maybeRefreshTokens(\n session,\n config,\n res\n );\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/passkey/register/start`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: refreshed.accessToken,\n },\n }\n );\n const data = await response.json();\n return res.status(response.status).send(data);\n }\n );\n\n // POST /passkey/register/finish — CSRF required\n router.post(\n '/passkey/register/finish',\n async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res.status(403).send({\n status: 'csrf-invalid',\n message: 'CSRF token invalid',\n });\n }\n const session = readSession(req, config);\n if (!session) {\n return res.status(401).send({\n status: 'no-session',\n message: 'Not authenticated',\n });\n }\n const refreshed = await maybeRefreshTokens(\n session,\n config,\n res\n );\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/passkey/register/finish`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: refreshed.accessToken,\n },\n body: JSON.stringify(req.body),\n }\n );\n const data = await response.json();\n return res.status(response.status).send(data);\n }\n );\n\n // DELETE /passkey/credentials/:credentialId — CSRF required\n router.delete(\n '/passkey/credentials/:credentialId',\n async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res.status(403).send({\n status: 'csrf-invalid',\n message: 'CSRF token invalid',\n });\n }\n const session = readSession(req, config);\n if (!session) {\n return res.status(401).send({\n status: 'no-session',\n message: 'Not authenticated',\n });\n }\n const refreshed = await maybeRefreshTokens(\n session,\n config,\n res\n );\n\n const { credentialId } = req.params;\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/passkey/credentials/${credentialId}`,\n {\n method: 'DELETE',\n headers: { Authorization: refreshed.accessToken },\n }\n );\n const data = await response.json();\n return res.status(response.status).send(data);\n }\n );\n\n // POST /avatar — CSRF required, multipart forwarding\n router.post('/avatar', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res.status(403).send({\n status: 'csrf-invalid',\n message: 'CSRF token invalid',\n });\n }\n const session = readSession(req, config);\n if (!session) {\n return res.status(401).send({\n status: 'no-session',\n message: 'Not authenticated',\n });\n }\n const refreshed = await maybeRefreshTokens(\n session,\n config,\n res\n );\n\n // Collect raw multipart body (express.json() does not consume it)\n const chunks: Buffer[] = [];\n for await (const chunk of req) {\n chunks.push(Buffer.from(chunk));\n }\n const body = Buffer.concat(chunks);\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user/avatar`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': req.headers['content-type'] || '',\n Authorization: refreshed.accessToken,\n },\n body,\n }\n );\n const data = await response.json();\n return res.status(response.status).send(data);\n });\n\n return router;\n}\n","import crypto from 'crypto';\nimport type { Request } from 'express';\n\nexport function generateCsrfToken(): string {\n return crypto.randomBytes(32).toString('hex');\n}\n\nexport function verifyCsrf(req: Request, csrfCookieName: string): boolean {\n const headerToken = req.headers['x-csrf-token'] as string | undefined;\n const cookieToken = req.cookies?.[csrfCookieName] as string | undefined;\n if (!headerToken || !cookieToken) return false;\n if (headerToken.length !== cookieToken.length) return false;\n return crypto.timingSafeEqual(\n Buffer.from(headerToken),\n Buffer.from(cookieToken)\n );\n}\n"],"mappings":"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","names":["result"]}
|
package/package.json
CHANGED
package/src/api/dauth.api.ts
CHANGED
|
@@ -57,8 +57,7 @@ export async function searchUserByEmail(
|
|
|
57
57
|
},
|
|
58
58
|
}
|
|
59
59
|
);
|
|
60
|
-
const data =
|
|
61
|
-
(await response.json()) as TenantUserResponse['data'];
|
|
60
|
+
const data = (await response.json()) as TenantUserResponse['data'];
|
|
62
61
|
return { response: { status: response.status }, data };
|
|
63
62
|
}
|
|
64
63
|
|
|
@@ -77,8 +76,7 @@ export async function getUserById(
|
|
|
77
76
|
},
|
|
78
77
|
}
|
|
79
78
|
);
|
|
80
|
-
const data =
|
|
81
|
-
(await response.json()) as TenantUserResponse['data'];
|
|
79
|
+
const data = (await response.json()) as TenantUserResponse['data'];
|
|
82
80
|
return { response: { status: response.status }, data };
|
|
83
81
|
}
|
|
84
82
|
|
|
@@ -98,7 +96,6 @@ export async function batchGetUsers(
|
|
|
98
96
|
body: JSON.stringify({ userIds }),
|
|
99
97
|
}
|
|
100
98
|
);
|
|
101
|
-
const data =
|
|
102
|
-
(await response.json()) as BatchUsersResponse['data'];
|
|
99
|
+
const data = (await response.json()) as BatchUsersResponse['data'];
|
|
103
100
|
return { response: { status: response.status }, data };
|
|
104
101
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { Request, NextFunction, Response as ExpressResponse } from 'express';
|
|
2
2
|
import jwt from 'jsonwebtoken';
|
|
3
3
|
import { getUser } from './api/dauth.api';
|
|
4
|
-
export {
|
|
5
|
-
searchUserByEmail,
|
|
6
|
-
getUserById,
|
|
7
|
-
batchGetUsers,
|
|
8
|
-
} from './api/dauth.api';
|
|
4
|
+
export { searchUserByEmail, getUserById, batchGetUsers } from './api/dauth.api';
|
|
9
5
|
export type { TenantUser } from './api/dauth.api';
|
|
10
6
|
import { UserCache } from './cache';
|
|
11
7
|
import type { CacheOptions } from './cache';
|
package/src/router.ts
CHANGED
|
@@ -374,5 +374,201 @@ export function dauthRouter(opts: DauthRouterOptions): Router {
|
|
|
374
374
|
return res.status(response.status).send(data);
|
|
375
375
|
});
|
|
376
376
|
|
|
377
|
+
// GET /passkey/credentials — CSRF required
|
|
378
|
+
router.get(
|
|
379
|
+
'/passkey/credentials',
|
|
380
|
+
async (req: Request, res: Response) => {
|
|
381
|
+
const config = await getConfig();
|
|
382
|
+
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
383
|
+
return res.status(403).send({
|
|
384
|
+
status: 'csrf-invalid',
|
|
385
|
+
message: 'CSRF token invalid',
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
const session = readSession(req, config);
|
|
389
|
+
if (!session) {
|
|
390
|
+
return res.status(401).send({
|
|
391
|
+
status: 'no-session',
|
|
392
|
+
message: 'Not authenticated',
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
const refreshed = await maybeRefreshTokens(
|
|
396
|
+
session,
|
|
397
|
+
config,
|
|
398
|
+
res
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const response = await fetch(
|
|
402
|
+
`${config.dauthBasePath}/app/${config.domainName}/passkey/credentials`,
|
|
403
|
+
{
|
|
404
|
+
method: 'GET',
|
|
405
|
+
headers: { Authorization: refreshed.accessToken },
|
|
406
|
+
}
|
|
407
|
+
);
|
|
408
|
+
const data = await response.json();
|
|
409
|
+
return res.status(response.status).send(data);
|
|
410
|
+
}
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
// POST /passkey/register/start — CSRF required
|
|
414
|
+
router.post(
|
|
415
|
+
'/passkey/register/start',
|
|
416
|
+
async (req: Request, res: Response) => {
|
|
417
|
+
const config = await getConfig();
|
|
418
|
+
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
419
|
+
return res.status(403).send({
|
|
420
|
+
status: 'csrf-invalid',
|
|
421
|
+
message: 'CSRF token invalid',
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
const session = readSession(req, config);
|
|
425
|
+
if (!session) {
|
|
426
|
+
return res.status(401).send({
|
|
427
|
+
status: 'no-session',
|
|
428
|
+
message: 'Not authenticated',
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
const refreshed = await maybeRefreshTokens(
|
|
432
|
+
session,
|
|
433
|
+
config,
|
|
434
|
+
res
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const response = await fetch(
|
|
438
|
+
`${config.dauthBasePath}/app/${config.domainName}/passkey/register/start`,
|
|
439
|
+
{
|
|
440
|
+
method: 'POST',
|
|
441
|
+
headers: {
|
|
442
|
+
'Content-Type': 'application/json',
|
|
443
|
+
Authorization: refreshed.accessToken,
|
|
444
|
+
},
|
|
445
|
+
}
|
|
446
|
+
);
|
|
447
|
+
const data = await response.json();
|
|
448
|
+
return res.status(response.status).send(data);
|
|
449
|
+
}
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
// POST /passkey/register/finish — CSRF required
|
|
453
|
+
router.post(
|
|
454
|
+
'/passkey/register/finish',
|
|
455
|
+
async (req: Request, res: Response) => {
|
|
456
|
+
const config = await getConfig();
|
|
457
|
+
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
458
|
+
return res.status(403).send({
|
|
459
|
+
status: 'csrf-invalid',
|
|
460
|
+
message: 'CSRF token invalid',
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
const session = readSession(req, config);
|
|
464
|
+
if (!session) {
|
|
465
|
+
return res.status(401).send({
|
|
466
|
+
status: 'no-session',
|
|
467
|
+
message: 'Not authenticated',
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
const refreshed = await maybeRefreshTokens(
|
|
471
|
+
session,
|
|
472
|
+
config,
|
|
473
|
+
res
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
const response = await fetch(
|
|
477
|
+
`${config.dauthBasePath}/app/${config.domainName}/passkey/register/finish`,
|
|
478
|
+
{
|
|
479
|
+
method: 'POST',
|
|
480
|
+
headers: {
|
|
481
|
+
'Content-Type': 'application/json',
|
|
482
|
+
Authorization: refreshed.accessToken,
|
|
483
|
+
},
|
|
484
|
+
body: JSON.stringify(req.body),
|
|
485
|
+
}
|
|
486
|
+
);
|
|
487
|
+
const data = await response.json();
|
|
488
|
+
return res.status(response.status).send(data);
|
|
489
|
+
}
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
// DELETE /passkey/credentials/:credentialId — CSRF required
|
|
493
|
+
router.delete(
|
|
494
|
+
'/passkey/credentials/:credentialId',
|
|
495
|
+
async (req: Request, res: Response) => {
|
|
496
|
+
const config = await getConfig();
|
|
497
|
+
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
498
|
+
return res.status(403).send({
|
|
499
|
+
status: 'csrf-invalid',
|
|
500
|
+
message: 'CSRF token invalid',
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
const session = readSession(req, config);
|
|
504
|
+
if (!session) {
|
|
505
|
+
return res.status(401).send({
|
|
506
|
+
status: 'no-session',
|
|
507
|
+
message: 'Not authenticated',
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
const refreshed = await maybeRefreshTokens(
|
|
511
|
+
session,
|
|
512
|
+
config,
|
|
513
|
+
res
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
const { credentialId } = req.params;
|
|
517
|
+
const response = await fetch(
|
|
518
|
+
`${config.dauthBasePath}/app/${config.domainName}/passkey/credentials/${credentialId}`,
|
|
519
|
+
{
|
|
520
|
+
method: 'DELETE',
|
|
521
|
+
headers: { Authorization: refreshed.accessToken },
|
|
522
|
+
}
|
|
523
|
+
);
|
|
524
|
+
const data = await response.json();
|
|
525
|
+
return res.status(response.status).send(data);
|
|
526
|
+
}
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
// POST /avatar — CSRF required, multipart forwarding
|
|
530
|
+
router.post('/avatar', async (req: Request, res: Response) => {
|
|
531
|
+
const config = await getConfig();
|
|
532
|
+
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
533
|
+
return res.status(403).send({
|
|
534
|
+
status: 'csrf-invalid',
|
|
535
|
+
message: 'CSRF token invalid',
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
const session = readSession(req, config);
|
|
539
|
+
if (!session) {
|
|
540
|
+
return res.status(401).send({
|
|
541
|
+
status: 'no-session',
|
|
542
|
+
message: 'Not authenticated',
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
const refreshed = await maybeRefreshTokens(
|
|
546
|
+
session,
|
|
547
|
+
config,
|
|
548
|
+
res
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
// Collect raw multipart body (express.json() does not consume it)
|
|
552
|
+
const chunks: Buffer[] = [];
|
|
553
|
+
for await (const chunk of req) {
|
|
554
|
+
chunks.push(Buffer.from(chunk));
|
|
555
|
+
}
|
|
556
|
+
const body = Buffer.concat(chunks);
|
|
557
|
+
|
|
558
|
+
const response = await fetch(
|
|
559
|
+
`${config.dauthBasePath}/app/${config.domainName}/user/avatar`,
|
|
560
|
+
{
|
|
561
|
+
method: 'POST',
|
|
562
|
+
headers: {
|
|
563
|
+
'Content-Type': req.headers['content-type'] || '',
|
|
564
|
+
Authorization: refreshed.accessToken,
|
|
565
|
+
},
|
|
566
|
+
body,
|
|
567
|
+
}
|
|
568
|
+
);
|
|
569
|
+
const data = await response.json();
|
|
570
|
+
return res.status(response.status).send(data);
|
|
571
|
+
});
|
|
572
|
+
|
|
377
573
|
return router;
|
|
378
574
|
}
|