@nya-account/node-sdk 2.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/LICENSE +21 -0
- package/README.md +172 -0
- package/dist/express-BHHzodXb.js +83 -0
- package/dist/express-BHHzodXb.js.map +1 -0
- package/dist/express-yO7hxKKd.d.ts +118 -0
- package/dist/express-yO7hxKKd.d.ts.map +1 -0
- package/dist/express.d.ts +2 -0
- package/dist/express.js +3 -0
- package/dist/index.d.ts +307 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +727 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
import { extractBearerToken, getAuth, sendOAuthError, setAuth } from "./express-BHHzodXb.js";
|
|
2
|
+
import axios, { AxiosError } from "axios";
|
|
3
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { createRemoteJWKSet, errors, jwtVerify } from "jose";
|
|
6
|
+
|
|
7
|
+
//#region src/core/schemas.ts
|
|
8
|
+
const TokenResponseSchema = z.object({
|
|
9
|
+
access_token: z.string(),
|
|
10
|
+
token_type: z.string(),
|
|
11
|
+
expires_in: z.number(),
|
|
12
|
+
refresh_token: z.string(),
|
|
13
|
+
scope: z.string(),
|
|
14
|
+
id_token: z.string().optional()
|
|
15
|
+
});
|
|
16
|
+
const OAuthErrorSchema = z.object({
|
|
17
|
+
error: z.string(),
|
|
18
|
+
error_description: z.string().optional()
|
|
19
|
+
});
|
|
20
|
+
const IntrospectionResponseSchema = z.object({
|
|
21
|
+
active: z.boolean(),
|
|
22
|
+
scope: z.string().optional(),
|
|
23
|
+
client_id: z.string().optional(),
|
|
24
|
+
username: z.string().optional(),
|
|
25
|
+
token_type: z.string().optional(),
|
|
26
|
+
exp: z.number().optional(),
|
|
27
|
+
iat: z.number().optional(),
|
|
28
|
+
sub: z.string().optional(),
|
|
29
|
+
aud: z.string().optional(),
|
|
30
|
+
iss: z.string().optional(),
|
|
31
|
+
jti: z.string().optional()
|
|
32
|
+
});
|
|
33
|
+
const UserInfoSchema = z.object({
|
|
34
|
+
sub: z.string(),
|
|
35
|
+
name: z.string().optional(),
|
|
36
|
+
preferred_username: z.string().optional(),
|
|
37
|
+
email: z.string().optional(),
|
|
38
|
+
email_verified: z.boolean().optional(),
|
|
39
|
+
updated_at: z.number().optional()
|
|
40
|
+
});
|
|
41
|
+
const DiscoveryDocumentSchema = z.object({
|
|
42
|
+
issuer: z.string(),
|
|
43
|
+
authorization_endpoint: z.string(),
|
|
44
|
+
token_endpoint: z.string(),
|
|
45
|
+
userinfo_endpoint: z.string().optional(),
|
|
46
|
+
jwks_uri: z.string(),
|
|
47
|
+
revocation_endpoint: z.string().optional(),
|
|
48
|
+
introspection_endpoint: z.string().optional(),
|
|
49
|
+
pushed_authorization_request_endpoint: z.string().optional(),
|
|
50
|
+
end_session_endpoint: z.string().optional(),
|
|
51
|
+
response_types_supported: z.array(z.string()),
|
|
52
|
+
grant_types_supported: z.array(z.string()),
|
|
53
|
+
id_token_signing_alg_values_supported: z.array(z.string()),
|
|
54
|
+
scopes_supported: z.array(z.string()),
|
|
55
|
+
subject_types_supported: z.array(z.string()),
|
|
56
|
+
token_endpoint_auth_methods_supported: z.array(z.string()),
|
|
57
|
+
code_challenge_methods_supported: z.array(z.string()).optional(),
|
|
58
|
+
claims_supported: z.array(z.string()).optional(),
|
|
59
|
+
dpop_signing_alg_values_supported: z.array(z.string()).optional(),
|
|
60
|
+
request_parameter_supported: z.boolean().optional(),
|
|
61
|
+
request_uri_parameter_supported: z.boolean().optional()
|
|
62
|
+
});
|
|
63
|
+
const AccessTokenPayloadSchema = z.object({
|
|
64
|
+
iss: z.string(),
|
|
65
|
+
sub: z.string(),
|
|
66
|
+
aud: z.string(),
|
|
67
|
+
scope: z.string(),
|
|
68
|
+
ver: z.string(),
|
|
69
|
+
iat: z.number(),
|
|
70
|
+
exp: z.number(),
|
|
71
|
+
jti: z.string(),
|
|
72
|
+
cnf: z.object({ jkt: z.string() }).optional()
|
|
73
|
+
});
|
|
74
|
+
const IdTokenPayloadSchema = z.object({
|
|
75
|
+
iss: z.string(),
|
|
76
|
+
sub: z.string(),
|
|
77
|
+
aud: z.string(),
|
|
78
|
+
iat: z.number(),
|
|
79
|
+
exp: z.number(),
|
|
80
|
+
nonce: z.string().optional(),
|
|
81
|
+
name: z.string().optional(),
|
|
82
|
+
preferred_username: z.string().optional(),
|
|
83
|
+
email: z.string().optional(),
|
|
84
|
+
email_verified: z.boolean().optional(),
|
|
85
|
+
updated_at: z.number().optional()
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/core/errors.ts
|
|
90
|
+
/**
|
|
91
|
+
|
|
92
|
+
* Base error class for the SDK.
|
|
93
|
+
|
|
94
|
+
*/
|
|
95
|
+
var NyaAccountError = class extends Error {
|
|
96
|
+
constructor(code, description) {
|
|
97
|
+
super(`[${code}] ${description}`);
|
|
98
|
+
this.code = void 0;
|
|
99
|
+
this.description = void 0;
|
|
100
|
+
this.name = "NyaAccountError";
|
|
101
|
+
this.code = code;
|
|
102
|
+
this.description = description;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
|
|
107
|
+
* OAuth protocol error (from server error / error_description response).
|
|
108
|
+
|
|
109
|
+
*/
|
|
110
|
+
var OAuthError = class extends NyaAccountError {
|
|
111
|
+
constructor(error, errorDescription) {
|
|
112
|
+
super(error, errorDescription);
|
|
113
|
+
this.name = "OAuthError";
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
|
|
118
|
+
* JWT verification error.
|
|
119
|
+
|
|
120
|
+
*/
|
|
121
|
+
var TokenVerificationError = class extends NyaAccountError {
|
|
122
|
+
constructor(description) {
|
|
123
|
+
super("token_verification_failed", description);
|
|
124
|
+
this.name = "TokenVerificationError";
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
|
|
129
|
+
* OIDC Discovery error.
|
|
130
|
+
|
|
131
|
+
*/
|
|
132
|
+
var DiscoveryError = class extends NyaAccountError {
|
|
133
|
+
constructor(description) {
|
|
134
|
+
super("discovery_error", description);
|
|
135
|
+
this.name = "DiscoveryError";
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/utils/pkce.ts
|
|
141
|
+
/**
|
|
142
|
+
|
|
143
|
+
* Generate a PKCE code_verifier (43-128 character random string).
|
|
144
|
+
|
|
145
|
+
*/
|
|
146
|
+
function generateCodeVerifier() {
|
|
147
|
+
return randomBytes(32).toString("base64url");
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
|
|
151
|
+
* Generate an S256 code_challenge from a code_verifier.
|
|
152
|
+
|
|
153
|
+
*/
|
|
154
|
+
function generateCodeChallenge(codeVerifier) {
|
|
155
|
+
return createHash("sha256").update(codeVerifier).digest("base64url");
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
|
|
159
|
+
* Generate a PKCE code_verifier and code_challenge pair.
|
|
160
|
+
|
|
161
|
+
*/
|
|
162
|
+
function generatePkce() {
|
|
163
|
+
const codeVerifier = generateCodeVerifier();
|
|
164
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
165
|
+
return {
|
|
166
|
+
codeVerifier,
|
|
167
|
+
codeChallenge
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/utils/jwt.ts
|
|
173
|
+
/**
|
|
174
|
+
|
|
175
|
+
* JWT verifier using remote JWKS for signature verification.
|
|
176
|
+
|
|
177
|
+
*/
|
|
178
|
+
var JwtVerifier = class {
|
|
179
|
+
constructor(jwksUri, issuer, defaultAudience) {
|
|
180
|
+
this.jwks = void 0;
|
|
181
|
+
this.issuer = void 0;
|
|
182
|
+
this.defaultAudience = void 0;
|
|
183
|
+
this.jwks = createRemoteJWKSet(new URL(jwksUri));
|
|
184
|
+
this.issuer = issuer;
|
|
185
|
+
this.defaultAudience = defaultAudience;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
|
|
189
|
+
* Verify an OAuth 2.0 Access Token (JWT, RFC 9068).
|
|
190
|
+
|
|
191
|
+
*/
|
|
192
|
+
async verifyAccessToken(token, audience) {
|
|
193
|
+
try {
|
|
194
|
+
const { payload } = await jwtVerify(token, this.jwks, {
|
|
195
|
+
algorithms: ["RS256"],
|
|
196
|
+
issuer: this.issuer,
|
|
197
|
+
audience: audience ?? this.defaultAudience
|
|
198
|
+
});
|
|
199
|
+
return AccessTokenPayloadSchema.parse(payload);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
throw this.wrapError(error, "Access Token");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
|
|
206
|
+
* Verify an OIDC ID Token.
|
|
207
|
+
|
|
208
|
+
*/
|
|
209
|
+
async verifyIdToken(token, options) {
|
|
210
|
+
try {
|
|
211
|
+
const { payload } = await jwtVerify(token, this.jwks, {
|
|
212
|
+
algorithms: ["RS256"],
|
|
213
|
+
issuer: this.issuer,
|
|
214
|
+
audience: options?.audience ?? this.defaultAudience
|
|
215
|
+
});
|
|
216
|
+
const parsed = IdTokenPayloadSchema.parse(payload);
|
|
217
|
+
if (options?.nonce !== void 0 && parsed.nonce !== options.nonce) throw new TokenVerificationError("ID Token nonce mismatch");
|
|
218
|
+
return parsed;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if (error instanceof TokenVerificationError) throw error;
|
|
221
|
+
throw this.wrapError(error, "ID Token");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
wrapError(error, tokenType) {
|
|
225
|
+
if (error instanceof TokenVerificationError) return error;
|
|
226
|
+
if (error instanceof errors.JWTExpired) return new TokenVerificationError(`${tokenType} has expired`);
|
|
227
|
+
if (error instanceof errors.JWTClaimValidationFailed) return new TokenVerificationError(`${tokenType} claim validation failed: ${error.message}`);
|
|
228
|
+
if (error instanceof errors.JWSSignatureVerificationFailed) return new TokenVerificationError(`${tokenType} signature verification failed`);
|
|
229
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
230
|
+
return new TokenVerificationError(`${tokenType} verification failed: ${message}`);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/client.ts
|
|
236
|
+
const DISCOVERY_ENDPOINT_MAP = {
|
|
237
|
+
authorization: "authorizationEndpoint",
|
|
238
|
+
token: "tokenEndpoint",
|
|
239
|
+
userinfo: "userinfoEndpoint",
|
|
240
|
+
revocation: "revocationEndpoint",
|
|
241
|
+
introspection: "introspectionEndpoint",
|
|
242
|
+
jwks: "jwksUri",
|
|
243
|
+
endSession: "endSessionEndpoint"
|
|
244
|
+
};
|
|
245
|
+
/** Default discovery cache TTL: 1 hour */
|
|
246
|
+
const DEFAULT_DISCOVERY_CACHE_TTL = 36e5;
|
|
247
|
+
/**
|
|
248
|
+
|
|
249
|
+
* Nya Account Node.js SDK client.
|
|
250
|
+
|
|
251
|
+
*
|
|
252
|
+
|
|
253
|
+
* Provides full OAuth 2.1 / OIDC flow support:
|
|
254
|
+
|
|
255
|
+
* - Authorization Code + PKCE
|
|
256
|
+
|
|
257
|
+
* - Token exchange / refresh / revocation / introspection
|
|
258
|
+
|
|
259
|
+
* - Local JWT verification (via JWKS)
|
|
260
|
+
|
|
261
|
+
* - OIDC UserInfo
|
|
262
|
+
|
|
263
|
+
* - OIDC Discovery auto-discovery
|
|
264
|
+
|
|
265
|
+
* - Express middleware (Bearer Token auth + scope validation)
|
|
266
|
+
|
|
267
|
+
*
|
|
268
|
+
|
|
269
|
+
* @example
|
|
270
|
+
|
|
271
|
+
* ```typescript
|
|
272
|
+
|
|
273
|
+
* const client = new NyaAccountClient({
|
|
274
|
+
|
|
275
|
+
* issuer: 'https://account.example.com',
|
|
276
|
+
|
|
277
|
+
* clientId: 'my-app',
|
|
278
|
+
|
|
279
|
+
* clientSecret: 'my-secret',
|
|
280
|
+
|
|
281
|
+
* })
|
|
282
|
+
|
|
283
|
+
*
|
|
284
|
+
|
|
285
|
+
* // Create authorization URL (with PKCE)
|
|
286
|
+
|
|
287
|
+
* const { url, codeVerifier, state } = await client.createAuthorizationUrl({
|
|
288
|
+
|
|
289
|
+
* redirectUri: 'https://myapp.com/callback',
|
|
290
|
+
|
|
291
|
+
* scope: 'openid profile email',
|
|
292
|
+
|
|
293
|
+
* })
|
|
294
|
+
|
|
295
|
+
*
|
|
296
|
+
|
|
297
|
+
* // Exchange code for tokens
|
|
298
|
+
|
|
299
|
+
* const tokens = await client.exchangeCode({
|
|
300
|
+
|
|
301
|
+
* code: callbackCode,
|
|
302
|
+
|
|
303
|
+
* redirectUri: 'https://myapp.com/callback',
|
|
304
|
+
|
|
305
|
+
* codeVerifier,
|
|
306
|
+
|
|
307
|
+
* })
|
|
308
|
+
|
|
309
|
+
*
|
|
310
|
+
|
|
311
|
+
* // Get user info
|
|
312
|
+
|
|
313
|
+
* const userInfo = await client.getUserInfo(tokens.accessToken)
|
|
314
|
+
|
|
315
|
+
* ```
|
|
316
|
+
|
|
317
|
+
*/
|
|
318
|
+
var NyaAccountClient = class {
|
|
319
|
+
constructor(config) {
|
|
320
|
+
this.httpClient = void 0;
|
|
321
|
+
this.config = void 0;
|
|
322
|
+
this.discoveryCache = null;
|
|
323
|
+
this.discoveryCacheTimestamp = 0;
|
|
324
|
+
this.discoveryCacheTtl = void 0;
|
|
325
|
+
this.jwtVerifier = null;
|
|
326
|
+
this.config = config;
|
|
327
|
+
this.discoveryCacheTtl = config.discoveryCacheTtl ?? DEFAULT_DISCOVERY_CACHE_TTL;
|
|
328
|
+
this.httpClient = axios.create({ timeout: config.timeout ?? 1e4 });
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
|
|
332
|
+
* Fetch the OIDC Discovery document. Results are cached with a configurable TTL.
|
|
333
|
+
|
|
334
|
+
*/
|
|
335
|
+
async discover() {
|
|
336
|
+
if (this.discoveryCache && Date.now() - this.discoveryCacheTimestamp < this.discoveryCacheTtl) return this.discoveryCache;
|
|
337
|
+
try {
|
|
338
|
+
const url = `${this.config.issuer}/.well-known/openid-configuration`;
|
|
339
|
+
const response = await this.httpClient.get(url);
|
|
340
|
+
const raw = DiscoveryDocumentSchema.parse(response.data);
|
|
341
|
+
this.discoveryCache = {
|
|
342
|
+
issuer: raw.issuer,
|
|
343
|
+
authorizationEndpoint: raw.authorization_endpoint,
|
|
344
|
+
tokenEndpoint: raw.token_endpoint,
|
|
345
|
+
userinfoEndpoint: raw.userinfo_endpoint,
|
|
346
|
+
jwksUri: raw.jwks_uri,
|
|
347
|
+
revocationEndpoint: raw.revocation_endpoint,
|
|
348
|
+
introspectionEndpoint: raw.introspection_endpoint,
|
|
349
|
+
pushedAuthorizationRequestEndpoint: raw.pushed_authorization_request_endpoint,
|
|
350
|
+
endSessionEndpoint: raw.end_session_endpoint,
|
|
351
|
+
responseTypesSupported: raw.response_types_supported,
|
|
352
|
+
grantTypesSupported: raw.grant_types_supported,
|
|
353
|
+
idTokenSigningAlgValuesSupported: raw.id_token_signing_alg_values_supported,
|
|
354
|
+
scopesSupported: raw.scopes_supported,
|
|
355
|
+
subjectTypesSupported: raw.subject_types_supported,
|
|
356
|
+
tokenEndpointAuthMethodsSupported: raw.token_endpoint_auth_methods_supported,
|
|
357
|
+
codeChallengeMethodsSupported: raw.code_challenge_methods_supported,
|
|
358
|
+
claimsSupported: raw.claims_supported
|
|
359
|
+
};
|
|
360
|
+
this.discoveryCacheTimestamp = Date.now();
|
|
361
|
+
return this.discoveryCache;
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (error instanceof DiscoveryError) throw error;
|
|
364
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
365
|
+
throw new DiscoveryError(`Failed to fetch OIDC Discovery document: ${message}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
|
|
370
|
+
* Clear the cached Discovery document and JWT verifier, forcing a re-fetch on next use.
|
|
371
|
+
|
|
372
|
+
*/
|
|
373
|
+
clearCache() {
|
|
374
|
+
this.discoveryCache = null;
|
|
375
|
+
this.discoveryCacheTimestamp = 0;
|
|
376
|
+
this.jwtVerifier = null;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
|
|
380
|
+
* Create an OAuth authorization URL (automatically includes PKCE).
|
|
381
|
+
|
|
382
|
+
*
|
|
383
|
+
|
|
384
|
+
* The returned `codeVerifier` and `state` must be saved to the session
|
|
385
|
+
|
|
386
|
+
* for later use in token exchange and CSRF validation.
|
|
387
|
+
|
|
388
|
+
*/
|
|
389
|
+
async createAuthorizationUrl(options) {
|
|
390
|
+
const authorizationEndpoint = await this.resolveEndpoint("authorization");
|
|
391
|
+
const codeVerifier = generateCodeVerifier();
|
|
392
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
393
|
+
const state = options.state ?? randomBytes(16).toString("base64url");
|
|
394
|
+
const params = new URLSearchParams({
|
|
395
|
+
client_id: this.config.clientId,
|
|
396
|
+
redirect_uri: options.redirectUri,
|
|
397
|
+
response_type: "code",
|
|
398
|
+
scope: options.scope ?? "openid",
|
|
399
|
+
state,
|
|
400
|
+
code_challenge: codeChallenge,
|
|
401
|
+
code_challenge_method: "S256"
|
|
402
|
+
});
|
|
403
|
+
if (options.nonce) params.set("nonce", options.nonce);
|
|
404
|
+
return {
|
|
405
|
+
url: `${authorizationEndpoint}?${params.toString()}`,
|
|
406
|
+
codeVerifier,
|
|
407
|
+
state
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
|
|
412
|
+
* Exchange an authorization code for tokens (Authorization Code Grant).
|
|
413
|
+
|
|
414
|
+
*/
|
|
415
|
+
async exchangeCode(options) {
|
|
416
|
+
const tokenEndpoint = await this.resolveEndpoint("token");
|
|
417
|
+
try {
|
|
418
|
+
const response = await this.httpClient.post(tokenEndpoint, {
|
|
419
|
+
grant_type: "authorization_code",
|
|
420
|
+
code: options.code,
|
|
421
|
+
redirect_uri: options.redirectUri,
|
|
422
|
+
client_id: this.config.clientId,
|
|
423
|
+
client_secret: this.config.clientSecret,
|
|
424
|
+
code_verifier: options.codeVerifier
|
|
425
|
+
});
|
|
426
|
+
return this.mapTokenResponse(response.data);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
throw this.handleTokenError(error);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
|
|
433
|
+
* Refresh an Access Token using a Refresh Token.
|
|
434
|
+
|
|
435
|
+
*/
|
|
436
|
+
async refreshToken(refreshToken) {
|
|
437
|
+
const tokenEndpoint = await this.resolveEndpoint("token");
|
|
438
|
+
try {
|
|
439
|
+
const response = await this.httpClient.post(tokenEndpoint, {
|
|
440
|
+
grant_type: "refresh_token",
|
|
441
|
+
refresh_token: refreshToken,
|
|
442
|
+
client_id: this.config.clientId,
|
|
443
|
+
client_secret: this.config.clientSecret
|
|
444
|
+
});
|
|
445
|
+
return this.mapTokenResponse(response.data);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
throw this.handleTokenError(error);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
|
|
452
|
+
* Revoke a token (RFC 7009).
|
|
453
|
+
|
|
454
|
+
*
|
|
455
|
+
|
|
456
|
+
* Supports revoking Access Tokens or Refresh Tokens.
|
|
457
|
+
|
|
458
|
+
* Revoking a Refresh Token also revokes its entire token family.
|
|
459
|
+
|
|
460
|
+
*/
|
|
461
|
+
async revokeToken(token) {
|
|
462
|
+
const revocationEndpoint = await this.resolveEndpoint("revocation");
|
|
463
|
+
try {
|
|
464
|
+
await this.httpClient.post(revocationEndpoint, {
|
|
465
|
+
token,
|
|
466
|
+
client_id: this.config.clientId,
|
|
467
|
+
client_secret: this.config.clientSecret
|
|
468
|
+
});
|
|
469
|
+
} catch {}
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
|
|
473
|
+
* Token introspection (RFC 7662).
|
|
474
|
+
|
|
475
|
+
*
|
|
476
|
+
|
|
477
|
+
* Query the server for the current state of a token (active status, associated user info, etc.).
|
|
478
|
+
|
|
479
|
+
*/
|
|
480
|
+
async introspectToken(token) {
|
|
481
|
+
const introspectionEndpoint = await this.resolveEndpoint("introspection");
|
|
482
|
+
try {
|
|
483
|
+
const response = await this.httpClient.post(introspectionEndpoint, {
|
|
484
|
+
token,
|
|
485
|
+
client_id: this.config.clientId,
|
|
486
|
+
client_secret: this.config.clientSecret
|
|
487
|
+
});
|
|
488
|
+
const raw = IntrospectionResponseSchema.parse(response.data);
|
|
489
|
+
return {
|
|
490
|
+
active: raw.active,
|
|
491
|
+
scope: raw.scope,
|
|
492
|
+
clientId: raw.client_id,
|
|
493
|
+
username: raw.username,
|
|
494
|
+
tokenType: raw.token_type,
|
|
495
|
+
exp: raw.exp,
|
|
496
|
+
iat: raw.iat,
|
|
497
|
+
sub: raw.sub,
|
|
498
|
+
aud: raw.aud,
|
|
499
|
+
iss: raw.iss,
|
|
500
|
+
jti: raw.jti
|
|
501
|
+
};
|
|
502
|
+
} catch (error) {
|
|
503
|
+
throw this.handleTokenError(error);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
|
|
508
|
+
* Get user info using an Access Token (OIDC UserInfo Endpoint).
|
|
509
|
+
|
|
510
|
+
*
|
|
511
|
+
|
|
512
|
+
* The returned fields depend on the scopes included in the token:
|
|
513
|
+
|
|
514
|
+
* - `profile`: name, preferredUsername, updatedAt
|
|
515
|
+
|
|
516
|
+
* - `email`: email, emailVerified
|
|
517
|
+
|
|
518
|
+
*/
|
|
519
|
+
async getUserInfo(accessToken) {
|
|
520
|
+
const userinfoEndpoint = await this.resolveEndpoint("userinfo");
|
|
521
|
+
try {
|
|
522
|
+
const response = await this.httpClient.get(userinfoEndpoint, { headers: { Authorization: `Bearer ${accessToken}` } });
|
|
523
|
+
const raw = UserInfoSchema.parse(response.data);
|
|
524
|
+
return {
|
|
525
|
+
sub: raw.sub,
|
|
526
|
+
name: raw.name,
|
|
527
|
+
preferredUsername: raw.preferred_username,
|
|
528
|
+
email: raw.email,
|
|
529
|
+
emailVerified: raw.email_verified,
|
|
530
|
+
updatedAt: raw.updated_at
|
|
531
|
+
};
|
|
532
|
+
} catch (error) {
|
|
533
|
+
throw this.handleTokenError(error);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
|
|
538
|
+
* Locally verify a JWT Access Token (RFC 9068).
|
|
539
|
+
|
|
540
|
+
*
|
|
541
|
+
|
|
542
|
+
* Uses remote JWKS for signature verification, and validates issuer, audience, expiry, etc.
|
|
543
|
+
|
|
544
|
+
*
|
|
545
|
+
|
|
546
|
+
* @param token JWT Access Token string
|
|
547
|
+
|
|
548
|
+
* @param options.audience Custom audience validation value (defaults to clientId)
|
|
549
|
+
|
|
550
|
+
*/
|
|
551
|
+
async verifyAccessToken(token, options) {
|
|
552
|
+
const verifier = await this.getJwtVerifier();
|
|
553
|
+
return verifier.verifyAccessToken(token, options?.audience);
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
|
|
557
|
+
* Locally verify an OIDC ID Token.
|
|
558
|
+
|
|
559
|
+
*
|
|
560
|
+
|
|
561
|
+
* @param token JWT ID Token string
|
|
562
|
+
|
|
563
|
+
* @param options.audience Custom audience validation value (defaults to clientId)
|
|
564
|
+
|
|
565
|
+
* @param options.nonce Validate the nonce claim (required if nonce was sent during authorization)
|
|
566
|
+
|
|
567
|
+
*/
|
|
568
|
+
async verifyIdToken(token, options) {
|
|
569
|
+
const verifier = await this.getJwtVerifier();
|
|
570
|
+
return verifier.verifyIdToken(token, options);
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
|
|
574
|
+
* Express middleware: verify the Bearer Token in the request.
|
|
575
|
+
|
|
576
|
+
*
|
|
577
|
+
|
|
578
|
+
* After successful verification, use `getAuth(req)` to retrieve the token payload.
|
|
579
|
+
|
|
580
|
+
*
|
|
581
|
+
|
|
582
|
+
* @param options.strategy Verification strategy: 'local' (default, JWT local verification) or 'introspection' (remote introspection)
|
|
583
|
+
|
|
584
|
+
*
|
|
585
|
+
|
|
586
|
+
* @example
|
|
587
|
+
|
|
588
|
+
* ```typescript
|
|
589
|
+
|
|
590
|
+
* import { getAuth } from '@nya-account/node-sdk/express'
|
|
591
|
+
|
|
592
|
+
*
|
|
593
|
+
|
|
594
|
+
* app.use('/api', client.authenticate())
|
|
595
|
+
|
|
596
|
+
*
|
|
597
|
+
|
|
598
|
+
* app.get('/api/me', (req, res) => {
|
|
599
|
+
|
|
600
|
+
* const auth = getAuth(req)
|
|
601
|
+
|
|
602
|
+
* res.json({ userId: auth?.sub })
|
|
603
|
+
|
|
604
|
+
* })
|
|
605
|
+
|
|
606
|
+
* ```
|
|
607
|
+
|
|
608
|
+
*/
|
|
609
|
+
authenticate(options) {
|
|
610
|
+
const strategy = options?.strategy ?? "local";
|
|
611
|
+
return (req, res, next) => {
|
|
612
|
+
const token = extractBearerToken(req);
|
|
613
|
+
if (!token) {
|
|
614
|
+
sendOAuthError(res, 401, "invalid_token", "Missing Bearer token in Authorization header");
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const handleVerification = async () => {
|
|
618
|
+
let payload;
|
|
619
|
+
if (strategy === "introspection") {
|
|
620
|
+
const introspection = await this.introspectToken(token);
|
|
621
|
+
if (!introspection.active) {
|
|
622
|
+
sendOAuthError(res, 401, "invalid_token", "Token is not active");
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
payload = {
|
|
626
|
+
iss: introspection.iss ?? "",
|
|
627
|
+
sub: introspection.sub ?? "",
|
|
628
|
+
aud: introspection.aud ?? "",
|
|
629
|
+
scope: introspection.scope ?? "",
|
|
630
|
+
ver: "1",
|
|
631
|
+
iat: introspection.iat ?? 0,
|
|
632
|
+
exp: introspection.exp ?? 0,
|
|
633
|
+
jti: introspection.jti ?? ""
|
|
634
|
+
};
|
|
635
|
+
} else payload = await this.verifyAccessToken(token);
|
|
636
|
+
setAuth(req, payload);
|
|
637
|
+
next();
|
|
638
|
+
};
|
|
639
|
+
handleVerification().catch(() => {
|
|
640
|
+
sendOAuthError(res, 401, "invalid_token", "Invalid or expired access token");
|
|
641
|
+
});
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
|
|
646
|
+
* Express middleware: validate that the token in the request contains the specified scopes.
|
|
647
|
+
|
|
648
|
+
*
|
|
649
|
+
|
|
650
|
+
* Must be used after the `authenticate()` middleware.
|
|
651
|
+
|
|
652
|
+
*
|
|
653
|
+
|
|
654
|
+
* @example
|
|
655
|
+
|
|
656
|
+
* ```typescript
|
|
657
|
+
|
|
658
|
+
* app.get('/api/profile',
|
|
659
|
+
|
|
660
|
+
* client.authenticate(),
|
|
661
|
+
|
|
662
|
+
* client.requireScopes('profile'),
|
|
663
|
+
|
|
664
|
+
* (req, res) => { ... }
|
|
665
|
+
|
|
666
|
+
* )
|
|
667
|
+
|
|
668
|
+
* ```
|
|
669
|
+
|
|
670
|
+
*/
|
|
671
|
+
requireScopes(...scopes) {
|
|
672
|
+
return (req, res, next) => {
|
|
673
|
+
const auth = getAuth(req);
|
|
674
|
+
if (!auth) {
|
|
675
|
+
sendOAuthError(res, 401, "invalid_token", "Request not authenticated");
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const tokenScopes = auth.scope.split(" ");
|
|
679
|
+
const missingScopes = scopes.filter((s) => !tokenScopes.includes(s));
|
|
680
|
+
if (missingScopes.length > 0) {
|
|
681
|
+
sendOAuthError(res, 403, "insufficient_scope", `Missing required scopes: ${missingScopes.join(" ")}`);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
next();
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
async resolveEndpoint(name) {
|
|
688
|
+
const explicit = this.config.endpoints?.[name];
|
|
689
|
+
if (explicit) return explicit;
|
|
690
|
+
const discovery = await this.discover();
|
|
691
|
+
const discoveryKey = DISCOVERY_ENDPOINT_MAP[name];
|
|
692
|
+
const endpoint = discovery[discoveryKey];
|
|
693
|
+
if (!endpoint || typeof endpoint !== "string") throw new NyaAccountError("endpoint_not_found", `Endpoint '${name}' not found in Discovery document`);
|
|
694
|
+
return endpoint;
|
|
695
|
+
}
|
|
696
|
+
async getJwtVerifier() {
|
|
697
|
+
if (this.jwtVerifier) return this.jwtVerifier;
|
|
698
|
+
const jwksUri = await this.resolveEndpoint("jwks");
|
|
699
|
+
const discovery = await this.discover();
|
|
700
|
+
this.jwtVerifier = new JwtVerifier(jwksUri, discovery.issuer, this.config.clientId);
|
|
701
|
+
return this.jwtVerifier;
|
|
702
|
+
}
|
|
703
|
+
mapTokenResponse(data) {
|
|
704
|
+
const raw = TokenResponseSchema.parse(data);
|
|
705
|
+
return {
|
|
706
|
+
accessToken: raw.access_token,
|
|
707
|
+
tokenType: raw.token_type,
|
|
708
|
+
expiresIn: raw.expires_in,
|
|
709
|
+
refreshToken: raw.refresh_token,
|
|
710
|
+
scope: raw.scope,
|
|
711
|
+
idToken: raw.id_token
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
handleTokenError(error) {
|
|
715
|
+
if (error instanceof NyaAccountError) return error;
|
|
716
|
+
if (error instanceof AxiosError && error.response) {
|
|
717
|
+
const parsed = OAuthErrorSchema.safeParse(error.response.data);
|
|
718
|
+
if (parsed.success) return new OAuthError(parsed.data.error, parsed.data.error_description ?? "Unknown error");
|
|
719
|
+
}
|
|
720
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
721
|
+
return new NyaAccountError("request_failed", `Request failed: ${message}`);
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
//#endregion
|
|
726
|
+
export { DiscoveryError, NyaAccountClient, NyaAccountError, OAuthError, TokenVerificationError, generateCodeChallenge, generateCodeVerifier, generatePkce, getAuth };
|
|
727
|
+
//# sourceMappingURL=index.js.map
|