ldap-rest 0.0.1 → 0.1.1
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 +38 -0
- package/bin/sync-app-accounts.mjs +276 -0
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/abstract/plugin.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/logger/winston.js +1 -1
- package/dist/plugins/auth/hmac.js +1 -0
- package/dist/plugins/auth/totp.js +1 -0
- package/dist/plugins/demo/helloworld.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/onChange.js +1 -1
- package/dist/plugins/static.js +1 -1
- package/dist/plugins/twake/appAccountsApi.js +1 -0
- package/dist/plugins/twake/appAccountsConsistency.js +1 -0
- package/dist/plugins/twake/james.js +1 -1
- package/dist/plugins/weblogs.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +0 -5
- package/dist/src/abstract/plugin.d.ts +1 -0
- package/dist/src/bin/index.d.ts +1 -0
- package/dist/src/browser/shared/index.d.ts +1 -0
- package/dist/src/browser/shared/utils/hmac.d.ts +73 -0
- package/dist/src/browser/shared/utils/totp.d.ts +64 -0
- package/dist/src/config/args.d.ts +9 -1
- package/dist/src/hooks.d.ts +2 -2
- package/dist/src/lib/ldapActions.d.ts +3 -7
- package/dist/src/lib/utils.d.ts +17 -0
- package/dist/src/logger/winston.d.ts +5 -0
- package/dist/src/plugins/auth/hmac.d.ts +24 -0
- package/dist/src/plugins/auth/totp.d.ts +28 -0
- package/dist/src/plugins/ldap/onChange.d.ts +1 -0
- package/dist/src/plugins/static.d.ts +4 -0
- package/dist/src/plugins/twake/appAccountsApi.d.ts +52 -0
- package/dist/src/plugins/twake/appAccountsConsistency.d.ts +58 -0
- package/docs/DEVELOPER_GUIDE.md +1 -0
- package/docs/PLUGIN_DEPENDENCIES.md +19 -0
- package/docs/README.md +76 -30
- package/docs/authentication.md +604 -12
- package/docs/plugins/DEVELOPMENT.md +162 -76
- package/docs/testing-with-embedded-ldap.md +347 -0
- package/docs/twakeAppAccountsApi.md +485 -0
- package/docs/weblogs.md +37 -8
- package/examples/web/README.md +175 -0
- package/examples/web/ldap-tree-viewer.html +16 -2
- package/examples/web/totp-client.html +541 -0
- package/package.json +132 -87
- package/static/browser/shared/index.js +1 -0
- package/static/browser/shared/index.js.map +1 -1
- package/static/browser/shared/utils/hmac.js +202 -0
- package/static/browser/shared/utils/hmac.js.map +1 -0
- package/static/browser/shared/utils/totp.js +182 -0
- package/static/browser/shared/utils/totp.js.map +1 -0
- package/static/browser/shared.esm.js +181 -1
- package/static/browser/shared.esm.js.map +1 -1
- package/static/browser/shared.js +184 -0
- package/static/browser/shared.js.map +1 -1
- package/static/browser/shared.min.js +1 -1
- package/static/browser/shared.min.js.map +1 -1
- package/static/schemas/obm/organizations.json +24 -0
- package/static/schemas/obm/users.json +192 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HMAC-SHA256 request signing utility for browser
|
|
3
|
+
* For backend services authentication
|
|
4
|
+
* @module browser/shared/utils/hmac
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Calculate SHA-256 hash of data
|
|
8
|
+
*/
|
|
9
|
+
async function sha256(data) {
|
|
10
|
+
const encoder = new TextEncoder();
|
|
11
|
+
const dataBuffer = encoder.encode(data);
|
|
12
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer);
|
|
13
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
14
|
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Calculate HMAC-SHA256 signature
|
|
18
|
+
*/
|
|
19
|
+
async function hmacSha256(secret, message) {
|
|
20
|
+
const encoder = new TextEncoder();
|
|
21
|
+
const keyData = encoder.encode(secret);
|
|
22
|
+
const messageData = encoder.encode(message);
|
|
23
|
+
// Import secret as HMAC key
|
|
24
|
+
const cryptoKey = await crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
25
|
+
// Calculate HMAC
|
|
26
|
+
const signature = await crypto.subtle.sign('HMAC', cryptoKey, messageData);
|
|
27
|
+
const signatureArray = Array.from(new Uint8Array(signature));
|
|
28
|
+
return signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Generate HMAC signature for a request
|
|
32
|
+
*
|
|
33
|
+
* Signature = HMAC-SHA256(secret, "METHOD|PATH|timestamp|body-hash")
|
|
34
|
+
*/
|
|
35
|
+
async function generateHmacSignature(secret, method, path, timestamp, body) {
|
|
36
|
+
// Calculate body hash for POST/PATCH/PUT methods
|
|
37
|
+
let bodyHash = '';
|
|
38
|
+
const methodUpper = method.toUpperCase();
|
|
39
|
+
if (body &&
|
|
40
|
+
(methodUpper === 'POST' || methodUpper === 'PATCH' || methodUpper === 'PUT')) {
|
|
41
|
+
const bodyString = typeof body === 'string' ? body : JSON.stringify(body);
|
|
42
|
+
bodyHash = await sha256(bodyString);
|
|
43
|
+
}
|
|
44
|
+
// Create signing string: METHOD|PATH|timestamp|body-hash
|
|
45
|
+
const signingString = `${methodUpper}|${path}|${timestamp}|${bodyHash}`;
|
|
46
|
+
// Calculate HMAC-SHA256 signature
|
|
47
|
+
return await hmacSha256(secret, signingString);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* HMAC Authentication Client
|
|
51
|
+
* Provides HTTP client with HMAC-SHA256 request signing
|
|
52
|
+
*/
|
|
53
|
+
class HmacAuthClient {
|
|
54
|
+
constructor(config) {
|
|
55
|
+
this.config = config;
|
|
56
|
+
if (!this.config.serviceId) {
|
|
57
|
+
throw new Error('Service ID is required');
|
|
58
|
+
}
|
|
59
|
+
if (!this.config.secret || this.config.secret.length < 32) {
|
|
60
|
+
throw new Error('Secret must be at least 32 characters long');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Generate Authorization header for a request
|
|
65
|
+
*
|
|
66
|
+
* Format: HMAC-SHA256 service-id:timestamp:signature
|
|
67
|
+
*/
|
|
68
|
+
async getAuthHeader(method, path, body) {
|
|
69
|
+
const timestamp = Date.now();
|
|
70
|
+
const signature = await generateHmacSignature(this.config.secret, method, path, timestamp, body);
|
|
71
|
+
return `HMAC-SHA256 ${this.config.serviceId}:${timestamp}:${signature}`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Extract path with query string from URL
|
|
75
|
+
*/
|
|
76
|
+
extractPath(url) {
|
|
77
|
+
try {
|
|
78
|
+
const urlObj = new URL(url, window.location.origin);
|
|
79
|
+
return urlObj.pathname + urlObj.search;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// If URL parsing fails, assume it's already a path
|
|
83
|
+
return url;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Perform an authenticated fetch request
|
|
88
|
+
*/
|
|
89
|
+
async fetch(url, options = {}) {
|
|
90
|
+
const method = options.method || 'GET';
|
|
91
|
+
const path = this.extractPath(url);
|
|
92
|
+
// Get body for signature calculation
|
|
93
|
+
let body = undefined;
|
|
94
|
+
if (options.body) {
|
|
95
|
+
if (typeof options.body === 'string') {
|
|
96
|
+
body = options.body;
|
|
97
|
+
}
|
|
98
|
+
else if (options.body instanceof FormData) {
|
|
99
|
+
// For FormData, we can't easily sign it, so skip body hash
|
|
100
|
+
body = undefined;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
body = options.body;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const authHeader = await this.getAuthHeader(method, path, body);
|
|
107
|
+
const headers = new Headers(options.headers);
|
|
108
|
+
headers.set('Authorization', authHeader);
|
|
109
|
+
return fetch(url, {
|
|
110
|
+
...options,
|
|
111
|
+
headers,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Prepare request body and headers for methods with body
|
|
116
|
+
*/
|
|
117
|
+
prepareRequestBody(body, headers) {
|
|
118
|
+
if (!body) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
if (typeof body === 'string') {
|
|
122
|
+
return body;
|
|
123
|
+
}
|
|
124
|
+
if (body instanceof FormData) {
|
|
125
|
+
return body;
|
|
126
|
+
}
|
|
127
|
+
if (typeof body === 'object') {
|
|
128
|
+
headers.set('Content-Type', 'application/json');
|
|
129
|
+
return JSON.stringify(body);
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Perform an authenticated GET request
|
|
135
|
+
*/
|
|
136
|
+
async get(url, options = {}) {
|
|
137
|
+
return this.fetch(url, { ...options, method: 'GET' });
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Perform an authenticated POST request
|
|
141
|
+
*/
|
|
142
|
+
async post(url, body, options = {}) {
|
|
143
|
+
const headers = new Headers(options.headers);
|
|
144
|
+
const requestBody = this.prepareRequestBody(body, headers);
|
|
145
|
+
return this.fetch(url, {
|
|
146
|
+
...options,
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers,
|
|
149
|
+
body: requestBody,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Perform an authenticated PUT request
|
|
154
|
+
*/
|
|
155
|
+
async put(url, body, options = {}) {
|
|
156
|
+
const headers = new Headers(options.headers);
|
|
157
|
+
const requestBody = this.prepareRequestBody(body, headers);
|
|
158
|
+
return this.fetch(url, {
|
|
159
|
+
...options,
|
|
160
|
+
method: 'PUT',
|
|
161
|
+
headers,
|
|
162
|
+
body: requestBody,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Perform an authenticated DELETE request
|
|
167
|
+
*/
|
|
168
|
+
async delete(url, options = {}) {
|
|
169
|
+
return this.fetch(url, { ...options, method: 'DELETE' });
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Perform an authenticated PATCH request
|
|
173
|
+
*/
|
|
174
|
+
async patch(url, body, options = {}) {
|
|
175
|
+
const headers = new Headers(options.headers);
|
|
176
|
+
const requestBody = this.prepareRequestBody(body, headers);
|
|
177
|
+
return this.fetch(url, {
|
|
178
|
+
...options,
|
|
179
|
+
method: 'PATCH',
|
|
180
|
+
headers,
|
|
181
|
+
body: requestBody,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Verify the minimum recommended secret length
|
|
187
|
+
*/
|
|
188
|
+
function isSecretSecure(secret) {
|
|
189
|
+
return secret.length >= 32;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Generate a random secure secret for HMAC (for testing/development)
|
|
193
|
+
* In production, secrets should be generated securely on the backend
|
|
194
|
+
*/
|
|
195
|
+
async function generateRandomSecret(length = 64) {
|
|
196
|
+
const array = new Uint8Array(length);
|
|
197
|
+
crypto.getRandomValues(array);
|
|
198
|
+
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export { HmacAuthClient, generateHmacSignature, generateRandomSecret, isSecretSecure };
|
|
202
|
+
//# sourceMappingURL=hmac.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hmac.js","sources":["../../../../../src/browser/shared/utils/hmac.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOTP (Time-based One-Time Password) utility for browser
|
|
3
|
+
* @module browser/shared/utils/totp
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Generate a TOTP code from a Base32 secret
|
|
7
|
+
*/
|
|
8
|
+
async function generateTotp(config) {
|
|
9
|
+
const digits = config.digits ?? 6;
|
|
10
|
+
const step = config.step ?? 30;
|
|
11
|
+
// Get current time counter
|
|
12
|
+
const now = Math.floor(Date.now() / 1000);
|
|
13
|
+
const counter = Math.floor(now / step);
|
|
14
|
+
return await generateTotpAtTime(config.secret, counter, digits);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Generate a TOTP code for a specific counter value
|
|
18
|
+
*/
|
|
19
|
+
async function generateTotpAtTime(secret, counter, digits) {
|
|
20
|
+
// Decode Base32 secret
|
|
21
|
+
const key = base32Decode(secret);
|
|
22
|
+
// Convert counter to 8-byte array (big-endian)
|
|
23
|
+
const counterBytes = new Uint8Array(8);
|
|
24
|
+
const view = new DataView(counterBytes.buffer);
|
|
25
|
+
view.setBigUint64(0, BigInt(counter), false); // false = big-endian
|
|
26
|
+
// Import key for HMAC-SHA1
|
|
27
|
+
const cryptoKey = await crypto.subtle.importKey('raw', key.buffer, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
|
|
28
|
+
// Generate HMAC-SHA1
|
|
29
|
+
const signature = await crypto.subtle.sign('HMAC', cryptoKey, counterBytes);
|
|
30
|
+
const hash = new Uint8Array(signature);
|
|
31
|
+
// Dynamic truncation (RFC 4226)
|
|
32
|
+
const offset = hash[hash.length - 1] & 0x0f;
|
|
33
|
+
const truncatedHash = ((hash[offset] & 0x7f) << 24) |
|
|
34
|
+
((hash[offset + 1] & 0xff) << 16) |
|
|
35
|
+
((hash[offset + 2] & 0xff) << 8) |
|
|
36
|
+
(hash[offset + 3] & 0xff);
|
|
37
|
+
// Generate N-digit code
|
|
38
|
+
const code = truncatedHash % Math.pow(10, digits);
|
|
39
|
+
return code.toString().padStart(digits, '0');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Decode a Base32 string to Uint8Array
|
|
43
|
+
*/
|
|
44
|
+
function base32Decode(input) {
|
|
45
|
+
const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
46
|
+
// Remove trailing padding characters (avoid ReDoS by using simple loop instead of regex)
|
|
47
|
+
let cleanInput = input.toUpperCase();
|
|
48
|
+
while (cleanInput.endsWith('=')) {
|
|
49
|
+
cleanInput = cleanInput.slice(0, -1);
|
|
50
|
+
}
|
|
51
|
+
let bits = 0;
|
|
52
|
+
let value = 0;
|
|
53
|
+
const output = [];
|
|
54
|
+
for (let i = 0; i < cleanInput.length; i++) {
|
|
55
|
+
const idx = base32Chars.indexOf(cleanInput[i]);
|
|
56
|
+
if (idx === -1) {
|
|
57
|
+
throw new Error(`Invalid Base32 character: ${cleanInput[i]}`);
|
|
58
|
+
}
|
|
59
|
+
value = (value << 5) | idx;
|
|
60
|
+
bits += 5;
|
|
61
|
+
if (bits >= 8) {
|
|
62
|
+
output.push((value >>> (bits - 8)) & 0xff);
|
|
63
|
+
bits -= 8;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return new Uint8Array(output);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Validate if a string is valid Base32
|
|
70
|
+
*/
|
|
71
|
+
function isValidBase32(input) {
|
|
72
|
+
const base32Regex = /^[A-Z2-7]+=*$/i;
|
|
73
|
+
return base32Regex.test(input);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get remaining seconds until next TOTP code
|
|
77
|
+
*/
|
|
78
|
+
function getRemainingSeconds(step = 30) {
|
|
79
|
+
const now = Math.floor(Date.now() / 1000);
|
|
80
|
+
return step - (now % step);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* TOTP Authentication Client
|
|
84
|
+
* Provides HTTP client with TOTP authentication header
|
|
85
|
+
*/
|
|
86
|
+
class TotpAuthClient {
|
|
87
|
+
constructor(config) {
|
|
88
|
+
this.config = {
|
|
89
|
+
secret: config.secret,
|
|
90
|
+
digits: config.digits ?? 6,
|
|
91
|
+
step: config.step ?? 30,
|
|
92
|
+
};
|
|
93
|
+
if (!isValidBase32(this.config.secret)) {
|
|
94
|
+
throw new Error('Invalid Base32 secret');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get current TOTP code
|
|
99
|
+
*/
|
|
100
|
+
async getCode() {
|
|
101
|
+
return await generateTotp(this.config);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get Authorization header with Bearer token
|
|
105
|
+
*/
|
|
106
|
+
async getAuthHeader() {
|
|
107
|
+
const code = await this.getCode();
|
|
108
|
+
return `Bearer ${code}`;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Perform an authenticated fetch request
|
|
112
|
+
*/
|
|
113
|
+
async fetch(url, options = {}) {
|
|
114
|
+
const authHeader = await this.getAuthHeader();
|
|
115
|
+
const headers = new Headers(options.headers);
|
|
116
|
+
headers.set('Authorization', authHeader);
|
|
117
|
+
return fetch(url, {
|
|
118
|
+
...options,
|
|
119
|
+
headers,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Perform an authenticated GET request
|
|
124
|
+
*/
|
|
125
|
+
async get(url, options = {}) {
|
|
126
|
+
return this.fetch(url, { ...options, method: 'GET' });
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Perform an authenticated POST request
|
|
130
|
+
*/
|
|
131
|
+
async post(url, body, options = {}) {
|
|
132
|
+
const headers = new Headers(options.headers);
|
|
133
|
+
if (body && typeof body === 'object') {
|
|
134
|
+
headers.set('Content-Type', 'application/json');
|
|
135
|
+
}
|
|
136
|
+
return this.fetch(url, {
|
|
137
|
+
...options,
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers,
|
|
140
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Perform an authenticated PUT request
|
|
145
|
+
*/
|
|
146
|
+
async put(url, body, options = {}) {
|
|
147
|
+
const headers = new Headers(options.headers);
|
|
148
|
+
if (body && typeof body === 'object') {
|
|
149
|
+
headers.set('Content-Type', 'application/json');
|
|
150
|
+
}
|
|
151
|
+
return this.fetch(url, {
|
|
152
|
+
...options,
|
|
153
|
+
method: 'PUT',
|
|
154
|
+
headers,
|
|
155
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Perform an authenticated DELETE request
|
|
160
|
+
*/
|
|
161
|
+
async delete(url, options = {}) {
|
|
162
|
+
return this.fetch(url, { ...options, method: 'DELETE' });
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Perform an authenticated PATCH request
|
|
166
|
+
*/
|
|
167
|
+
async patch(url, body, options = {}) {
|
|
168
|
+
const headers = new Headers(options.headers);
|
|
169
|
+
if (body && typeof body === 'object') {
|
|
170
|
+
headers.set('Content-Type', 'application/json');
|
|
171
|
+
}
|
|
172
|
+
return this.fetch(url, {
|
|
173
|
+
...options,
|
|
174
|
+
method: 'PATCH',
|
|
175
|
+
headers,
|
|
176
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export { TotpAuthClient, generateTotp, getRemainingSeconds, isValidBase32 };
|
|
182
|
+
//# sourceMappingURL=totp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"totp.js","sources":["../../../../../src/browser/shared/utils/totp.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
|
|
@@ -423,6 +423,186 @@ async function loadSchema(schemaSource) {
|
|
|
423
423
|
return schemaSource;
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
+
/**
|
|
427
|
+
* TOTP (Time-based One-Time Password) utility for browser
|
|
428
|
+
* @module browser/shared/utils/totp
|
|
429
|
+
*/
|
|
430
|
+
/**
|
|
431
|
+
* Generate a TOTP code from a Base32 secret
|
|
432
|
+
*/
|
|
433
|
+
async function generateTotp(config) {
|
|
434
|
+
const digits = config.digits ?? 6;
|
|
435
|
+
const step = config.step ?? 30;
|
|
436
|
+
// Get current time counter
|
|
437
|
+
const now = Math.floor(Date.now() / 1000);
|
|
438
|
+
const counter = Math.floor(now / step);
|
|
439
|
+
return await generateTotpAtTime(config.secret, counter, digits);
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Generate a TOTP code for a specific counter value
|
|
443
|
+
*/
|
|
444
|
+
async function generateTotpAtTime(secret, counter, digits) {
|
|
445
|
+
// Decode Base32 secret
|
|
446
|
+
const key = base32Decode(secret);
|
|
447
|
+
// Convert counter to 8-byte array (big-endian)
|
|
448
|
+
const counterBytes = new Uint8Array(8);
|
|
449
|
+
const view = new DataView(counterBytes.buffer);
|
|
450
|
+
view.setBigUint64(0, BigInt(counter), false); // false = big-endian
|
|
451
|
+
// Import key for HMAC-SHA1
|
|
452
|
+
const cryptoKey = await crypto.subtle.importKey('raw', key.buffer, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
|
|
453
|
+
// Generate HMAC-SHA1
|
|
454
|
+
const signature = await crypto.subtle.sign('HMAC', cryptoKey, counterBytes);
|
|
455
|
+
const hash = new Uint8Array(signature);
|
|
456
|
+
// Dynamic truncation (RFC 4226)
|
|
457
|
+
const offset = hash[hash.length - 1] & 0x0f;
|
|
458
|
+
const truncatedHash = ((hash[offset] & 0x7f) << 24) |
|
|
459
|
+
((hash[offset + 1] & 0xff) << 16) |
|
|
460
|
+
((hash[offset + 2] & 0xff) << 8) |
|
|
461
|
+
(hash[offset + 3] & 0xff);
|
|
462
|
+
// Generate N-digit code
|
|
463
|
+
const code = truncatedHash % Math.pow(10, digits);
|
|
464
|
+
return code.toString().padStart(digits, '0');
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Decode a Base32 string to Uint8Array
|
|
468
|
+
*/
|
|
469
|
+
function base32Decode(input) {
|
|
470
|
+
const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
471
|
+
// Remove trailing padding characters (avoid ReDoS by using simple loop instead of regex)
|
|
472
|
+
let cleanInput = input.toUpperCase();
|
|
473
|
+
while (cleanInput.endsWith('=')) {
|
|
474
|
+
cleanInput = cleanInput.slice(0, -1);
|
|
475
|
+
}
|
|
476
|
+
let bits = 0;
|
|
477
|
+
let value = 0;
|
|
478
|
+
const output = [];
|
|
479
|
+
for (let i = 0; i < cleanInput.length; i++) {
|
|
480
|
+
const idx = base32Chars.indexOf(cleanInput[i]);
|
|
481
|
+
if (idx === -1) {
|
|
482
|
+
throw new Error(`Invalid Base32 character: ${cleanInput[i]}`);
|
|
483
|
+
}
|
|
484
|
+
value = (value << 5) | idx;
|
|
485
|
+
bits += 5;
|
|
486
|
+
if (bits >= 8) {
|
|
487
|
+
output.push((value >>> (bits - 8)) & 0xff);
|
|
488
|
+
bits -= 8;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return new Uint8Array(output);
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Validate if a string is valid Base32
|
|
495
|
+
*/
|
|
496
|
+
function isValidBase32(input) {
|
|
497
|
+
const base32Regex = /^[A-Z2-7]+=*$/i;
|
|
498
|
+
return base32Regex.test(input);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Get remaining seconds until next TOTP code
|
|
502
|
+
*/
|
|
503
|
+
function getRemainingSeconds(step = 30) {
|
|
504
|
+
const now = Math.floor(Date.now() / 1000);
|
|
505
|
+
return step - (now % step);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* TOTP Authentication Client
|
|
509
|
+
* Provides HTTP client with TOTP authentication header
|
|
510
|
+
*/
|
|
511
|
+
class TotpAuthClient {
|
|
512
|
+
constructor(config) {
|
|
513
|
+
this.config = {
|
|
514
|
+
secret: config.secret,
|
|
515
|
+
digits: config.digits ?? 6,
|
|
516
|
+
step: config.step ?? 30,
|
|
517
|
+
};
|
|
518
|
+
if (!isValidBase32(this.config.secret)) {
|
|
519
|
+
throw new Error('Invalid Base32 secret');
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Get current TOTP code
|
|
524
|
+
*/
|
|
525
|
+
async getCode() {
|
|
526
|
+
return await generateTotp(this.config);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Get Authorization header with Bearer token
|
|
530
|
+
*/
|
|
531
|
+
async getAuthHeader() {
|
|
532
|
+
const code = await this.getCode();
|
|
533
|
+
return `Bearer ${code}`;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Perform an authenticated fetch request
|
|
537
|
+
*/
|
|
538
|
+
async fetch(url, options = {}) {
|
|
539
|
+
const authHeader = await this.getAuthHeader();
|
|
540
|
+
const headers = new Headers(options.headers);
|
|
541
|
+
headers.set('Authorization', authHeader);
|
|
542
|
+
return fetch(url, {
|
|
543
|
+
...options,
|
|
544
|
+
headers,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Perform an authenticated GET request
|
|
549
|
+
*/
|
|
550
|
+
async get(url, options = {}) {
|
|
551
|
+
return this.fetch(url, { ...options, method: 'GET' });
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Perform an authenticated POST request
|
|
555
|
+
*/
|
|
556
|
+
async post(url, body, options = {}) {
|
|
557
|
+
const headers = new Headers(options.headers);
|
|
558
|
+
if (body && typeof body === 'object') {
|
|
559
|
+
headers.set('Content-Type', 'application/json');
|
|
560
|
+
}
|
|
561
|
+
return this.fetch(url, {
|
|
562
|
+
...options,
|
|
563
|
+
method: 'POST',
|
|
564
|
+
headers,
|
|
565
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Perform an authenticated PUT request
|
|
570
|
+
*/
|
|
571
|
+
async put(url, body, options = {}) {
|
|
572
|
+
const headers = new Headers(options.headers);
|
|
573
|
+
if (body && typeof body === 'object') {
|
|
574
|
+
headers.set('Content-Type', 'application/json');
|
|
575
|
+
}
|
|
576
|
+
return this.fetch(url, {
|
|
577
|
+
...options,
|
|
578
|
+
method: 'PUT',
|
|
579
|
+
headers,
|
|
580
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Perform an authenticated DELETE request
|
|
585
|
+
*/
|
|
586
|
+
async delete(url, options = {}) {
|
|
587
|
+
return this.fetch(url, { ...options, method: 'DELETE' });
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Perform an authenticated PATCH request
|
|
591
|
+
*/
|
|
592
|
+
async patch(url, body, options = {}) {
|
|
593
|
+
const headers = new Headers(options.headers);
|
|
594
|
+
if (body && typeof body === 'object') {
|
|
595
|
+
headers.set('Content-Type', 'application/json');
|
|
596
|
+
}
|
|
597
|
+
return this.fetch(url, {
|
|
598
|
+
...options,
|
|
599
|
+
method: 'PATCH',
|
|
600
|
+
headers,
|
|
601
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
426
606
|
/**
|
|
427
607
|
* Reusable Modal component
|
|
428
608
|
* @module browser/shared/components/Modal
|
|
@@ -690,5 +870,5 @@ const THEMES = {
|
|
|
690
870
|
},
|
|
691
871
|
};
|
|
692
872
|
|
|
693
|
-
export { Modal, StatusMessage, THEMES, clearPointerOptionsCache, collectPointerArrayValues, createElement, escapeHtml, generateFormField, getElementByIdSafe, getFieldLabel, getOrganizationPath, groupSchemaFields, loadPointerOptions, loadSchema, processFormData, querySelectorAllSafe, querySelectorSafe, replacePlaceholders, replaceSchemaPlaceholders, setInnerHTML, showStatus, toTitleCase };
|
|
873
|
+
export { Modal, StatusMessage, THEMES, TotpAuthClient, clearPointerOptionsCache, collectPointerArrayValues, createElement, escapeHtml, generateFormField, generateTotp, getElementByIdSafe, getFieldLabel, getOrganizationPath, getRemainingSeconds, groupSchemaFields, isValidBase32, loadPointerOptions, loadSchema, processFormData, querySelectorAllSafe, querySelectorSafe, replacePlaceholders, replaceSchemaPlaceholders, setInnerHTML, showStatus, toTitleCase };
|
|
694
874
|
//# sourceMappingURL=shared.esm.js.map
|