Skip to content

Commit d166cc3

Browse files
docs: add dynamic authentication guide for JWT signing in SDKs
Co-Authored-By: Chris McDonnell <[email protected]>
1 parent 10b748c commit d166cc3

File tree

2 files changed

+254
-0
lines changed

2 files changed

+254
-0
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
---
2+
title: Dynamic authentication
3+
description: Implement dynamic authentication patterns like short-lived JWT signing in your SDKs.
4+
---
5+
6+
Your API may require dynamic authentication where credentials need to be generated or refreshed for each request, such as signing short-lived JWTs or rotating tokens. Fern-generated SDKs support this pattern through request-level header injection.
7+
8+
## How it works
9+
10+
Fern-generated SDKs accept a `headers` parameter in the `RequestOptions` for every API call. This allows you to inject dynamically computed headers (like signed JWTs) on a per-request basis without needing to override each method.
11+
12+
## TypeScript example: Short-lived JWT signing
13+
14+
Here's how to implement a client wrapper that signs a JWT valid for 15 seconds before each request:
15+
16+
<Steps>
17+
18+
### Create a wrapper client
19+
20+
Create a custom client class that extends the generated Fern client and adds JWT signing logic:
21+
22+
```typescript title="src/wrapper/MyClient.ts"
23+
import { MyClient as FernClient } from "../Client";
24+
import * as jwt from "jsonwebtoken";
25+
26+
export class MyClient extends FernClient {
27+
private privateKey: string;
28+
29+
constructor(options: { privateKey: string; environment: string }) {
30+
super({
31+
environment: options.environment,
32+
});
33+
this.privateKey = options.privateKey;
34+
}
35+
36+
/**
37+
* Generate a short-lived JWT token valid for 15 seconds
38+
*/
39+
private generateJWT(): string {
40+
const now = Math.floor(Date.now() / 1000);
41+
const payload = {
42+
iat: now,
43+
exp: now + 15, // Expires in 15 seconds
44+
};
45+
46+
return jwt.sign(payload, this.privateKey, { algorithm: "RS256" });
47+
}
48+
49+
/**
50+
* Helper method to inject JWT into request options
51+
*/
52+
private withJWT(requestOptions?: any): any {
53+
const token = this.generateJWT();
54+
return {
55+
...requestOptions,
56+
headers: {
57+
...requestOptions?.headers,
58+
Authorization: `Bearer ${token}`,
59+
},
60+
};
61+
}
62+
63+
// Override methods to automatically inject JWT
64+
async getUser(userId: string, requestOptions?: any) {
65+
return super.getUser(userId, this.withJWT(requestOptions));
66+
}
67+
68+
async createUser(request: any, requestOptions?: any) {
69+
return super.createUser(request, this.withJWT(requestOptions));
70+
}
71+
72+
// Add overrides for all other methods...
73+
}
74+
```
75+
76+
### Export the wrapper client
77+
78+
Update your `index.ts` to export the wrapper instead of the generated client:
79+
80+
```typescript title="src/index.ts"
81+
export { MyClient } from "./wrapper/MyClient";
82+
export * from "./api"; // Export types
83+
```
84+
85+
### Add to `.fernignore`
86+
87+
Protect your custom code from being overwritten:
88+
89+
```diff title=".fernignore"
90+
+ src/wrapper
91+
+ src/index.ts
92+
```
93+
94+
### Use the client
95+
96+
Your users can now use the client with automatic JWT signing:
97+
98+
```typescript
99+
import { MyClient } from "my-sdk";
100+
101+
const client = new MyClient({
102+
privateKey: process.env.PRIVATE_KEY,
103+
environment: "https://api.example.com",
104+
});
105+
106+
// JWT is automatically signed and injected for each request
107+
const user = await client.getUser("user-123");
108+
const newUser = await client.createUser({ name: "Alice" });
109+
```
110+
111+
</Steps>
112+
113+
## Alternative: Proxy pattern without method overrides
114+
115+
If you want to avoid overriding each method individually, you can use a Proxy to intercept all method calls:
116+
117+
```typescript title="src/wrapper/MyClient.ts"
118+
import { MyClient as FernClient } from "../Client";
119+
import * as jwt from "jsonwebtoken";
120+
121+
export class MyClient {
122+
private client: FernClient;
123+
private privateKey: string;
124+
125+
constructor(options: { privateKey: string; environment: string }) {
126+
this.client = new FernClient({
127+
environment: options.environment,
128+
});
129+
this.privateKey = options.privateKey;
130+
131+
// Return a proxy that intercepts all method calls
132+
return new Proxy(this, {
133+
get(target, prop) {
134+
// If accessing the client property itself, return it
135+
if (prop === "client" || prop === "privateKey") {
136+
return target[prop as keyof typeof target];
137+
}
138+
139+
// Get the property from the underlying client
140+
const value = (target.client as any)[prop];
141+
142+
// If it's a function, wrap it to inject JWT
143+
if (typeof value === "function") {
144+
return function (...args: any[]) {
145+
// The last argument is typically requestOptions
146+
const lastArg = args[args.length - 1];
147+
const isRequestOptions =
148+
lastArg && typeof lastArg === "object" && !Array.isArray(lastArg);
149+
150+
if (isRequestOptions) {
151+
// Inject JWT into existing request options
152+
args[args.length - 1] = target.withJWT(lastArg);
153+
} else {
154+
// Add JWT as new request options
155+
args.push(target.withJWT());
156+
}
157+
158+
return value.apply(target.client, args);
159+
};
160+
}
161+
162+
return value;
163+
},
164+
});
165+
}
166+
167+
private generateJWT(): string {
168+
const now = Math.floor(Date.now() / 1000);
169+
const payload = {
170+
iat: now,
171+
exp: now + 15,
172+
};
173+
return jwt.sign(payload, this.privateKey, { algorithm: "RS256" });
174+
}
175+
176+
private withJWT(requestOptions?: any): any {
177+
const token = this.generateJWT();
178+
return {
179+
...requestOptions,
180+
headers: {
181+
...requestOptions?.headers,
182+
Authorization: `Bearer ${token}`,
183+
},
184+
};
185+
}
186+
}
187+
```
188+
189+
This approach automatically wraps all methods without needing to override each one individually.
190+
191+
## Python example: Short-lived JWT signing
192+
193+
For Python SDKs, you can use a similar pattern:
194+
195+
```python title="src/wrapper/client.py"
196+
from .client import Client as FernClient
197+
import jwt
198+
import time
199+
from typing import Optional, Dict, Any
200+
201+
class Client(FernClient):
202+
def __init__(self, *, private_key: str, environment: str):
203+
super().__init__(environment=environment)
204+
self._private_key = private_key
205+
206+
def _generate_jwt(self) -> str:
207+
"""Generate a short-lived JWT token valid for 15 seconds"""
208+
now = int(time.time())
209+
payload = {
210+
"iat": now,
211+
"exp": now + 15,
212+
}
213+
return jwt.encode(payload, self._private_key, algorithm="RS256")
214+
215+
def _with_jwt(self, request_options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
216+
"""Inject JWT into request options"""
217+
token = self._generate_jwt()
218+
options = request_options or {}
219+
headers = options.get("headers", {})
220+
headers["Authorization"] = f"Bearer {token}"
221+
options["headers"] = headers
222+
return options
223+
224+
def get_user(self, user_id: str, *, request_options: Optional[Dict[str, Any]] = None):
225+
return super().get_user(user_id, request_options=self._with_jwt(request_options))
226+
227+
def create_user(self, request: Any, *, request_options: Optional[Dict[str, Any]] = None):
228+
return super().create_user(request, request_options=self._with_jwt(request_options))
229+
```
230+
231+
## Other authentication patterns
232+
233+
This same pattern works for other dynamic authentication scenarios:
234+
235+
- **OAuth token refresh**: Automatically refresh expired access tokens before each request
236+
- **HMAC signing**: Sign requests with HMAC signatures based on request content
237+
- **Rotating API keys**: Switch between multiple API keys based on rate limits
238+
- **Time-based tokens**: Generate tokens that include timestamps or nonces
239+
240+
## Best practices
241+
242+
- **Cache when possible**: If your tokens are valid for longer periods, cache them to avoid regenerating on every request
243+
- **Handle errors gracefully**: Implement retry logic for authentication failures
244+
- **Secure key storage**: Never hardcode private keys; use environment variables or secure key management systems
245+
- **Test thoroughly**: Ensure your wrapper handles all edge cases, including concurrent requests
246+
247+
## See also
248+
249+
- [Adding custom code](/sdks/capabilities/custom-code) - Learn more about extending generated SDKs
250+
- [TypeScript custom code](/sdks/generators/typescript/custom-code) - TypeScript-specific customization guide
251+
- [Python custom code](/sdks/generators/python/custom-code) - Python-specific customization guide

fern/products/sdks/sdks.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@ navigation:
223223
- page: Configure global headers
224224
path: ./guides/configure-global-headers.mdx
225225
slug: global-headers
226+
- page: Dynamic authentication
227+
path: ./guides/dynamic-authentication.mdx
228+
slug: dynamic-authentication
226229
- page: Configure auto-pagination
227230
path: ./guides/configure-auto-pagination.mdx
228231
slug: auto-pagination

0 commit comments

Comments
 (0)