|
| 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 |
0 commit comments