vault: session-gated auth-injecting proxy + token broker
Implements the platform half of VAULT_AUTH_SPEC.md. /api/vault is now gated on
an owner platform session (userMiddleware, no bodyParser → streaming preserved)
and origin-scoped as before; the device holds no Vaultwarden token.
- POST /session/login {email, authHash, kdf, device*} → broker calls Vaultwarden
/identity/connect/token via the sidecar, stores the encrypted token set tied to
the owner, and returns {protectedUserKey, privateKey, kdf} (ciphertext to us).
- GET/PUT /unlock-key → store/release the Officer-app protector key (owner only).
- Catch-all proxy swaps the incoming platform JWT for the stored Vaultwarden
access token, proactively refreshes near expiry, and retries once on a 401 for
replayable requests. Bodies are never parsed.
client_id column added to vault_tokens (needed to refresh). Broker error text is
read across Vaultwarden's message/errorModel/error fields.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -105,6 +105,7 @@ export type {
|
||||
export {
|
||||
getVaultTokens,
|
||||
setVaultTokens,
|
||||
updateVaultAccess,
|
||||
clearVaultTokens,
|
||||
getVaultUnlockKey,
|
||||
setVaultUnlockKey,
|
||||
|
||||
@@ -11,6 +11,7 @@ export type VaultTokenSet = {
|
||||
refreshToken: string;
|
||||
expiresAt: Date | null;
|
||||
deviceIdentifier: string | null;
|
||||
clientId: string | null;
|
||||
};
|
||||
|
||||
/** The owner's brokered Vaultwarden token set (decrypted), or null if none is stored. */
|
||||
@@ -22,6 +23,7 @@ export async function getVaultTokens(userId: number): Promise<VaultTokenSet | nu
|
||||
refreshToken: decryptSecret(row.refreshToken),
|
||||
expiresAt: row.expiresAt,
|
||||
deviceIdentifier: row.deviceIdentifier,
|
||||
clientId: row.clientId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,6 +35,7 @@ export async function setVaultTokens(userId: number, t: VaultTokenSet): Promise<
|
||||
refreshToken: encryptSecret(t.refreshToken),
|
||||
expiresAt: t.expiresAt,
|
||||
deviceIdentifier: t.deviceIdentifier,
|
||||
clientId: t.clientId,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
await db
|
||||
@@ -45,11 +48,30 @@ export async function setVaultTokens(userId: number, t: VaultTokenSet): Promise<
|
||||
refreshToken: values.refreshToken,
|
||||
expiresAt: values.expiresAt,
|
||||
deviceIdentifier: values.deviceIdentifier,
|
||||
clientId: values.clientId,
|
||||
updatedAt: values.updatedAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Update only the token pair + expiry after a refresh (keeps device/client). */
|
||||
export async function updateVaultAccess(
|
||||
userId: number,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
expiresAt: Date | null,
|
||||
): Promise<void> {
|
||||
await db
|
||||
.update(vaultTokens)
|
||||
.set({
|
||||
accessToken: encryptSecret(accessToken),
|
||||
refreshToken: encryptSecret(refreshToken),
|
||||
expiresAt,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(vaultTokens.userId, userId));
|
||||
}
|
||||
|
||||
/** Drop the token set (platform logout / distress / panic). */
|
||||
export async function clearVaultTokens(userId: number): Promise<void> {
|
||||
await db.delete(vaultTokens).where(eq(vaultTokens.userId, userId));
|
||||
|
||||
@@ -15,6 +15,7 @@ export const vaultTokens = pgTable('vault_tokens', {
|
||||
refreshToken: text('refresh_token').notNull(), // encrypted
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }),
|
||||
deviceIdentifier: text('device_identifier'), // plaintext; the last device that brokered a login
|
||||
clientId: text('client_id'), // plaintext; the connect/token client_id, needed to refresh
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user