# User Credentials Module Secure storage and management of user API keys, OAuth tokens, and other credentials. ## Overview The User Credentials module provides encrypted storage for sensitive user credentials with: - **Encryption**: OpenBao Transit (preferred) or AES-256-GCM fallback - **Access Control**: Row-Level Security (RLS) with scope-based isolation - **Audit Logging**: Automatic tracking of credential access and modifications - **Multi-tenancy**: USER, WORKSPACE, and SYSTEM scope support ## Database Model ### UserCredential | Field | Type | Description | | ---------------- | --------------- | ----------------------------------------------- | | `id` | UUID | Primary key | | `userId` | UUID | Owner (FK to users) | | `workspaceId` | UUID? | Optional workspace (FK to workspaces) | | `name` | String | Display name (e.g., "GitHub Personal Token") | | `provider` | String | Provider identifier (e.g., "github", "openai") | | `type` | CredentialType | Type of credential (API_KEY, OAUTH_TOKEN, etc.) | | `scope` | CredentialScope | Access scope (USER, WORKSPACE, SYSTEM) | | `encryptedValue` | Text | Encrypted credential value | | `maskedValue` | String? | Masked value for display (e.g., "\*\*\*\*abcd") | | `description` | Text? | Optional description | | `expiresAt` | Timestamp? | Optional expiration date | | `lastUsedAt` | Timestamp? | Last access timestamp | | `metadata` | JSONB | Provider-specific metadata | | `isActive` | Boolean | Soft delete flag (default: true) | | `rotatedAt` | Timestamp? | Last rotation timestamp | | `createdAt` | Timestamp | Creation timestamp | | `updatedAt` | Timestamp | Last update timestamp | ### Enums **CredentialType** - `API_KEY` - API keys (GitHub, GitLab, etc.) - `OAUTH_TOKEN` - OAuth tokens - `ACCESS_TOKEN` - Access tokens - `SECRET` - Generic secrets - `PASSWORD` - Passwords - `CUSTOM` - Custom credential types **CredentialScope** - `USER` - Personal credentials (visible only to owner) - `WORKSPACE` - Workspace-scoped credentials (visible to workspace admins) - `SYSTEM` - System-level credentials (admin access only) ## Security ### Row-Level Security (RLS) All credential access is enforced via PostgreSQL RLS policies: ```sql -- USER scope: Owner access only scope = 'USER' AND user_id = current_user_id() -- WORKSPACE scope: Workspace admin access scope = 'WORKSPACE' AND is_workspace_admin(workspace_id, current_user_id()) -- SYSTEM scope: Admin/migration bypass only current_user_id() IS NULL ``` ### Encryption Credentials are encrypted at rest using: 1. **OpenBao Transit** (preferred) - Format: `vault:v1:base64data` - Named key: `mosaic-credentials` - Handled by `VaultService` 2. **AES-256-GCM** (fallback) - Format: `iv:authTag:encrypted` - Handled by `CryptoService` - Uses `ENCRYPTION_KEY` environment variable The encryption middleware automatically falls back to AES when OpenBao is unavailable. ### Activity Logging All credential operations are logged: - `CREDENTIAL_CREATED` - Credential created - `CREDENTIAL_ACCESSED` - Credential value decrypted - `CREDENTIAL_ROTATED` - Credential value rotated - `CREDENTIAL_REVOKED` - Credential soft-deleted ## Usage ### Creating a Credential ```typescript const credential = await prisma.userCredential.create({ data: { userId: currentUserId, name: "GitHub Personal Token", provider: "github", type: CredentialType.API_KEY, scope: CredentialScope.USER, encryptedValue: await vaultService.encrypt(plainValue, TransitKey.CREDENTIALS), maskedValue: maskValue(plainValue), // "****abcd" metadata: { scopes: ["repo", "user"] }, }, }); ``` ### Retrieving a Credential ```typescript // List credentials (encrypted values NOT returned) const credentials = await prisma.userCredential.findMany({ where: { userId: currentUserId, scope: CredentialScope.USER }, select: { id: true, name: true, provider: true, type: true, maskedValue: true, lastUsedAt: true, }, }); // Decrypt a specific credential (audit logged) const credential = await prisma.userCredential.findUnique({ where: { id: credentialId }, }); const plainValue = await vaultService.decrypt(credential.encryptedValue, TransitKey.CREDENTIALS); // Update lastUsedAt await prisma.userCredential.update({ where: { id: credentialId }, data: { lastUsedAt: new Date() }, }); // Log access await activityLog.log({ action: ActivityAction.CREDENTIAL_ACCESSED, entityType: EntityType.CREDENTIAL, entityId: credentialId, }); ``` ### Rotating a Credential ```typescript await prisma.userCredential.update({ where: { id: credentialId }, data: { encryptedValue: await vaultService.encrypt(newPlainValue, TransitKey.CREDENTIALS), maskedValue: maskValue(newPlainValue), rotatedAt: new Date(), }, }); ``` ### Soft Deleting (Revoking) ```typescript await prisma.userCredential.update({ where: { id: credentialId }, data: { isActive: false }, }); ``` ## API Endpoints (Planned - Issue #356) ``` POST /api/credentials Create credential GET /api/credentials List credentials (masked values only) GET /api/credentials/:id Get single credential (masked) GET /api/credentials/:id/value Decrypt and return value (audit logged) PATCH /api/credentials/:id Update metadata POST /api/credentials/:id/rotate Rotate credential value DELETE /api/credentials/:id Soft-delete (revoke) ``` ## Testing Run tests with: ```bash pnpm test apps/api/src/credentials/user-credential.model.spec.ts ``` Tests cover: - Model structure and constraints - Enum validation - Unique constraints - Foreign key relations - Cascade deletes - Timestamps - JSONB metadata ## Related Issues - **#355** - Create UserCredential Prisma model with RLS policies (this issue) - **#356** - Build credential CRUD API endpoints - **#353** - VaultService NestJS module for OpenBao Transit - **#350** - Add RLS policies to auth tables with FORCE enforcement ## Migration Migration: `20260207_add_user_credentials` Apply with: ```bash cd apps/api pnpm prisma migrate deploy ``` ## References - Design doc: `docs/design/credential-security.md` - OpenBao setup: `docs/OPENBAO.md` - RLS context: `apps/api/src/lib/db-context.ts` - CryptoService: `apps/api/src/federation/crypto.service.ts`