@tekcify/auth-core-client 1.0.0
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/README.md +374 -0
- package/package.json +42 -0
- package/src/__tests__/pkce.test.ts +31 -0
- package/src/index.ts +3 -0
- package/src/oauth.ts +250 -0
- package/src/pkce.ts +38 -0
- package/src/types.ts +37 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +9 -0
package/README.md
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# @tekcify/auth-core-client
|
|
2
|
+
|
|
3
|
+
Core OAuth 2.0 client utilities for Tekcify Auth. This package provides low-level OAuth helpers that can be used across different frameworks and environments (browser, Node.js, React, Vue, etc.).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @tekcify/auth-core-client
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @tekcify/auth-core-client
|
|
11
|
+
# or
|
|
12
|
+
yarn add @tekcify/auth-core-client
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Overview
|
|
16
|
+
|
|
17
|
+
This package provides the foundational OAuth 2.0 functionality for Tekcify Auth, including:
|
|
18
|
+
|
|
19
|
+
- **PKCE (Proof Key for Code Exchange)** generation for secure OAuth flows
|
|
20
|
+
- **OAuth Client** class for managing authentication flows
|
|
21
|
+
- **Standalone functions** for framework-agnostic usage
|
|
22
|
+
- **Type definitions** for TypeScript support
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### 1. Generate PKCE Parameters
|
|
27
|
+
|
|
28
|
+
PKCE is required for secure OAuth flows, especially for public clients (SPAs, mobile apps).
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { generateCodeVerifier, generateCodeChallenge } from '@tekcify/auth-core-client';
|
|
32
|
+
|
|
33
|
+
// Generate a code verifier (43-128 characters)
|
|
34
|
+
const codeVerifier = generateCodeVerifier();
|
|
35
|
+
|
|
36
|
+
// Generate the code challenge (SHA256 hash)
|
|
37
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier, 'S256');
|
|
38
|
+
|
|
39
|
+
// Store the verifier securely (you'll need it later)
|
|
40
|
+
localStorage.setItem('code_verifier', codeVerifier);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Build Authorization URL
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { buildAuthorizeUrl } from '@tekcify/auth-core-client';
|
|
47
|
+
|
|
48
|
+
const authUrl = buildAuthorizeUrl('https://auth.example.com', {
|
|
49
|
+
clientId: 'your-client-id',
|
|
50
|
+
redirectUri: 'https://yourapp.com/callback',
|
|
51
|
+
scopes: ['read:profile', 'write:profile'],
|
|
52
|
+
state: crypto.randomUUID(), // CSRF protection
|
|
53
|
+
codeChallenge: codeChallenge,
|
|
54
|
+
codeChallengeMethod: 'S256',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Redirect user to authUrl
|
|
58
|
+
window.location.href = authUrl;
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 3. Exchange Authorization Code for Tokens
|
|
62
|
+
|
|
63
|
+
After the user is redirected back with an authorization code:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { exchangeCode } from '@tekcify/auth-core-client';
|
|
67
|
+
|
|
68
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
69
|
+
const code = urlParams.get('code');
|
|
70
|
+
const state = urlParams.get('state');
|
|
71
|
+
|
|
72
|
+
// Verify state matches (CSRF protection)
|
|
73
|
+
if (state !== expectedState) {
|
|
74
|
+
throw new Error('Invalid state parameter');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Retrieve the stored code verifier
|
|
78
|
+
const codeVerifier = localStorage.getItem('code_verifier');
|
|
79
|
+
|
|
80
|
+
const tokens = await exchangeCode('https://auth.example.com', {
|
|
81
|
+
code: code!,
|
|
82
|
+
clientId: 'your-client-id',
|
|
83
|
+
clientSecret: 'your-client-secret', // Only for confidential clients
|
|
84
|
+
redirectUri: 'https://yourapp.com/callback',
|
|
85
|
+
codeVerifier: codeVerifier!,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// tokens.accessToken - Use for API requests
|
|
89
|
+
// tokens.refreshToken - Store securely for token refresh
|
|
90
|
+
// tokens.expiresIn - Token expiration time in seconds
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 4. Refresh Access Token
|
|
94
|
+
|
|
95
|
+
Access tokens expire. Use the refresh token to get a new one:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { refreshAccessToken } from '@tekcify/auth-core-client';
|
|
99
|
+
|
|
100
|
+
const newTokens = await refreshAccessToken('https://auth.example.com', {
|
|
101
|
+
refreshToken: storedRefreshToken,
|
|
102
|
+
clientId: 'your-client-id',
|
|
103
|
+
clientSecret: 'your-client-secret',
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Update stored tokens
|
|
107
|
+
localStorage.setItem('access_token', newTokens.accessToken);
|
|
108
|
+
localStorage.setItem('refresh_token', newTokens.refreshToken);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 5. Get User Information
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { getUserInfo } from '@tekcify/auth-core-client';
|
|
115
|
+
|
|
116
|
+
const userInfo = await getUserInfo('https://auth.example.com', accessToken);
|
|
117
|
+
|
|
118
|
+
console.log(userInfo.email); // appdever01@gmail.com
|
|
119
|
+
console.log(userInfo.name); // John Doe
|
|
120
|
+
console.log(userInfo.email_verified); // true
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Using the OAuth Client Class
|
|
124
|
+
|
|
125
|
+
For a more object-oriented approach, use the `OAuthClient` class:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { OAuthClient } from '@tekcify/auth-core-client';
|
|
129
|
+
|
|
130
|
+
const client = new OAuthClient({
|
|
131
|
+
authServerUrl: 'https://auth.example.com',
|
|
132
|
+
clientId: 'your-client-id',
|
|
133
|
+
clientSecret: 'your-client-secret',
|
|
134
|
+
redirectUri: 'https://yourapp.com/callback',
|
|
135
|
+
scopes: ['read:profile', 'write:profile'],
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Build authorization URL
|
|
139
|
+
const authUrl = await client.buildAuthorizeUrl({
|
|
140
|
+
state: 'random-state',
|
|
141
|
+
codeChallenge: codeChallenge,
|
|
142
|
+
codeChallengeMethod: 'S256',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Exchange code
|
|
146
|
+
const tokens = await client.exchangeCode(code, codeVerifier);
|
|
147
|
+
|
|
148
|
+
// Refresh token
|
|
149
|
+
const newTokens = await client.refreshAccessToken(refreshToken);
|
|
150
|
+
|
|
151
|
+
// Get user info
|
|
152
|
+
const userInfo = await client.getUserInfo(accessToken);
|
|
153
|
+
|
|
154
|
+
// Revoke token (logout)
|
|
155
|
+
await client.revokeToken(accessToken);
|
|
156
|
+
|
|
157
|
+
// Introspect token (check if valid)
|
|
158
|
+
const introspection = await client.introspectToken(accessToken);
|
|
159
|
+
if (introspection.active) {
|
|
160
|
+
console.log('Token is valid');
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Complete OAuth Flow Example
|
|
165
|
+
|
|
166
|
+
Here's a complete example of implementing the OAuth flow:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import {
|
|
170
|
+
OAuthClient,
|
|
171
|
+
generateCodeVerifier,
|
|
172
|
+
generateCodeChallenge,
|
|
173
|
+
} from '@tekcify/auth-core-client';
|
|
174
|
+
|
|
175
|
+
// Initialize client
|
|
176
|
+
const client = new OAuthClient({
|
|
177
|
+
authServerUrl: process.env.AUTH_SERVER_URL!,
|
|
178
|
+
clientId: process.env.CLIENT_ID!,
|
|
179
|
+
clientSecret: process.env.CLIENT_SECRET!,
|
|
180
|
+
redirectUri: process.env.REDIRECT_URI!,
|
|
181
|
+
scopes: ['read:profile'],
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Step 1: Start login flow
|
|
185
|
+
async function login() {
|
|
186
|
+
const verifier = generateCodeVerifier();
|
|
187
|
+
const challenge = await generateCodeChallenge(verifier, 'S256');
|
|
188
|
+
const state = crypto.randomUUID();
|
|
189
|
+
|
|
190
|
+
// Store verifier and state securely
|
|
191
|
+
sessionStorage.setItem('oauth_verifier', verifier);
|
|
192
|
+
sessionStorage.setItem('oauth_state', state);
|
|
193
|
+
|
|
194
|
+
// Build and redirect to authorization URL
|
|
195
|
+
const authUrl = await client.buildAuthorizeUrl({
|
|
196
|
+
state,
|
|
197
|
+
codeChallenge: challenge,
|
|
198
|
+
codeChallengeMethod: 'S256',
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
window.location.href = authUrl;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Step 2: Handle callback
|
|
205
|
+
async function handleCallback() {
|
|
206
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
207
|
+
const code = urlParams.get('code');
|
|
208
|
+
const state = urlParams.get('state');
|
|
209
|
+
const storedState = sessionStorage.getItem('oauth_state');
|
|
210
|
+
const verifier = sessionStorage.getItem('oauth_verifier');
|
|
211
|
+
|
|
212
|
+
// Verify state
|
|
213
|
+
if (state !== storedState) {
|
|
214
|
+
throw new Error('Invalid state parameter');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!code || !verifier) {
|
|
218
|
+
throw new Error('Missing authorization code or verifier');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Exchange code for tokens
|
|
222
|
+
const tokens = await client.exchangeCode(code, verifier);
|
|
223
|
+
|
|
224
|
+
// Store tokens securely
|
|
225
|
+
localStorage.setItem('access_token', tokens.accessToken);
|
|
226
|
+
localStorage.setItem('refresh_token', tokens.refreshToken);
|
|
227
|
+
localStorage.setItem('token_expires_at', String(Date.now() + tokens.expiresIn * 1000));
|
|
228
|
+
|
|
229
|
+
// Clean up
|
|
230
|
+
sessionStorage.removeItem('oauth_verifier');
|
|
231
|
+
sessionStorage.removeItem('oauth_state');
|
|
232
|
+
|
|
233
|
+
return tokens;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Step 3: Make authenticated requests
|
|
237
|
+
async function fetchProtectedData() {
|
|
238
|
+
let accessToken = localStorage.getItem('access_token');
|
|
239
|
+
const expiresAt = parseInt(localStorage.getItem('token_expires_at') || '0');
|
|
240
|
+
|
|
241
|
+
// Refresh if expired
|
|
242
|
+
if (Date.now() >= expiresAt) {
|
|
243
|
+
const refreshToken = localStorage.getItem('refresh_token');
|
|
244
|
+
if (!refreshToken) {
|
|
245
|
+
throw new Error('No refresh token available');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const newTokens = await client.refreshAccessToken(refreshToken);
|
|
249
|
+
accessToken = newTokens.accessToken;
|
|
250
|
+
localStorage.setItem('access_token', newTokens.accessToken);
|
|
251
|
+
localStorage.setItem('refresh_token', newTokens.refreshToken);
|
|
252
|
+
localStorage.setItem('token_expires_at', String(Date.now() + newTokens.expiresIn * 1000));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Use access token
|
|
256
|
+
const response = await fetch('https://api.example.com/protected', {
|
|
257
|
+
headers: {
|
|
258
|
+
Authorization: `Bearer ${accessToken}`,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return response.json();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Step 4: Logout
|
|
266
|
+
async function logout() {
|
|
267
|
+
const accessToken = localStorage.getItem('access_token');
|
|
268
|
+
if (accessToken) {
|
|
269
|
+
await client.revokeToken(accessToken);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
localStorage.removeItem('access_token');
|
|
273
|
+
localStorage.removeItem('refresh_token');
|
|
274
|
+
localStorage.removeItem('token_expires_at');
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## API Reference
|
|
279
|
+
|
|
280
|
+
### Functions
|
|
281
|
+
|
|
282
|
+
#### `generateCodeVerifier(): string`
|
|
283
|
+
|
|
284
|
+
Generates a cryptographically random code verifier (43-128 characters) for PKCE.
|
|
285
|
+
|
|
286
|
+
#### `generateCodeChallenge(verifier: string, method?: 'plain' | 'S256'): Promise<string>`
|
|
287
|
+
|
|
288
|
+
Generates a code challenge from a verifier. Use `S256` (SHA256) for security.
|
|
289
|
+
|
|
290
|
+
#### `buildAuthorizeUrl(authServerUrl: string, config: AuthorizeConfig): string`
|
|
291
|
+
|
|
292
|
+
Builds the OAuth authorization URL.
|
|
293
|
+
|
|
294
|
+
#### `exchangeCode(authServerUrl: string, config: ExchangeCodeConfig): Promise<TokenResponse>`
|
|
295
|
+
|
|
296
|
+
Exchanges an authorization code for access and refresh tokens.
|
|
297
|
+
|
|
298
|
+
#### `refreshAccessToken(authServerUrl: string, config: RefreshTokenConfig): Promise<TokenResponse>`
|
|
299
|
+
|
|
300
|
+
Refreshes an expired access token using a refresh token.
|
|
301
|
+
|
|
302
|
+
#### `revokeToken(authServerUrl: string, config: RevokeTokenConfig): Promise<void>`
|
|
303
|
+
|
|
304
|
+
Revokes an access or refresh token.
|
|
305
|
+
|
|
306
|
+
#### `introspectToken(authServerUrl: string, config: IntrospectConfig): Promise<IntrospectResult>`
|
|
307
|
+
|
|
308
|
+
Checks if a token is valid and returns its metadata.
|
|
309
|
+
|
|
310
|
+
#### `getUserInfo(authServerUrl: string, accessToken: string): Promise<UserInfo>`
|
|
311
|
+
|
|
312
|
+
Retrieves user information using an access token.
|
|
313
|
+
|
|
314
|
+
### Types
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
interface TokenResponse {
|
|
318
|
+
accessToken: string;
|
|
319
|
+
refreshToken: string;
|
|
320
|
+
tokenType: string; // Usually "Bearer"
|
|
321
|
+
expiresIn: number; // Seconds until expiration
|
|
322
|
+
scope: string; // Space-separated scopes
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
interface UserInfo {
|
|
326
|
+
sub: string; // User ID
|
|
327
|
+
email: string;
|
|
328
|
+
email_verified: boolean;
|
|
329
|
+
given_name: string | null;
|
|
330
|
+
family_name: string | null;
|
|
331
|
+
name: string | null;
|
|
332
|
+
picture: string | null;
|
|
333
|
+
updated_at: number; // Unix timestamp
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
interface IntrospectResult {
|
|
337
|
+
active: boolean;
|
|
338
|
+
clientId?: string;
|
|
339
|
+
username?: string;
|
|
340
|
+
scope?: string;
|
|
341
|
+
sub?: string;
|
|
342
|
+
exp?: number;
|
|
343
|
+
iat?: number;
|
|
344
|
+
tokenType?: string;
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Security Best Practices
|
|
349
|
+
|
|
350
|
+
1. **Always use PKCE**: Required for public clients, recommended for all
|
|
351
|
+
2. **Store tokens securely**: Use httpOnly cookies or secure storage
|
|
352
|
+
3. **Validate state parameter**: Prevents CSRF attacks
|
|
353
|
+
4. **Never expose client secrets**: Only use in server-side code
|
|
354
|
+
5. **Handle token expiration**: Implement automatic refresh logic
|
|
355
|
+
6. **Revoke tokens on logout**: Prevents unauthorized access
|
|
356
|
+
|
|
357
|
+
## Browser Compatibility
|
|
358
|
+
|
|
359
|
+
This package uses modern Web APIs:
|
|
360
|
+
- `crypto.getRandomValues()` for random number generation
|
|
361
|
+
- `crypto.subtle.digest()` for SHA256 hashing
|
|
362
|
+
- `fetch()` for HTTP requests
|
|
363
|
+
|
|
364
|
+
Supported in all modern browsers (Chrome, Firefox, Safari, Edge).
|
|
365
|
+
|
|
366
|
+
## Node.js Usage
|
|
367
|
+
|
|
368
|
+
For Node.js environments, ensure you have:
|
|
369
|
+
- Node.js 18+ (for native fetch support)
|
|
370
|
+
- Or use a fetch polyfill like `node-fetch`
|
|
371
|
+
|
|
372
|
+
## License
|
|
373
|
+
|
|
374
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tekcify/auth-core-client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Core OAuth 2.0 client utilities for Tekcify Auth",
|
|
5
|
+
"author": "Tekcify",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/tekcify/auth.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/tekcify/auth/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/tekcify/auth#readme",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js",
|
|
20
|
+
"require": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"clean": "rm -rf dist",
|
|
26
|
+
"lint": "eslint \"src/**/*.ts\" --max-warnings=0",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"test:watch": "vitest"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"oauth",
|
|
32
|
+
"oauth2",
|
|
33
|
+
"pkce",
|
|
34
|
+
"auth"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"typescript": "^5.7.3",
|
|
39
|
+
"vitest": "^4.0.15"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generateCodeVerifier, generateCodeChallenge } from '../pkce';
|
|
3
|
+
|
|
4
|
+
describe('PKCE', () => {
|
|
5
|
+
it('should generate a code verifier', () => {
|
|
6
|
+
const verifier = generateCodeVerifier();
|
|
7
|
+
expect(verifier).toBeDefined();
|
|
8
|
+
expect(verifier.length).toBeGreaterThanOrEqual(43);
|
|
9
|
+
expect(verifier.length).toBeLessThanOrEqual(128);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should generate different verifiers each time', () => {
|
|
13
|
+
const verifier1 = generateCodeVerifier();
|
|
14
|
+
const verifier2 = generateCodeVerifier();
|
|
15
|
+
expect(verifier1).not.toBe(verifier2);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should generate a code challenge from verifier (S256)', async () => {
|
|
19
|
+
const verifier = generateCodeVerifier();
|
|
20
|
+
const challenge = await generateCodeChallenge(verifier, 'S256');
|
|
21
|
+
expect(challenge).toBeDefined();
|
|
22
|
+
expect(challenge).not.toBe(verifier);
|
|
23
|
+
expect(challenge.length).toBeGreaterThan(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should generate a plain code challenge', async () => {
|
|
27
|
+
const verifier = generateCodeVerifier();
|
|
28
|
+
const challenge = await generateCodeChallenge(verifier, 'plain');
|
|
29
|
+
expect(challenge).toBe(verifier);
|
|
30
|
+
});
|
|
31
|
+
});
|
package/src/index.ts
ADDED
package/src/oauth.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TokenResponse,
|
|
3
|
+
UserInfo,
|
|
4
|
+
IntrospectResult,
|
|
5
|
+
OAuthConfig,
|
|
6
|
+
} from './types';
|
|
7
|
+
import { generateCodeVerifier, generateCodeChallenge } from './pkce';
|
|
8
|
+
|
|
9
|
+
export function buildAuthorizeUrl(
|
|
10
|
+
authServerUrl: string,
|
|
11
|
+
config: {
|
|
12
|
+
clientId: string;
|
|
13
|
+
redirectUri: string;
|
|
14
|
+
scopes: string[];
|
|
15
|
+
state?: string;
|
|
16
|
+
codeChallenge?: string;
|
|
17
|
+
codeChallengeMethod?: 'plain' | 'S256';
|
|
18
|
+
},
|
|
19
|
+
): string {
|
|
20
|
+
const url = new URL(`${authServerUrl}/oauth/authorize`);
|
|
21
|
+
url.searchParams.set('clientId', config.clientId);
|
|
22
|
+
url.searchParams.set('redirectUri', config.redirectUri);
|
|
23
|
+
url.searchParams.set('scopes', config.scopes.join(' '));
|
|
24
|
+
if (config.state) {
|
|
25
|
+
url.searchParams.set('state', config.state);
|
|
26
|
+
}
|
|
27
|
+
if (config.codeChallenge) {
|
|
28
|
+
url.searchParams.set('codeChallenge', config.codeChallenge);
|
|
29
|
+
url.searchParams.set(
|
|
30
|
+
'codeChallengeMethod',
|
|
31
|
+
config.codeChallengeMethod ?? 'S256',
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return url.toString();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function exchangeCode(
|
|
38
|
+
authServerUrl: string,
|
|
39
|
+
config: {
|
|
40
|
+
code: string;
|
|
41
|
+
clientId: string;
|
|
42
|
+
clientSecret: string;
|
|
43
|
+
redirectUri: string;
|
|
44
|
+
codeVerifier?: string;
|
|
45
|
+
},
|
|
46
|
+
): Promise<TokenResponse> {
|
|
47
|
+
const response = await fetch(`${authServerUrl}/oauth/token`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
grant_type: 'authorization_code',
|
|
54
|
+
code: config.code,
|
|
55
|
+
clientId: config.clientId,
|
|
56
|
+
clientSecret: config.clientSecret,
|
|
57
|
+
redirectUri: config.redirectUri,
|
|
58
|
+
codeVerifier: config.codeVerifier,
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const error = (await response.json().catch(() => ({}))) as {
|
|
64
|
+
message?: string;
|
|
65
|
+
};
|
|
66
|
+
throw new Error(error.message ?? 'Failed to exchange authorization code');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (await response.json()) as TokenResponse;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function refreshAccessToken(
|
|
73
|
+
authServerUrl: string,
|
|
74
|
+
config: {
|
|
75
|
+
refreshToken: string;
|
|
76
|
+
clientId: string;
|
|
77
|
+
clientSecret: string;
|
|
78
|
+
},
|
|
79
|
+
): Promise<TokenResponse> {
|
|
80
|
+
const response = await fetch(`${authServerUrl}/oauth/token`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'application/json',
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify({
|
|
86
|
+
grant_type: 'refresh_token',
|
|
87
|
+
refreshToken: config.refreshToken,
|
|
88
|
+
clientId: config.clientId,
|
|
89
|
+
clientSecret: config.clientSecret,
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const error = (await response.json().catch(() => ({}))) as {
|
|
95
|
+
message?: string;
|
|
96
|
+
};
|
|
97
|
+
throw new Error(error.message ?? 'Failed to refresh access token');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (await response.json()) as TokenResponse;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function revokeToken(
|
|
104
|
+
authServerUrl: string,
|
|
105
|
+
config: {
|
|
106
|
+
token: string;
|
|
107
|
+
clientId: string;
|
|
108
|
+
clientSecret: string;
|
|
109
|
+
},
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
const response = await fetch(`${authServerUrl}/oauth/revoke`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: {
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
},
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
token: config.token,
|
|
118
|
+
clientId: config.clientId,
|
|
119
|
+
clientSecret: config.clientSecret,
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
const error = (await response.json().catch(() => ({}))) as {
|
|
125
|
+
message?: string;
|
|
126
|
+
};
|
|
127
|
+
throw new Error(error.message ?? 'Failed to revoke token');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function introspectToken(
|
|
132
|
+
authServerUrl: string,
|
|
133
|
+
config: {
|
|
134
|
+
token: string;
|
|
135
|
+
clientId?: string;
|
|
136
|
+
clientSecret?: string;
|
|
137
|
+
},
|
|
138
|
+
): Promise<IntrospectResult> {
|
|
139
|
+
const body: Record<string, string> = {
|
|
140
|
+
token: config.token,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (config.clientId) {
|
|
144
|
+
body.clientId = config.clientId;
|
|
145
|
+
}
|
|
146
|
+
if (config.clientSecret) {
|
|
147
|
+
body.clientSecret = config.clientSecret;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const response = await fetch(`${authServerUrl}/oauth/token/introspect`, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: {
|
|
153
|
+
'Content-Type': 'application/json',
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify(body),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
const error = (await response.json().catch(() => ({}))) as {
|
|
160
|
+
message?: string;
|
|
161
|
+
};
|
|
162
|
+
throw new Error(error.message ?? 'Failed to introspect token');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return (await response.json()) as IntrospectResult;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function getUserInfo(
|
|
169
|
+
authServerUrl: string,
|
|
170
|
+
accessToken: string,
|
|
171
|
+
): Promise<UserInfo> {
|
|
172
|
+
const response = await fetch(`${authServerUrl}/oauth/userinfo`, {
|
|
173
|
+
method: 'GET',
|
|
174
|
+
headers: {
|
|
175
|
+
Authorization: `Bearer ${accessToken}`,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
const error = (await response.json().catch(() => ({}))) as {
|
|
181
|
+
message?: string;
|
|
182
|
+
};
|
|
183
|
+
throw new Error(error.message ?? 'Failed to get user info');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return (await response.json()) as UserInfo;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export class OAuthClient {
|
|
190
|
+
constructor(private config: OAuthConfig) {}
|
|
191
|
+
|
|
192
|
+
async buildAuthorizeUrl(options?: {
|
|
193
|
+
state?: string;
|
|
194
|
+
codeChallenge?: string;
|
|
195
|
+
codeChallengeMethod?: 'plain' | 'S256';
|
|
196
|
+
}): Promise<string> {
|
|
197
|
+
return buildAuthorizeUrl(this.config.authServerUrl, {
|
|
198
|
+
clientId: this.config.clientId,
|
|
199
|
+
redirectUri: this.config.redirectUri,
|
|
200
|
+
scopes: this.config.scopes,
|
|
201
|
+
...options,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async exchangeCode(
|
|
206
|
+
code: string,
|
|
207
|
+
codeVerifier?: string,
|
|
208
|
+
): Promise<TokenResponse> {
|
|
209
|
+
return exchangeCode(this.config.authServerUrl, {
|
|
210
|
+
code,
|
|
211
|
+
clientId: this.config.clientId,
|
|
212
|
+
clientSecret: this.config.clientSecret,
|
|
213
|
+
redirectUri: this.config.redirectUri,
|
|
214
|
+
codeVerifier,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async refreshAccessToken(refreshToken: string): Promise<TokenResponse> {
|
|
219
|
+
return refreshAccessToken(this.config.authServerUrl, {
|
|
220
|
+
refreshToken,
|
|
221
|
+
clientId: this.config.clientId,
|
|
222
|
+
clientSecret: this.config.clientSecret,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async revokeToken(token: string): Promise<void> {
|
|
227
|
+
return revokeToken(this.config.authServerUrl, {
|
|
228
|
+
token,
|
|
229
|
+
clientId: this.config.clientId,
|
|
230
|
+
clientSecret: this.config.clientSecret,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async introspectToken(
|
|
235
|
+
token: string,
|
|
236
|
+
options?: { clientId?: string; clientSecret?: string },
|
|
237
|
+
): Promise<IntrospectResult> {
|
|
238
|
+
return introspectToken(this.config.authServerUrl, {
|
|
239
|
+
token,
|
|
240
|
+
clientId: options?.clientId ?? this.config.clientId,
|
|
241
|
+
clientSecret: options?.clientSecret ?? this.config.clientSecret,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async getUserInfo(accessToken: string): Promise<UserInfo> {
|
|
246
|
+
return getUserInfo(this.config.authServerUrl, accessToken);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export { generateCodeVerifier, generateCodeChallenge };
|