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 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
@@ -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;
@@ -0,0 +1,6 @@
1
+ export declare class ICAError extends Error {
2
+ error: string;
3
+ error_description: string;
4
+ status?: number;
5
+ constructor(error: string, error_description: string, status?: number);
6
+ }
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;
@@ -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
@@ -0,0 +1,7 @@
1
+ import { JWTPayload } from './types';
2
+ export declare class JWKSService {
3
+ private client;
4
+ constructor(jwksUri: string);
5
+ private getKey;
6
+ verifyToken(token: string, expectedAud: string, expectedIss: string): Promise<JWTPayload>;
7
+ }
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
@@ -0,0 +1,4 @@
1
+ export declare function generatePKCE(): {
2
+ codeVerifier: string;
3
+ codeChallenge: string;
4
+ };
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
+ }
@@ -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
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
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
+ }