cynx-lib 1.3.83 → 1.3.85
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/gen.js +77 -77
- package/package.json +5 -1
- package/ui/components/Drawer.jsx +208 -208
- package/ui/components/Layout.jsx +37 -0
- package/ui/components/Pagination.jsx +145 -145
- package/ui/components/PrivacyBlur.jsx +603 -603
- package/ui/components/Sidebar.jsx +300 -134
- package/utils/index.cjs +5 -0
- package/utils/index.js +22 -20
- package/utils/utils/duration.js +139 -139
- package/utils/utils/oidc.js +214 -214
- package/utils/utils/realtime.js +1634 -1634
- package/utils/utils/session.cjs +15 -2
- package/utils/utils/session.js +67 -58
- package/utils/utils/session_manager.cjs +490 -0
- package/utils/utils/session_manager.js +549 -0
- package/utils/utils/storage.js +393 -393
- package/utils/utils/ui.js +111 -111
- package/vue/components/Badge.vue +79 -79
- package/vue/components/Button.vue +209 -209
- package/vue/components/Checkbox.vue +103 -103
- package/vue/components/CircularProgress.vue +383 -383
- package/vue/components/Divider.vue +94 -94
- package/vue/components/Drawer.vue +243 -243
- package/vue/components/InfoBadge.vue +223 -223
- package/vue/components/Input.vue +341 -341
- package/vue/components/LinearProgress.vue +268 -268
- package/vue/components/Modal.vue +226 -226
- package/vue/components/Pagination.vue +199 -199
- package/vue/components/Select.vue +713 -713
- package/vue/components/Sidebar.vue +955 -955
- package/vue/components/Skeleton.vue +91 -91
- package/vue/components/Spinner.vue +263 -263
- package/vue/components/StatusTag.vue +89 -89
- package/vue/components/TabBar.vue +176 -176
- package/vue/components/Table.vue +388 -388
- package/vue/components/TimePicker.vue +399 -399
- package/vue/components/Toggle.vue +115 -115
- package/vue/components/XTable.vue +119 -119
- package/vue/composables/useAutoSkeletonRows.ts +71 -71
- package/vue/index.d.ts +67 -67
- package/vue/index.js +86 -86
package/utils/utils/oidc.js
CHANGED
|
@@ -1,214 +1,214 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cynx/utils — OpenID Connect & OAuth 2.0 Engine (oidc.js)
|
|
3
|
-
*
|
|
4
|
-
* Lightweight, zero-external-dependency OIDC/OAuth 2.0 client utility for Node.js / SSR.
|
|
5
|
-
* Supports Discovery, PKCE (RFC 7636), signed state tokens (CSRF protection),
|
|
6
|
-
* token exchange, and profile resolution.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import crypto from 'crypto';
|
|
10
|
-
|
|
11
|
-
const DEFAULT_SECRET = (typeof process !== 'undefined' && (process.env?.OIDC_STATE_SECRET || process.env?.JWT_SECRET)) || 'cynx-oidc-state-secret-salt';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Generate a cryptographically signed state token with embedded payload and timestamp.
|
|
15
|
-
*
|
|
16
|
-
* @param {object} [payload={}] Data to embed (e.g. { codeVerifier, returnTo })
|
|
17
|
-
* @param {string} [secret] Custom HMAC signing secret
|
|
18
|
-
* @returns {string} Signed state token in format "base64urlData.base64urlSig"
|
|
19
|
-
*/
|
|
20
|
-
export function generateState(payload = {}, secret = DEFAULT_SECRET) {
|
|
21
|
-
const data = Buffer.from(JSON.stringify({ ...payload, ts: Date.now() })).toString('base64url');
|
|
22
|
-
const sig = crypto.createHmac('sha256', secret).update(data).digest('base64url');
|
|
23
|
-
return `${data}.${sig}`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Verify, parse, and consume a signed state token with replay & TTL validation.
|
|
28
|
-
*
|
|
29
|
-
* @param {string} stateStr The state token received from redirect
|
|
30
|
-
* @param {string} [secret] Custom HMAC signing secret
|
|
31
|
-
* @param {number} [ttlMs=600000] Maximum token lifetime in ms (default: 10 minutes)
|
|
32
|
-
* @returns {object|null} Decoded payload if valid and not expired, null otherwise
|
|
33
|
-
*/
|
|
34
|
-
export function consumeState(stateStr, secret = DEFAULT_SECRET, ttlMs = 600_000) {
|
|
35
|
-
if (!stateStr || typeof stateStr !== 'string') return null;
|
|
36
|
-
const parts = stateStr.split('.');
|
|
37
|
-
if (parts.length !== 2) return null;
|
|
38
|
-
const [data, sig] = parts;
|
|
39
|
-
const expectedSig = crypto.createHmac('sha256', secret).update(data).digest('base64url');
|
|
40
|
-
|
|
41
|
-
if (sig.length !== expectedSig.length) return null;
|
|
42
|
-
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig))) return null;
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
const payload = JSON.parse(Buffer.from(data, 'base64url').toString('utf8'));
|
|
46
|
-
if (!payload.ts || Date.now() - payload.ts > ttlMs) return null;
|
|
47
|
-
return payload;
|
|
48
|
-
} catch {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Generate a secure PKCE (Proof Key for Code Exchange) pair for Authorization Code flow.
|
|
55
|
-
*
|
|
56
|
-
* @returns {{ codeVerifier: string, codeChallenge: string }}
|
|
57
|
-
*/
|
|
58
|
-
export function generatePKCE() {
|
|
59
|
-
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
60
|
-
const codeChallenge = crypto.createHash('sha256')
|
|
61
|
-
.update(codeVerifier)
|
|
62
|
-
.digest('base64url');
|
|
63
|
-
return { codeVerifier, codeChallenge };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Fetch and discover OpenID Connect provider configuration document.
|
|
68
|
-
*
|
|
69
|
-
* @param {string} issuer Base issuer URL (e.g. "https://auth.example.com/realms/master")
|
|
70
|
-
* @param {string} [discoveryPath='/.well-known/openid-configuration'] Custom discovery path
|
|
71
|
-
* @returns {Promise<object>} Discovery metadata (authorization_endpoint, token_endpoint, etc.)
|
|
72
|
-
*/
|
|
73
|
-
export async function discover(issuer, discoveryPath = '/.well-known/openid-configuration') {
|
|
74
|
-
let base = issuer.replace(/\/+$/, '');
|
|
75
|
-
let path = discoveryPath || '/.well-known/openid-configuration';
|
|
76
|
-
if (base.endsWith(path)) {
|
|
77
|
-
base = base.slice(0, -path.length).replace(/\/+$/, '');
|
|
78
|
-
}
|
|
79
|
-
if (!path.startsWith('/')) path = '/' + path;
|
|
80
|
-
const url = base + path;
|
|
81
|
-
const res = await fetch(url);
|
|
82
|
-
if (!res.ok) throw new Error(`OIDC Discovery failed: ${res.status} — ${url}`);
|
|
83
|
-
return res.json();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Build the authorization redirect URL to initiate login.
|
|
88
|
-
*
|
|
89
|
-
* @param {object} config
|
|
90
|
-
* @param {string} config.issuer Issuer base URL
|
|
91
|
-
* @param {string} config.clientId Client ID
|
|
92
|
-
* @param {string} config.redirectUri Callback redirect URI
|
|
93
|
-
* @param {string} [config.scopes='openid profile email'] Space-separated scopes
|
|
94
|
-
* @param {boolean} [config.pkceEnabled=true] Whether PKCE is enabled
|
|
95
|
-
* @param {string} [config.discoveryPath]
|
|
96
|
-
* @param {string} state Signed state parameter
|
|
97
|
-
* @param {string} [codeChallenge] PKCE code challenge
|
|
98
|
-
* @returns {string} Full authorization URL
|
|
99
|
-
*/
|
|
100
|
-
export function buildAuthorizeUrl(config, state, codeChallenge) {
|
|
101
|
-
const params = new URLSearchParams({
|
|
102
|
-
response_type: 'code',
|
|
103
|
-
client_id: config.clientId,
|
|
104
|
-
redirect_uri: config.redirectUri,
|
|
105
|
-
scope: config.scopes || 'openid profile email',
|
|
106
|
-
state,
|
|
107
|
-
});
|
|
108
|
-
if (config.pkceEnabled !== false && codeChallenge) {
|
|
109
|
-
params.set('code_challenge', codeChallenge);
|
|
110
|
-
params.set('code_challenge_method', 'S256');
|
|
111
|
-
}
|
|
112
|
-
const base = config.issuer.replace(/\/+$/, '');
|
|
113
|
-
return `${base}/authorize?${params.toString()}`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Exchange an authorization code for access, ID, and refresh tokens.
|
|
118
|
-
*
|
|
119
|
-
* @param {object} config
|
|
120
|
-
* @param {string} config.issuer Issuer base URL
|
|
121
|
-
* @param {string} config.clientId Client ID
|
|
122
|
-
* @param {string} [config.clientSecret] Client secret (for confidential clients)
|
|
123
|
-
* @param {string} config.redirectUri Callback redirect URI
|
|
124
|
-
* @param {boolean} [config.pkceEnabled=true]
|
|
125
|
-
* @param {string} [config.discoveryPath]
|
|
126
|
-
* @param {string} code Authorization code from callback
|
|
127
|
-
* @param {string} [codeVerifier] PKCE code verifier
|
|
128
|
-
* @returns {Promise<{ access_token: string, id_token?: string, refresh_token?: string, expires_in?: number, token_type?: string }>}
|
|
129
|
-
*/
|
|
130
|
-
export async function exchangeCode(config, code, codeVerifier) {
|
|
131
|
-
const discoverDoc = await discover(config.issuer, config.discoveryPath);
|
|
132
|
-
const tokenUrl = discoverDoc.token_endpoint;
|
|
133
|
-
|
|
134
|
-
const body = new URLSearchParams({
|
|
135
|
-
grant_type: 'authorization_code',
|
|
136
|
-
code,
|
|
137
|
-
redirect_uri: config.redirectUri,
|
|
138
|
-
client_id: config.clientId,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
if (config.clientSecret) {
|
|
142
|
-
body.set('client_secret', config.clientSecret);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (config.pkceEnabled !== false && codeVerifier) {
|
|
146
|
-
body.set('code_verifier', codeVerifier);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const res = await fetch(tokenUrl, {
|
|
150
|
-
method: 'POST',
|
|
151
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
152
|
-
body,
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
if (!res.ok) {
|
|
156
|
-
const err = await res.text();
|
|
157
|
-
throw new Error(`Token exchange failed: ${res.status} ${err}`);
|
|
158
|
-
}
|
|
159
|
-
return res.json();
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Fetch user profile from the UserInfo endpoint using an access token.
|
|
164
|
-
*
|
|
165
|
-
* @param {object} config
|
|
166
|
-
* @param {string} config.issuer Issuer base URL
|
|
167
|
-
* @param {string} [config.discoveryPath]
|
|
168
|
-
* @param {string} accessToken Bearer access token
|
|
169
|
-
* @returns {Promise<object>} User claims and profile info
|
|
170
|
-
*/
|
|
171
|
-
export async function getUserInfo(config, accessToken) {
|
|
172
|
-
const discoverDoc = await discover(config.issuer, config.discoveryPath);
|
|
173
|
-
const userinfoUrl = discoverDoc.userinfo_endpoint;
|
|
174
|
-
if (!userinfoUrl) throw new Error('No userinfo_endpoint in discovery document');
|
|
175
|
-
|
|
176
|
-
const res = await fetch(userinfoUrl, {
|
|
177
|
-
headers: { Authorization: `Bearer ${accessToken}` },
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
if (!res.ok) throw new Error(`Userinfo failed: ${res.status}`);
|
|
181
|
-
return res.json();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Safely decode payload from an ID token (JWT) without verification.
|
|
186
|
-
*
|
|
187
|
-
* @param {string} idToken Raw JWT id_token string
|
|
188
|
-
* @returns {object} Decoded JSON payload claims
|
|
189
|
-
*/
|
|
190
|
-
export function decodeIdToken(idToken) {
|
|
191
|
-
if (!idToken || typeof idToken !== 'string') throw new Error('Invalid id_token');
|
|
192
|
-
const parts = idToken.split('.');
|
|
193
|
-
if (parts.length !== 3) throw new Error('Invalid id_token format (expected 3 parts)');
|
|
194
|
-
const payload = Buffer.from(parts[1], 'base64url').toString('utf8');
|
|
195
|
-
return JSON.parse(payload);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export function storeVerifier() {
|
|
199
|
-
// Verifier is stored directly inside the signed state token or handled by PKCE
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// ─── MAIN EXPORT ────────────────────────────────────────────────────────────────
|
|
203
|
-
|
|
204
|
-
export default {
|
|
205
|
-
generateState,
|
|
206
|
-
storeVerifier,
|
|
207
|
-
consumeState,
|
|
208
|
-
generatePKCE,
|
|
209
|
-
discover,
|
|
210
|
-
buildAuthorizeUrl,
|
|
211
|
-
exchangeCode,
|
|
212
|
-
getUserInfo,
|
|
213
|
-
decodeIdToken,
|
|
214
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* cynx/utils — OpenID Connect & OAuth 2.0 Engine (oidc.js)
|
|
3
|
+
*
|
|
4
|
+
* Lightweight, zero-external-dependency OIDC/OAuth 2.0 client utility for Node.js / SSR.
|
|
5
|
+
* Supports Discovery, PKCE (RFC 7636), signed state tokens (CSRF protection),
|
|
6
|
+
* token exchange, and profile resolution.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import crypto from 'crypto';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_SECRET = (typeof process !== 'undefined' && (process.env?.OIDC_STATE_SECRET || process.env?.JWT_SECRET)) || 'cynx-oidc-state-secret-salt';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate a cryptographically signed state token with embedded payload and timestamp.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} [payload={}] Data to embed (e.g. { codeVerifier, returnTo })
|
|
17
|
+
* @param {string} [secret] Custom HMAC signing secret
|
|
18
|
+
* @returns {string} Signed state token in format "base64urlData.base64urlSig"
|
|
19
|
+
*/
|
|
20
|
+
export function generateState(payload = {}, secret = DEFAULT_SECRET) {
|
|
21
|
+
const data = Buffer.from(JSON.stringify({ ...payload, ts: Date.now() })).toString('base64url');
|
|
22
|
+
const sig = crypto.createHmac('sha256', secret).update(data).digest('base64url');
|
|
23
|
+
return `${data}.${sig}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Verify, parse, and consume a signed state token with replay & TTL validation.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} stateStr The state token received from redirect
|
|
30
|
+
* @param {string} [secret] Custom HMAC signing secret
|
|
31
|
+
* @param {number} [ttlMs=600000] Maximum token lifetime in ms (default: 10 minutes)
|
|
32
|
+
* @returns {object|null} Decoded payload if valid and not expired, null otherwise
|
|
33
|
+
*/
|
|
34
|
+
export function consumeState(stateStr, secret = DEFAULT_SECRET, ttlMs = 600_000) {
|
|
35
|
+
if (!stateStr || typeof stateStr !== 'string') return null;
|
|
36
|
+
const parts = stateStr.split('.');
|
|
37
|
+
if (parts.length !== 2) return null;
|
|
38
|
+
const [data, sig] = parts;
|
|
39
|
+
const expectedSig = crypto.createHmac('sha256', secret).update(data).digest('base64url');
|
|
40
|
+
|
|
41
|
+
if (sig.length !== expectedSig.length) return null;
|
|
42
|
+
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig))) return null;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const payload = JSON.parse(Buffer.from(data, 'base64url').toString('utf8'));
|
|
46
|
+
if (!payload.ts || Date.now() - payload.ts > ttlMs) return null;
|
|
47
|
+
return payload;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generate a secure PKCE (Proof Key for Code Exchange) pair for Authorization Code flow.
|
|
55
|
+
*
|
|
56
|
+
* @returns {{ codeVerifier: string, codeChallenge: string }}
|
|
57
|
+
*/
|
|
58
|
+
export function generatePKCE() {
|
|
59
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
60
|
+
const codeChallenge = crypto.createHash('sha256')
|
|
61
|
+
.update(codeVerifier)
|
|
62
|
+
.digest('base64url');
|
|
63
|
+
return { codeVerifier, codeChallenge };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Fetch and discover OpenID Connect provider configuration document.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} issuer Base issuer URL (e.g. "https://auth.example.com/realms/master")
|
|
70
|
+
* @param {string} [discoveryPath='/.well-known/openid-configuration'] Custom discovery path
|
|
71
|
+
* @returns {Promise<object>} Discovery metadata (authorization_endpoint, token_endpoint, etc.)
|
|
72
|
+
*/
|
|
73
|
+
export async function discover(issuer, discoveryPath = '/.well-known/openid-configuration') {
|
|
74
|
+
let base = issuer.replace(/\/+$/, '');
|
|
75
|
+
let path = discoveryPath || '/.well-known/openid-configuration';
|
|
76
|
+
if (base.endsWith(path)) {
|
|
77
|
+
base = base.slice(0, -path.length).replace(/\/+$/, '');
|
|
78
|
+
}
|
|
79
|
+
if (!path.startsWith('/')) path = '/' + path;
|
|
80
|
+
const url = base + path;
|
|
81
|
+
const res = await fetch(url);
|
|
82
|
+
if (!res.ok) throw new Error(`OIDC Discovery failed: ${res.status} — ${url}`);
|
|
83
|
+
return res.json();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build the authorization redirect URL to initiate login.
|
|
88
|
+
*
|
|
89
|
+
* @param {object} config
|
|
90
|
+
* @param {string} config.issuer Issuer base URL
|
|
91
|
+
* @param {string} config.clientId Client ID
|
|
92
|
+
* @param {string} config.redirectUri Callback redirect URI
|
|
93
|
+
* @param {string} [config.scopes='openid profile email'] Space-separated scopes
|
|
94
|
+
* @param {boolean} [config.pkceEnabled=true] Whether PKCE is enabled
|
|
95
|
+
* @param {string} [config.discoveryPath]
|
|
96
|
+
* @param {string} state Signed state parameter
|
|
97
|
+
* @param {string} [codeChallenge] PKCE code challenge
|
|
98
|
+
* @returns {string} Full authorization URL
|
|
99
|
+
*/
|
|
100
|
+
export function buildAuthorizeUrl(config, state, codeChallenge) {
|
|
101
|
+
const params = new URLSearchParams({
|
|
102
|
+
response_type: 'code',
|
|
103
|
+
client_id: config.clientId,
|
|
104
|
+
redirect_uri: config.redirectUri,
|
|
105
|
+
scope: config.scopes || 'openid profile email',
|
|
106
|
+
state,
|
|
107
|
+
});
|
|
108
|
+
if (config.pkceEnabled !== false && codeChallenge) {
|
|
109
|
+
params.set('code_challenge', codeChallenge);
|
|
110
|
+
params.set('code_challenge_method', 'S256');
|
|
111
|
+
}
|
|
112
|
+
const base = config.issuer.replace(/\/+$/, '');
|
|
113
|
+
return `${base}/authorize?${params.toString()}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Exchange an authorization code for access, ID, and refresh tokens.
|
|
118
|
+
*
|
|
119
|
+
* @param {object} config
|
|
120
|
+
* @param {string} config.issuer Issuer base URL
|
|
121
|
+
* @param {string} config.clientId Client ID
|
|
122
|
+
* @param {string} [config.clientSecret] Client secret (for confidential clients)
|
|
123
|
+
* @param {string} config.redirectUri Callback redirect URI
|
|
124
|
+
* @param {boolean} [config.pkceEnabled=true]
|
|
125
|
+
* @param {string} [config.discoveryPath]
|
|
126
|
+
* @param {string} code Authorization code from callback
|
|
127
|
+
* @param {string} [codeVerifier] PKCE code verifier
|
|
128
|
+
* @returns {Promise<{ access_token: string, id_token?: string, refresh_token?: string, expires_in?: number, token_type?: string }>}
|
|
129
|
+
*/
|
|
130
|
+
export async function exchangeCode(config, code, codeVerifier) {
|
|
131
|
+
const discoverDoc = await discover(config.issuer, config.discoveryPath);
|
|
132
|
+
const tokenUrl = discoverDoc.token_endpoint;
|
|
133
|
+
|
|
134
|
+
const body = new URLSearchParams({
|
|
135
|
+
grant_type: 'authorization_code',
|
|
136
|
+
code,
|
|
137
|
+
redirect_uri: config.redirectUri,
|
|
138
|
+
client_id: config.clientId,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (config.clientSecret) {
|
|
142
|
+
body.set('client_secret', config.clientSecret);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (config.pkceEnabled !== false && codeVerifier) {
|
|
146
|
+
body.set('code_verifier', codeVerifier);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const res = await fetch(tokenUrl, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
152
|
+
body,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!res.ok) {
|
|
156
|
+
const err = await res.text();
|
|
157
|
+
throw new Error(`Token exchange failed: ${res.status} ${err}`);
|
|
158
|
+
}
|
|
159
|
+
return res.json();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Fetch user profile from the UserInfo endpoint using an access token.
|
|
164
|
+
*
|
|
165
|
+
* @param {object} config
|
|
166
|
+
* @param {string} config.issuer Issuer base URL
|
|
167
|
+
* @param {string} [config.discoveryPath]
|
|
168
|
+
* @param {string} accessToken Bearer access token
|
|
169
|
+
* @returns {Promise<object>} User claims and profile info
|
|
170
|
+
*/
|
|
171
|
+
export async function getUserInfo(config, accessToken) {
|
|
172
|
+
const discoverDoc = await discover(config.issuer, config.discoveryPath);
|
|
173
|
+
const userinfoUrl = discoverDoc.userinfo_endpoint;
|
|
174
|
+
if (!userinfoUrl) throw new Error('No userinfo_endpoint in discovery document');
|
|
175
|
+
|
|
176
|
+
const res = await fetch(userinfoUrl, {
|
|
177
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (!res.ok) throw new Error(`Userinfo failed: ${res.status}`);
|
|
181
|
+
return res.json();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Safely decode payload from an ID token (JWT) without verification.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} idToken Raw JWT id_token string
|
|
188
|
+
* @returns {object} Decoded JSON payload claims
|
|
189
|
+
*/
|
|
190
|
+
export function decodeIdToken(idToken) {
|
|
191
|
+
if (!idToken || typeof idToken !== 'string') throw new Error('Invalid id_token');
|
|
192
|
+
const parts = idToken.split('.');
|
|
193
|
+
if (parts.length !== 3) throw new Error('Invalid id_token format (expected 3 parts)');
|
|
194
|
+
const payload = Buffer.from(parts[1], 'base64url').toString('utf8');
|
|
195
|
+
return JSON.parse(payload);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function storeVerifier() {
|
|
199
|
+
// Verifier is stored directly inside the signed state token or handled by PKCE
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ─── MAIN EXPORT ────────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
export default {
|
|
205
|
+
generateState,
|
|
206
|
+
storeVerifier,
|
|
207
|
+
consumeState,
|
|
208
|
+
generatePKCE,
|
|
209
|
+
discover,
|
|
210
|
+
buildAuthorizeUrl,
|
|
211
|
+
exchangeCode,
|
|
212
|
+
getUserInfo,
|
|
213
|
+
decodeIdToken,
|
|
214
|
+
};
|