@unireq/oauth 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Olivier Orabona
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,268 @@
1
+ import { Policy } from '@unireq/core';
2
+
3
+ /**
4
+ * OAuth 2.0 Bearer token authentication (RFC 6750)
5
+ * @see https://datatracker.ietf.org/doc/html/rfc6750
6
+ */
7
+
8
+ /** Token supplier function (lazy evaluation) */
9
+ type TokenSupplier = () => string | Promise<string>;
10
+ /**
11
+ * JWKS (JSON Web Key Set) for JWT verification
12
+ * Can be a URL to fetch JWKS or a static key
13
+ */
14
+ type JWKSSource = {
15
+ type: 'url';
16
+ url: string;
17
+ } | {
18
+ type: 'key';
19
+ key: string;
20
+ };
21
+ /**
22
+ * OAuth Bearer options
23
+ */
24
+ interface OAuthBearerOptions {
25
+ /** Token supplier function (lazy evaluation) */
26
+ readonly tokenSupplier: TokenSupplier;
27
+ /** JWKS source for secure JWT verification (highly recommended for production) */
28
+ readonly jwks?: JWKSSource;
29
+ /** Clock skew tolerance in seconds (default: 60) */
30
+ readonly skew?: number;
31
+ /** Auto-refresh on 401 (default: true) */
32
+ readonly autoRefresh?: boolean;
33
+ /** Callback before token refresh */
34
+ readonly onRefresh?: () => void | Promise<void>;
35
+ /**
36
+ * Allow unsafe mode without JWKS verification (NOT RECOMMENDED FOR PRODUCTION)
37
+ * When false (default), throws error if jwks is not provided
38
+ * When true, only logs warning and checks expiration without signature verification
39
+ * @security OWASP A02:2021 - Enabling this bypasses cryptographic verification
40
+ * @default false
41
+ */
42
+ readonly allowUnsafeMode?: boolean;
43
+ }
44
+ /**
45
+ * Creates an OAuth 2.0 Bearer token authentication policy
46
+ * - Lazy token evaluation via supplier
47
+ * - JWT exp + skew validation
48
+ * - Optional secure JWT verification with jose (RECOMMENDED)
49
+ * - Single-flight refresh on 401
50
+ * - Single replay after refresh
51
+ *
52
+ * @param options - OAuth Bearer options
53
+ * @returns Policy that handles Bearer authentication
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * // Secure verification with JWKS URL
58
+ * const authPolicy = oauthBearer({
59
+ * tokenSupplier: async () => 'eyJhbGci...',
60
+ * jwks: { type: 'url', url: 'https://example.com/.well-known/jwks.json' },
61
+ * skew: 60,
62
+ * onRefresh: () => console.log('Refreshing token...')
63
+ * });
64
+ *
65
+ * // Secure verification with static public key
66
+ * const authPolicy = oauthBearer({
67
+ * tokenSupplier: async () => 'eyJhbGci...',
68
+ * jwks: { type: 'key', key: publicKeyPEM },
69
+ * skew: 60
70
+ * });
71
+ * ```
72
+ *
73
+ * @security IMPORTANT: Provide `jwks` option for secure JWT signature verification.
74
+ * Without it, only expiration is checked (no signature validation).
75
+ */
76
+ declare function oauthBearer(options: OAuthBearerOptions): Policy;
77
+
78
+ /**
79
+ * OAuth 2.0 DX helpers - transparent utility functions
80
+ * These helpers make common OAuth configurations easier without hiding complexity.
81
+ * You can always use the raw oauthBearer() options directly for full control.
82
+ */
83
+
84
+ /**
85
+ * Creates a JWKS source from a URL
86
+ * This is the most common approach for production - fetch keys from identity provider
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * const auth = oauthBearer({
91
+ * tokenSupplier: myTokenSupplier,
92
+ * jwks: jwksFromUrl('https://login.example.com/.well-known/jwks.json'),
93
+ * });
94
+ * ```
95
+ */
96
+ declare function jwksFromUrl(url: string): JWKSSource;
97
+ /**
98
+ * Creates a JWKS source from a static PEM public key
99
+ * Use this when you have the signing key directly (e.g., from config/secrets)
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * const publicKeyPEM = `-----BEGIN PUBLIC KEY-----
104
+ * MIIBIjANBgkqh...
105
+ * -----END PUBLIC KEY-----`;
106
+ *
107
+ * const auth = oauthBearer({
108
+ * tokenSupplier: myTokenSupplier,
109
+ * jwks: jwksFromKey(publicKeyPEM),
110
+ * });
111
+ * ```
112
+ */
113
+ declare function jwksFromKey(pemKey: string): JWKSSource;
114
+ /**
115
+ * Derives JWKS URL from OpenID Connect issuer
116
+ * Follows the standard .well-known/openid-configuration path
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * // For Auth0: https://your-tenant.auth0.com/.well-known/jwks.json
121
+ * // For Okta: https://your-domain.okta.com/oauth2/default/v1/keys
122
+ * const auth = oauthBearer({
123
+ * tokenSupplier: myTokenSupplier,
124
+ * jwks: jwksFromIssuer('https://your-tenant.auth0.com/'),
125
+ * });
126
+ * ```
127
+ */
128
+ declare function jwksFromIssuer(issuerUrl: string): JWKSSource;
129
+ /**
130
+ * Creates a token supplier from an environment variable
131
+ * Useful for static tokens (API keys) or tokens set at process startup
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * // Reads process.env.MY_API_TOKEN on each request
136
+ * const auth = oauthBearer({
137
+ * tokenSupplier: tokenFromEnv('MY_API_TOKEN'),
138
+ * allowUnsafeMode: true, // Static tokens often don't have JWKS
139
+ * });
140
+ * ```
141
+ */
142
+ declare function tokenFromEnv(varName: string): TokenSupplier;
143
+ /**
144
+ * Token refresh endpoint response (standard OAuth2)
145
+ */
146
+ interface TokenResponse {
147
+ access_token: string;
148
+ token_type?: string;
149
+ expires_in?: number;
150
+ refresh_token?: string;
151
+ }
152
+ /**
153
+ * Options for creating a refresh-based token supplier
154
+ */
155
+ interface RefreshTokenOptions {
156
+ /** Token endpoint URL (e.g., https://auth.example.com/oauth/token) */
157
+ readonly tokenEndpoint: string;
158
+ /** OAuth client ID */
159
+ readonly clientId: string;
160
+ /** OAuth client secret (optional for public clients) */
161
+ readonly clientSecret?: string;
162
+ /** Refresh token - can be a string or a function that returns one */
163
+ readonly refreshToken: string | (() => string | Promise<string>);
164
+ /** Additional parameters to send (e.g., scope, audience) */
165
+ readonly additionalParams?: Record<string, string>;
166
+ /** Callback when new tokens are received (for storing refresh token) */
167
+ readonly onTokens?: (tokens: TokenResponse) => void | Promise<void>;
168
+ }
169
+ /**
170
+ * Creates a token supplier that fetches tokens using a refresh token
171
+ * Handles the OAuth2 refresh_token grant type automatically
172
+ *
173
+ * This is NOT a black box - you control:
174
+ * - The token endpoint URL
175
+ * - Credentials (client ID/secret)
176
+ * - How refresh tokens are obtained and stored
177
+ * - Additional parameters (scope, audience)
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * // Basic refresh token flow
182
+ * const auth = oauthBearer({
183
+ * tokenSupplier: tokenFromRefresh({
184
+ * tokenEndpoint: 'https://auth.example.com/oauth/token',
185
+ * clientId: 'my-app',
186
+ * clientSecret: process.env.CLIENT_SECRET,
187
+ * refreshToken: () => getStoredRefreshToken(),
188
+ * onTokens: async (tokens) => {
189
+ * // Store the new refresh token if rotated
190
+ * if (tokens.refresh_token) {
191
+ * await storeRefreshToken(tokens.refresh_token);
192
+ * }
193
+ * },
194
+ * }),
195
+ * jwks: jwksFromIssuer('https://auth.example.com/'),
196
+ * });
197
+ * ```
198
+ */
199
+ declare function tokenFromRefresh(options: RefreshTokenOptions): TokenSupplier;
200
+ /**
201
+ * Options for client credentials grant
202
+ */
203
+ interface ClientCredentialsOptions {
204
+ /** Token endpoint URL */
205
+ readonly tokenEndpoint: string;
206
+ /** OAuth client ID */
207
+ readonly clientId: string;
208
+ /** OAuth client secret */
209
+ readonly clientSecret: string;
210
+ /** Requested scopes (optional) */
211
+ readonly scope?: string;
212
+ /** Additional parameters */
213
+ readonly additionalParams?: Record<string, string>;
214
+ }
215
+ /**
216
+ * Creates a token supplier using client credentials grant
217
+ * Ideal for server-to-server authentication (machine-to-machine)
218
+ *
219
+ * @example
220
+ * ```ts
221
+ * const auth = oauthBearer({
222
+ * tokenSupplier: tokenFromClientCredentials({
223
+ * tokenEndpoint: 'https://auth.example.com/oauth/token',
224
+ * clientId: process.env.CLIENT_ID,
225
+ * clientSecret: process.env.CLIENT_SECRET,
226
+ * scope: 'read:api write:api',
227
+ * }),
228
+ * jwks: jwksFromIssuer('https://auth.example.com/'),
229
+ * });
230
+ * ```
231
+ */
232
+ declare function tokenFromClientCredentials(options: ClientCredentialsOptions): TokenSupplier;
233
+ /**
234
+ * Creates a simple token supplier from a static string
235
+ * Useful for testing or when token is known at startup
236
+ *
237
+ * @example
238
+ * ```ts
239
+ * // For testing
240
+ * const auth = oauthBearer({
241
+ * tokenSupplier: tokenFromStatic('test-token'),
242
+ * allowUnsafeMode: true,
243
+ * });
244
+ * ```
245
+ */
246
+ declare function tokenFromStatic(token: string): TokenSupplier;
247
+ /**
248
+ * Creates a token supplier that caches the result of another supplier
249
+ * Reduces calls to expensive token operations
250
+ *
251
+ * @param supplier - The underlying token supplier
252
+ * @param ttlMs - Cache TTL in milliseconds (default: 5 minutes)
253
+ *
254
+ * @example
255
+ * ```ts
256
+ * const auth = oauthBearer({
257
+ * // Cache client credentials token for 5 minutes
258
+ * tokenSupplier: tokenWithCache(
259
+ * tokenFromClientCredentials({ ... }),
260
+ * 5 * 60 * 1000
261
+ * ),
262
+ * jwks: jwksFromIssuer('https://auth.example.com/'),
263
+ * });
264
+ * ```
265
+ */
266
+ declare function tokenWithCache(supplier: TokenSupplier, ttlMs?: number): TokenSupplier;
267
+
268
+ export { type ClientCredentialsOptions, type JWKSSource, type OAuthBearerOptions, type RefreshTokenOptions, type TokenSupplier, jwksFromIssuer, jwksFromKey, jwksFromUrl, oauthBearer, tokenFromClientCredentials, tokenFromEnv, tokenFromRefresh, tokenFromStatic, tokenWithCache };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import {OAUTH_CONFIG}from'@unireq/config';import {policy,getHeader}from'@unireq/core';import {createRemoteJWKSet,jwtVerify,importSPKI}from'jose';function R(e){try{let r=e.split(".");if(r.length!==3)return null;let t=r[1];if(!t)return null;let n=atob(t.replace(/-/g,"+").replace(/_/g,"/"));return JSON.parse(n)}catch{return null}}async function P(e,r){if(r.type==="url"){let o=createRemoteJWKSet(new URL(r.url)),{payload:s}=await jwtVerify(e,o);return s}let t=await importSPKI(r.key,"RS256"),{payload:n}=await jwtVerify(e,t);return n}async function O(e,r,t){let n;if(t?n=await P(e,t):(console.warn("[SECURITY WARNING] JWT signature not verified. Provide JWKS for secure verification."),n=R(e)),!n?.exp)return false;let o=Math.floor(Date.now()/1e3);return n.exp<=o+r}var d=new Map,f=new Map,W=3e4;async function k(e){let r=d.get(e);if(r)return r;let t=setTimeout(()=>{d.delete(e),f.delete(e);},W);f.set(e,t);let n=Promise.resolve(e()).finally(()=>{d.delete(e);let o=f.get(e);o&&(clearTimeout(o),f.delete(e));});return d.set(e,n),n}function x(e){let{tokenSupplier:r,jwks:t,skew:n=OAUTH_CONFIG.JWT_CLOCK_SKEW,autoRefresh:o=OAUTH_CONFIG.AUTO_REFRESH,onRefresh:s,allowUnsafeMode:c=false}=e;if(!t&&!c)throw new Error('[SECURITY ERROR] JWT signature verification disabled without explicit acknowledgment. Either provide "jwks" option for secure verification OR set "allowUnsafeMode: true" (NOT RECOMMENDED FOR PRODUCTION). See OWASP A02:2021 - Cryptographic Failures: https://owasp.org/Top10/A02_2021-Cryptographic_Failures/');!t&&c&&console.error('[SECURITY WARNING] Running in UNSAFE MODE. JWT signatures NOT verified. Tokens can be forged by attackers. DO NOT use in production! Provide "jwks" option for secure JWT verification.');let i=null;return policy(async(p,l)=>{let a=i;a||(a=await k(r),i=a),await O(a,n,t)&&(s&&await Promise.resolve(s()),a=await k(r),i=a);let u=await l({...p,headers:{...p.headers,authorization:`Bearer ${a}`}});if(u.status===401&&o&&getHeader(u.headers,"www-authenticate")?.toLowerCase().includes("bearer")){s&&await Promise.resolve(s());let y=await k(r);return i=y,l({...p,headers:{...p.headers,authorization:`Bearer ${y}`}})}return u},{name:"oauthBearer",kind:"auth",options:{jwks:t?`[${t.type}]`:void 0,skew:n,autoRefresh:o,allowUnsafeMode:c}})}function E(e){return {type:"url",url:e}}function C(e){return {type:"key",key:e}}function J(e){return {type:"url",url:`${e.replace(/\/$/,"")}/.well-known/jwks.json`}}function _(e){return ()=>{let r=process.env[e];if(!r)throw new Error(`Environment variable ${e} is not set`);return r}}function b(e){let{tokenEndpoint:r,clientId:t,clientSecret:n,refreshToken:o,additionalParams:s={},onTokens:c}=e;return async()=>{let i=typeof o=="function"?await o():o,p=new URLSearchParams({grant_type:"refresh_token",client_id:t,refresh_token:i,...s});n&&p.append("client_secret",n);let l=await fetch(r,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:p.toString()});if(!l.ok){let u=await l.text().catch(()=>"Unknown error");throw new Error(`Token refresh failed: ${l.status} - ${u}`)}let a=await l.json();return c&&await c(a),a.access_token}}function v(e){let{tokenEndpoint:r,clientId:t,clientSecret:n,scope:o,additionalParams:s={}}=e;return async()=>{let c=new URLSearchParams({grant_type:"client_credentials",client_id:t,client_secret:n,...s});o&&c.append("scope",o);let i=await fetch(r,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:c.toString()});if(!i.ok){let l=await i.text().catch(()=>"Unknown error");throw new Error(`Client credentials grant failed: ${i.status} - ${l}`)}return (await i.json()).access_token}}function F(e){return ()=>e}function U(e,r=300*1e3){let t=null,n=0;return async()=>{if(t&&Date.now()<n)return t;let o=await e();return t=o,n=Date.now()+r,o}}
2
+ export{J as jwksFromIssuer,C as jwksFromKey,E as jwksFromUrl,x as oauthBearer,v as tokenFromClientCredentials,_ as tokenFromEnv,b as tokenFromRefresh,F as tokenFromStatic,U as tokenWithCache};//# sourceMappingURL=index.js.map
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/bearer.ts","../src/helpers.ts"],"names":["decodeJWTUnsafe","token","parts","payload","decoded","verifyJWT","jwks","getKey","createRemoteJWKSet","jwtVerify","key","importSPKI","isJWTExpired","skewSeconds","now","refreshLocks","refreshTimeouts","REFRESH_LOCK_TIMEOUT","getTokenWithRefresh","supplier","existingRefresh","timeoutId","refreshPromise","timeout","oauthBearer","options","tokenSupplier","skew","OAUTH_CONFIG","autoRefresh","onRefresh","allowUnsafeMode","cachedToken","policy","ctx","next","response","getHeader","newToken","jwksFromUrl","url","jwksFromKey","pemKey","jwksFromIssuer","issuerUrl","tokenFromEnv","varName","tokenFromRefresh","tokenEndpoint","clientId","clientSecret","refreshToken","additionalParams","onTokens","currentRefreshToken","body","errorText","tokens","tokenFromClientCredentials","scope","tokenFromStatic","tokenWithCache","ttlMs","expiresAt"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * OAuth 2.0 Bearer token authentication (RFC 6750)\n * @see https://datatracker.ietf.org/doc/html/rfc6750\n */\n\nimport { OAUTH_CONFIG } from '@unireq/config';\nimport type { Policy } from '@unireq/core';\nimport { getHeader, policy } from '@unireq/core';\nimport { createRemoteJWKSet, importSPKI, jwtVerify } from 'jose';\n\n/** Token supplier function (lazy evaluation) */\nexport type TokenSupplier = () => string | Promise<string>;\n\n/** JWT payload structure (minimal) */\ninterface JWTPayload {\n exp?: number;\n iat?: number;\n [key: string]: unknown;\n}\n\n/**\n * JWKS (JSON Web Key Set) for JWT verification\n * Can be a URL to fetch JWKS or a static key\n */\nexport type JWKSSource = { type: 'url'; url: string } | { type: 'key'; key: string };\n\n/**\n * Decodes JWT payload without verification (INSECURE - for exp check only)\n * @param token - JWT token\n * @returns Decoded payload\n * @deprecated Use verifyJWT with JWKS for secure verification\n */\nfunction decodeJWTUnsafe(token: string): JWTPayload | null {\n try {\n const parts = token.split('.');\n if (parts.length !== 3) return null;\n\n const payload = parts[1];\n if (!payload) return null;\n\n const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));\n return JSON.parse(decoded) as JWTPayload;\n } catch {\n return null;\n }\n}\n\n/**\n * Verifies JWT with jose library (SECURE)\n * @param token - JWT token\n * @param jwks - JWKS source for verification\n * @returns Decoded and verified payload\n * @throws Error if jose library is not installed\n */\nasync function verifyJWT(token: string, jwks: JWKSSource): Promise<JWTPayload | null> {\n if (jwks.type === 'url') {\n // JWKS URL - jose will fetch and cache\n const getKey = createRemoteJWKSet(new URL(jwks.url));\n const { payload } = await jwtVerify(token, getKey);\n return payload as JWTPayload;\n }\n\n // Static PEM key (RS256)\n const key = await importSPKI(jwks.key, 'RS256');\n const { payload } = await jwtVerify(token, key);\n return payload as JWTPayload;\n}\n\n/**\n * Checks if JWT is expired with clock skew tolerance\n * @param token - JWT token\n * @param skewSeconds - Clock skew tolerance in seconds\n * @param jwks - Optional JWKS for secure verification\n * @returns True if token is expired or about to expire\n */\nasync function isJWTExpired(token: string, skewSeconds: number, jwks?: JWKSSource): Promise<boolean> {\n let payload: JWTPayload | null;\n\n if (jwks) {\n // Secure verification with jose\n payload = await verifyJWT(token, jwks);\n } else {\n // Fallback: unsafe decode (no signature verification)\n console.warn('[SECURITY WARNING] JWT signature not verified. Provide JWKS for secure verification.');\n payload = decodeJWTUnsafe(token);\n }\n\n if (!payload?.exp) return false;\n\n const now = Math.floor(Date.now() / 1000);\n return payload.exp <= now + skewSeconds;\n}\n\n/**\n * OAuth Bearer options\n */\nexport interface OAuthBearerOptions {\n /** Token supplier function (lazy evaluation) */\n readonly tokenSupplier: TokenSupplier;\n /** JWKS source for secure JWT verification (highly recommended for production) */\n readonly jwks?: JWKSSource;\n /** Clock skew tolerance in seconds (default: 60) */\n readonly skew?: number;\n /** Auto-refresh on 401 (default: true) */\n readonly autoRefresh?: boolean;\n /** Callback before token refresh */\n readonly onRefresh?: () => void | Promise<void>;\n /**\n * Allow unsafe mode without JWKS verification (NOT RECOMMENDED FOR PRODUCTION)\n * When false (default), throws error if jwks is not provided\n * When true, only logs warning and checks expiration without signature verification\n * @security OWASP A02:2021 - Enabling this bypasses cryptographic verification\n * @default false\n */\n readonly allowUnsafeMode?: boolean;\n}\n\n// Single-flight refresh lock with timeout cleanup\nconst refreshLocks = new Map<TokenSupplier, Promise<string>>();\nconst refreshTimeouts = new Map<TokenSupplier, ReturnType<typeof setTimeout>>();\n\n// Lock timeout duration (30 seconds - if refresh takes longer, something is wrong)\nconst REFRESH_LOCK_TIMEOUT = 30000;\n\n/**\n * Gets token with single-flight refresh guarantee\n * Ensures only one refresh happens at a time per supplier\n * Includes timeout cleanup to prevent memory leaks\n */\nasync function getTokenWithRefresh(supplier: TokenSupplier): Promise<string> {\n // Check if refresh is already in progress\n const existingRefresh = refreshLocks.get(supplier);\n if (existingRefresh) {\n return existingRefresh;\n }\n\n // Set timeout to cleanup lock in case of long-running or crashed refresh\n /* v8 ignore start - timeout cleanup path, difficult to test reliably */\n const timeoutId = setTimeout(() => {\n refreshLocks.delete(supplier);\n refreshTimeouts.delete(supplier);\n }, REFRESH_LOCK_TIMEOUT);\n /* v8 ignore stop */\n\n refreshTimeouts.set(supplier, timeoutId);\n\n // Start new refresh\n const refreshPromise = Promise.resolve(supplier()).finally(() => {\n // Clean up lock and timeout on success or failure\n // .finally() runs for both resolved and rejected promises\n refreshLocks.delete(supplier);\n const timeout = refreshTimeouts.get(supplier);\n if (timeout) {\n clearTimeout(timeout);\n refreshTimeouts.delete(supplier);\n }\n });\n\n refreshLocks.set(supplier, refreshPromise);\n return refreshPromise;\n}\n\n/**\n * Creates an OAuth 2.0 Bearer token authentication policy\n * - Lazy token evaluation via supplier\n * - JWT exp + skew validation\n * - Optional secure JWT verification with jose (RECOMMENDED)\n * - Single-flight refresh on 401\n * - Single replay after refresh\n *\n * @param options - OAuth Bearer options\n * @returns Policy that handles Bearer authentication\n *\n * @example\n * ```ts\n * // Secure verification with JWKS URL\n * const authPolicy = oauthBearer({\n * tokenSupplier: async () => 'eyJhbGci...',\n * jwks: { type: 'url', url: 'https://example.com/.well-known/jwks.json' },\n * skew: 60,\n * onRefresh: () => console.log('Refreshing token...')\n * });\n *\n * // Secure verification with static public key\n * const authPolicy = oauthBearer({\n * tokenSupplier: async () => 'eyJhbGci...',\n * jwks: { type: 'key', key: publicKeyPEM },\n * skew: 60\n * });\n * ```\n *\n * @security IMPORTANT: Provide `jwks` option for secure JWT signature verification.\n * Without it, only expiration is checked (no signature validation).\n */\nexport function oauthBearer(options: OAuthBearerOptions): Policy {\n const {\n tokenSupplier,\n jwks,\n skew = OAUTH_CONFIG.JWT_CLOCK_SKEW,\n autoRefresh = OAUTH_CONFIG.AUTO_REFRESH,\n onRefresh,\n allowUnsafeMode = false,\n } = options;\n\n // Security validation: Require JWKS or explicit unsafe mode acknowledgment\n if (!jwks && !allowUnsafeMode) {\n throw new Error(\n '[SECURITY ERROR] JWT signature verification disabled without explicit acknowledgment. ' +\n 'Either provide \"jwks\" option for secure verification OR set \"allowUnsafeMode: true\" ' +\n '(NOT RECOMMENDED FOR PRODUCTION). ' +\n 'See OWASP A02:2021 - Cryptographic Failures: https://owasp.org/Top10/A02_2021-Cryptographic_Failures/',\n );\n }\n\n // Log security warning if unsafe mode is explicitly enabled\n if (!jwks && allowUnsafeMode) {\n console.error(\n '[SECURITY WARNING] Running in UNSAFE MODE. JWT signatures NOT verified. ' +\n 'Tokens can be forged by attackers. DO NOT use in production! ' +\n 'Provide \"jwks\" option for secure JWT verification.',\n );\n }\n\n // Cache token in closure to avoid re-fetching on every request\n let cachedToken: string | null = null;\n\n return policy(\n async (ctx, next) => {\n // Get initial token (use cached if available)\n let token = cachedToken;\n if (!token) {\n // Use single-flight mechanism for initial fetch to handle concurrent requests\n token = await getTokenWithRefresh(tokenSupplier);\n cachedToken = token;\n }\n\n // Check if token is expired\n if (await isJWTExpired(token, skew, jwks)) {\n if (onRefresh) {\n await Promise.resolve(onRefresh());\n }\n token = await getTokenWithRefresh(tokenSupplier);\n cachedToken = token;\n }\n\n // Add Authorization header\n const response = await next({\n ...ctx,\n headers: {\n ...ctx.headers,\n authorization: `Bearer ${token}`,\n },\n });\n\n // Handle 401 Unauthorized - single refresh and replay\n if (response.status === 401 && autoRefresh) {\n // Check WWW-Authenticate header for Bearer realm\n const wwwAuth = getHeader(response.headers, 'www-authenticate');\n\n if (wwwAuth?.toLowerCase().includes('bearer')) {\n if (onRefresh) {\n await Promise.resolve(onRefresh());\n }\n\n // Refresh token (single-flight)\n const newToken = await getTokenWithRefresh(tokenSupplier);\n cachedToken = newToken;\n\n // Single replay\n return next({\n ...ctx,\n headers: {\n ...ctx.headers,\n authorization: `Bearer ${newToken}`,\n },\n });\n }\n }\n\n return response;\n },\n {\n name: 'oauthBearer',\n kind: 'auth',\n options: {\n jwks: jwks ? `[${jwks.type}]` : undefined,\n skew,\n autoRefresh,\n allowUnsafeMode,\n },\n },\n );\n}\n","/**\n * OAuth 2.0 DX helpers - transparent utility functions\n * These helpers make common OAuth configurations easier without hiding complexity.\n * You can always use the raw oauthBearer() options directly for full control.\n */\n\nimport type { JWKSSource, TokenSupplier } from './bearer.js';\n\n/**\n * Creates a JWKS source from a URL\n * This is the most common approach for production - fetch keys from identity provider\n *\n * @example\n * ```ts\n * const auth = oauthBearer({\n * tokenSupplier: myTokenSupplier,\n * jwks: jwksFromUrl('https://login.example.com/.well-known/jwks.json'),\n * });\n * ```\n */\nexport function jwksFromUrl(url: string): JWKSSource {\n return { type: 'url', url };\n}\n\n/**\n * Creates a JWKS source from a static PEM public key\n * Use this when you have the signing key directly (e.g., from config/secrets)\n *\n * @example\n * ```ts\n * const publicKeyPEM = `-----BEGIN PUBLIC KEY-----\n * MIIBIjANBgkqh...\n * -----END PUBLIC KEY-----`;\n *\n * const auth = oauthBearer({\n * tokenSupplier: myTokenSupplier,\n * jwks: jwksFromKey(publicKeyPEM),\n * });\n * ```\n */\nexport function jwksFromKey(pemKey: string): JWKSSource {\n return { type: 'key', key: pemKey };\n}\n\n/**\n * Derives JWKS URL from OpenID Connect issuer\n * Follows the standard .well-known/openid-configuration path\n *\n * @example\n * ```ts\n * // For Auth0: https://your-tenant.auth0.com/.well-known/jwks.json\n * // For Okta: https://your-domain.okta.com/oauth2/default/v1/keys\n * const auth = oauthBearer({\n * tokenSupplier: myTokenSupplier,\n * jwks: jwksFromIssuer('https://your-tenant.auth0.com/'),\n * });\n * ```\n */\nexport function jwksFromIssuer(issuerUrl: string): JWKSSource {\n // Normalize issuer URL (remove trailing slash)\n const baseUrl = issuerUrl.replace(/\\/$/, '');\n return { type: 'url', url: `${baseUrl}/.well-known/jwks.json` };\n}\n\n/**\n * Creates a token supplier from an environment variable\n * Useful for static tokens (API keys) or tokens set at process startup\n *\n * @example\n * ```ts\n * // Reads process.env.MY_API_TOKEN on each request\n * const auth = oauthBearer({\n * tokenSupplier: tokenFromEnv('MY_API_TOKEN'),\n * allowUnsafeMode: true, // Static tokens often don't have JWKS\n * });\n * ```\n */\nexport function tokenFromEnv(varName: string): TokenSupplier {\n return () => {\n const token = process.env[varName];\n if (!token) {\n throw new Error(`Environment variable ${varName} is not set`);\n }\n return token;\n };\n}\n\n/**\n * Token refresh endpoint response (standard OAuth2)\n */\ninterface TokenResponse {\n access_token: string;\n token_type?: string;\n expires_in?: number;\n refresh_token?: string;\n}\n\n/**\n * Options for creating a refresh-based token supplier\n */\nexport interface RefreshTokenOptions {\n /** Token endpoint URL (e.g., https://auth.example.com/oauth/token) */\n readonly tokenEndpoint: string;\n /** OAuth client ID */\n readonly clientId: string;\n /** OAuth client secret (optional for public clients) */\n readonly clientSecret?: string;\n /** Refresh token - can be a string or a function that returns one */\n readonly refreshToken: string | (() => string | Promise<string>);\n /** Additional parameters to send (e.g., scope, audience) */\n readonly additionalParams?: Record<string, string>;\n /** Callback when new tokens are received (for storing refresh token) */\n readonly onTokens?: (tokens: TokenResponse) => void | Promise<void>;\n}\n\n/**\n * Creates a token supplier that fetches tokens using a refresh token\n * Handles the OAuth2 refresh_token grant type automatically\n *\n * This is NOT a black box - you control:\n * - The token endpoint URL\n * - Credentials (client ID/secret)\n * - How refresh tokens are obtained and stored\n * - Additional parameters (scope, audience)\n *\n * @example\n * ```ts\n * // Basic refresh token flow\n * const auth = oauthBearer({\n * tokenSupplier: tokenFromRefresh({\n * tokenEndpoint: 'https://auth.example.com/oauth/token',\n * clientId: 'my-app',\n * clientSecret: process.env.CLIENT_SECRET,\n * refreshToken: () => getStoredRefreshToken(),\n * onTokens: async (tokens) => {\n * // Store the new refresh token if rotated\n * if (tokens.refresh_token) {\n * await storeRefreshToken(tokens.refresh_token);\n * }\n * },\n * }),\n * jwks: jwksFromIssuer('https://auth.example.com/'),\n * });\n * ```\n */\nexport function tokenFromRefresh(options: RefreshTokenOptions): TokenSupplier {\n const { tokenEndpoint, clientId, clientSecret, refreshToken, additionalParams = {}, onTokens } = options;\n\n return async (): Promise<string> => {\n // Get refresh token (can be dynamic)\n const currentRefreshToken = typeof refreshToken === 'function' ? await refreshToken() : refreshToken;\n\n // Build form body\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: clientId,\n refresh_token: currentRefreshToken,\n ...additionalParams,\n });\n\n if (clientSecret) {\n body.append('client_secret', clientSecret);\n }\n\n // Make token request\n const response = await fetch(tokenEndpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: body.toString(),\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => 'Unknown error');\n throw new Error(`Token refresh failed: ${response.status} - ${errorText}`);\n }\n\n const tokens = (await response.json()) as TokenResponse;\n\n // Notify caller of new tokens (for refresh token rotation)\n if (onTokens) {\n await onTokens(tokens);\n }\n\n return tokens.access_token;\n };\n}\n\n/**\n * Options for client credentials grant\n */\nexport interface ClientCredentialsOptions {\n /** Token endpoint URL */\n readonly tokenEndpoint: string;\n /** OAuth client ID */\n readonly clientId: string;\n /** OAuth client secret */\n readonly clientSecret: string;\n /** Requested scopes (optional) */\n readonly scope?: string;\n /** Additional parameters */\n readonly additionalParams?: Record<string, string>;\n}\n\n/**\n * Creates a token supplier using client credentials grant\n * Ideal for server-to-server authentication (machine-to-machine)\n *\n * @example\n * ```ts\n * const auth = oauthBearer({\n * tokenSupplier: tokenFromClientCredentials({\n * tokenEndpoint: 'https://auth.example.com/oauth/token',\n * clientId: process.env.CLIENT_ID,\n * clientSecret: process.env.CLIENT_SECRET,\n * scope: 'read:api write:api',\n * }),\n * jwks: jwksFromIssuer('https://auth.example.com/'),\n * });\n * ```\n */\nexport function tokenFromClientCredentials(options: ClientCredentialsOptions): TokenSupplier {\n const { tokenEndpoint, clientId, clientSecret, scope, additionalParams = {} } = options;\n\n return async (): Promise<string> => {\n const body = new URLSearchParams({\n grant_type: 'client_credentials',\n client_id: clientId,\n client_secret: clientSecret,\n ...additionalParams,\n });\n\n if (scope) {\n body.append('scope', scope);\n }\n\n const response = await fetch(tokenEndpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: body.toString(),\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => 'Unknown error');\n throw new Error(`Client credentials grant failed: ${response.status} - ${errorText}`);\n }\n\n const tokens = (await response.json()) as TokenResponse;\n return tokens.access_token;\n };\n}\n\n/**\n * Creates a simple token supplier from a static string\n * Useful for testing or when token is known at startup\n *\n * @example\n * ```ts\n * // For testing\n * const auth = oauthBearer({\n * tokenSupplier: tokenFromStatic('test-token'),\n * allowUnsafeMode: true,\n * });\n * ```\n */\nexport function tokenFromStatic(token: string): TokenSupplier {\n return () => token;\n}\n\n/**\n * Creates a token supplier that caches the result of another supplier\n * Reduces calls to expensive token operations\n *\n * @param supplier - The underlying token supplier\n * @param ttlMs - Cache TTL in milliseconds (default: 5 minutes)\n *\n * @example\n * ```ts\n * const auth = oauthBearer({\n * // Cache client credentials token for 5 minutes\n * tokenSupplier: tokenWithCache(\n * tokenFromClientCredentials({ ... }),\n * 5 * 60 * 1000\n * ),\n * jwks: jwksFromIssuer('https://auth.example.com/'),\n * });\n * ```\n */\nexport function tokenWithCache(supplier: TokenSupplier, ttlMs = 5 * 60 * 1000): TokenSupplier {\n let cachedToken: string | null = null;\n let expiresAt = 0;\n\n return async (): Promise<string> => {\n if (cachedToken && Date.now() < expiresAt) {\n return cachedToken;\n }\n\n const token = await supplier();\n cachedToken = token;\n expiresAt = Date.now() + ttlMs;\n return token;\n };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@unireq/oauth",
3
+ "version": "0.0.1",
4
+ "description": "OAuth Bearer authentication with JWT validation and auto-refresh for unireq",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "keywords": [
19
+ "oauth",
20
+ "bearer",
21
+ "jwt",
22
+ "authentication"
23
+ ],
24
+ "author": "Olivier Orabona",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "@unireq/core": "0.0.1",
28
+ "@unireq/config": "0.0.1"
29
+ },
30
+ "peerDependencies": {
31
+ "openid-client": "^6.1.3",
32
+ "jose": "^5.9.6"
33
+ },
34
+ "peerDependenciesMeta": {
35
+ "openid-client": {
36
+ "optional": true
37
+ },
38
+ "jose": {
39
+ "optional": true
40
+ }
41
+ },
42
+ "devDependencies": {
43
+ "typescript": "^5.9.3",
44
+ "tsup": "^8.5.1",
45
+ "vitest": "^4.0.16",
46
+ "jose": "^5.9.6"
47
+ },
48
+ "engines": {
49
+ "node": ">=18.0.0"
50
+ },
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "https://github.com/oorabona/unireq",
54
+ "directory": "packages/oauth"
55
+ },
56
+ "bugs": {
57
+ "url": "https://github.com/oorabona/unireq/issues"
58
+ },
59
+ "homepage": "https://github.com/oorabona/unireq/tree/main/packages/oauth",
60
+ "scripts": {
61
+ "build": "tsup",
62
+ "type-check": "tsc --noEmit",
63
+ "test": "vitest run",
64
+ "test:watch": "vitest",
65
+ "clean": "rm -rf dist *.tsbuildinfo"
66
+ }
67
+ }