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