Skip to content

Commit 9484ffb

Browse files
committed
chore: merge main into v2
Brings the identity-API fixes (#57, #59) and the EQL docs build fix (#58) into the v2 branch. Without this, merging v2 to main at launch would revert them: v2's copy of content/stack still taught LockContext.identify(), which is deprecated and no longer affects encryption. One conflict, in scripts/generate-eql-docs.ts. Both branches carry the same `<tt>`-stripping fix; they differ only in whether the .replace chain is wrapped in parentheses. Kept v2's formatting.
2 parents b4883db + 5aef217 commit 9484ffb

11 files changed

Lines changed: 212 additions & 194 deletions

File tree

content/stack/cipherstash/encryption/bulk-operations.mdx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,21 +160,18 @@ For the table setup and single-record insert pattern, see [Storing encrypted dat
160160

161161
## Identity-aware bulk encryption
162162

163-
Lock an entire batch to a user's identity by chaining `.withLockContext()`:
163+
Lock an entire batch to a user's identity by chaining `.withLockContext()`. The client must be authenticated as that user with `OidcFederationStrategy`.
164164

165165
```typescript filename="bulk-encrypt-identity.ts"
166-
import { LockContext } from "@cipherstash/stack/identity"
167-
168-
const lc = new LockContext()
169-
const lockContext = (await lc.identify(userJwt)).data!
166+
const claim = { identityClaim: ["sub"] }
170167

171168
const encrypted = await client
172169
.bulkEncrypt(plaintexts, { column: users.email, table: users })
173-
.withLockContext(lockContext)
170+
.withLockContext(claim)
174171

175172
const decrypted = await client
176173
.bulkDecrypt(encrypted.data)
177-
.withLockContext(lockContext)
174+
.withLockContext(claim)
178175
```
179176

180177
See [Identity-aware encryption](/stack/cipherstash/encryption/identity) for the full lock context flow.

content/stack/cipherstash/encryption/encrypt-decrypt.mdx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,26 +165,28 @@ const back = await client.bulkDecryptModels<User>(encrypted.data)
165165

166166
## Identity-aware operations
167167

168-
Any encrypt or decrypt operation can be scoped to a specific user with a lock context.
168+
Any encrypt or decrypt operation can be bound to a claim from the signed-in user's JWT, provided the client is authenticated as that user with `OidcFederationStrategy`.
169169
See [Identity-aware encryption](/stack/cipherstash/encryption/identity) for details.
170170

171171
```typescript filename="identity-encrypt.ts"
172+
const claim = { identityClaim: ["sub"] }
173+
172174
const encrypted = await client
173175
.encryptModel(user, users)
174-
.withLockContext(lockContext)
176+
.withLockContext(claim)
175177

176178
const decrypted = await client
177179
.decryptModel(encryptedUser)
178-
.withLockContext(lockContext)
180+
.withLockContext(claim)
179181

180182
// Also works with bulk operations
181183
const bulkEncrypted = await client
182184
.bulkEncryptModels(userModels, users)
183-
.withLockContext(lockContext)
185+
.withLockContext(claim)
184186

185187
const bulkDecrypted = await client
186188
.bulkDecryptModels(encryptedUsers)
187-
.withLockContext(lockContext)
189+
.withLockContext(claim)
188190
```
189191

190192
## Audit logging
@@ -202,6 +204,6 @@ Chain `.audit()` with `.withLockContext()` on the same operation:
202204
```typescript filename="audit-model.ts"
203205
const result = await client
204206
.encryptModel(user, users)
205-
.withLockContext(lockContext)
207+
.withLockContext({ identityClaim: ["sub"] })
206208
.audit({ metadata: { action: "user-signup", requestId: "abc-123" } })
207209
```
Lines changed: 96 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,195 +1,175 @@
11
---
22
title: Identity-aware encryption
3-
description: Use LockContext in @cipherstash/stack to tie encryption to a user JWT so only that identity can decrypt their data, including Clerk and Next.js setup.
3+
description: Authenticate as the end user with OidcFederationStrategy and bind encryption to a JWT claim with withLockContext, so only that identity can decrypt their data.
44
---
55

6-
Lock encryption to a specific user by requiring a valid JWT for decryption. When a value is encrypted with a lock context, it can only be decrypted by presenting the same user's identity token.
6+
Lock encryption to a specific user, so a value can only be decrypted while the client is authenticated as the identity that encrypted it.
77

88
## How it works
99

1010
<Callout type="warn">
1111
Lock contexts require a Business or Enterprise workspace plan.
1212
</Callout>
1313

14+
Two pieces combine.
15+
16+
**An auth strategy** decides who the client is when it talks to ZeroKMS. `OidcFederationStrategy` federates a signed-in user's OIDC JWT (from Supabase, Clerk, Auth0, or Okta) into a CipherStash token, so every ZeroKMS request is made as that user rather than as your service.
17+
18+
**A lock context** names which claim from that user's JWT to bind the value to, typically `sub`. ZeroKMS resolves the claim's value from the token authenticating the request and bakes it into the data key's tag.
19+
20+
Lock context is layered on top of the strategy: it requires `OidcFederationStrategy`, but the strategy does not require lock context. Authenticating as the user gives you a per-user audit trail; adding lock context gives you the cryptographic binding.
21+
1422
Lock contexts are useful for:
1523

1624
- Multi-tenant applications where each user's data must be isolated
1725
- Compliance requirements that demand per-user encryption boundaries
1826
- Applications where you need to prove that only authorized users accessed specific records
1927

20-
The flow is:
28+
## Basic usage
2129

22-
1. Create a `LockContext` instance.
23-
2. Identify the user with their JWT.
24-
3. Pass the lock context to encrypt and decrypt operations.
30+
Register your identity provider with the workspace first, on the [OIDC providers](https://dashboard.cipherstash.com/workspaces/_/oidc-providers) page in the Dashboard. The `_` in that URL resolves to whichever workspace you have selected.
2531

26-
## Basic usage
32+
Construct the client with `OidcFederationStrategy`. Pass a function returning the **current** provider JWT: the strategy calls it again whenever it needs to re-federate, so do not capture a token once.
2733

28-
```typescript filename="identity.ts"
29-
import { LockContext } from "@cipherstash/stack/identity"
34+
```typescript filename="client.ts"
35+
import { Encryption, OidcFederationStrategy } from "@cipherstash/stack"
36+
import { users } from "./schema"
3037

31-
// 1. Create a lock context (defaults to the "sub" claim)
32-
const lc = new LockContext()
38+
export const client = await Encryption({
39+
schemas: [users],
40+
config: {
41+
authStrategy: OidcFederationStrategy.create(
42+
process.env.CS_WORKSPACE_CRN,
43+
() => getUserJwt(),
44+
),
45+
},
46+
})
47+
```
3348

34-
// 2. Identify the user with their JWT
35-
const identifyResult = await lc.identify(userJwt)
49+
`OidcFederationStrategy` is re-exported from `@cipherstash/stack`, so a separate `@cipherstash/auth` import is not needed.
3650

37-
if (identifyResult.failure) {
38-
throw new Error(identifyResult.failure.message)
39-
}
51+
<Callout type="warn">
52+
`OidcFederationStrategy` is the only strategy that supports lock contexts. `AccessKeyStrategy` authenticates as your service rather than as an end user, so there is no user identity for ZeroKMS to resolve the claim against.
53+
</Callout>
4054

41-
const lockContext = identifyResult.data
55+
Then bind operations to the user's claim. Every example below uses the `client` above: without its `authStrategy`, `.withLockContext()` has no end-user identity to bind to.
4256

43-
// 3. Encrypt with lock context
57+
```typescript filename="identity.ts"
58+
// Encrypt, binding the data key to the user's `sub` claim
4459
const encrypted = await client
4560
.encrypt("sensitive data", { column: users.email, table: users })
46-
.withLockContext(lockContext)
61+
.withLockContext({ identityClaim: ["sub"] })
4762

48-
// 4. Decrypt with the same lock context
63+
// Decrypt with the same claim, as the same user
4964
const decrypted = await client
5065
.decrypt(encrypted.data)
51-
.withLockContext(lockContext)
66+
.withLockContext({ identityClaim: ["sub"] })
5267
```
5368

69+
<Callout type="warn">
70+
`lockContext.identify(jwt)` is **deprecated**. Per-operation CTS tokens were removed in `protect-ffi` 0.25, so `identify()` no longer affects encryption: code that still calls it compiles and logs a deprecation warning, but the token it fetches is unused. Authenticate the client with `OidcFederationStrategy` and pass the claim to `.withLockContext()` instead.
71+
72+
Constructing a `LockContext` is not deprecated, it is simply optional here. `.withLockContext()` accepts either a `LockContext` or a plain `{ identityClaim }`.
73+
</Callout>
74+
5475
## Supported operations
5576

5677
Lock contexts work with all encrypt and decrypt operations:
5778

5879
```typescript filename="identity.ts"
59-
// Single operations
60-
const encrypted = await client
61-
.encryptModel(user, users)
62-
.withLockContext(lockContext)
80+
const claim = { identityClaim: ["sub"] }
6381

64-
const decrypted = await client
65-
.decryptModel(encryptedUser)
66-
.withLockContext(lockContext)
82+
// Single operations
83+
const encrypted = await client.encryptModel(user, users).withLockContext(claim)
84+
const decrypted = await client.decryptModel(encryptedUser).withLockContext(claim)
6785

6886
// Bulk operations
6987
const bulkEncrypted = await client
7088
.bulkEncryptModels(userModels, users)
71-
.withLockContext(lockContext)
89+
.withLockContext(claim)
7290

7391
const bulkDecrypted = await client
7492
.bulkDecryptModels(encryptedUsers)
75-
.withLockContext(lockContext)
93+
.withLockContext(claim)
7694

7795
// Query operations
7896
const term = await client
79-
.encryptQuery("user@example.com", {
80-
column: users.email,
81-
table: users,
82-
})
83-
.withLockContext(lockContext)
97+
.encryptQuery("user@example.com", { column: users.email, table: users })
98+
.withLockContext(claim)
8499
```
85100

86101
## Custom identity claims
87102

88-
Override the default context by specifying which identity claims to use:
89-
90-
```typescript filename="identity.ts"
91-
const lc = new LockContext({
92-
context: {
93-
identityClaim: ["sub"], // this is the default
94-
},
95-
})
96-
```
103+
`identityClaim` selects which claim (or claims) of the user's JWT ZeroKMS binds to.
97104

98105
| Identity claim | Description |
99106
|---|---|
100107
| `sub` | The user's subject identifier |
101-
| `scopes` | The user's scopes set by your IDP policy |
108+
| `scopes` | The user's scopes, set by your IdP policy |
102109

103-
Combine claims for identity and permissions scoping:
110+
Combine claims to scope by identity and permissions together:
104111

105112
```typescript filename="identity.ts"
106-
const lc = new LockContext({
107-
context: {
108-
identityClaim: ["sub", "scopes"],
109-
},
110-
})
111-
```
112-
113-
## Using with Clerk and Next.js
114-
115-
Install the `@cipherstash/nextjs` package for automatic CTS token setup with [Clerk](https://clerk.com/):
116-
117-
```bash cta cta-type="install" example-id="install-nextjs-identity"
118-
npm install @cipherstash/nextjs
119-
```
120-
121-
### Set up middleware
122-
123-
In your `middleware.ts`, use `protectClerkMiddleware` to automatically generate CTS tokens for every user session:
124-
125-
```typescript filename="middleware.ts"
126-
import { clerkMiddleware } from "@clerk/nextjs/server"
127-
import { protectClerkMiddleware } from "@cipherstash/nextjs/clerk"
128-
129-
export default clerkMiddleware(async (auth, req) => {
130-
return protectClerkMiddleware(auth, req)
131-
})
113+
await client
114+
.encrypt("sensitive data", { column: users.email, table: users })
115+
.withLockContext({ identityClaim: ["sub", "scopes"] })
132116
```
133117

134-
### Retrieve the CTS token
135-
136-
Use `getCtsToken` to get the CTS token for the current user:
137-
138-
```typescript filename="page.tsx"
139-
import { getCtsToken } from "@cipherstash/nextjs"
140-
141-
export default async function Page() {
142-
const ctsToken = await getCtsToken()
118+
The same claim must be supplied to decrypt. A value encrypted under `["sub"]` will not decrypt under `["sub", "scopes"]`.
143119

144-
if (!ctsToken.success) {
145-
// handle error
146-
}
120+
## Using with Clerk and Next.js
147121

148-
// ctsToken is ready to use
122+
Clerk is an OIDC provider like any other: hand `OidcFederationStrategy` a function that returns the current Clerk session token.
123+
124+
```typescript filename="client.ts"
125+
import { auth } from "@clerk/nextjs/server"
126+
import { Encryption, OidcFederationStrategy } from "@cipherstash/stack"
127+
import { users } from "./schema"
128+
129+
export async function getClient() {
130+
return Encryption({
131+
schemas: [users],
132+
config: {
133+
authStrategy: OidcFederationStrategy.create(
134+
process.env.CS_WORKSPACE_CRN,
135+
async () => {
136+
const { getToken } = await auth()
137+
return await getToken()
138+
},
139+
),
140+
},
141+
})
149142
}
150143
```
151144

152-
### Create a LockContext with an existing CTS token
153-
154-
Since the CTS token is already available from the middleware, construct the `LockContext` directly. The CTS token has the shape `{ accessToken: string, expiry: number }`. Passing it directly avoids a second round-trip to CTS.
155-
156-
```typescript filename="page.tsx"
157-
import { LockContext } from "@cipherstash/stack/identity"
158-
import { getCtsToken } from "@cipherstash/nextjs"
159-
160-
export default async function Page() {
161-
const ctsToken = await getCtsToken()
162-
163-
if (!ctsToken.success) {
164-
// handle error
165-
}
166-
167-
const lockContext = new LockContext({ ctsToken })
145+
Because the callback is re-invoked on every re-federation, it picks up a refreshed Clerk token automatically.
168146

169-
// Use lockContext with encrypt/decrypt operations
170-
}
171-
```
172-
173-
`getCtsToken` returns `{ success: true, ctsToken: CtsToken }` on success or `{ success: false, error: string }` on failure.
147+
<Callout type="info">
148+
The `@cipherstash/nextjs` package (`protectClerkMiddleware`, `getCtsToken`) belongs to the earlier CTS-token flow, in which a token was fetched per request and handed to `new LockContext({ ctsToken })`. Encryption operations no longer consume a CTS token, so that middleware is not required for identity-aware encryption.
149+
</Callout>
174150

175151
## Error handling
176152

177-
The `identify` method returns a `Result` type:
153+
Encryption operations return a `Result`.
178154

179155
```typescript filename="identity.ts"
180-
const result = await lc.identify(userJwt)
156+
const result = await client
157+
.encrypt("sensitive data", { column: users.email, table: users })
158+
.withLockContext({ identityClaim: ["sub"] })
181159

182160
if (result.failure) {
183-
// result.failure.type is 'CtsTokenError'
184-
console.error("CTS token exchange failed:", result.failure.message)
161+
console.error("Encryption failed:", result.failure.message)
185162
}
186163
```
187164

188165
Common failure scenarios:
189166

190-
| Scenario | Error type | Description |
191-
|---|---|---|
192-
| Invalid JWT | `CtsTokenError` | The JWT token was rejected by CTS |
193-
| Network failure | `CtsTokenError` | Could not reach the CTS endpoint |
194-
| Missing workspace | Runtime error | `CS_WORKSPACE_CRN` is not configured |
195-
| Expired CTS token | `LockContextError` | The CTS token has expired. Call `identify` again |
167+
| Scenario | Description |
168+
|---|---|
169+
| Invalid or expired provider JWT | Federation is rejected. The callback should return a live token, not one captured earlier |
170+
| Provider not registered | The OIDC provider has not been added to the workspace |
171+
| Network failure | The CipherStash API could not be reached |
172+
| Missing workspace | `CS_WORKSPACE_CRN` is not configured |
173+
| Claim mismatch on decrypt | The value was encrypted under a different `identityClaim`, or as a different user |
174+
175+
See [Error handling](/stack/reference/error-handling) for the full set of error types.

content/stack/cipherstash/encryption/models.mdx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -216,27 +216,24 @@ See [Error handling](/stack/reference/error-handling) for the full set of error
216216

217217
## Identity-aware model operations
218218

219-
Chain `.withLockContext()` to bind encryption to a user's JWT:
219+
Chain `.withLockContext()` to bind encryption to a claim from the signed-in user's JWT. The client must be authenticated as that user with `OidcFederationStrategy`.
220220

221221
```typescript filename="model-with-identity.ts"
222-
import { LockContext } from "@cipherstash/stack/identity"
223-
224-
const lc = new LockContext()
225-
const lockContext = (await lc.identify(userJwt)).data!
222+
const claim = { identityClaim: ["sub"] }
226223

227224
// Single record
228225
const encrypted = await client
229226
.encryptModel(user, users)
230-
.withLockContext(lockContext)
227+
.withLockContext(claim)
231228

232229
// Bulk records — one ZeroKMS call, all locked to the same identity
233230
const bulkEncrypted = await client
234231
.bulkEncryptModels(records, users)
235-
.withLockContext(lockContext)
232+
.withLockContext(claim)
236233

237234
const bulkDecrypted = await client
238235
.bulkDecryptModels(encryptedRecords)
239-
.withLockContext(lockContext)
236+
.withLockContext(claim)
240237
```
241238

242239
See [Identity-aware encryption](/stack/cipherstash/encryption/identity) for the full flow.

0 commit comments

Comments
 (0)