-
Notifications
You must be signed in to change notification settings - Fork 3
docs: add dynamic authentication guide for JWT signing in SDKs #1788
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
d166cc3
4bf04e2
fd3ec20
e2b730a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,279 @@ | ||
| --- | ||
| title: Dynamic authentication | ||
| description: Implement dynamic authentication patterns like short-lived JWT signing in your SDKs. | ||
| --- | ||
|
|
||
| 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. | ||
|
|
||
| ## Recommended: Custom fetcher middleware | ||
|
|
||
| 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. | ||
|
|
||
| ### How it works | ||
|
|
||
| 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. | ||
|
|
||
| ### TypeScript example: Short-lived JWT signing | ||
|
|
||
| Here's how to implement JWT signing with token memoization: | ||
|
|
||
| <Steps> | ||
|
|
||
| ### Enable custom fetcher in generator configuration | ||
|
|
||
| Add `allowCustomFetcher: true` to your `generators.yml`: | ||
|
|
||
| ```yaml title="generators.yml" | ||
| default-group: local | ||
| groups: | ||
| local: | ||
| generators: | ||
| - name: fernapi/fern-typescript-node-sdk | ||
| version: 0.x.x | ||
| output: | ||
| location: local-file-system | ||
| path: ../generated/typescript | ||
| config: | ||
| allowCustomFetcher: true | ||
| ``` | ||
|
|
||
| ### Create a custom fetcher with JWT signing | ||
|
|
||
| Create a fetcher function that wraps the default fetcher and injects JWT authentication: | ||
|
|
||
| ```typescript title="src/wrapper/jwtFetcher.ts" | ||
| import * as jwt from "jsonwebtoken"; | ||
| import { fetcher as defaultFetcher, type FetchFunction } from "../core/fetcher"; | ||
|
|
||
| export function createJwtFetcher(privateKey: string): FetchFunction { | ||
| // Cache token to avoid regenerating on every request | ||
| let cachedToken: { value: string; expiresAt: number } | undefined; | ||
|
|
||
| return async (args) => { | ||
| const now = Math.floor(Date.now() / 1000); | ||
|
|
||
| // Regenerate token if expired or about to expire (within 2 seconds) | ||
| if (!cachedToken || cachedToken.expiresAt - 2 <= now) { | ||
| const payload = { | ||
| iat: now, | ||
| exp: now + 15, // Token valid for 15 seconds | ||
| }; | ||
| const token = jwt.sign(payload, privateKey, { algorithm: "RS256" }); | ||
| cachedToken = { value: token, expiresAt: payload.exp }; | ||
| } | ||
|
|
||
| // Inject JWT into request headers | ||
| const headers = { | ||
| ...(args.headers ?? {}), | ||
| Authorization: `Bearer ${cachedToken.value}`, | ||
| }; | ||
|
|
||
| // Call the default fetcher with modified headers | ||
| return defaultFetcher({ ...args, headers }); | ||
| }; | ||
| } | ||
| ``` | ||
|
|
||
| ### Create a wrapper client | ||
|
|
||
| Extend the generated client to use the custom fetcher: | ||
|
|
||
| ```typescript title="src/wrapper/PlantStoreClient.ts" | ||
| import { PlantStoreClient as FernClient } from "../Client"; | ||
| import { createJwtFetcher } from "./jwtFetcher"; | ||
|
|
||
| export class PlantStoreClient extends FernClient { | ||
| constructor(options: { privateKey: string; environment: string }) { | ||
|
||
| super({ | ||
| environment: options.environment, | ||
| fetcher: createJwtFetcher(options.privateKey), | ||
| }); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Export the wrapper client | ||
|
|
||
| Update your `index.ts` to export the wrapper instead of the generated client: | ||
|
|
||
| ```typescript title="src/index.ts" | ||
| export { PlantStoreClient } from "./wrapper/PlantStoreClient"; | ||
| export * from "./api"; // Export types | ||
| ``` | ||
|
|
||
| ### Add to `.fernignore` | ||
|
|
||
| Protect your custom code from being overwritten: | ||
|
|
||
| ```diff title=".fernignore" | ||
| + src/wrapper | ||
| + src/index.ts | ||
| ``` | ||
|
|
||
| ### Use the client | ||
|
|
||
| Users can use the client with automatic JWT signing on all requests: | ||
|
|
||
| ```typescript | ||
| import { PlantStoreClient } from "plant-store-sdk"; | ||
|
|
||
| const client = new PlantStoreClient({ | ||
| privateKey: process.env.PRIVATE_KEY, | ||
| environment: "https://api.plantstore.com", | ||
| }); | ||
|
|
||
| // JWT is automatically signed and injected for each request | ||
| const plant = await client.plants.get("monstera-123"); | ||
| const newPlant = await client.plants.create({ | ||
| name: "Fiddle Leaf Fig", | ||
| species: "Ficus lyrata" | ||
| }); | ||
| ``` | ||
|
|
||
| </Steps> | ||
|
|
||
| ## Alternative: Method overrides | ||
|
||
|
|
||
| If you cannot enable `allowCustomFetcher` or prefer a simpler approach, you can override individual methods: | ||
|
||
|
|
||
| ```typescript title="src/wrapper/PlantStoreClient.ts" | ||
| import { PlantStoreClient as FernClient } from "../Client"; | ||
| import * as jwt from "jsonwebtoken"; | ||
|
|
||
| export class PlantStoreClient extends FernClient { | ||
| private privateKey: string; | ||
|
|
||
| constructor(options: { privateKey: string; environment: string }) { | ||
| super({ | ||
| environment: options.environment, | ||
| }); | ||
| this.privateKey = options.privateKey; | ||
| } | ||
|
|
||
| private generateJWT(): string { | ||
| const now = Math.floor(Date.now() / 1000); | ||
| const payload = { | ||
| iat: now, | ||
| exp: now + 15, | ||
| }; | ||
| return jwt.sign(payload, this.privateKey, { algorithm: "RS256" }); | ||
| } | ||
|
|
||
| private withJWT(requestOptions?: any): any { | ||
| const token = this.generateJWT(); | ||
| return { | ||
| ...requestOptions, | ||
| headers: { | ||
| ...requestOptions?.headers, | ||
| Authorization: `Bearer ${token}`, | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| // Override each method to inject JWT | ||
| async getPlant(plantId: string, requestOptions?: any) { | ||
| return super.plants.get(plantId, this.withJWT(requestOptions)); | ||
| } | ||
|
|
||
| async createPlant(request: any, requestOptions?: any) { | ||
| return super.plants.create(request, this.withJWT(requestOptions)); | ||
| } | ||
|
|
||
| // Override additional methods as needed... | ||
| } | ||
| ``` | ||
|
|
||
| <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> | ||
|
|
||
| ## Python example: Short-lived JWT signing | ||
|
|
||
| For Python SDKs, you can use a similar method override pattern: | ||
|
|
||
| ```python title="src/wrapper/client.py" | ||
| from .client import PlantStoreClient as FernClient | ||
| import jwt | ||
| import time | ||
| from typing import Optional, Dict, Any | ||
|
|
||
| class PlantStoreClient(FernClient): | ||
| def __init__(self, *, private_key: str, environment: str): | ||
| super().__init__(environment=environment) | ||
| self._private_key = private_key | ||
|
|
||
| def _generate_jwt(self) -> str: | ||
| """Generate a short-lived JWT token valid for 15 seconds""" | ||
| now = int(time.time()) | ||
| payload = { | ||
| "iat": now, | ||
| "exp": now + 15, | ||
| } | ||
| return jwt.encode(payload, self._private_key, algorithm="RS256") | ||
|
|
||
| def _with_jwt(self, request_options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: | ||
| """Inject JWT into request options""" | ||
| token = self._generate_jwt() | ||
| options = request_options or {} | ||
| headers = options.get("headers", {}) | ||
| headers["Authorization"] = f"Bearer {token}" | ||
| options["headers"] = headers | ||
| return options | ||
|
|
||
| def get_plant(self, plant_id: str, *, request_options: Optional[Dict[str, Any]] = None): | ||
| return super().plants.get(plant_id, request_options=self._with_jwt(request_options)) | ||
|
|
||
| def create_plant(self, request: Any, *, request_options: Optional[Dict[str, Any]] = None): | ||
| return super().plants.create(request, request_options=self._with_jwt(request_options)) | ||
| ``` | ||
|
|
||
| ## Other authentication patterns | ||
|
|
||
| This same pattern works for other dynamic authentication scenarios: | ||
|
|
||
| - **OAuth token refresh**: Automatically refresh expired access tokens before each request | ||
| - **HMAC (Hash-based Message Authentication Code) signing**: Sign requests with HMAC signatures based on request content | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 [vale] reported by reviewdog 🐶
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 [vale] reported by reviewdog 🐶 |
||
| - **Rotating API keys**: Switch between multiple API keys based on rate limits | ||
| - **Time-based tokens**: Generate tokens that include timestamps or nonces | ||
|
|
||
| ## Important considerations | ||
|
|
||
| ### Custom fetcher requirements | ||
|
|
||
| - **Generator configuration**: The `allowCustomFetcher` option must be enabled in your `generators.yml` for the `fetcher` parameter to be available in `BaseClientOptions` | ||
| - **Import path**: Import the default fetcher from `../core/fetcher` (or the appropriate path in your generated SDK) to wrap it with your custom logic | ||
| - **Preserve all arguments**: When wrapping the default fetcher, ensure you pass through all arguments to maintain compatibility with the SDK's internal behavior | ||
|
|
||
| ### Security considerations | ||
|
|
||
| - **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) | ||
| - **Secure key storage**: Never hardcode private keys; use environment variables or secure key management systems | ||
| - **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 | ||
|
|
||
| ### Performance and concurrency | ||
|
|
||
| - **Token memoization**: Cache tokens to avoid regenerating them on every request. The example above caches tokens and refreshes them 2 seconds before expiration | ||
| - **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 | ||
| - **Grace period**: Refresh tokens slightly before they expire (e.g., 2 seconds early) to avoid edge cases where a token expires during request processing | ||
|
|
||
| ### Header merging | ||
|
|
||
| - **Preserve existing headers**: When injecting authentication headers, always spread existing headers to avoid overwriting headers set by the SDK or user | ||
| - **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 | ||
|
|
||
| ### Time synchronization | ||
|
|
||
| - **Clock drift**: Be aware of potential clock drift between your client and server. Consider adding tolerance to your token expiration checks | ||
| - **Timestamp precision**: Use Unix timestamps (seconds since epoch) for `iat` and `exp` claims to match JWT standards | ||
|
|
||
| ## Best practices | ||
|
|
||
| - **Use custom fetcher for TypeScript**: The custom fetcher approach is the most maintainable solution for TypeScript SDKs when `allowCustomFetcher` is available | ||
| - **Cache tokens appropriately**: Balance between security (shorter token lifetime) and performance (less frequent regeneration) | ||
| - **Handle errors gracefully**: Implement retry logic for authentication failures and token refresh errors | ||
|
||
| - **Test thoroughly**: Ensure your wrapper handles all edge cases, including concurrent requests, token expiration, and network failures | ||
|
||
| - **Monitor token usage**: Log token generation and refresh events to help debug authentication issues in production | ||
|
|
||
| ## See also | ||
|
|
||
| - [Adding custom code](/sdks/capabilities/custom-code) - Learn more about extending generated SDKs | ||
| - [TypeScript custom code](/sdks/generators/typescript/custom-code) - TypeScript-specific customization guide | ||
| - [Python custom code](/sdks/generators/python/custom-code) - Python-specific customization guide | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Put the TypeScript guidance under the TypeScript documentation, and the Python under the Python documentation.