Skip to content

Commit 4bf04e2

Browse files
docs: update dynamic authentication guide with custom fetcher approach
- Add custom fetcher middleware as recommended approach per Swimburger feedback - Replace Proxy pattern with simpler method override alternative - Add comprehensive gotchas and security considerations - Update all examples to use plant-themed content (PlantStoreClient, plants.get, etc.) - Fix Vale linting issues: remove time-relative 'now', define HMAC acronym - Add allowCustomFetcher configuration example - Include token memoization pattern with grace period - Document import paths, header merging, and thread safety considerations Co-Authored-By: Chris McDonnell <[email protected]>
1 parent d166cc3 commit 4bf04e2

File tree

1 file changed

+139
-111
lines changed

1 file changed

+139
-111
lines changed

fern/products/sdks/guides/dynamic-authentication.mdx

Lines changed: 139 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -3,73 +3,92 @@ title: Dynamic authentication
33
description: Implement dynamic authentication patterns like short-lived JWT signing in your SDKs.
44
---
55

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.
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 custom fetcher middleware.
77

8-
## How it works
8+
## Recommended: Custom fetcher middleware
99

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.
10+
The best way to implement dynamic authentication in TypeScript SDKs is to use a custom fetcher. This acts as middleware for all requests, allowing you to inject authentication logic in a single place without overriding individual methods.
1111

12-
## TypeScript example: Short-lived JWT signing
12+
### How it works
1313

14-
Here's how to implement a client wrapper that signs a JWT valid for 15 seconds before each request:
14+
When you enable `allowCustomFetcher` in your generator configuration, the generated SDK accepts a `fetcher` parameter in the client options. This fetcher wraps all HTTP requests, giving you a single injection point for authentication logic.
15+
16+
### TypeScript example: Short-lived JWT signing
17+
18+
Here's how to implement JWT signing with token memoization:
1519

1620
<Steps>
1721

18-
### Create a wrapper client
22+
### Enable custom fetcher in generator configuration
23+
24+
Add `allowCustomFetcher: true` to your `generators.yml`:
25+
26+
```yaml title="generators.yml"
27+
default-group: local
28+
groups:
29+
local:
30+
generators:
31+
- name: fernapi/fern-typescript-node-sdk
32+
version: 0.x.x
33+
output:
34+
location: local-file-system
35+
path: ../generated/typescript
36+
config:
37+
allowCustomFetcher: true
38+
```
39+
40+
### Create a custom fetcher with JWT signing
1941
20-
Create a custom client class that extends the generated Fern client and adds JWT signing logic:
42+
Create a fetcher function that wraps the default fetcher and injects JWT authentication:
2143
22-
```typescript title="src/wrapper/MyClient.ts"
23-
import { MyClient as FernClient } from "../Client";
44+
```typescript title="src/wrapper/jwtFetcher.ts"
2445
import * as jwt from "jsonwebtoken";
46+
import { fetcher as defaultFetcher, type FetchFunction } from "../core/fetcher";
2547

26-
export class MyClient extends FernClient {
27-
private privateKey: string;
48+
export function createJwtFetcher(privateKey: string): FetchFunction {
49+
// Cache token to avoid regenerating on every request
50+
let cachedToken: { value: string; expiresAt: number } | undefined;
2851

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 {
52+
return async (args) => {
4053
const now = Math.floor(Date.now() / 1000);
41-
const payload = {
42-
iat: now,
43-
exp: now + 15, // Expires in 15 seconds
44-
};
4554

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-
},
55+
// Regenerate token if expired or about to expire (within 2 seconds)
56+
if (!cachedToken || cachedToken.expiresAt - 2 <= now) {
57+
const payload = {
58+
iat: now,
59+
exp: now + 15, // Token valid for 15 seconds
60+
};
61+
const token = jwt.sign(payload, privateKey, { algorithm: "RS256" });
62+
cachedToken = { value: token, expiresAt: payload.exp };
63+
}
64+
65+
// Inject JWT into request headers
66+
const headers = {
67+
...(args.headers ?? {}),
68+
Authorization: `Bearer ${cachedToken.value}`,
6069
};
61-
}
6270

63-
// Override methods to automatically inject JWT
64-
async getUser(userId: string, requestOptions?: any) {
65-
return super.getUser(userId, this.withJWT(requestOptions));
66-
}
71+
// Call the default fetcher with modified headers
72+
return defaultFetcher({ ...args, headers });
73+
};
74+
}
75+
```
6776

68-
async createUser(request: any, requestOptions?: any) {
69-
return super.createUser(request, this.withJWT(requestOptions));
70-
}
77+
### Create a wrapper client
7178

72-
// Add overrides for all other methods...
79+
Extend the generated client to use the custom fetcher:
80+
81+
```typescript title="src/wrapper/PlantStoreClient.ts"
82+
import { PlantStoreClient as FernClient } from "../Client";
83+
import { createJwtFetcher } from "./jwtFetcher";
84+
85+
export class PlantStoreClient extends FernClient {
86+
constructor(options: { privateKey: string; environment: string }) {
87+
super({
88+
environment: options.environment,
89+
fetcher: createJwtFetcher(options.privateKey),
90+
});
91+
}
7392
}
7493
```
7594

@@ -78,7 +97,7 @@ export class MyClient extends FernClient {
7897
Update your `index.ts` to export the wrapper instead of the generated client:
7998

8099
```typescript title="src/index.ts"
81-
export { MyClient } from "./wrapper/MyClient";
100+
export { PlantStoreClient } from "./wrapper/PlantStoreClient";
82101
export * from "./api"; // Export types
83102
```
84103

@@ -93,75 +112,42 @@ Protect your custom code from being overwritten:
93112

94113
### Use the client
95114

96-
Your users can now use the client with automatic JWT signing:
115+
Users can use the client with automatic JWT signing on all requests:
97116

98117
```typescript
99-
import { MyClient } from "my-sdk";
118+
import { PlantStoreClient } from "plant-store-sdk";
100119

101-
const client = new MyClient({
120+
const client = new PlantStoreClient({
102121
privateKey: process.env.PRIVATE_KEY,
103-
environment: "https://api.example.com",
122+
environment: "https://api.plantstore.com",
104123
});
105124

106125
// 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" });
126+
const plant = await client.plants.get("monstera-123");
127+
const newPlant = await client.plants.create({
128+
name: "Fiddle Leaf Fig",
129+
species: "Ficus lyrata"
130+
});
109131
```
110132

111133
</Steps>
112134

113-
## Alternative: Proxy pattern without method overrides
135+
## Alternative: Method overrides
114136

115-
If you want to avoid overriding each method individually, you can use a Proxy to intercept all method calls:
137+
If you cannot enable `allowCustomFetcher` or prefer a simpler approach, you can override individual methods:
116138

117-
```typescript title="src/wrapper/MyClient.ts"
118-
import { MyClient as FernClient } from "../Client";
139+
```typescript title="src/wrapper/PlantStoreClient.ts"
140+
import { PlantStoreClient as FernClient } from "../Client";
119141
import * as jwt from "jsonwebtoken";
120142

121-
export class MyClient {
122-
private client: FernClient;
143+
export class PlantStoreClient extends FernClient {
123144
private privateKey: string;
124145

125146
constructor(options: { privateKey: string; environment: string }) {
126-
this.client = new FernClient({
147+
super({
127148
environment: options.environment,
128149
});
129150
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-
});
165151
}
166152

167153
private generateJWT(): string {
@@ -183,22 +169,33 @@ export class MyClient {
183169
},
184170
};
185171
}
172+
173+
// Override each method to inject JWT
174+
async getPlant(plantId: string, requestOptions?: any) {
175+
return super.plants.get(plantId, this.withJWT(requestOptions));
176+
}
177+
178+
async createPlant(request: any, requestOptions?: any) {
179+
return super.plants.create(request, this.withJWT(requestOptions));
180+
}
181+
182+
// Override additional methods as needed...
186183
}
187184
```
188185

189-
This approach automatically wraps all methods without needing to override each one individually.
186+
<Note>This approach requires overriding each method individually, which can be tedious for large APIs. The custom fetcher approach is recommended for better maintainability.</Note>
190187

191188
## Python example: Short-lived JWT signing
192189

193-
For Python SDKs, you can use a similar pattern:
190+
For Python SDKs, you can use a similar method override pattern:
194191

195192
```python title="src/wrapper/client.py"
196-
from .client import Client as FernClient
193+
from .client import PlantStoreClient as FernClient
197194
import jwt
198195
import time
199196
from typing import Optional, Dict, Any
200197

201-
class Client(FernClient):
198+
class PlantStoreClient(FernClient):
202199
def __init__(self, *, private_key: str, environment: str):
203200
super().__init__(environment=environment)
204201
self._private_key = private_key
@@ -221,28 +218,59 @@ class Client(FernClient):
221218
options["headers"] = headers
222219
return options
223220

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))
221+
def get_plant(self, plant_id: str, *, request_options: Optional[Dict[str, Any]] = None):
222+
return super().plants.get(plant_id, request_options=self._with_jwt(request_options))
226223

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))
224+
def create_plant(self, request: Any, *, request_options: Optional[Dict[str, Any]] = None):
225+
return super().plants.create(request, request_options=self._with_jwt(request_options))
229226
```
230227

231228
## Other authentication patterns
232229

233230
This same pattern works for other dynamic authentication scenarios:
234231

235232
- **OAuth token refresh**: Automatically refresh expired access tokens before each request
236-
- **HMAC signing**: Sign requests with HMAC signatures based on request content
233+
- **HMAC (Hash-based Message Authentication Code) signing**: Sign requests with HMAC signatures based on request content
237234
- **Rotating API keys**: Switch between multiple API keys based on rate limits
238235
- **Time-based tokens**: Generate tokens that include timestamps or nonces
239236

240-
## Best practices
237+
## Important considerations
241238

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
239+
### Custom fetcher requirements
240+
241+
- **Generator configuration**: The `allowCustomFetcher` option must be enabled in your `generators.yml` for the `fetcher` parameter to be available in `BaseClientOptions`
242+
- **Import path**: Import the default fetcher from `../core/fetcher` (or the appropriate path in your generated SDK) to wrap it with your custom logic
243+
- **Preserve all arguments**: When wrapping the default fetcher, ensure you pass through all arguments to maintain compatibility with the SDK's internal behavior
244+
245+
### Security considerations
246+
247+
- **Server-side only**: Never expose private keys in browser environments. JWT signing with private keys should only be done in server-side code (Node.js, Deno, Bun)
244248
- **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
249+
- **Avoid double authentication**: If your API already uses bearer token authentication in the Fern definition, be careful not to override the existing `Authorization` header. Consider using a different header name or conditionally setting the header
250+
251+
### Performance and concurrency
252+
253+
- **Token memoization**: Cache tokens to avoid regenerating them on every request. The example above caches tokens and refreshes them 2 seconds before expiration
254+
- **Thread safety**: The memoization pattern shown is safe for concurrent requests in JavaScript's single-threaded event loop, but be mindful of race conditions in other environments
255+
- **Grace period**: Refresh tokens slightly before they expire (e.g., 2 seconds early) to avoid edge cases where a token expires during request processing
256+
257+
### Header merging
258+
259+
- **Preserve existing headers**: When injecting authentication headers, always spread existing headers to avoid overwriting headers set by the SDK or user
260+
- **Header precedence**: Headers are merged in order: SDK defaults → client options → request options → custom fetcher. Your custom fetcher runs last and can override previous headers
261+
262+
### Time synchronization
263+
264+
- **Clock drift**: Be aware of potential clock drift between your client and server. Consider adding tolerance to your token expiration checks
265+
- **Timestamp precision**: Use Unix timestamps (seconds since epoch) for `iat` and `exp` claims to match JWT standards
266+
267+
## Best practices
268+
269+
- **Use custom fetcher for TypeScript**: The custom fetcher approach is the most maintainable solution for TypeScript SDKs when `allowCustomFetcher` is available
270+
- **Cache tokens appropriately**: Balance between security (shorter token lifetime) and performance (less frequent regeneration)
271+
- **Handle errors gracefully**: Implement retry logic for authentication failures and token refresh errors
272+
- **Test thoroughly**: Ensure your wrapper handles all edge cases, including concurrent requests, token expiration, and network failures
273+
- **Monitor token usage**: Log token generation and refresh events to help debug authentication issues in production
246274

247275
## See also
248276

0 commit comments

Comments
 (0)