diff --git a/fern/products/sdks/guides/dynamic-authentication.mdx b/fern/products/sdks/guides/dynamic-authentication.mdx new file mode 100644 index 000000000..2662911d5 --- /dev/null +++ b/fern/products/sdks/guides/dynamic-authentication.mdx @@ -0,0 +1,56 @@ +--- +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 language-specific approaches. + +## Language-specific guides + +Each language has its own recommended approach for implementing dynamic authentication: + + + + Use custom fetcher middleware to inject authentication logic in a single place for all requests. Supports JWT signing, OAuth token refresh, and more. + + + Use method overrides to inject authentication logic for each API call. Supports JWT signing, OAuth token refresh, and more. + + + +## Common use cases + +Dynamic authentication is useful for several scenarios: + +- **Short-lived JWT signing**: Generate and sign JWTs that expire after a short period (e.g., 15 seconds) for enhanced security +- **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 +- **Rotating API keys**: Switch between multiple API keys based on rate limits or other criteria +- **Time-based tokens**: Generate tokens that include timestamps or nonces for replay protection + +## Important considerations + +When implementing dynamic authentication, keep these language-agnostic considerations in mind: + +### Security + +- **Secure key storage**: Never hardcode private keys or secrets; use environment variables or secure key management systems +- **Server-side only**: For JWT signing with private keys, ensure this is only done in server-side code, never in browser environments +- **Avoid double authentication**: If your API already uses bearer token authentication in the Fern definition, be careful not to override existing authentication headers + +### Performance + +- **Token caching**: Cache tokens to avoid regenerating them on every request, balancing security (shorter token lifetime) with performance (less frequent regeneration) +- **Grace period**: Refresh tokens slightly before they expire to avoid edge cases where a token expires during request processing +- **Concurrency**: Be mindful of race conditions when caching tokens in multi-threaded environments + +### Time synchronization + +- **Clock drift**: Be aware of potential clock drift between your client and server; consider adding tolerance to token expiration checks +- **Timestamp precision**: Use Unix timestamps (seconds since epoch) for JWT `iat` and `exp` claims to match standards + +## See also + +- [Adding custom code](/sdks/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 diff --git a/fern/products/sdks/overview/python/dynamic-authentication.mdx b/fern/products/sdks/overview/python/dynamic-authentication.mdx new file mode 100644 index 000000000..43ce67c78 --- /dev/null +++ b/fern/products/sdks/overview/python/dynamic-authentication.mdx @@ -0,0 +1,148 @@ +--- +title: Dynamic authentication +description: Implement dynamic authentication patterns like short-lived JWT signing in Python SDKs using method overrides. +--- + +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. Python SDKs support this pattern through method overrides. + +## Method override pattern + +For Python SDKs, you can implement dynamic authentication by extending the generated client and overriding methods to inject authentication logic. + +### Example: Short-lived JWT signing + +Here's how to implement JWT signing for Python SDKs: + + + +### Create a wrapper client + +Create a custom client class that extends the generated Fern client and adds JWT signing logic: + +```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)) + + # Override additional methods as needed... +``` + +### Export the wrapper client + +Update your `__init__.py` to export the wrapper instead of the generated client: + +```python title="src/__init__.py" +from .wrapper.client import PlantStoreClient + +__all__ = ["PlantStoreClient"] +``` + +### Add to `.fernignore` + +Protect your custom code from being overwritten: + +```diff title=".fernignore" ++ src/wrapper ++ src/__init__.py +``` + +### Use the client + +Users can use the client with automatic JWT signing: + +```python +from plant_store_sdk import PlantStoreClient +import os + +client = PlantStoreClient( + private_key=os.environ["PRIVATE_KEY"], + environment="https://api.plantstore.com" +) + +# JWT is automatically signed and injected for each request +plant = client.get_plant("monstera-123") +new_plant = client.create_plant({ + "name": "Fiddle Leaf Fig", + "species": "Ficus lyrata" +}) +``` + + + + +This approach requires overriding each method individually. You'll need to override all methods that make API calls to ensure JWT authentication is applied consistently across your SDK. + + +## 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 +- **Rotating API keys**: Switch between multiple API keys based on rate limits +- **Time-based tokens**: Generate tokens that include timestamps or nonces + +## Important considerations + +### Security considerations + +- **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 + +- **Token memoization**: Consider caching tokens to avoid regenerating them on every request. You can add a simple cache that refreshes tokens before they expire +- **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 merge with existing headers to avoid overwriting headers set by the SDK or user +- **Request options**: Python SDKs accept `request_options` as a keyword argument that can include custom 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 + +- **Override all methods**: Ensure you override all API methods to maintain consistent authentication across your SDK +- **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/generators/python/custom-code) - Python-specific customization guide +- [Python configuration](/sdks/generators/python/configuration) - Full list of configuration options diff --git a/fern/products/sdks/overview/typescript/dynamic-authentication.mdx b/fern/products/sdks/overview/typescript/dynamic-authentication.mdx new file mode 100644 index 000000000..16eb130fb --- /dev/null +++ b/fern/products/sdks/overview/typescript/dynamic-authentication.mdx @@ -0,0 +1,195 @@ +--- +title: Dynamic authentication +description: Implement dynamic authentication patterns like short-lived JWT signing in TypeScript SDKs using custom fetcher middleware. +--- + +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. TypeScript SDKs support this pattern through custom fetcher middleware. + +## Custom fetcher middleware + +The recommended 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. + +### Example: Short-lived JWT signing + +Here's how to implement JWT signing with token memoization: + + + +### 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"; + +// Infer the exact options type from the generated client's constructor +type FernClientOptions = ConstructorParameters[0]; +// Accept all options except 'fetcher', and add our 'privateKey' +type Options = Omit & { privateKey: string }; + +export class PlantStoreClient extends FernClient { + constructor(options: Options) { + // Extract privateKey and pass all other options to the parent + const { privateKey, ...clientOptions } = options; + super({ + ...clientOptions, + fetcher: createJwtFetcher(privateKey), + }); + } +} +``` + + +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. + + +### 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" +}); +``` + + + +## 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 +- **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 + +- **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/generators/typescript/custom-code) - TypeScript-specific customization guide +- [TypeScript configuration](/sdks/generators/typescript/configuration) - Full list of configuration options diff --git a/fern/products/sdks/sdks.yml b/fern/products/sdks/sdks.yml index ac3c9b610..fea04a3b9 100644 --- a/fern/products/sdks/sdks.yml +++ b/fern/products/sdks/sdks.yml @@ -41,6 +41,9 @@ navigation: - page: Adding custom code path: ./overview/typescript/custom-code.mdx slug: custom-code + - page: Dynamic authentication + path: ./overview/typescript/dynamic-authentication.mdx + slug: dynamic-authentication - page: Enabling the serde layer path: ./overview/typescript/serde-layer.mdx slug: serde-layer @@ -66,6 +69,9 @@ navigation: - page: Adding custom code path: ./overview/python/custom-code.mdx slug: custom-code + - page: Dynamic authentication + path: ./overview/python/dynamic-authentication.mdx + slug: dynamic-authentication - changelog: ./overview/python/changelog slug: changelog - link: Customer showcase @@ -223,6 +229,9 @@ navigation: - page: Configure global headers path: ./guides/configure-global-headers.mdx slug: global-headers + - page: Dynamic authentication + path: ./guides/dynamic-authentication.mdx + slug: dynamic-authentication - page: Configure auto-pagination path: ./guides/configure-auto-pagination.mdx slug: auto-pagination