@dloizides/auth-client 3.2.1 → 3.3.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/CHANGELOG.md +38 -0
- package/README.md +38 -1
- package/dist/{AuthClient-D8Ul-aGa.d.mts → AuthClient-3lu6Y1bY.d.mts} +1 -1
- package/dist/{AuthClient-Cv7btBX0.d.ts → AuthClient-Bb7N2shJ.d.ts} +1 -1
- package/dist/{TokenResponse-CY1CaU2l.d.mts → TokenResponse-BkIDjenX.d.mts} +7 -0
- package/dist/{TokenResponse-CY1CaU2l.d.ts → TokenResponse-BkIDjenX.d.ts} +7 -0
- package/dist/index.d.mts +149 -5
- package/dist/index.d.ts +149 -5
- package/dist/index.js +198 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +198 -6
- package/dist/index.mjs.map +1 -1
- package/dist/oidc/index.d.mts +1 -1
- package/dist/oidc/index.d.ts +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/package.json +124 -124
package/dist/index.mjs
CHANGED
|
@@ -942,7 +942,8 @@ function createFetchHttpClient(fetchImpl) {
|
|
|
942
942
|
return {
|
|
943
943
|
status: response.status,
|
|
944
944
|
ok: response.ok,
|
|
945
|
-
data
|
|
945
|
+
data,
|
|
946
|
+
header: (name) => response.headers.get(name)
|
|
946
947
|
};
|
|
947
948
|
};
|
|
948
949
|
}
|
|
@@ -1070,8 +1071,20 @@ var ENDPOINTS = {
|
|
|
1070
1071
|
resetPassword: "/bff/reset-password",
|
|
1071
1072
|
otpRequest: "/bff/otp/request",
|
|
1072
1073
|
otpVerify: "/bff/otp/verify",
|
|
1073
|
-
pinLogin: "/bff/pin/login"
|
|
1074
|
+
pinLogin: "/bff/pin/login",
|
|
1075
|
+
config: "/bff/config",
|
|
1076
|
+
pinEnroll: "/bff/pin/enroll",
|
|
1077
|
+
pinUnlock: "/bff/pin/unlock",
|
|
1078
|
+
pinDisable: "/bff/pin/disable"
|
|
1074
1079
|
};
|
|
1080
|
+
var HTTP_OK = 200;
|
|
1081
|
+
var HTTP_MULTIPLE_CHOICES = 300;
|
|
1082
|
+
var HTTP_BAD_REQUEST = 400;
|
|
1083
|
+
var HTTP_UNAUTHORIZED = 401;
|
|
1084
|
+
var HTTP_FORBIDDEN = 403;
|
|
1085
|
+
var HTTP_TOO_MANY_REQUESTS = 429;
|
|
1086
|
+
var FALLBACK_METHODS = ["password"];
|
|
1087
|
+
var ALLOWED_PIN_DIGITS = [4, 6, 8];
|
|
1075
1088
|
function isRecord(value) {
|
|
1076
1089
|
return typeof value === "object" && value !== null;
|
|
1077
1090
|
}
|
|
@@ -1092,6 +1105,81 @@ function toOtpRequestResult(data) {
|
|
|
1092
1105
|
code: typeof data.code === "string" ? data.code : null
|
|
1093
1106
|
};
|
|
1094
1107
|
}
|
|
1108
|
+
var FALLBACK_LOGIN_CONFIG = {
|
|
1109
|
+
methods: [...FALLBACK_METHODS],
|
|
1110
|
+
registrationEnabled: false,
|
|
1111
|
+
deviceState: {
|
|
1112
|
+
rememberedUsername: null,
|
|
1113
|
+
hasPin: false,
|
|
1114
|
+
pinDigits: null,
|
|
1115
|
+
preferredMethod: null
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
function readOptionalString(record, key) {
|
|
1119
|
+
const value = record[key];
|
|
1120
|
+
if (typeof value !== "string" || value === "") {
|
|
1121
|
+
return null;
|
|
1122
|
+
}
|
|
1123
|
+
return value;
|
|
1124
|
+
}
|
|
1125
|
+
function parseDeviceState(body) {
|
|
1126
|
+
const rawDigits = body.pinDigits;
|
|
1127
|
+
const hasValidDigits = typeof rawDigits === "number" && ALLOWED_PIN_DIGITS.includes(rawDigits);
|
|
1128
|
+
return {
|
|
1129
|
+
rememberedUsername: readOptionalString(body, "rememberedUsername"),
|
|
1130
|
+
hasPin: body.hasPin === true,
|
|
1131
|
+
pinDigits: hasValidDigits ? rawDigits : null,
|
|
1132
|
+
preferredMethod: readOptionalString(body, "preferredMethod")
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
function parseMethods(body) {
|
|
1136
|
+
const raw = body.methods;
|
|
1137
|
+
if (!Array.isArray(raw)) {
|
|
1138
|
+
return [...FALLBACK_METHODS];
|
|
1139
|
+
}
|
|
1140
|
+
const parsed = raw.filter((value) => typeof value === "string" && value !== "").map((value) => value.toLowerCase());
|
|
1141
|
+
return parsed.length === 0 ? [...FALLBACK_METHODS] : Array.from(new Set(parsed));
|
|
1142
|
+
}
|
|
1143
|
+
function parseLoginConfig(data) {
|
|
1144
|
+
if (!isRecord(data)) {
|
|
1145
|
+
return FALLBACK_LOGIN_CONFIG;
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
methods: parseMethods(data),
|
|
1149
|
+
registrationEnabled: data.registrationEnabled === true,
|
|
1150
|
+
deviceState: parseDeviceState(data)
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
function parseRetryAfter(response) {
|
|
1154
|
+
const raw = response.header?.("Retry-After");
|
|
1155
|
+
if (raw === null || raw === void 0) {
|
|
1156
|
+
return null;
|
|
1157
|
+
}
|
|
1158
|
+
const seconds = Number.parseInt(raw, 10);
|
|
1159
|
+
if (Number.isNaN(seconds) || seconds < 0) {
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
return seconds;
|
|
1163
|
+
}
|
|
1164
|
+
function classifyTooManyRequests(response) {
|
|
1165
|
+
const retryAfterSeconds = parseRetryAfter(response);
|
|
1166
|
+
if (isRecord(response.data)) {
|
|
1167
|
+
return { status: "locked", retryAfterSeconds };
|
|
1168
|
+
}
|
|
1169
|
+
return { status: "rateLimited", retryAfterSeconds };
|
|
1170
|
+
}
|
|
1171
|
+
function classifyEnrollFailure(status) {
|
|
1172
|
+
if (status === HTTP_UNAUTHORIZED) {
|
|
1173
|
+
return { status: "unauthorized" };
|
|
1174
|
+
}
|
|
1175
|
+
if (status === HTTP_FORBIDDEN) {
|
|
1176
|
+
return { status: "forbidden" };
|
|
1177
|
+
}
|
|
1178
|
+
if (status === HTTP_BAD_REQUEST) {
|
|
1179
|
+
return { status: "invalidPin" };
|
|
1180
|
+
}
|
|
1181
|
+
return { status: "error" };
|
|
1182
|
+
}
|
|
1095
1183
|
var BffAuthClient = class {
|
|
1096
1184
|
constructor(options) {
|
|
1097
1185
|
this.http = options.http;
|
|
@@ -1207,22 +1295,126 @@ var BffAuthClient = class {
|
|
|
1207
1295
|
return user;
|
|
1208
1296
|
}
|
|
1209
1297
|
/**
|
|
1210
|
-
*
|
|
1211
|
-
*
|
|
1298
|
+
* `GET /bff/config` — which login methods this BFF advertises, whether
|
|
1299
|
+
* registration is enabled, and the optional per-device state (remembered
|
|
1300
|
+
* username + device PIN). Unauthenticated, no CSRF (GET).
|
|
1301
|
+
*
|
|
1302
|
+
* NEVER throws: on a non-2xx, a network error, or a malformed body it returns
|
|
1303
|
+
* a safe fallback (`{ methods: ['password'], registrationEnabled: false,
|
|
1304
|
+
* deviceState: { rememberedUsername: null, hasPin: false, pinDigits: null,
|
|
1305
|
+
* preferredMethod: null } }`) so the login surface stays usable even when the
|
|
1306
|
+
* config endpoint is unreachable.
|
|
1212
1307
|
*/
|
|
1213
|
-
async
|
|
1308
|
+
async getLoginConfig() {
|
|
1309
|
+
try {
|
|
1310
|
+
const response = await this.http({
|
|
1311
|
+
url: `${this.baseUrl}${ENDPOINTS.config}`,
|
|
1312
|
+
method: "GET",
|
|
1313
|
+
headers: { Accept: JSON_CONTENT_TYPE },
|
|
1314
|
+
credentials: "include"
|
|
1315
|
+
});
|
|
1316
|
+
if (!response.ok) {
|
|
1317
|
+
return FALLBACK_LOGIN_CONFIG;
|
|
1318
|
+
}
|
|
1319
|
+
return parseLoginConfig(response.data);
|
|
1320
|
+
} catch {
|
|
1321
|
+
return FALLBACK_LOGIN_CONFIG;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* `POST /bff/pin/enroll` — bind a device PIN to the CURRENT strong session.
|
|
1326
|
+
* Requires an authenticated session (the cookie travels via
|
|
1327
|
+
* `credentials: 'include'`); the BFF requests an offline token, hashes the
|
|
1328
|
+
* PIN, and sets the device cookie.
|
|
1329
|
+
*
|
|
1330
|
+
* NEVER throws — resolves a discriminated {@link DevicePinEnrollResult}:
|
|
1331
|
+
* - 200 → `success`;
|
|
1332
|
+
* - 401 → `unauthorized` (no session);
|
|
1333
|
+
* - 403 → `forbidden` (a PIN-established session can't enrol);
|
|
1334
|
+
* - 400 → `invalidPin` (bad format);
|
|
1335
|
+
* - anything else (501 disabled / 502 grant failed) / network → `error`.
|
|
1336
|
+
*/
|
|
1337
|
+
async enrollDevicePin(request) {
|
|
1338
|
+
try {
|
|
1339
|
+
const response = await this.postRaw(ENDPOINTS.pinEnroll, request);
|
|
1340
|
+
if (response.ok) {
|
|
1341
|
+
return { status: "success" };
|
|
1342
|
+
}
|
|
1343
|
+
return classifyEnrollFailure(response.status);
|
|
1344
|
+
} catch {
|
|
1345
|
+
return { status: "error" };
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* `POST /bff/pin/unlock` — re-establish a session from a remembered device.
|
|
1350
|
+
* No prior session; the device cookie travels via `credentials: 'include'`.
|
|
1351
|
+
*
|
|
1352
|
+
* NEVER throws — resolves a discriminated {@link DevicePinUnlockResult}:
|
|
1353
|
+
* - 200 + `{ user }` → `success` (a session cookie was set);
|
|
1354
|
+
* - 401 → `invalid` (wrong PIN / unknown-or-revoked device);
|
|
1355
|
+
* - 429 + JSON body → `locked` (device lockout; `Retry-After` parsed);
|
|
1356
|
+
* - 429 + empty body → `rateLimited` (per-IP limiter; `Retry-After` parsed);
|
|
1357
|
+
* - anything else / malformed 200 body / network → `error`.
|
|
1358
|
+
*/
|
|
1359
|
+
async unlockWithDevicePin(request) {
|
|
1360
|
+
try {
|
|
1361
|
+
const response = await this.postRaw(ENDPOINTS.pinUnlock, request);
|
|
1362
|
+
const isSuccess = response.status >= HTTP_OK && response.status < HTTP_MULTIPLE_CHOICES;
|
|
1363
|
+
if (isSuccess) {
|
|
1364
|
+
const user = extractUser(response.data);
|
|
1365
|
+
return user === null ? { status: "error" } : { status: "success", user };
|
|
1366
|
+
}
|
|
1367
|
+
if (response.status === HTTP_UNAUTHORIZED) {
|
|
1368
|
+
return { status: "invalid" };
|
|
1369
|
+
}
|
|
1370
|
+
if (response.status === HTTP_TOO_MANY_REQUESTS) {
|
|
1371
|
+
return classifyTooManyRequests(response);
|
|
1372
|
+
}
|
|
1373
|
+
return { status: "error" };
|
|
1374
|
+
} catch {
|
|
1375
|
+
return { status: "error" };
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* `POST /bff/pin/disable` — drop the device PIN for the CURRENT session. The
|
|
1380
|
+
* BFF revokes the offline token at Keycloak, deletes the device record, and
|
|
1381
|
+
* clears the device cookie. Requires an authenticated session.
|
|
1382
|
+
*
|
|
1383
|
+
* NEVER throws — resolves `true` on a 2xx, `false` on anything else.
|
|
1384
|
+
*/
|
|
1385
|
+
async disableDevicePin() {
|
|
1386
|
+
try {
|
|
1387
|
+
const response = await this.postRaw(ENDPOINTS.pinDisable, void 0);
|
|
1388
|
+
return response.ok;
|
|
1389
|
+
} catch {
|
|
1390
|
+
return false;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Shared POST for the never-throw device-PIN calls: same-origin, cookie
|
|
1395
|
+
* included, `X-BFF-Csrf` header attached. Returns the raw {@link HttpResponse}
|
|
1396
|
+
* (status + body + headers) so the caller can route on it instead of throwing.
|
|
1397
|
+
*/
|
|
1398
|
+
postRaw(path, body) {
|
|
1214
1399
|
const headers = {
|
|
1215
1400
|
"Content-Type": JSON_CONTENT_TYPE,
|
|
1216
1401
|
Accept: JSON_CONTENT_TYPE,
|
|
1217
1402
|
[CSRF_HEADER]: CSRF_HEADER_VALUE
|
|
1218
1403
|
};
|
|
1219
|
-
|
|
1404
|
+
return this.http({
|
|
1220
1405
|
url: `${this.baseUrl}${path}`,
|
|
1221
1406
|
method: "POST",
|
|
1222
1407
|
headers,
|
|
1223
1408
|
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
1224
1409
|
credentials: "include"
|
|
1225
1410
|
});
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Shared POST for every state-changing `/bff/*` call: same-origin, cookie
|
|
1414
|
+
* included, `X-BFF-Csrf` header attached. Throws a labelled error on non-2xx.
|
|
1415
|
+
*/
|
|
1416
|
+
async postState(path, body, label) {
|
|
1417
|
+
const response = await this.postRaw(path, body);
|
|
1226
1418
|
if (!response.ok) {
|
|
1227
1419
|
throw new Error(`${label} failed with status ${String(response.status)}`);
|
|
1228
1420
|
}
|