@@ -144,6 +144,212 @@ const fgaClient = new OpenFgaClient({
144144});
145145``` 
146146
147+ ### OIDC Token Endpoint Configuration  
148+ 
149+ The SDK supports custom OIDC token endpoints for compatibility with various OIDC providers.
150+ 
151+ #### Default Behavior  
152+ -  When ` apiTokenIssuer `  is just a domain (e.g., ` "auth.example.com" ` ), the SDK appends ` /oauth/token ` 
153+ -  Example: ` auth.example.com `  → ` https://auth.example.com/oauth/token ` 
154+ 
155+ #### Custom Token Paths  
156+ -  When ` apiTokenIssuer `  includes a path, that path is used as-is
157+ -  Examples:
158+   -  ` auth.example.com/oauth/v2 `  → ` https://auth.example.com/oauth/v2 ` 
159+   -  ` https://auth.example.com/oauth/v2/token `  → ` https://auth.example.com/oauth/v2/token ` 
160+ 
161+ #### Provider Examples  
162+ 
163+ ** Zitadel:** 
164+ ``` javascript 
165+ apiTokenIssuer:  ' https://auth.zitadel.example/oauth/v2/token' 
166+ ``` 
167+ 
168+ Entra ID (Azure AD):
169+ ``` 
170+ apiTokenIssuer: 'https://login.microsoftonline.com/tenant-id/oauth2/v2.0/token' 
171+ ``` 
172+ 
173+ Auth0:
174+ 
175+ ``` 
176+ apiTokenIssuer: 'https://your-domain.auth0.com/oauth/token' 
177+ ``` 
178+ 
179+ This ensures compatibility with OIDC providers that use non-standard token endpoint paths.
180+ 
181+ ``` 
182+ ## 3. Create the OIDC Test File 
183+ 
184+ Create `tests/credentials-oidc.test.ts`: 
185+ 
186+ ```typescript 
187+ import nock from 'nock'; 
188+ import { OpenFgaClient, UserClientConfigurationParams } from '../src'; 
189+ import { CredentialsMethod } from '../src/credentials'; 
190+ import { baseConfig } from './helpers/default-config'; 
191+ 
192+ describe('OIDC Token Path Handling', () => { 
193+   beforeEach(() => { 
194+     nock.disableNetConnect(); 
195+   }); 
196+ 
197+   afterEach(() => { 
198+     nock.cleanAll(); 
199+     nock.enableNetConnect(); 
200+   }); 
201+ 
202+   const testOidcConfig = (apiTokenIssuer: string, expectedTokenUrl: string) => { 
203+     const config: UserClientConfigurationParams = { 
204+       ...baseConfig, 
205+       credentials: { 
206+         method: CredentialsMethod.ClientCredentials, 
207+         config: { 
208+           clientId: 'test-client', 
209+           clientSecret: 'test-secret', 
210+           apiTokenIssuer, 
211+           apiAudience: 'https://api.fga.example' 
212+         } 
213+       } 
214+     }; 
215+ 
216+     // Mock the token endpoint 
217+     const tokenScope = nock(expectedTokenUrl.split('/').slice(0, 3).join('/')) 
218+       .post(expectedTokenUrl.split('/').slice(3).join('/')) 
219+       .reply(200, { 
220+         access_token: 'test-token', 
221+         expires_in: 300 
222+       }); 
223+ 
224+     // Mock the FGA API call 
225+     const apiScope = nock('https://api.fga.example') 
226+       .post(`/stores/${baseConfig.storeId}/check`) 
227+       .reply(200, { allowed: true }); 
228+ 
229+     return { config, tokenScope, apiScope }; 
230+   }; 
231+ 
232+   it('should append /oauth/token when no path is provided', async () => { 
233+     const { config, tokenScope, apiScope } = testOidcConfig( 
234+       'auth.example.com', 
235+       'https://auth.example.com/oauth/token' 
236+     ); 
237+ 
238+     const client = new OpenFgaClient(config); 
239+     await client.check({ 
240+       user: 'user:test', 
241+       relation: 'reader', 
242+       object: 'document:test' 
243+     }); 
244+ 
245+     expect(tokenScope.isDone()).toBe(true); 
246+     expect(apiScope.isDone()).toBe(true); 
247+   }); 
248+ 
249+   it('should respect custom token paths', async () => { 
250+     const { config, tokenScope, apiScope } = testOidcConfig( 
251+       'auth.example.com/oauth/v2/token', 
252+       'https://auth.example.com/oauth/v2/token' 
253+     ); 
254+ 
255+     const client = new OpenFgaClient(config); 
256+     await client.check({ 
257+       user: 'user:test', 
258+       relation: 'reader', 
259+       object: 'document:test' 
260+     }); 
261+ 
262+     expect(tokenScope.isDone()).toBe(true); 
263+     expect(apiScope.isDone()).toBe(true); 
264+   }); 
265+ 
266+   it('should handle full URLs with custom paths', async () => { 
267+     const { config, tokenScope, apiScope } = testOidcConfig( 
268+       'https://auth.example.com/oauth/v2/token', 
269+       'https://auth.example.com/oauth/v2/token' 
270+     ); 
271+ 
272+     const client = new OpenFgaClient(config); 
273+     await client.check({ 
274+       user: 'user:test', 
275+       relation: 'reader', 
276+       object: 'document:test' 
277+     }); 
278+ 
279+     expect(tokenScope.isDone()).toBe(true); 
280+     expect(apiScope.isDone()).toBe(true); 
281+   }); 
282+ 
283+   it('should handle paths with trailing slashes', async () => { 
284+     const { config, tokenScope, apiScope } = testOidcConfig( 
285+       'auth.example.com/oauth/v2/', 
286+       'https://auth.example.com/oauth/v2' 
287+     ); 
288+ 
289+     const client = new OpenFgaClient(config); 
290+     await client.check({ 
291+       user: 'user:test', 
292+       relation: 'reader', 
293+       object: 'document:test' 
294+     }); 
295+ 
296+     expect(tokenScope.isDone()).toBe(true); 
297+     expect(apiScope.isDone()).toBe(true); 
298+   }); 
299+ 
300+   it('should handle root path correctly', async () => { 
301+     const { config, tokenScope, apiScope } = testOidcConfig( 
302+       'auth.example.com/', 
303+       'https://auth.example.com/oauth/token' 
304+     ); 
305+ 
306+     const client = new OpenFgaClient(config); 
307+     await client.check({ 
308+       user: 'user:test', 
309+       relation: 'reader', 
310+       object: 'document:test' 
311+     }); 
312+ 
313+     expect(tokenScope.isDone()).toBe(true); 
314+     expect(apiScope.isDone()).toBe(true); 
315+   }); 
316+ 
317+   it('should work with Zitadel-style paths (/oauth/v2/token)', async () => { 
318+     const { config, tokenScope, apiScope } = testOidcConfig( 
319+       'https://auth.zitadel.example/oauth/v2/token', 
320+       'https://auth.zitadel.example/oauth/v2/token' 
321+     ); 
322+ 
323+     const client = new OpenFgaClient(config); 
324+     await client.check({ 
325+       user: 'user:test', 
326+       relation: 'reader', 
327+       object: 'document:test' 
328+     }); 
329+ 
330+     expect(tokenScope.isDone()).toBe(true); 
331+     expect(apiScope.isDone()).toBe(true); 
332+   }); 
333+ 
334+   it('should work with Entra ID/Azure AD style paths', async () => { 
335+     const { config, tokenScope, apiScope } = testOidcConfig( 
336+       'https://login.microsoftonline.com/tenant-id/oauth2/v2.0/token', 
337+       'https://login.microsoftonline.com/tenant-id/oauth2/v2.0/token' 
338+     ); 
339+ 
340+     const client = new OpenFgaClient(config); 
341+     await client.check({ 
342+       user: 'user:test', 
343+       relation: 'reader', 
344+       object: 'document:test' 
345+     }); 
346+ 
347+     expect(tokenScope.isDone()).toBe(true); 
348+     expect(apiScope.isDone()).toBe(true); 
349+   }); 
350+ }); 
351+ ``` 
352+ 
147353### Custom Headers  
148354
149355#### Default Headers  
0 commit comments