@multiplatform.one/keycloak 7.90.6 → 8.1.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/README.md +0 -2
- package/dist/cjs/index.cjs +11 -9
- package/dist/esm/index.mjs +11 -9
- package/package.json +10 -16
- package/src/betterAuth/accountSelection.spec.ts +22 -10
- package/src/frappeToken.ts +9 -14
- package/types/frappeToken.d.ts.map +1 -1
- package/dist/cjs/one.cjs +0 -82
- package/dist/esm/one.mjs +0 -61
- package/src/one.expoCallback.spec.ts +0 -107
- package/src/one.ts +0 -111
- package/types/one.d.ts +0 -35
- package/types/one.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -27,8 +27,6 @@ Peers: `react`, `@multiplatform.one/logger`, `@multiplatform.one/platform`.
|
|
|
27
27
|
- **Token helpers** — `isTokenExpired`, `validOrRefreshableToken`,
|
|
28
28
|
`getKeycloakBearerToken`, wallet-claim readers (`getWalletsFromToken`,
|
|
29
29
|
`getWalletAddresses`, `hasWallets`)
|
|
30
|
-
- **`@multiplatform.one/keycloak/one`** — `createAuth()` Better Auth server
|
|
31
|
-
factory (stateless JWE sessions, Keycloak OAuth, Expo support)
|
|
32
30
|
|
|
33
31
|
## What it must not do
|
|
34
32
|
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -365,6 +365,15 @@ function getAuthClient(baseURL) {
|
|
|
365
365
|
return _client;
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
// ../auth/src/accessTokenRefresh.ts
|
|
369
|
+
var FAILED_TO_GET_ACCESS_TOKEN = "FAILED_TO_GET_ACCESS_TOKEN";
|
|
370
|
+
var ACCESS_TOKEN_RETRY_DELAYS_MS = [0, 400, 1e3];
|
|
371
|
+
function classifyAccessTokenResponse(status, body) {
|
|
372
|
+
if (status === 401 || status === 403) return "signed-out";
|
|
373
|
+
if (status === 400 && body?.code === FAILED_TO_GET_ACCESS_TOKEN) return "signed-out";
|
|
374
|
+
return "transient";
|
|
375
|
+
}
|
|
376
|
+
|
|
368
377
|
// src/betterAuth/accountSelection.ts
|
|
369
378
|
var validationErrorCode = "VALIDATION_ERROR";
|
|
370
379
|
var known;
|
|
@@ -399,13 +408,6 @@ var signedOutUntil = 0;
|
|
|
399
408
|
var unavailableUntil = 0;
|
|
400
409
|
var SIGNED_OUT_COOLDOWN_MS = 15e3;
|
|
401
410
|
var TRANSIENT_COOLDOWN_MS = 3e3;
|
|
402
|
-
var RETRY_DELAYS_MS = [0, 400, 1e3];
|
|
403
|
-
var FAILED_TO_GET_ACCESS_TOKEN = "FAILED_TO_GET_ACCESS_TOKEN";
|
|
404
|
-
function classifyAccessTokenResponse(status, body) {
|
|
405
|
-
if (status === 401 || status === 403) return "signed-out";
|
|
406
|
-
if (status === 400 && body?.code === FAILED_TO_GET_ACCESS_TOKEN) return "signed-out";
|
|
407
|
-
return "transient";
|
|
408
|
-
}
|
|
409
411
|
function notifySessionInvalidated() {
|
|
410
412
|
try {
|
|
411
413
|
const client = getAuthClient();
|
|
@@ -458,8 +460,8 @@ async function fetchTokenOnce() {
|
|
|
458
460
|
}
|
|
459
461
|
async function fetchToken() {
|
|
460
462
|
let last = "transient";
|
|
461
|
-
for (let i = 0; i <
|
|
462
|
-
const wait =
|
|
463
|
+
for (let i = 0; i < ACCESS_TOKEN_RETRY_DELAYS_MS.length; i++) {
|
|
464
|
+
const wait = ACCESS_TOKEN_RETRY_DELAYS_MS[i];
|
|
463
465
|
if (wait) await sleep(wait);
|
|
464
466
|
const result = await fetchTokenOnce();
|
|
465
467
|
if (result.ok) {
|
package/dist/esm/index.mjs
CHANGED
|
@@ -311,6 +311,15 @@ function getAuthClient(baseURL) {
|
|
|
311
311
|
return _client;
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
// ../auth/src/accessTokenRefresh.ts
|
|
315
|
+
var FAILED_TO_GET_ACCESS_TOKEN = "FAILED_TO_GET_ACCESS_TOKEN";
|
|
316
|
+
var ACCESS_TOKEN_RETRY_DELAYS_MS = [0, 400, 1e3];
|
|
317
|
+
function classifyAccessTokenResponse(status, body) {
|
|
318
|
+
if (status === 401 || status === 403) return "signed-out";
|
|
319
|
+
if (status === 400 && body?.code === FAILED_TO_GET_ACCESS_TOKEN) return "signed-out";
|
|
320
|
+
return "transient";
|
|
321
|
+
}
|
|
322
|
+
|
|
314
323
|
// src/betterAuth/accountSelection.ts
|
|
315
324
|
var validationErrorCode = "VALIDATION_ERROR";
|
|
316
325
|
var known;
|
|
@@ -345,13 +354,6 @@ var signedOutUntil = 0;
|
|
|
345
354
|
var unavailableUntil = 0;
|
|
346
355
|
var SIGNED_OUT_COOLDOWN_MS = 15e3;
|
|
347
356
|
var TRANSIENT_COOLDOWN_MS = 3e3;
|
|
348
|
-
var RETRY_DELAYS_MS = [0, 400, 1e3];
|
|
349
|
-
var FAILED_TO_GET_ACCESS_TOKEN = "FAILED_TO_GET_ACCESS_TOKEN";
|
|
350
|
-
function classifyAccessTokenResponse(status, body) {
|
|
351
|
-
if (status === 401 || status === 403) return "signed-out";
|
|
352
|
-
if (status === 400 && body?.code === FAILED_TO_GET_ACCESS_TOKEN) return "signed-out";
|
|
353
|
-
return "transient";
|
|
354
|
-
}
|
|
355
357
|
function notifySessionInvalidated() {
|
|
356
358
|
try {
|
|
357
359
|
const client = getAuthClient();
|
|
@@ -404,8 +406,8 @@ async function fetchTokenOnce() {
|
|
|
404
406
|
}
|
|
405
407
|
async function fetchToken() {
|
|
406
408
|
let last = "transient";
|
|
407
|
-
for (let i = 0; i <
|
|
408
|
-
const wait =
|
|
409
|
+
for (let i = 0; i < ACCESS_TOKEN_RETRY_DELAYS_MS.length; i++) {
|
|
410
|
+
const wait = ACCESS_TOKEN_RETRY_DELAYS_MS[i];
|
|
409
411
|
if (wait) await sleep(wait);
|
|
410
412
|
const result = await fetchTokenOnce();
|
|
411
413
|
if (result.ok) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/keycloak",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.1.0",
|
|
4
4
|
"description": "keycloak client for multiplatform.one ecosystem",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|
|
@@ -42,13 +42,6 @@
|
|
|
42
42
|
"require": "./dist/cjs/index.cjs",
|
|
43
43
|
"default": "./dist/esm/index.mjs"
|
|
44
44
|
},
|
|
45
|
-
"./one": {
|
|
46
|
-
"source": "./src/one.ts",
|
|
47
|
-
"types": "./types/one.d.ts",
|
|
48
|
-
"import": "./dist/esm/one.mjs",
|
|
49
|
-
"require": "./dist/cjs/one.cjs",
|
|
50
|
-
"default": "./dist/esm/one.mjs"
|
|
51
|
-
},
|
|
52
45
|
"./oauth": {
|
|
53
46
|
"source": "./src/oauth/index.ts",
|
|
54
47
|
"types": "./types/oauth/index.d.ts",
|
|
@@ -62,16 +55,17 @@
|
|
|
62
55
|
},
|
|
63
56
|
"dependencies": {
|
|
64
57
|
"@better-auth/expo": "~1.6.33",
|
|
65
|
-
"@multiplatform.one/
|
|
66
|
-
"@multiplatform.one/
|
|
58
|
+
"@multiplatform.one/auth": "^8.1.0",
|
|
59
|
+
"@multiplatform.one/keycloak-js": "^8.1.0",
|
|
60
|
+
"@multiplatform.one/store": "^8.1.0",
|
|
67
61
|
"better-auth": "~1.6.33",
|
|
68
62
|
"jwt-decode": "^4.0.0"
|
|
69
63
|
},
|
|
70
64
|
"devDependencies": {
|
|
71
|
-
"@multiplatform.one/config": "
|
|
72
|
-
"@multiplatform.one/logger": "
|
|
73
|
-
"@multiplatform.one/platform": "
|
|
74
|
-
"@multiplatform.one/test-utils": "
|
|
65
|
+
"@multiplatform.one/config": "8.1.0",
|
|
66
|
+
"@multiplatform.one/logger": "8.1.0",
|
|
67
|
+
"@multiplatform.one/platform": "8.1.0",
|
|
68
|
+
"@multiplatform.one/test-utils": "8.1.0",
|
|
75
69
|
"@tamagui/build": "2.7.6",
|
|
76
70
|
"@testing-library/jest-dom": "^7.0.1",
|
|
77
71
|
"@testing-library/react": "^16.3.3",
|
|
@@ -85,8 +79,8 @@
|
|
|
85
79
|
"vitest": "^4.1.12"
|
|
86
80
|
},
|
|
87
81
|
"peerDependencies": {
|
|
88
|
-
"@multiplatform.one/logger": "^
|
|
89
|
-
"@multiplatform.one/platform": "^
|
|
82
|
+
"@multiplatform.one/logger": "^8.1.0",
|
|
83
|
+
"@multiplatform.one/platform": "^8.1.0",
|
|
90
84
|
"react": "^19.1.0",
|
|
91
85
|
"react-native": ">=0.83.0"
|
|
92
86
|
},
|
|
@@ -8,15 +8,16 @@
|
|
|
8
8
|
* token fetch then 400'd at validation, the Frappe layer fell back to guest
|
|
9
9
|
* cookies, and a signed-in user was told to sign in.
|
|
10
10
|
*
|
|
11
|
-
* The last case drives the REAL Better Auth router
|
|
11
|
+
* The last case drives the REAL Better Auth router,
|
|
12
12
|
* so the shape is checked against the installed version rather than against a
|
|
13
13
|
* hand-copied schema. A 400 VALIDATION_ERROR fails; 401 is the pass, because
|
|
14
14
|
* the request is unauthenticated by design — the point is that the body
|
|
15
15
|
* reached the auth check at all.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { betterAuth } from "better-auth";
|
|
19
|
+
import { genericOAuth, keycloak } from "better-auth/plugins";
|
|
18
20
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
19
|
-
import { createAuth } from "../one";
|
|
20
21
|
import {
|
|
21
22
|
currentSelection,
|
|
22
23
|
isBodyRefusal,
|
|
@@ -99,16 +100,27 @@ describe("requestAccessToken negotiation", () => {
|
|
|
99
100
|
});
|
|
100
101
|
|
|
101
102
|
describe("against the installed Better Auth router", () => {
|
|
102
|
-
const auth =
|
|
103
|
-
|
|
103
|
+
const auth = betterAuth({
|
|
104
|
+
basePath: "/api/auth",
|
|
105
|
+
baseURL: baseUrl,
|
|
104
106
|
secret: "test-secret-not-a-real-one-0123456789",
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
clientId: "test-client",
|
|
110
|
-
clientSecret: "",
|
|
107
|
+
trustedOrigins: [baseUrl],
|
|
108
|
+
advanced: { disableOriginCheck: false },
|
|
109
|
+
session: {
|
|
110
|
+
cookieCache: { enabled: true, maxAge: 60, strategy: "jwe", refreshCache: true },
|
|
111
111
|
},
|
|
112
|
+
account: { storeStateStrategy: "cookie", storeAccountCookie: true },
|
|
113
|
+
plugins: [
|
|
114
|
+
genericOAuth({
|
|
115
|
+
config: [
|
|
116
|
+
keycloak({
|
|
117
|
+
clientId: "test-client",
|
|
118
|
+
clientSecret: "",
|
|
119
|
+
issuer: "https://auth.example.test/realms/test",
|
|
120
|
+
}),
|
|
121
|
+
],
|
|
122
|
+
}),
|
|
123
|
+
],
|
|
112
124
|
});
|
|
113
125
|
|
|
114
126
|
it("negotiates a body this server validates", async () => {
|
package/src/frappeToken.ts
CHANGED
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
* backoff, keep the session. A Keycloak restart must not look like logout.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import {
|
|
23
|
+
ACCESS_TOKEN_RETRY_DELAYS_MS,
|
|
24
|
+
classifyAccessTokenResponse,
|
|
25
|
+
type AccessTokenFailureKind,
|
|
26
|
+
} from "@multiplatform.one/auth/access-token";
|
|
22
27
|
import { requestAccessToken } from "./betterAuth/accountSelection";
|
|
23
28
|
import { getAuthClient } from "./betterAuth/client";
|
|
24
29
|
|
|
@@ -32,16 +37,6 @@ let unavailableUntil = 0;
|
|
|
32
37
|
|
|
33
38
|
const SIGNED_OUT_COOLDOWN_MS = 15_000;
|
|
34
39
|
const TRANSIENT_COOLDOWN_MS = 3_000;
|
|
35
|
-
const RETRY_DELAYS_MS = [0, 400, 1000] as const;
|
|
36
|
-
const FAILED_TO_GET_ACCESS_TOKEN = "FAILED_TO_GET_ACCESS_TOKEN";
|
|
37
|
-
|
|
38
|
-
type FailureKind = "signed-out" | "transient";
|
|
39
|
-
|
|
40
|
-
function classifyAccessTokenResponse(status: number, body?: { code?: string } | null): FailureKind {
|
|
41
|
-
if (status === 401 || status === 403) return "signed-out";
|
|
42
|
-
if (status === 400 && body?.code === FAILED_TO_GET_ACCESS_TOKEN) return "signed-out";
|
|
43
|
-
return "transient";
|
|
44
|
-
}
|
|
45
40
|
|
|
46
41
|
/**
|
|
47
42
|
* A definitive refresh failure (Keycloak declared the refresh token dead —
|
|
@@ -90,7 +85,7 @@ export async function getKeycloakBearerToken(): Promise<string | undefined> {
|
|
|
90
85
|
}
|
|
91
86
|
|
|
92
87
|
async function fetchTokenOnce(): Promise<
|
|
93
|
-
{ ok: true; token: string; expiresAt: number } | { ok: false; kind:
|
|
88
|
+
{ ok: true; token: string; expiresAt: number } | { ok: false; kind: AccessTokenFailureKind }
|
|
94
89
|
> {
|
|
95
90
|
try {
|
|
96
91
|
let payload: unknown = null;
|
|
@@ -127,9 +122,9 @@ async function fetchTokenOnce(): Promise<
|
|
|
127
122
|
}
|
|
128
123
|
|
|
129
124
|
async function fetchToken(): Promise<string | undefined> {
|
|
130
|
-
let last:
|
|
131
|
-
for (let i = 0; i <
|
|
132
|
-
const wait =
|
|
125
|
+
let last: AccessTokenFailureKind = "transient";
|
|
126
|
+
for (let i = 0; i < ACCESS_TOKEN_RETRY_DELAYS_MS.length; i++) {
|
|
127
|
+
const wait = ACCESS_TOKEN_RETRY_DELAYS_MS[i];
|
|
133
128
|
if (wait) await sleep(wait);
|
|
134
129
|
const result = await fetchTokenOnce();
|
|
135
130
|
if (result.ok) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frappeToken.d.ts","sourceRoot":"","sources":["../src/frappeToken.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;
|
|
1
|
+
{"version":3,"file":"frappeToken.d.ts","sourceRoot":"","sources":["../src/frappeToken.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AA4CH;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAW1E;AAgED,wDAAwD;AACxD,wBAAgB,wBAAwB,SAKvC;AAED;;;;GAIG;AACH,wBAAgB,kCAAkC,IAAI,MAAM,IAAI,CAiB/D"}
|
package/dist/cjs/one.cjs
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/one.ts
|
|
21
|
-
var one_exports = {};
|
|
22
|
-
__export(one_exports, {
|
|
23
|
-
createAuth: () => createAuth
|
|
24
|
-
});
|
|
25
|
-
module.exports = __toCommonJS(one_exports);
|
|
26
|
-
var import_better_auth = require("better-auth");
|
|
27
|
-
var import_plugins = require("better-auth/plugins");
|
|
28
|
-
var import_expo = require("@better-auth/expo");
|
|
29
|
-
var _auth;
|
|
30
|
-
function createAuth(config) {
|
|
31
|
-
if (_auth) return _auth;
|
|
32
|
-
const baseUrl = config.baseUrl || (typeof process !== "undefined" ? process.env?.BASE_URL || `http://localhost:${process.env?.ONE_PORT || "3000"}` : "http://localhost:3000");
|
|
33
|
-
const keycloakBaseUrl = config.keycloak?.baseUrl || `${baseUrl}/auth`;
|
|
34
|
-
const realm = config.keycloak?.realm || "master";
|
|
35
|
-
const issuer = `${keycloakBaseUrl}/realms/${realm}`;
|
|
36
|
-
const envTrustedOrigins = (typeof process !== "undefined" && process.env?.KEYCLOAK_TRUSTED_ORIGINS || "").split(",").map((origin) => origin.trim()).filter(Boolean);
|
|
37
|
-
_auth = (0, import_better_auth.betterAuth)({
|
|
38
|
-
basePath: "/api/auth",
|
|
39
|
-
baseURL: baseUrl,
|
|
40
|
-
secret: config.secret || "",
|
|
41
|
-
trustedOrigins: [
|
|
42
|
-
.../* @__PURE__ */ new Set([
|
|
43
|
-
baseUrl,
|
|
44
|
-
keycloakBaseUrl,
|
|
45
|
-
...config.expoScheme ? [`${config.expoScheme}://`] : [],
|
|
46
|
-
...envTrustedOrigins,
|
|
47
|
-
...config.trustedOrigins || []
|
|
48
|
-
])
|
|
49
|
-
],
|
|
50
|
-
advanced: {
|
|
51
|
-
// better-auth skips origin checks entirely when NODE_ENV=test; pin the
|
|
52
|
-
// check on so the trustedOrigins contract is enforced (and testable)
|
|
53
|
-
// regardless of environment.
|
|
54
|
-
disableOriginCheck: false
|
|
55
|
-
},
|
|
56
|
-
session: {
|
|
57
|
-
cookieCache: {
|
|
58
|
-
enabled: true,
|
|
59
|
-
maxAge: 7 * 24 * 60 * 60,
|
|
60
|
-
strategy: "jwe",
|
|
61
|
-
refreshCache: true
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
account: {
|
|
65
|
-
storeStateStrategy: "cookie",
|
|
66
|
-
storeAccountCookie: true
|
|
67
|
-
},
|
|
68
|
-
plugins: [
|
|
69
|
-
(0, import_expo.expo)(),
|
|
70
|
-
(0, import_plugins.genericOAuth)({
|
|
71
|
-
config: [
|
|
72
|
-
(0, import_plugins.keycloak)({
|
|
73
|
-
clientId: config.keycloak.clientId || "",
|
|
74
|
-
clientSecret: config.keycloak.clientSecret || "",
|
|
75
|
-
issuer
|
|
76
|
-
})
|
|
77
|
-
]
|
|
78
|
-
})
|
|
79
|
-
]
|
|
80
|
-
});
|
|
81
|
-
return _auth;
|
|
82
|
-
}
|
package/dist/esm/one.mjs
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
// src/one.ts
|
|
2
|
-
import { betterAuth } from "better-auth";
|
|
3
|
-
import { genericOAuth, keycloak } from "better-auth/plugins";
|
|
4
|
-
import { expo } from "@better-auth/expo";
|
|
5
|
-
var _auth;
|
|
6
|
-
function createAuth(config) {
|
|
7
|
-
if (_auth) return _auth;
|
|
8
|
-
const baseUrl = config.baseUrl || (typeof process !== "undefined" ? process.env?.BASE_URL || `http://localhost:${process.env?.ONE_PORT || "3000"}` : "http://localhost:3000");
|
|
9
|
-
const keycloakBaseUrl = config.keycloak?.baseUrl || `${baseUrl}/auth`;
|
|
10
|
-
const realm = config.keycloak?.realm || "master";
|
|
11
|
-
const issuer = `${keycloakBaseUrl}/realms/${realm}`;
|
|
12
|
-
const envTrustedOrigins = (typeof process !== "undefined" && process.env?.KEYCLOAK_TRUSTED_ORIGINS || "").split(",").map((origin) => origin.trim()).filter(Boolean);
|
|
13
|
-
_auth = betterAuth({
|
|
14
|
-
basePath: "/api/auth",
|
|
15
|
-
baseURL: baseUrl,
|
|
16
|
-
secret: config.secret || "",
|
|
17
|
-
trustedOrigins: [
|
|
18
|
-
.../* @__PURE__ */ new Set([
|
|
19
|
-
baseUrl,
|
|
20
|
-
keycloakBaseUrl,
|
|
21
|
-
...config.expoScheme ? [`${config.expoScheme}://`] : [],
|
|
22
|
-
...envTrustedOrigins,
|
|
23
|
-
...config.trustedOrigins || []
|
|
24
|
-
])
|
|
25
|
-
],
|
|
26
|
-
advanced: {
|
|
27
|
-
// better-auth skips origin checks entirely when NODE_ENV=test; pin the
|
|
28
|
-
// check on so the trustedOrigins contract is enforced (and testable)
|
|
29
|
-
// regardless of environment.
|
|
30
|
-
disableOriginCheck: false
|
|
31
|
-
},
|
|
32
|
-
session: {
|
|
33
|
-
cookieCache: {
|
|
34
|
-
enabled: true,
|
|
35
|
-
maxAge: 7 * 24 * 60 * 60,
|
|
36
|
-
strategy: "jwe",
|
|
37
|
-
refreshCache: true
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
account: {
|
|
41
|
-
storeStateStrategy: "cookie",
|
|
42
|
-
storeAccountCookie: true
|
|
43
|
-
},
|
|
44
|
-
plugins: [
|
|
45
|
-
expo(),
|
|
46
|
-
genericOAuth({
|
|
47
|
-
config: [
|
|
48
|
-
keycloak({
|
|
49
|
-
clientId: config.keycloak.clientId || "",
|
|
50
|
-
clientSecret: config.keycloak.clientSecret || "",
|
|
51
|
-
issuer
|
|
52
|
-
})
|
|
53
|
-
]
|
|
54
|
-
})
|
|
55
|
-
]
|
|
56
|
-
});
|
|
57
|
-
return _auth;
|
|
58
|
-
}
|
|
59
|
-
export {
|
|
60
|
-
createAuth
|
|
61
|
-
};
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
// jsdom, not the package's happy-dom: happy-dom's Request and Response drop
|
|
3
|
-
// Cookie and Set-Cookie (forbidden headers in a browser), and the cookies are
|
|
4
|
-
// the point here. jsdom leaves Node's own fetch classes in place.
|
|
5
|
-
/**
|
|
6
|
-
* FZ-8696: native sign-in through createAuth must come back signed in.
|
|
7
|
-
*
|
|
8
|
-
* createAuth wires Keycloak through Better Auth's genericOAuth, whose callback
|
|
9
|
-
* is /oauth2/callback/keycloak. @better-auth/expo hands the session to the
|
|
10
|
-
* app by appending ?cookie=<set-cookie> to the app-scheme redirect, but only
|
|
11
|
-
* on the callback paths its after-hook matches. Under better-auth 1.6.30 that
|
|
12
|
-
* was /callback* alone, so the genericOAuth callback came back as a bare
|
|
13
|
-
* <scheme>:/// and the app stayed signed out with no error (Lookout FZ-8618).
|
|
14
|
-
*
|
|
15
|
-
* This drives the real sign-in and callback against a stubbed Keycloak, under
|
|
16
|
-
* whatever better-auth version MPO pins, and checks the session rides the
|
|
17
|
-
* redirect.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
21
|
-
import { createAuth } from "./one";
|
|
22
|
-
|
|
23
|
-
const appUrl = "http://app.test";
|
|
24
|
-
const keycloakUrl = "http://kc.test/auth";
|
|
25
|
-
const realm = "mpo";
|
|
26
|
-
const issuer = `${keycloakUrl}/realms/${realm}`;
|
|
27
|
-
const scheme = "mpo-test";
|
|
28
|
-
|
|
29
|
-
function stubKeycloak() {
|
|
30
|
-
const realFetch = globalThis.fetch;
|
|
31
|
-
vi.stubGlobal(
|
|
32
|
-
"fetch",
|
|
33
|
-
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
34
|
-
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
35
|
-
const json = (body: unknown) =>
|
|
36
|
-
new Response(JSON.stringify(body), { headers: { "content-type": "application/json" } });
|
|
37
|
-
if (url === `${issuer}/.well-known/openid-configuration`) {
|
|
38
|
-
return json({
|
|
39
|
-
issuer,
|
|
40
|
-
authorization_endpoint: `${issuer}/protocol/openid-connect/auth`,
|
|
41
|
-
token_endpoint: `${issuer}/protocol/openid-connect/token`,
|
|
42
|
-
userinfo_endpoint: `${issuer}/protocol/openid-connect/userinfo`,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
if (url === `${issuer}/protocol/openid-connect/token`) {
|
|
46
|
-
return json({ access_token: "access", token_type: "Bearer", expires_in: 300 });
|
|
47
|
-
}
|
|
48
|
-
if (url === `${issuer}/protocol/openid-connect/userinfo`) {
|
|
49
|
-
return json({
|
|
50
|
-
sub: "user-1",
|
|
51
|
-
email: "ada@example.test",
|
|
52
|
-
name: "Ada",
|
|
53
|
-
email_verified: true,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return realFetch(input, init);
|
|
57
|
-
}),
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
beforeEach(stubKeycloak);
|
|
62
|
-
afterEach(() => {
|
|
63
|
-
vi.unstubAllGlobals();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe("createAuth native sign-in (FZ-8696)", () => {
|
|
67
|
-
it("hands the session back on the genericOAuth callback's app-scheme redirect", async () => {
|
|
68
|
-
const auth = createAuth({
|
|
69
|
-
baseUrl: appUrl,
|
|
70
|
-
secret: "a-test-secret-that-is-long-enough-32",
|
|
71
|
-
expoScheme: scheme,
|
|
72
|
-
keycloak: { baseUrl: keycloakUrl, realm, clientId: "app", clientSecret: "secret" },
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const signIn = await auth.handler(
|
|
76
|
-
new Request(`${appUrl}/api/auth/sign-in/oauth2`, {
|
|
77
|
-
method: "POST",
|
|
78
|
-
headers: { "content-type": "application/json", "expo-origin": `${scheme}://` },
|
|
79
|
-
body: JSON.stringify({
|
|
80
|
-
providerId: "keycloak",
|
|
81
|
-
callbackURL: `${scheme}://`,
|
|
82
|
-
disableRedirect: true,
|
|
83
|
-
}),
|
|
84
|
-
}),
|
|
85
|
-
);
|
|
86
|
-
expect(signIn.status).toBe(200);
|
|
87
|
-
const { url } = (await signIn.json()) as { url: string };
|
|
88
|
-
const state = new URL(url).searchParams.get("state");
|
|
89
|
-
expect(state).toBeTruthy();
|
|
90
|
-
const cookies = signIn.headers
|
|
91
|
-
.getSetCookie()
|
|
92
|
-
.map((cookie) => cookie.split(";")[0])
|
|
93
|
-
.join("; ");
|
|
94
|
-
expect(cookies).toMatch(/oauth_state=/);
|
|
95
|
-
|
|
96
|
-
const callback = await auth.handler(
|
|
97
|
-
new Request(`${appUrl}/api/auth/oauth2/callback/keycloak?code=abc&state=${state}`, {
|
|
98
|
-
headers: { cookie: cookies },
|
|
99
|
-
}),
|
|
100
|
-
);
|
|
101
|
-
expect(callback.status).toBe(302);
|
|
102
|
-
const location = new URL(callback.headers.get("location") ?? "");
|
|
103
|
-
expect(location.protocol).toBe(`${scheme}:`);
|
|
104
|
-
const handedBack = location.searchParams.get("cookie") ?? "";
|
|
105
|
-
expect(handedBack, `redirect was ${location.href}`).toMatch(/better-auth\.session_token=/);
|
|
106
|
-
});
|
|
107
|
-
});
|
package/src/one.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File: /src/one.ts
|
|
3
|
-
* Project: @multiplatform.one/keycloak
|
|
4
|
-
*
|
|
5
|
-
* Better Auth server configuration factory for apps using One.
|
|
6
|
-
* Replaces the previous Auth.js (createAuthConfig) approach.
|
|
7
|
-
* Stateless sessions — no database required.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { betterAuth } from "better-auth";
|
|
11
|
-
import { genericOAuth, keycloak } from "better-auth/plugins";
|
|
12
|
-
import { expo } from "@better-auth/expo";
|
|
13
|
-
|
|
14
|
-
let _auth: ReturnType<typeof betterAuth<any>> | undefined;
|
|
15
|
-
|
|
16
|
-
export function createAuth(config: AuthHandlerConfig) {
|
|
17
|
-
if (_auth) return _auth;
|
|
18
|
-
const baseUrl =
|
|
19
|
-
config.baseUrl ||
|
|
20
|
-
(typeof process !== "undefined"
|
|
21
|
-
? process.env?.BASE_URL || `http://localhost:${process.env?.ONE_PORT || "3000"}`
|
|
22
|
-
: "http://localhost:3000");
|
|
23
|
-
const keycloakBaseUrl = config.keycloak?.baseUrl || `${baseUrl}/auth`;
|
|
24
|
-
const realm = config.keycloak?.realm || "master";
|
|
25
|
-
const issuer = `${keycloakBaseUrl}/realms/${realm}`;
|
|
26
|
-
// Extra origins (e.g. LAN devices hitting the server by IP) come from the
|
|
27
|
-
// KEYCLOAK_TRUSTED_ORIGINS env var (comma-separated) and/or the
|
|
28
|
-
// trustedOrigins option; both extend the always-trusted defaults.
|
|
29
|
-
const envTrustedOrigins = (
|
|
30
|
-
(typeof process !== "undefined" && process.env?.KEYCLOAK_TRUSTED_ORIGINS) ||
|
|
31
|
-
""
|
|
32
|
-
)
|
|
33
|
-
.split(",")
|
|
34
|
-
.map((origin) => origin.trim())
|
|
35
|
-
.filter(Boolean);
|
|
36
|
-
|
|
37
|
-
_auth = betterAuth({
|
|
38
|
-
basePath: "/api/auth",
|
|
39
|
-
baseURL: baseUrl,
|
|
40
|
-
secret: config.secret || "",
|
|
41
|
-
trustedOrigins: [
|
|
42
|
-
...new Set([
|
|
43
|
-
baseUrl,
|
|
44
|
-
keycloakBaseUrl,
|
|
45
|
-
...(config.expoScheme ? [`${config.expoScheme}://`] : []),
|
|
46
|
-
...envTrustedOrigins,
|
|
47
|
-
...(config.trustedOrigins || []),
|
|
48
|
-
]),
|
|
49
|
-
],
|
|
50
|
-
advanced: {
|
|
51
|
-
// better-auth skips origin checks entirely when NODE_ENV=test; pin the
|
|
52
|
-
// check on so the trustedOrigins contract is enforced (and testable)
|
|
53
|
-
// regardless of environment.
|
|
54
|
-
disableOriginCheck: false,
|
|
55
|
-
},
|
|
56
|
-
session: {
|
|
57
|
-
cookieCache: {
|
|
58
|
-
enabled: true,
|
|
59
|
-
maxAge: 7 * 24 * 60 * 60,
|
|
60
|
-
strategy: "jwe",
|
|
61
|
-
refreshCache: true,
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
account: {
|
|
65
|
-
storeStateStrategy: "cookie",
|
|
66
|
-
storeAccountCookie: true,
|
|
67
|
-
},
|
|
68
|
-
plugins: [
|
|
69
|
-
expo(),
|
|
70
|
-
genericOAuth({
|
|
71
|
-
config: [
|
|
72
|
-
keycloak({
|
|
73
|
-
clientId: config.keycloak.clientId || "",
|
|
74
|
-
clientSecret: config.keycloak.clientSecret || "",
|
|
75
|
-
issuer,
|
|
76
|
-
}),
|
|
77
|
-
],
|
|
78
|
-
}),
|
|
79
|
-
],
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
return _auth;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export interface KeycloakOptions {
|
|
86
|
-
adminPassword?: string;
|
|
87
|
-
adminUsername?: string;
|
|
88
|
-
baseUrl?: string;
|
|
89
|
-
clientId: string;
|
|
90
|
-
clientSecret: string;
|
|
91
|
-
realm?: string;
|
|
92
|
-
authorization?: Record<string, any>;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export interface AuthHandlerConfig {
|
|
96
|
-
baseUrl?: string;
|
|
97
|
-
keycloak: KeycloakOptions;
|
|
98
|
-
secret?: string;
|
|
99
|
-
scopes?: string[];
|
|
100
|
-
/** Expo deep-link scheme (e.g. "multiplatform-one") — added to trustedOrigins. */
|
|
101
|
-
expoScheme?: string;
|
|
102
|
-
/**
|
|
103
|
-
* Additional origins allowed to hit the auth route (e.g. LAN device
|
|
104
|
-
* origins). Extends — never replaces — the defaults (baseUrl, keycloak
|
|
105
|
-
* URL, expo scheme). Also readable from the KEYCLOAK_TRUSTED_ORIGINS env
|
|
106
|
-
* var as a comma-separated list.
|
|
107
|
-
*/
|
|
108
|
-
trustedOrigins?: string[];
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export type Auth = ReturnType<typeof createAuth>;
|
package/types/one.d.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File: /src/one.ts
|
|
3
|
-
* Project: @multiplatform.one/keycloak
|
|
4
|
-
*
|
|
5
|
-
* Better Auth server configuration factory for apps using One.
|
|
6
|
-
* Replaces the previous Auth.js (createAuthConfig) approach.
|
|
7
|
-
* Stateless sessions — no database required.
|
|
8
|
-
*/
|
|
9
|
-
export declare function createAuth(config: AuthHandlerConfig): import("better-auth").Auth<any>;
|
|
10
|
-
export interface KeycloakOptions {
|
|
11
|
-
adminPassword?: string;
|
|
12
|
-
adminUsername?: string;
|
|
13
|
-
baseUrl?: string;
|
|
14
|
-
clientId: string;
|
|
15
|
-
clientSecret: string;
|
|
16
|
-
realm?: string;
|
|
17
|
-
authorization?: Record<string, any>;
|
|
18
|
-
}
|
|
19
|
-
export interface AuthHandlerConfig {
|
|
20
|
-
baseUrl?: string;
|
|
21
|
-
keycloak: KeycloakOptions;
|
|
22
|
-
secret?: string;
|
|
23
|
-
scopes?: string[];
|
|
24
|
-
/** Expo deep-link scheme (e.g. "multiplatform-one") — added to trustedOrigins. */
|
|
25
|
-
expoScheme?: string;
|
|
26
|
-
/**
|
|
27
|
-
* Additional origins allowed to hit the auth route (e.g. LAN device
|
|
28
|
-
* origins). Extends — never replaces — the defaults (baseUrl, keycloak
|
|
29
|
-
* URL, expo scheme). Also readable from the KEYCLOAK_TRUSTED_ORIGINS env
|
|
30
|
-
* var as a comma-separated list.
|
|
31
|
-
*/
|
|
32
|
-
trustedOrigins?: string[];
|
|
33
|
-
}
|
|
34
|
-
export type Auth = ReturnType<typeof createAuth>;
|
|
35
|
-
//# sourceMappingURL=one.d.ts.map
|
package/types/one.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"one.d.ts","sourceRoot":"","sources":["../src/one.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,wBAAgB,UAAU,CAAC,MAAM,EAAE,iBAAiB,mCAmEnD;AAED,MAAM,WAAW,eAAe;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,eAAe,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC"}
|