Skip to content

Commit e2b730a

Browse files
docs: reorganize dynamic authentication by language
- Remove 'Alternative: Method overrides' section from TypeScript docs - Create TypeScript-specific page with custom fetcher approach only - Create Python-specific page with method override approach - Convert main guide to lightweight overview hub with language cards - Add navigation entries for both language-specific pages - Keep language-agnostic considerations in overview Addresses Swimburger's feedback to organize content by language. Co-Authored-By: Chris McDonnell <[email protected]>
1 parent fd3ec20 commit e2b730a

File tree

4 files changed

+377
-264
lines changed

4 files changed

+377
-264
lines changed

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

Lines changed: 28 additions & 264 deletions
Original file line numberDiff line numberDiff line change
@@ -3,290 +3,54 @@ 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 custom fetcher middleware.
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 language-specific approaches.
77

8-
## Recommended: Custom fetcher middleware
8+
## Language-specific guides
99

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.
10+
Each language has its own recommended approach for implementing dynamic authentication:
1111

12-
### How it works
12+
<Cards>
13+
<Card title="TypeScript" icon="fa-brands fa-js" href="/sdks/generators/typescript/dynamic-authentication">
14+
Use custom fetcher middleware to inject authentication logic in a single place for all requests. Supports JWT signing, OAuth token refresh, and more.
15+
</Card>
16+
<Card title="Python" icon="fa-brands fa-python" href="/sdks/generators/python/dynamic-authentication">
17+
Use method overrides to inject authentication logic for each API call. Supports JWT signing, OAuth token refresh, and more.
18+
</Card>
19+
</Cards>
1320

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.
21+
## Common use cases
1522

16-
### TypeScript example: Short-lived JWT signing
17-
18-
Here's how to implement JWT signing with token memoization:
19-
20-
<Steps>
21-
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
41-
42-
Create a fetcher function that wraps the default fetcher and injects JWT authentication:
43-
44-
```typescript title="src/wrapper/jwtFetcher.ts"
45-
import * as jwt from "jsonwebtoken";
46-
import { fetcher as defaultFetcher, type FetchFunction } from "../core/fetcher";
47-
48-
export function createJwtFetcher(privateKey: string): FetchFunction {
49-
// Cache token to avoid regenerating on every request
50-
let cachedToken: { value: string; expiresAt: number } | undefined;
51-
52-
return async (args) => {
53-
const now = Math.floor(Date.now() / 1000);
54-
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}`,
69-
};
70-
71-
// Call the default fetcher with modified headers
72-
return defaultFetcher({ ...args, headers });
73-
};
74-
}
75-
```
76-
77-
### Create a wrapper client
78-
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-
// Infer the exact options type from the generated client's constructor
86-
type FernClientOptions = ConstructorParameters<typeof FernClient>[0];
87-
// Accept all options except 'fetcher', and add our 'privateKey'
88-
type Options = Omit<FernClientOptions, "fetcher"> & { privateKey: string };
89-
90-
export class PlantStoreClient extends FernClient {
91-
constructor(options: Options) {
92-
// Extract privateKey and pass all other options to the parent
93-
const { privateKey, ...clientOptions } = options;
94-
super({
95-
...clientOptions,
96-
fetcher: createJwtFetcher(privateKey),
97-
});
98-
}
99-
}
100-
```
101-
102-
<Note>
103-
This pattern uses `ConstructorParameters` to infer the exact options type from the generated client, ensuring compatibility with all client options (headers, timeoutInSeconds, maxRetries, etc.) without hardcoding them. This keeps the wrapper future-proof as the generator adds new options.
104-
</Note>
105-
106-
### Export the wrapper client
107-
108-
Update your `index.ts` to export the wrapper instead of the generated client:
109-
110-
```typescript title="src/index.ts"
111-
export { PlantStoreClient } from "./wrapper/PlantStoreClient";
112-
export * from "./api"; // Export types
113-
```
114-
115-
### Add to `.fernignore`
116-
117-
Protect your custom code from being overwritten:
118-
119-
```diff title=".fernignore"
120-
+ src/wrapper
121-
+ src/index.ts
122-
```
123-
124-
### Use the client
125-
126-
Users can use the client with automatic JWT signing on all requests:
127-
128-
```typescript
129-
import { PlantStoreClient } from "plant-store-sdk";
130-
131-
const client = new PlantStoreClient({
132-
privateKey: process.env.PRIVATE_KEY,
133-
environment: "https://api.plantstore.com",
134-
});
135-
136-
// JWT is automatically signed and injected for each request
137-
const plant = await client.plants.get("monstera-123");
138-
const newPlant = await client.plants.create({
139-
name: "Fiddle Leaf Fig",
140-
species: "Ficus lyrata"
141-
});
142-
```
143-
144-
</Steps>
145-
146-
## Alternative: Method overrides
147-
148-
If you cannot enable `allowCustomFetcher` or prefer a simpler approach, you can override individual methods:
149-
150-
```typescript title="src/wrapper/PlantStoreClient.ts"
151-
import { PlantStoreClient as FernClient } from "../Client";
152-
import * as jwt from "jsonwebtoken";
153-
154-
type FernClientOptions = ConstructorParameters<typeof FernClient>[0];
155-
type Options = Omit<FernClientOptions, "fetcher"> & { privateKey: string };
156-
157-
export class PlantStoreClient extends FernClient {
158-
private privateKey: string;
159-
160-
constructor(options: Options) {
161-
const { privateKey, ...clientOptions } = options;
162-
super(clientOptions);
163-
this.privateKey = privateKey;
164-
}
165-
166-
private generateJWT(): string {
167-
const now = Math.floor(Date.now() / 1000);
168-
const payload = {
169-
iat: now,
170-
exp: now + 15,
171-
};
172-
return jwt.sign(payload, this.privateKey, { algorithm: "RS256" });
173-
}
174-
175-
private withJWT(requestOptions?: any): any {
176-
const token = this.generateJWT();
177-
return {
178-
...requestOptions,
179-
headers: {
180-
...requestOptions?.headers,
181-
Authorization: `Bearer ${token}`,
182-
},
183-
};
184-
}
185-
186-
// Override each method to inject JWT
187-
async getPlant(plantId: string, requestOptions?: any) {
188-
return super.plants.get(plantId, this.withJWT(requestOptions));
189-
}
190-
191-
async createPlant(request: any, requestOptions?: any) {
192-
return super.plants.create(request, this.withJWT(requestOptions));
193-
}
194-
195-
// Override additional methods as needed...
196-
}
197-
```
198-
199-
<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>
200-
201-
## Python example: Short-lived JWT signing
202-
203-
For Python SDKs, you can use a similar method override pattern:
204-
205-
```python title="src/wrapper/client.py"
206-
from .client import PlantStoreClient as FernClient
207-
import jwt
208-
import time
209-
from typing import Optional, Dict, Any
210-
211-
class PlantStoreClient(FernClient):
212-
def __init__(self, *, private_key: str, environment: str):
213-
super().__init__(environment=environment)
214-
self._private_key = private_key
215-
216-
def _generate_jwt(self) -> str:
217-
"""Generate a short-lived JWT token valid for 15 seconds"""
218-
now = int(time.time())
219-
payload = {
220-
"iat": now,
221-
"exp": now + 15,
222-
}
223-
return jwt.encode(payload, self._private_key, algorithm="RS256")
224-
225-
def _with_jwt(self, request_options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
226-
"""Inject JWT into request options"""
227-
token = self._generate_jwt()
228-
options = request_options or {}
229-
headers = options.get("headers", {})
230-
headers["Authorization"] = f"Bearer {token}"
231-
options["headers"] = headers
232-
return options
233-
234-
def get_plant(self, plant_id: str, *, request_options: Optional[Dict[str, Any]] = None):
235-
return super().plants.get(plant_id, request_options=self._with_jwt(request_options))
236-
237-
def create_plant(self, request: Any, *, request_options: Optional[Dict[str, Any]] = None):
238-
return super().plants.create(request, request_options=self._with_jwt(request_options))
239-
```
240-
241-
## Other authentication patterns
242-
243-
This same pattern works for other dynamic authentication scenarios:
23+
Dynamic authentication is useful for several scenarios:
24424

25+
- **Short-lived JWT signing**: Generate and sign JWTs that expire after a short period (e.g., 15 seconds) for enhanced security
24526
- **OAuth token refresh**: Automatically refresh expired access tokens before each request
24627
- **HMAC (Hash-based Message Authentication Code) signing**: Sign requests with HMAC signatures based on request content
247-
- **Rotating API keys**: Switch between multiple API keys based on rate limits
248-
- **Time-based tokens**: Generate tokens that include timestamps or nonces
28+
- **Rotating API keys**: Switch between multiple API keys based on rate limits or other criteria
29+
- **Time-based tokens**: Generate tokens that include timestamps or nonces for replay protection
24930

25031
## Important considerations
25132

252-
### Custom fetcher requirements
253-
254-
- **Generator configuration**: The `allowCustomFetcher` option must be enabled in your `generators.yml` for the `fetcher` parameter to be available in `BaseClientOptions`
255-
- **Import path**: Import the default fetcher from `../core/fetcher` (or the appropriate path in your generated SDK) to wrap it with your custom logic
256-
- **Preserve all arguments**: When wrapping the default fetcher, ensure you pass through all arguments to maintain compatibility with the SDK's internal behavior
257-
258-
### Security considerations
33+
When implementing dynamic authentication, keep these language-agnostic considerations in mind:
25934

260-
- **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)
261-
- **Secure key storage**: Never hardcode private keys; use environment variables or secure key management systems
262-
- **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
35+
### Security
26336

264-
### Performance and concurrency
37+
- **Secure key storage**: Never hardcode private keys or secrets; use environment variables or secure key management systems
38+
- **Server-side only**: For JWT signing with private keys, ensure this is only done in server-side code, never in browser environments
39+
- **Avoid double authentication**: If your API already uses bearer token authentication in the Fern definition, be careful not to override existing authentication headers
26540

266-
- **Token memoization**: Cache tokens to avoid regenerating them on every request. The example above caches tokens and refreshes them 2 seconds before expiration
267-
- **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
268-
- **Grace period**: Refresh tokens slightly before they expire (e.g., 2 seconds early) to avoid edge cases where a token expires during request processing
41+
### Performance
26942

270-
### Header merging
271-
272-
- **Preserve existing headers**: When injecting authentication headers, always spread existing headers to avoid overwriting headers set by the SDK or user
273-
- **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
43+
- **Token caching**: Cache tokens to avoid regenerating them on every request, balancing security (shorter token lifetime) with performance (less frequent regeneration)
44+
- **Grace period**: Refresh tokens slightly before they expire to avoid edge cases where a token expires during request processing
45+
- **Concurrency**: Be mindful of race conditions when caching tokens in multi-threaded environments
27446

27547
### Time synchronization
27648

277-
- **Clock drift**: Be aware of potential clock drift between your client and server. Consider adding tolerance to your token expiration checks
278-
- **Timestamp precision**: Use Unix timestamps (seconds since epoch) for `iat` and `exp` claims to match JWT standards
279-
280-
## Best practices
281-
282-
- **Use custom fetcher for TypeScript**: The custom fetcher approach is the most maintainable solution for TypeScript SDKs when `allowCustomFetcher` is available
283-
- **Cache tokens appropriately**: Balance between security (shorter token lifetime) and performance (less frequent regeneration)
284-
- **Handle errors gracefully**: Implement retry logic for authentication failures and token refresh errors
285-
- **Test thoroughly**: Ensure your wrapper handles all edge cases, including concurrent requests, token expiration, and network failures
286-
- **Monitor token usage**: Log token generation and refresh events to help debug authentication issues in production
49+
- **Clock drift**: Be aware of potential clock drift between your client and server; consider adding tolerance to token expiration checks
50+
- **Timestamp precision**: Use Unix timestamps (seconds since epoch) for JWT `iat` and `exp` claims to match standards
28751

28852
## See also
28953

290-
- [Adding custom code](/sdks/capabilities/custom-code) - Learn more about extending generated SDKs
54+
- [Adding custom code](/sdks/custom-code) - Learn more about extending generated SDKs
29155
- [TypeScript custom code](/sdks/generators/typescript/custom-code) - TypeScript-specific customization guide
29256
- [Python custom code](/sdks/generators/python/custom-code) - Python-specific customization guide

0 commit comments

Comments
 (0)