iglobals-auth-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 +367 -0
- package/dist/client.d.ts +19 -0
- package/dist/client.js +105 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +17 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +31 -0
- package/dist/jwks.d.ts +7 -0
- package/dist/jwks.js +46 -0
- package/dist/middleware.d.ts +12 -0
- package/dist/middleware.js +39 -0
- package/dist/pkce.d.ts +4 -0
- package/dist/pkce.js +15 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.js +2 -0
- package/package.json +34 -0
- package/src/client.ts +117 -0
- package/src/errors.ts +14 -0
- package/src/index.ts +22 -0
- package/src/jwks.ts +48 -0
- package/src/middleware.ts +48 -0
- package/src/pkce.ts +11 -0
- package/src/types.ts +50 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# iGlobals Auth Client - JavaScript/TypeScript SDK
|
|
2
|
+
|
|
3
|
+
Official JavaScript/TypeScript SDK for integrating with iGlobals Central Auth (ICA) - an OAuth 2.0 and OpenID Connect authentication server.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @iglobals/auth-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { ICAClient } from '@iglobals/auth-client';
|
|
15
|
+
|
|
16
|
+
// Initialize the client
|
|
17
|
+
const client = new ICAClient({
|
|
18
|
+
baseUrl: 'https://auth.yourdomain.com', // Your ICA deployment URL
|
|
19
|
+
clientId: 'your-client-id', // From admin portal
|
|
20
|
+
clientSecret: 'your-client-secret', // From admin portal
|
|
21
|
+
redirectUri: 'https://yourapp.com/callback', // Your callback URL
|
|
22
|
+
scopes: ['openid', 'profile', 'email'] // Optional, defaults shown
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Generate PKCE challenge
|
|
26
|
+
const { codeVerifier, codeChallenge } = client.generatePKCE();
|
|
27
|
+
const state = crypto.randomUUID();
|
|
28
|
+
|
|
29
|
+
// Get authorization URL
|
|
30
|
+
const authUrl = client.getAuthorizationUrl(state, codeChallenge);
|
|
31
|
+
|
|
32
|
+
// Redirect user to authUrl
|
|
33
|
+
window.location.href = authUrl;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Complete OAuth Flow Example
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import express from 'express';
|
|
40
|
+
import { ICAClient } from '@iglobals/auth-client';
|
|
41
|
+
|
|
42
|
+
const app = express();
|
|
43
|
+
|
|
44
|
+
const client = new ICAClient({
|
|
45
|
+
baseUrl: 'https://auth.yourdomain.com',
|
|
46
|
+
clientId: process.env.ICA_CLIENT_ID!,
|
|
47
|
+
clientSecret: process.env.ICA_CLIENT_SECRET!,
|
|
48
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Store PKCE values per session (use proper session management in production)
|
|
52
|
+
const sessions = new Map();
|
|
53
|
+
|
|
54
|
+
// Login route
|
|
55
|
+
app.get('/login', (req, res) => {
|
|
56
|
+
const { codeVerifier, codeChallenge } = client.generatePKCE();
|
|
57
|
+
const state = crypto.randomUUID();
|
|
58
|
+
|
|
59
|
+
// Store for callback
|
|
60
|
+
sessions.set(state, { codeVerifier });
|
|
61
|
+
|
|
62
|
+
const authUrl = client.getAuthorizationUrl(state, codeChallenge);
|
|
63
|
+
res.redirect(authUrl);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Callback route
|
|
67
|
+
app.get('/callback', async (req, res) => {
|
|
68
|
+
const { code, state } = req.query;
|
|
69
|
+
|
|
70
|
+
const session = sessions.get(state);
|
|
71
|
+
if (!session) {
|
|
72
|
+
return res.status(400).send('Invalid state');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// Exchange code for tokens
|
|
77
|
+
const tokens = await client.exchangeCode(code as string, session.codeVerifier);
|
|
78
|
+
|
|
79
|
+
// Get user info
|
|
80
|
+
const userInfo = await client.getUserInfo(tokens.access_token);
|
|
81
|
+
|
|
82
|
+
// Store tokens securely (use httpOnly cookies in production)
|
|
83
|
+
res.json({
|
|
84
|
+
user: userInfo,
|
|
85
|
+
tokens: tokens
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Auth error:', error);
|
|
89
|
+
res.status(500).send('Authentication failed');
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
app.listen(3000);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## API Reference
|
|
97
|
+
|
|
98
|
+
### `ICAClient`
|
|
99
|
+
|
|
100
|
+
#### Constructor
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
new ICAClient(config: ICAConfig)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Config Options:**
|
|
107
|
+
- `baseUrl` (string, required): Your ICA server URL (e.g., `https://auth.yourdomain.com`)
|
|
108
|
+
- `clientId` (string, required): OAuth client ID from ICA admin portal
|
|
109
|
+
- `clientSecret` (string, required): OAuth client secret from ICA admin portal
|
|
110
|
+
- `redirectUri` (string, required): Your application's callback URL
|
|
111
|
+
- `scopes` (string[], optional): OAuth scopes. Default: `['openid', 'profile', 'email']`
|
|
112
|
+
|
|
113
|
+
#### Methods
|
|
114
|
+
|
|
115
|
+
##### `generatePKCE()`
|
|
116
|
+
Generates a PKCE code verifier and challenge.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
const { codeVerifier, codeChallenge } = client.generatePKCE();
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Returns:**
|
|
123
|
+
- `codeVerifier`: Random string to verify the authorization
|
|
124
|
+
- `codeChallenge`: SHA-256 hash for the authorization request
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
##### `getAuthorizationUrl(state: string, codeChallenge: string)`
|
|
129
|
+
Builds the OAuth authorization URL.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
const authUrl = client.getAuthorizationUrl(state, codeChallenge);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Parameters:**
|
|
136
|
+
- `state`: Random string for CSRF protection
|
|
137
|
+
- `codeChallenge`: From `generatePKCE()`
|
|
138
|
+
|
|
139
|
+
**Returns:** Authorization URL to redirect the user to
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
##### `exchangeCode(code: string, codeVerifier: string)`
|
|
144
|
+
Exchanges authorization code for tokens.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
const tokens = await client.exchangeCode(code, codeVerifier);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Parameters:**
|
|
151
|
+
- `code`: Authorization code from callback
|
|
152
|
+
- `codeVerifier`: From `generatePKCE()`
|
|
153
|
+
|
|
154
|
+
**Returns:** `TokenSet`
|
|
155
|
+
```typescript
|
|
156
|
+
{
|
|
157
|
+
access_token: string;
|
|
158
|
+
token_type: 'Bearer';
|
|
159
|
+
expires_in: number; // Seconds until expiration
|
|
160
|
+
refresh_token: string;
|
|
161
|
+
id_token?: string; // If 'openid' scope requested
|
|
162
|
+
scope: string;
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
##### `refreshAccessToken(refreshToken: string)`
|
|
169
|
+
Gets a new access token using a refresh token.
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
const newTokens = await client.refreshAccessToken(tokens.refresh_token);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Returns:** New `TokenSet`
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
##### `getUserInfo(accessToken: string)`
|
|
180
|
+
Fetches user profile information.
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
const userInfo = await client.getUserInfo(tokens.access_token);
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Returns:** `UserInfoClaims`
|
|
187
|
+
```typescript
|
|
188
|
+
{
|
|
189
|
+
sub: string; // User ID
|
|
190
|
+
email?: string; // If 'email' scope
|
|
191
|
+
email_verified?: boolean;
|
|
192
|
+
given_name?: string; // If 'profile' scope
|
|
193
|
+
family_name?: string;
|
|
194
|
+
phone_number?: string; // If 'phone' scope
|
|
195
|
+
phone_number_verified?: boolean;
|
|
196
|
+
address?: { // If 'address' scope
|
|
197
|
+
street_address?: string;
|
|
198
|
+
locality?: string;
|
|
199
|
+
region?: string;
|
|
200
|
+
postal_code?: string;
|
|
201
|
+
country?: string;
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
##### `verifyToken(jwt: string)`
|
|
209
|
+
Verifies a JWT token's signature using JWKS.
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
const payload = await client.verifyToken(tokens.access_token);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Returns:** Decoded JWT payload
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
##### `revokeToken(refreshToken: string)`
|
|
220
|
+
Revokes a refresh token.
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
await client.revokeToken(tokens.refresh_token);
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Returns:** `{ revoked: boolean }`
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Express Middleware
|
|
231
|
+
|
|
232
|
+
Protect routes with automatic token verification:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { ICAMiddleware } from '@iglobals/auth-client';
|
|
236
|
+
|
|
237
|
+
app.use(ICAMiddleware({
|
|
238
|
+
baseUrl: 'https://auth.yourdomain.com',
|
|
239
|
+
clientId: 'your-client-id'
|
|
240
|
+
}));
|
|
241
|
+
|
|
242
|
+
// Protected route
|
|
243
|
+
app.get('/profile', (req, res) => {
|
|
244
|
+
// req.user contains verified user info
|
|
245
|
+
res.json({ user: req.user });
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Error Handling
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
import { ICAError } from '@iglobals/auth-client';
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const tokens = await client.exchangeCode(code, verifier);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (error instanceof ICAError) {
|
|
258
|
+
console.error('OAuth error:', error.error);
|
|
259
|
+
console.error('Description:', error.description);
|
|
260
|
+
console.error('Status:', error.statusCode);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## TypeScript Support
|
|
266
|
+
|
|
267
|
+
Fully typed with TypeScript definitions included.
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import {
|
|
271
|
+
ICAClient,
|
|
272
|
+
ICAConfig,
|
|
273
|
+
TokenSet,
|
|
274
|
+
UserInfoClaims,
|
|
275
|
+
JWTPayload
|
|
276
|
+
} from '@iglobals/auth-client';
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Security Best Practices
|
|
280
|
+
|
|
281
|
+
1. **Never expose client secret in frontend code** - use backend-only
|
|
282
|
+
2. **Use HTTPS** in production for `redirectUri` and `baseUrl`
|
|
283
|
+
3. **Store tokens securely** - use httpOnly cookies, not localStorage
|
|
284
|
+
4. **Implement proper session management** - don't store PKCE in memory for production
|
|
285
|
+
5. **Validate state parameter** - prevent CSRF attacks
|
|
286
|
+
6. **Use short-lived access tokens** - rely on refresh tokens for long sessions
|
|
287
|
+
|
|
288
|
+
## Configuration Requirements
|
|
289
|
+
|
|
290
|
+
Your ICA server must have:
|
|
291
|
+
- A registered OAuth client with your `clientId` and `clientSecret`
|
|
292
|
+
- Your `redirectUri` added to the client's allowed redirect URIs
|
|
293
|
+
- Appropriate scopes enabled for the client
|
|
294
|
+
|
|
295
|
+
## Complete Next.js Example
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
// app/login/page.tsx
|
|
299
|
+
'use client';
|
|
300
|
+
|
|
301
|
+
export default function LoginPage() {
|
|
302
|
+
const handleLogin = () => {
|
|
303
|
+
window.location.href = '/api/auth/login';
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
return <button onClick={handleLogin}>Login with ICA</button>;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// app/api/auth/login/route.ts
|
|
310
|
+
import { ICAClient } from '@iglobals/auth-client';
|
|
311
|
+
import { redirect } from 'next/navigation';
|
|
312
|
+
import { cookies } from 'next/headers';
|
|
313
|
+
|
|
314
|
+
const client = new ICAClient({
|
|
315
|
+
baseUrl: process.env.ICA_BASE_URL!,
|
|
316
|
+
clientId: process.env.ICA_CLIENT_ID!,
|
|
317
|
+
clientSecret: process.env.ICA_CLIENT_SECRET!,
|
|
318
|
+
redirectUri: process.env.ICA_REDIRECT_URI!,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
export async function GET() {
|
|
322
|
+
const { codeVerifier, codeChallenge } = client.generatePKCE();
|
|
323
|
+
const state = crypto.randomUUID();
|
|
324
|
+
|
|
325
|
+
const cookieStore = await cookies();
|
|
326
|
+
cookieStore.set('ica_state', state);
|
|
327
|
+
cookieStore.set('ica_verifier', codeVerifier);
|
|
328
|
+
|
|
329
|
+
const authUrl = client.getAuthorizationUrl(state, codeChallenge);
|
|
330
|
+
redirect(authUrl);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// app/api/auth/callback/route.ts
|
|
334
|
+
export async function GET(req: Request) {
|
|
335
|
+
const { searchParams } = new URL(req.url);
|
|
336
|
+
const code = searchParams.get('code');
|
|
337
|
+
const state = searchParams.get('state');
|
|
338
|
+
|
|
339
|
+
const cookieStore = await cookies();
|
|
340
|
+
const savedState = cookieStore.get('ica_state')?.value;
|
|
341
|
+
const verifier = cookieStore.get('ica_verifier')?.value;
|
|
342
|
+
|
|
343
|
+
if (!code || !verifier || state !== savedState) {
|
|
344
|
+
return new Response('Invalid request', { status: 400 });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const tokens = await client.exchangeCode(code, verifier);
|
|
348
|
+
|
|
349
|
+
// Store tokens securely and redirect
|
|
350
|
+
cookieStore.set('ica_access_token', tokens.access_token, {
|
|
351
|
+
httpOnly: true,
|
|
352
|
+
secure: true,
|
|
353
|
+
sameSite: 'lax'
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
redirect('/dashboard');
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Support
|
|
361
|
+
|
|
362
|
+
- Documentation: [https://github.com/yourusername/iglobals-cauth](https://github.com/yourusername/iglobals-cauth)
|
|
363
|
+
- Issues: [https://github.com/yourusername/iglobals-cauth/issues](https://github.com/yourusername/iglobals-cauth/issues)
|
|
364
|
+
|
|
365
|
+
## License
|
|
366
|
+
|
|
367
|
+
MIT
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ICAConfig, TokenSet, UserInfoClaims, JWTPayload } from './types';
|
|
2
|
+
export declare class ICAClient {
|
|
3
|
+
private config;
|
|
4
|
+
private jwksService;
|
|
5
|
+
constructor(config: ICAConfig);
|
|
6
|
+
getAuthorizationUrl(state: string, codeChallenge: string): string;
|
|
7
|
+
generatePKCE(): {
|
|
8
|
+
codeVerifier: string;
|
|
9
|
+
codeChallenge: string;
|
|
10
|
+
};
|
|
11
|
+
private requestToken;
|
|
12
|
+
exchangeCode(code: string, codeVerifier: string): Promise<TokenSet>;
|
|
13
|
+
refreshAccessToken(refreshToken: string): Promise<TokenSet>;
|
|
14
|
+
getUserInfo(accessToken: string): Promise<UserInfoClaims>;
|
|
15
|
+
verifyToken(jwt: string): Promise<JWTPayload>;
|
|
16
|
+
revokeToken(refreshToken: string): Promise<{
|
|
17
|
+
revoked: boolean;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ICAClient = void 0;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const pkce_1 = require("./pkce");
|
|
6
|
+
const jwks_1 = require("./jwks");
|
|
7
|
+
class ICAClient {
|
|
8
|
+
config;
|
|
9
|
+
jwksService;
|
|
10
|
+
constructor(config) {
|
|
11
|
+
if (!config.clientId)
|
|
12
|
+
throw new Error('clientId is required');
|
|
13
|
+
if (!config.redirectUri)
|
|
14
|
+
throw new Error('redirectUri is required');
|
|
15
|
+
if (!config.baseUrl)
|
|
16
|
+
throw new Error('baseUrl is required');
|
|
17
|
+
this.config = {
|
|
18
|
+
...config,
|
|
19
|
+
scopes: config.scopes || ['openid', 'profile', 'email']
|
|
20
|
+
};
|
|
21
|
+
this.jwksService = new jwks_1.JWKSService(`${this.config.baseUrl}/api/oauth/jwks`);
|
|
22
|
+
}
|
|
23
|
+
getAuthorizationUrl(state, codeChallenge) {
|
|
24
|
+
const params = new URLSearchParams({
|
|
25
|
+
client_id: this.config.clientId,
|
|
26
|
+
redirect_uri: this.config.redirectUri,
|
|
27
|
+
response_type: 'code',
|
|
28
|
+
scope: this.config.scopes.join(' '),
|
|
29
|
+
state: state,
|
|
30
|
+
code_challenge: codeChallenge,
|
|
31
|
+
code_challenge_method: 'S256',
|
|
32
|
+
});
|
|
33
|
+
return `${this.config.baseUrl}/api/oauth/authorize?${params.toString()}`;
|
|
34
|
+
}
|
|
35
|
+
generatePKCE() {
|
|
36
|
+
return (0, pkce_1.generatePKCE)();
|
|
37
|
+
}
|
|
38
|
+
async requestToken(body) {
|
|
39
|
+
const res = await fetch(`${this.config.baseUrl}/api/oauth/token`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: {
|
|
42
|
+
'Content-Type': 'application/json',
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
...body,
|
|
46
|
+
client_id: this.config.clientId,
|
|
47
|
+
client_secret: this.config.clientSecret,
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
const data = await res.json();
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
throw new errors_1.ICAError(data.error || 'request_failed', data.error_description || 'Failed to fetch token', res.status);
|
|
53
|
+
}
|
|
54
|
+
return data;
|
|
55
|
+
}
|
|
56
|
+
async exchangeCode(code, codeVerifier) {
|
|
57
|
+
return this.requestToken({
|
|
58
|
+
grant_type: 'authorization_code',
|
|
59
|
+
code,
|
|
60
|
+
redirect_uri: this.config.redirectUri,
|
|
61
|
+
code_verifier: codeVerifier,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async refreshAccessToken(refreshToken) {
|
|
65
|
+
return this.requestToken({
|
|
66
|
+
grant_type: 'refresh_token',
|
|
67
|
+
refresh_token: refreshToken,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async getUserInfo(accessToken) {
|
|
71
|
+
const res = await fetch(`${this.config.baseUrl}/api/oauth/userinfo`, {
|
|
72
|
+
method: 'GET',
|
|
73
|
+
headers: {
|
|
74
|
+
Authorization: `Bearer ${accessToken}`,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
const data = await res.json();
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
throw new errors_1.ICAError(data.error || 'request_failed', data.error_description || 'Failed to fetch userinfo', res.status);
|
|
80
|
+
}
|
|
81
|
+
return data;
|
|
82
|
+
}
|
|
83
|
+
async verifyToken(jwt) {
|
|
84
|
+
return this.jwksService.verifyToken(jwt, this.config.clientId, this.config.baseUrl);
|
|
85
|
+
}
|
|
86
|
+
async revokeToken(refreshToken) {
|
|
87
|
+
const res = await fetch(`${this.config.baseUrl}/api/oauth/revoke`, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: {
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
token: refreshToken,
|
|
94
|
+
client_id: this.config.clientId,
|
|
95
|
+
client_secret: this.config.clientSecret,
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
const data = await res.json();
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
throw new errors_1.ICAError(data.error || 'request_failed', data.error_description || 'Failed to revoke token', res.status);
|
|
101
|
+
}
|
|
102
|
+
return data;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
exports.ICAClient = ICAClient;
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ICAError = void 0;
|
|
4
|
+
class ICAError extends Error {
|
|
5
|
+
error;
|
|
6
|
+
error_description;
|
|
7
|
+
status;
|
|
8
|
+
constructor(error, error_description, status) {
|
|
9
|
+
super(`${error}: ${error_description}`);
|
|
10
|
+
this.name = 'ICAError';
|
|
11
|
+
this.error = error;
|
|
12
|
+
this.error_description = error_description;
|
|
13
|
+
this.status = status;
|
|
14
|
+
Object.setPrototypeOf(this, ICAError.prototype);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.ICAError = ICAError;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ICAClient } from './client';
|
|
2
|
+
import { ICAConfig } from './types';
|
|
3
|
+
import { createRequireAuth, createOptionalAuth } from './middleware';
|
|
4
|
+
export * from './types';
|
|
5
|
+
export * from './errors';
|
|
6
|
+
export { ICAClient } from './client';
|
|
7
|
+
export interface IGlobalsAuthInstance extends ICAClient {
|
|
8
|
+
requireAuth: ReturnType<typeof createRequireAuth>;
|
|
9
|
+
optionalAuth: ReturnType<typeof createOptionalAuth>;
|
|
10
|
+
}
|
|
11
|
+
export declare function createIGlobalsAuth(config: ICAConfig): IGlobalsAuthInstance;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.ICAClient = void 0;
|
|
18
|
+
exports.createIGlobalsAuth = createIGlobalsAuth;
|
|
19
|
+
const client_1 = require("./client");
|
|
20
|
+
const middleware_1 = require("./middleware");
|
|
21
|
+
__exportStar(require("./types"), exports);
|
|
22
|
+
__exportStar(require("./errors"), exports);
|
|
23
|
+
var client_2 = require("./client");
|
|
24
|
+
Object.defineProperty(exports, "ICAClient", { enumerable: true, get: function () { return client_2.ICAClient; } });
|
|
25
|
+
function createIGlobalsAuth(config) {
|
|
26
|
+
const client = new client_1.ICAClient(config);
|
|
27
|
+
const instance = client;
|
|
28
|
+
instance.requireAuth = (0, middleware_1.createRequireAuth)(client);
|
|
29
|
+
instance.optionalAuth = (0, middleware_1.createOptionalAuth)(client);
|
|
30
|
+
return instance;
|
|
31
|
+
}
|
package/dist/jwks.d.ts
ADDED
package/dist/jwks.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.JWKSService = void 0;
|
|
7
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
8
|
+
const jwks_rsa_1 = __importDefault(require("jwks-rsa"));
|
|
9
|
+
const errors_1 = require("./errors");
|
|
10
|
+
class JWKSService {
|
|
11
|
+
client;
|
|
12
|
+
constructor(jwksUri) {
|
|
13
|
+
this.client = (0, jwks_rsa_1.default)({
|
|
14
|
+
jwksUri,
|
|
15
|
+
cache: true,
|
|
16
|
+
cacheMaxEntries: 5,
|
|
17
|
+
cacheMaxAge: 3600000, // 1 hour
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
getKey = (header, callback) => {
|
|
21
|
+
this.client.getSigningKey(header.kid, (err, key) => {
|
|
22
|
+
if (err) {
|
|
23
|
+
return callback(err);
|
|
24
|
+
}
|
|
25
|
+
const signingKey = key?.getPublicKey();
|
|
26
|
+
callback(null, signingKey);
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
verifyToken(token, expectedAud, expectedIss) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
jsonwebtoken_1.default.verify(token, this.getKey, {
|
|
32
|
+
audience: expectedAud,
|
|
33
|
+
issuer: expectedIss,
|
|
34
|
+
algorithms: ['RS256'],
|
|
35
|
+
}, (err, decoded) => {
|
|
36
|
+
if (err) {
|
|
37
|
+
reject(new errors_1.ICAError('invalid_token', err.message, 401));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
resolve(decoded);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.JWKSService = JWKSService;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { RequestHandler } from 'express';
|
|
2
|
+
import { ICAClient } from './client';
|
|
3
|
+
import { JWTPayload } from './types';
|
|
4
|
+
declare global {
|
|
5
|
+
namespace Express {
|
|
6
|
+
interface Request {
|
|
7
|
+
icaUser?: JWTPayload;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export declare function createRequireAuth(ica: ICAClient): RequestHandler;
|
|
12
|
+
export declare function createOptionalAuth(ica: ICAClient): RequestHandler;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRequireAuth = createRequireAuth;
|
|
4
|
+
exports.createOptionalAuth = createOptionalAuth;
|
|
5
|
+
function createRequireAuth(ica) {
|
|
6
|
+
return async (req, res, next) => {
|
|
7
|
+
const authHeader = req.headers.authorization;
|
|
8
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
9
|
+
res.status(401).json({ error: 'unauthorized', error_description: 'Bearer token missing or malformed.', status: 401 });
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const token = authHeader.split(' ')[1];
|
|
13
|
+
try {
|
|
14
|
+
const payload = await ica.verifyToken(token);
|
|
15
|
+
req.icaUser = payload;
|
|
16
|
+
next();
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
res.status(401).json({ error: 'unauthorized', error_description: err.message, status: 401 });
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function createOptionalAuth(ica) {
|
|
24
|
+
return async (req, res, next) => {
|
|
25
|
+
const authHeader = req.headers.authorization;
|
|
26
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
27
|
+
return next();
|
|
28
|
+
}
|
|
29
|
+
const token = authHeader.split(' ')[1];
|
|
30
|
+
try {
|
|
31
|
+
const payload = await ica.verifyToken(token);
|
|
32
|
+
req.icaUser = payload;
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
// Ignore errors for optional auth
|
|
36
|
+
}
|
|
37
|
+
next();
|
|
38
|
+
};
|
|
39
|
+
}
|
package/dist/pkce.d.ts
ADDED
package/dist/pkce.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generatePKCE = generatePKCE;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
function generatePKCE() {
|
|
9
|
+
const codeVerifier = crypto_1.default.randomBytes(32).toString('base64url');
|
|
10
|
+
const codeChallenge = crypto_1.default
|
|
11
|
+
.createHash('sha256')
|
|
12
|
+
.update(codeVerifier)
|
|
13
|
+
.digest('base64url');
|
|
14
|
+
return { codeVerifier, codeChallenge };
|
|
15
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface ICAConfig {
|
|
2
|
+
clientId: string;
|
|
3
|
+
clientSecret?: string;
|
|
4
|
+
redirectUri: string;
|
|
5
|
+
scopes: string[];
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
}
|
|
8
|
+
export interface TokenSet {
|
|
9
|
+
access_token: string;
|
|
10
|
+
token_type: string;
|
|
11
|
+
expires_in: number;
|
|
12
|
+
refresh_token: string;
|
|
13
|
+
id_token?: string;
|
|
14
|
+
scope: string;
|
|
15
|
+
}
|
|
16
|
+
export interface UserInfoClaims {
|
|
17
|
+
sub: string;
|
|
18
|
+
given_name?: string;
|
|
19
|
+
family_name?: string;
|
|
20
|
+
email?: string;
|
|
21
|
+
email_verified?: boolean;
|
|
22
|
+
phone_number?: string;
|
|
23
|
+
phone_number_verified?: boolean;
|
|
24
|
+
address?: {
|
|
25
|
+
street_address?: string;
|
|
26
|
+
locality?: string;
|
|
27
|
+
region?: string;
|
|
28
|
+
postal_code?: string;
|
|
29
|
+
country?: string;
|
|
30
|
+
};
|
|
31
|
+
[key: string]: any;
|
|
32
|
+
}
|
|
33
|
+
export interface JWTPayload {
|
|
34
|
+
iss: string;
|
|
35
|
+
sub: string;
|
|
36
|
+
aud: string;
|
|
37
|
+
iat: number;
|
|
38
|
+
exp: number;
|
|
39
|
+
scope?: string;
|
|
40
|
+
email?: string;
|
|
41
|
+
email_verified?: boolean;
|
|
42
|
+
given_name?: string;
|
|
43
|
+
family_name?: string;
|
|
44
|
+
phone_number?: string;
|
|
45
|
+
phone_number_verified?: boolean;
|
|
46
|
+
[key: string]: any;
|
|
47
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "iglobals-auth-client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "iGlobals Central Auth SDK for Node.js",
|
|
5
|
+
"homepage": "https://github.com/Profeso1012/Iglobals-CAuth#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/Profeso1012/Iglobals-CAuth/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/Profeso1012/Iglobals-CAuth.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"author": "",
|
|
15
|
+
"type": "commonjs",
|
|
16
|
+
"main": "dist/index.js",
|
|
17
|
+
"types": "dist/index.d.ts",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"dev": "tsc -w"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"jsonwebtoken": "^9.0.2",
|
|
24
|
+
"jwks-rsa": "^3.1.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/jsonwebtoken": "^9.0.5",
|
|
28
|
+
"@types/express": "^4.17.21",
|
|
29
|
+
"typescript": "^5.3.3"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { ICAConfig, TokenSet, UserInfoClaims, JWTPayload } from './types';
|
|
2
|
+
import { ICAError } from './errors';
|
|
3
|
+
import { generatePKCE } from './pkce';
|
|
4
|
+
import { JWKSService } from './jwks';
|
|
5
|
+
|
|
6
|
+
export class ICAClient {
|
|
7
|
+
private config: ICAConfig;
|
|
8
|
+
private jwksService: JWKSService;
|
|
9
|
+
|
|
10
|
+
constructor(config: ICAConfig) {
|
|
11
|
+
if (!config.clientId) throw new Error('clientId is required');
|
|
12
|
+
if (!config.redirectUri) throw new Error('redirectUri is required');
|
|
13
|
+
if (!config.baseUrl) throw new Error('baseUrl is required');
|
|
14
|
+
|
|
15
|
+
this.config = {
|
|
16
|
+
...config,
|
|
17
|
+
scopes: config.scopes || ['openid', 'profile', 'email']
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
this.jwksService = new JWKSService(`${this.config.baseUrl}/api/oauth/jwks`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public getAuthorizationUrl(state: string, codeChallenge: string): string {
|
|
24
|
+
const params = new URLSearchParams({
|
|
25
|
+
client_id: this.config.clientId,
|
|
26
|
+
redirect_uri: this.config.redirectUri,
|
|
27
|
+
response_type: 'code',
|
|
28
|
+
scope: this.config.scopes.join(' '),
|
|
29
|
+
state: state,
|
|
30
|
+
code_challenge: codeChallenge,
|
|
31
|
+
code_challenge_method: 'S256',
|
|
32
|
+
});
|
|
33
|
+
return `${this.config.baseUrl}/api/oauth/authorize?${params.toString()}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public generatePKCE(): { codeVerifier: string; codeChallenge: string } {
|
|
37
|
+
return generatePKCE();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private async requestToken(body: Record<string, string>): Promise<TokenSet> {
|
|
41
|
+
const res = await fetch(`${this.config.baseUrl}/api/oauth/token`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
...body,
|
|
48
|
+
client_id: this.config.clientId,
|
|
49
|
+
client_secret: this.config.clientSecret,
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const data = await res.json();
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
throw new ICAError(data.error || 'request_failed', data.error_description || 'Failed to fetch token', res.status);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return data as TokenSet;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public async exchangeCode(code: string, codeVerifier: string): Promise<TokenSet> {
|
|
62
|
+
return this.requestToken({
|
|
63
|
+
grant_type: 'authorization_code',
|
|
64
|
+
code,
|
|
65
|
+
redirect_uri: this.config.redirectUri,
|
|
66
|
+
code_verifier: codeVerifier,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public async refreshAccessToken(refreshToken: string): Promise<TokenSet> {
|
|
71
|
+
return this.requestToken({
|
|
72
|
+
grant_type: 'refresh_token',
|
|
73
|
+
refresh_token: refreshToken,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public async getUserInfo(accessToken: string): Promise<UserInfoClaims> {
|
|
78
|
+
const res = await fetch(`${this.config.baseUrl}/api/oauth/userinfo`, {
|
|
79
|
+
method: 'GET',
|
|
80
|
+
headers: {
|
|
81
|
+
Authorization: `Bearer ${accessToken}`,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const data = await res.json();
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
throw new ICAError(data.error || 'request_failed', data.error_description || 'Failed to fetch userinfo', res.status);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return data as UserInfoClaims;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public async verifyToken(jwt: string): Promise<JWTPayload> {
|
|
94
|
+
return this.jwksService.verifyToken(jwt, this.config.clientId, this.config.baseUrl);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public async revokeToken(refreshToken: string): Promise<{ revoked: boolean }> {
|
|
98
|
+
const res = await fetch(`${this.config.baseUrl}/api/oauth/revoke`, {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: {
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
token: refreshToken,
|
|
105
|
+
client_id: this.config.clientId,
|
|
106
|
+
client_secret: this.config.clientSecret,
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
throw new ICAError(data.error || 'request_failed', data.error_description || 'Failed to revoke token', res.status);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return data as { revoked: boolean };
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class ICAError extends Error {
|
|
2
|
+
public error: string;
|
|
3
|
+
public error_description: string;
|
|
4
|
+
public status?: number;
|
|
5
|
+
|
|
6
|
+
constructor(error: string, error_description: string, status?: number) {
|
|
7
|
+
super(`${error}: ${error_description}`);
|
|
8
|
+
this.name = 'ICAError';
|
|
9
|
+
this.error = error;
|
|
10
|
+
this.error_description = error_description;
|
|
11
|
+
this.status = status;
|
|
12
|
+
Object.setPrototypeOf(this, ICAError.prototype);
|
|
13
|
+
}
|
|
14
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ICAClient } from './client';
|
|
2
|
+
import { ICAConfig } from './types';
|
|
3
|
+
import { createRequireAuth, createOptionalAuth } from './middleware';
|
|
4
|
+
|
|
5
|
+
export * from './types';
|
|
6
|
+
export * from './errors';
|
|
7
|
+
export { ICAClient } from './client';
|
|
8
|
+
|
|
9
|
+
export interface IGlobalsAuthInstance extends ICAClient {
|
|
10
|
+
requireAuth: ReturnType<typeof createRequireAuth>;
|
|
11
|
+
optionalAuth: ReturnType<typeof createOptionalAuth>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createIGlobalsAuth(config: ICAConfig): IGlobalsAuthInstance {
|
|
15
|
+
const client = new ICAClient(config);
|
|
16
|
+
|
|
17
|
+
const instance = client as IGlobalsAuthInstance;
|
|
18
|
+
instance.requireAuth = createRequireAuth(client);
|
|
19
|
+
instance.optionalAuth = createOptionalAuth(client);
|
|
20
|
+
|
|
21
|
+
return instance;
|
|
22
|
+
}
|
package/src/jwks.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import jwt, { JwtHeader, SigningKeyCallback } from 'jsonwebtoken';
|
|
2
|
+
import jwksClient from 'jwks-rsa';
|
|
3
|
+
import { JWTPayload } from './types';
|
|
4
|
+
import { ICAError } from './errors';
|
|
5
|
+
|
|
6
|
+
export class JWKSService {
|
|
7
|
+
private client: jwksClient.JwksClient;
|
|
8
|
+
|
|
9
|
+
constructor(jwksUri: string) {
|
|
10
|
+
this.client = jwksClient({
|
|
11
|
+
jwksUri,
|
|
12
|
+
cache: true,
|
|
13
|
+
cacheMaxEntries: 5,
|
|
14
|
+
cacheMaxAge: 3600000, // 1 hour
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private getKey = (header: JwtHeader, callback: SigningKeyCallback) => {
|
|
19
|
+
this.client.getSigningKey(header.kid, (err, key) => {
|
|
20
|
+
if (err) {
|
|
21
|
+
return callback(err);
|
|
22
|
+
}
|
|
23
|
+
const signingKey = key?.getPublicKey();
|
|
24
|
+
callback(null, signingKey);
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
public verifyToken(token: string, expectedAud: string, expectedIss: string): Promise<JWTPayload> {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
jwt.verify(
|
|
31
|
+
token,
|
|
32
|
+
this.getKey,
|
|
33
|
+
{
|
|
34
|
+
audience: expectedAud,
|
|
35
|
+
issuer: expectedIss,
|
|
36
|
+
algorithms: ['RS256'],
|
|
37
|
+
},
|
|
38
|
+
(err, decoded) => {
|
|
39
|
+
if (err) {
|
|
40
|
+
reject(new ICAError('invalid_token', err.message, 401));
|
|
41
|
+
} else {
|
|
42
|
+
resolve(decoded as JWTPayload);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
2
|
+
import { ICAClient } from './client';
|
|
3
|
+
import { JWTPayload } from './types';
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
namespace Express {
|
|
7
|
+
interface Request {
|
|
8
|
+
icaUser?: JWTPayload;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createRequireAuth(ica: ICAClient): RequestHandler {
|
|
14
|
+
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
15
|
+
const authHeader = req.headers.authorization;
|
|
16
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
17
|
+
res.status(401).json({ error: 'unauthorized', error_description: 'Bearer token missing or malformed.', status: 401 });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const token = authHeader.split(' ')[1];
|
|
22
|
+
try {
|
|
23
|
+
const payload = await ica.verifyToken(token);
|
|
24
|
+
req.icaUser = payload;
|
|
25
|
+
next();
|
|
26
|
+
} catch (err: any) {
|
|
27
|
+
res.status(401).json({ error: 'unauthorized', error_description: err.message, status: 401 });
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createOptionalAuth(ica: ICAClient): RequestHandler {
|
|
33
|
+
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
34
|
+
const authHeader = req.headers.authorization;
|
|
35
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
36
|
+
return next();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const token = authHeader.split(' ')[1];
|
|
40
|
+
try {
|
|
41
|
+
const payload = await ica.verifyToken(token);
|
|
42
|
+
req.icaUser = payload;
|
|
43
|
+
} catch (err) {
|
|
44
|
+
// Ignore errors for optional auth
|
|
45
|
+
}
|
|
46
|
+
next();
|
|
47
|
+
};
|
|
48
|
+
}
|
package/src/pkce.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
export function generatePKCE(): { codeVerifier: string; codeChallenge: string } {
|
|
4
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
5
|
+
const codeChallenge = crypto
|
|
6
|
+
.createHash('sha256')
|
|
7
|
+
.update(codeVerifier)
|
|
8
|
+
.digest('base64url');
|
|
9
|
+
|
|
10
|
+
return { codeVerifier, codeChallenge };
|
|
11
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface ICAConfig {
|
|
2
|
+
clientId: string;
|
|
3
|
+
clientSecret?: string;
|
|
4
|
+
redirectUri: string;
|
|
5
|
+
scopes: string[];
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TokenSet {
|
|
10
|
+
access_token: string;
|
|
11
|
+
token_type: string;
|
|
12
|
+
expires_in: number;
|
|
13
|
+
refresh_token: string;
|
|
14
|
+
id_token?: string;
|
|
15
|
+
scope: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UserInfoClaims {
|
|
19
|
+
sub: string;
|
|
20
|
+
given_name?: string;
|
|
21
|
+
family_name?: string;
|
|
22
|
+
email?: string;
|
|
23
|
+
email_verified?: boolean;
|
|
24
|
+
phone_number?: string;
|
|
25
|
+
phone_number_verified?: boolean;
|
|
26
|
+
address?: {
|
|
27
|
+
street_address?: string;
|
|
28
|
+
locality?: string;
|
|
29
|
+
region?: string;
|
|
30
|
+
postal_code?: string;
|
|
31
|
+
country?: string;
|
|
32
|
+
};
|
|
33
|
+
[key: string]: any;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface JWTPayload {
|
|
37
|
+
iss: string;
|
|
38
|
+
sub: string;
|
|
39
|
+
aud: string;
|
|
40
|
+
iat: number;
|
|
41
|
+
exp: number;
|
|
42
|
+
scope?: string;
|
|
43
|
+
email?: string;
|
|
44
|
+
email_verified?: boolean;
|
|
45
|
+
given_name?: string;
|
|
46
|
+
family_name?: string;
|
|
47
|
+
phone_number?: string;
|
|
48
|
+
phone_number_verified?: boolean;
|
|
49
|
+
[key: string]: any;
|
|
50
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"moduleResolution": "node"
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|