@soulcode/bexio-oauth 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +66 -2
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/bin.js.map +1 -0
- package/dist/claims.d.ts +31 -0
- package/dist/claims.d.ts.map +1 -0
- package/dist/claims.js +58 -0
- package/dist/claims.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +44 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +24 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +57 -0
- package/dist/config.js.map +1 -0
- package/dist/connect-flow.d.ts +15 -0
- package/dist/connect-flow.d.ts.map +1 -0
- package/dist/connect-flow.js +123 -0
- package/dist/connect-flow.js.map +1 -0
- package/dist/crypto.d.ts +27 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +106 -0
- package/dist/crypto.js.map +1 -0
- package/dist/duration.d.ts +4 -0
- package/dist/duration.d.ts.map +1 -0
- package/dist/duration.js +15 -0
- package/dist/duration.js.map +1 -0
- package/dist/errors.d.ts +38 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -0
- package/dist/external-tokens.d.ts +8 -0
- package/dist/external-tokens.d.ts.map +1 -0
- package/dist/external-tokens.js +73 -0
- package/dist/external-tokens.js.map +1 -0
- package/dist/flow-cookie.d.ts +23 -0
- package/dist/flow-cookie.d.ts.map +1 -0
- package/dist/flow-cookie.js +44 -0
- package/dist/flow-cookie.js.map +1 -0
- package/dist/grants.d.ts +36 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +115 -0
- package/dist/grants.js.map +1 -0
- package/dist/idle-sweep.d.ts +11 -0
- package/dist/idle-sweep.d.ts.map +1 -0
- package/dist/idle-sweep.js +70 -0
- package/dist/idle-sweep.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations.generated.d.ts +3 -0
- package/dist/migrations.generated.d.ts.map +1 -0
- package/dist/migrations.generated.js +40 -0
- package/dist/migrations.generated.js.map +1 -0
- package/dist/oidc.d.ts +30 -0
- package/dist/oidc.d.ts.map +1 -0
- package/dist/oidc.js +67 -0
- package/dist/oidc.js.map +1 -0
- package/dist/permissions.d.ts +9 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +29 -0
- package/dist/permissions.js.map +1 -0
- package/dist/refresh.d.ts +30 -0
- package/dist/refresh.d.ts.map +1 -0
- package/dist/refresh.js +139 -0
- package/dist/refresh.js.map +1 -0
- package/dist/return-to.d.ts +8 -0
- package/dist/return-to.d.ts.map +1 -0
- package/dist/return-to.js +31 -0
- package/dist/return-to.js.map +1 -0
- package/dist/sql.d.ts +14 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +11 -0
- package/dist/sql.js.map +1 -0
- package/dist/store/memory.d.ts +7 -0
- package/dist/store/memory.d.ts.map +1 -0
- package/dist/store/memory.js +104 -0
- package/dist/store/memory.js.map +1 -0
- package/dist/store/postgres.d.ts +17 -0
- package/dist/store/postgres.d.ts.map +1 -0
- package/dist/store/postgres.js +177 -0
- package/dist/store/postgres.js.map +1 -0
- package/dist/store/types.d.ts +105 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/types.js +2 -0
- package/dist/store/types.js.map +1 -0
- package/dist/types.d.ts +150 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -4
- package/sql/0001_bexio_grants.sql +32 -0
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createHash, hkdfSync, randomBytes } from "node:crypto";
|
|
2
|
+
import { CompactEncrypt, compactDecrypt, EncryptJWT, jwtDecrypt } from "jose";
|
|
3
|
+
import { BexioOAuthError } from "./errors.js";
|
|
4
|
+
const JWE_ALGORITHMS = {
|
|
5
|
+
keyManagementAlgorithms: ["dir"],
|
|
6
|
+
contentEncryptionAlgorithms: ["A256GCM"],
|
|
7
|
+
};
|
|
8
|
+
/** 32 bytes as unpadded base64url: exactly 43 characters of the base64url alphabet. */
|
|
9
|
+
const KEY_FORMAT = /^[A-Za-z0-9_-]{43}$/;
|
|
10
|
+
/** A new random key for the keyring, base64url-encoded. */
|
|
11
|
+
export function generateKey() {
|
|
12
|
+
return randomBytes(32).toString("base64url");
|
|
13
|
+
}
|
|
14
|
+
function hash(value) {
|
|
15
|
+
return createHash("sha256").update(value).digest("base64url");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Encrypts tokens at rest as compact JWE (`dir` + `A256GCM`) and seals the
|
|
19
|
+
* short-lived flow cookie. The `bxg` protected header binds a token to the
|
|
20
|
+
* grant it belongs to, so ciphertext copied to another row fails to decrypt.
|
|
21
|
+
*/
|
|
22
|
+
export class TokenCipher {
|
|
23
|
+
#current;
|
|
24
|
+
#keys;
|
|
25
|
+
#flowKeys = new Map();
|
|
26
|
+
constructor(keyring) {
|
|
27
|
+
if (!keyring || typeof keyring !== "object" || !keyring.keys) {
|
|
28
|
+
throw new BexioOAuthError("invalid_config", "keys must be { current, keys }");
|
|
29
|
+
}
|
|
30
|
+
this.#keys = new Map();
|
|
31
|
+
for (const [kid, encoded] of Object.entries(keyring.keys)) {
|
|
32
|
+
// Strict: Buffer.from(…, "base64url") silently drops characters it
|
|
33
|
+
// doesn't know, so a mangled key could still "decode" to 32 bytes and
|
|
34
|
+
// then differ between environments.
|
|
35
|
+
if (typeof encoded !== "string" || !KEY_FORMAT.test(encoded)) {
|
|
36
|
+
throw new BexioOAuthError("invalid_config", `key "${kid}" must be 32 bytes, base64url-encoded without padding (43 characters)`);
|
|
37
|
+
}
|
|
38
|
+
this.#keys.set(kid, new Uint8Array(Buffer.from(encoded, "base64url")));
|
|
39
|
+
}
|
|
40
|
+
if (!this.#keys.has(keyring.current)) {
|
|
41
|
+
throw new BexioOAuthError("invalid_config", `keys.current "${keyring.current}" is not in keys`);
|
|
42
|
+
}
|
|
43
|
+
this.#current = keyring.current;
|
|
44
|
+
}
|
|
45
|
+
async encrypt(plaintext, binding) {
|
|
46
|
+
return new CompactEncrypt(new TextEncoder().encode(plaintext))
|
|
47
|
+
.setProtectedHeader({
|
|
48
|
+
alg: "dir",
|
|
49
|
+
enc: "A256GCM",
|
|
50
|
+
kid: this.#current,
|
|
51
|
+
bxg: hash(binding),
|
|
52
|
+
})
|
|
53
|
+
.encrypt(this.#key(this.#current).key);
|
|
54
|
+
}
|
|
55
|
+
async decrypt(jwe, binding) {
|
|
56
|
+
try {
|
|
57
|
+
const { plaintext, protectedHeader } = await compactDecrypt(jwe, (header) => this.#key(header.kid).key, JWE_ALGORITHMS);
|
|
58
|
+
if (protectedHeader.bxg !== hash(binding)) {
|
|
59
|
+
throw new Error("binding mismatch");
|
|
60
|
+
}
|
|
61
|
+
return new TextDecoder().decode(plaintext);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new BexioOAuthError("decryption_failed", "stored token could not be decrypted (unknown key id, or it belongs to another grant)");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async seal(payload, ttlSeconds, now) {
|
|
68
|
+
const nowSeconds = Math.floor(now.getTime() / 1000);
|
|
69
|
+
return new EncryptJWT(payload)
|
|
70
|
+
.setProtectedHeader({ alg: "dir", enc: "A256GCM", kid: this.#current })
|
|
71
|
+
.setIssuedAt(nowSeconds)
|
|
72
|
+
.setExpirationTime(nowSeconds + ttlSeconds)
|
|
73
|
+
.encrypt(this.#flowKey(this.#current));
|
|
74
|
+
}
|
|
75
|
+
/** Returns the payload, or `null` when the value is invalid or expired. */
|
|
76
|
+
async unseal(token, now) {
|
|
77
|
+
try {
|
|
78
|
+
const { payload } = await jwtDecrypt(token, (header) => this.#flowKey(header.kid), {
|
|
79
|
+
...JWE_ALGORITHMS,
|
|
80
|
+
currentDate: now,
|
|
81
|
+
});
|
|
82
|
+
return payload;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** The key for `kid`, with `kid` narrowed to a known key id. */
|
|
89
|
+
#key(kid) {
|
|
90
|
+
const key = kid === undefined ? undefined : this.#keys.get(kid);
|
|
91
|
+
if (kid === undefined || !key)
|
|
92
|
+
throw new Error("unknown key id");
|
|
93
|
+
return { kid, key };
|
|
94
|
+
}
|
|
95
|
+
// The flow cookie uses a key derived from the keyring, never the token key itself.
|
|
96
|
+
#flowKey(requested) {
|
|
97
|
+
const { kid, key: base } = this.#key(requested);
|
|
98
|
+
let derived = this.#flowKeys.get(kid);
|
|
99
|
+
if (!derived) {
|
|
100
|
+
derived = new Uint8Array(hkdfSync("sha256", base, new Uint8Array(0), "bexio-oauth/flow-cookie", 32));
|
|
101
|
+
this.#flowKeys.set(kid, derived);
|
|
102
|
+
}
|
|
103
|
+
return derived;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duration.d.ts","sourceRoot":"","sources":["../src/duration.ts"],"names":[],"mappings":"AAEA,wEAAwE;AACxE,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,GAAG,MAAM,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,EAAE,CAAC;AAIpE,wBAAgB,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAY1D"}
|
package/dist/duration.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BexioOAuthError } from "./errors.js";
|
|
2
|
+
const UNIT_MS = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 };
|
|
3
|
+
export function toMs(value, name) {
|
|
4
|
+
if (typeof value === "number") {
|
|
5
|
+
if (Number.isFinite(value) && value > 0)
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
const match = /^(\d+(?:\.\d+)?)([smhd])$/.exec(value);
|
|
10
|
+
if (match)
|
|
11
|
+
return Number(match[1]) * UNIT_MS[match[2]];
|
|
12
|
+
}
|
|
13
|
+
throw new BexioOAuthError("invalid_config", `${name} must be a positive number of milliseconds or a string like "7d"`);
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=duration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duration.js","sourceRoot":"","sources":["../src/duration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAK9C,MAAM,OAAO,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAW,CAAC;AAE9E,MAAM,UAAU,IAAI,CAAC,KAAe,EAAE,IAAY;IAChD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;IACxD,CAAC;SAAM,CAAC;QACN,MAAM,KAAK,GAAG,2BAA2B,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACtD,IAAI,KAAK;YACP,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAyB,CAAC,CAAC;IACxE,CAAC;IACD,MAAM,IAAI,eAAe,CACvB,gBAAgB,EAChB,GAAG,IAAI,kEAAkE,CAC1E,CAAC;AACJ,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every failure the library reports is a `BexioOAuthError` with a stable
|
|
3
|
+
* `code`. Messages never contain token values; `cause`, when set, is a safe
|
|
4
|
+
* summary of the upstream error (name, OAuth error code, HTTP status).
|
|
5
|
+
*/
|
|
6
|
+
export type BexioOAuthErrorCode =
|
|
7
|
+
/** Flow cookie missing, expired, or `state` does not match. */
|
|
8
|
+
"state_mismatch"
|
|
9
|
+
/** The `ownerId` at callback differs from the one passed to `start`. */
|
|
10
|
+
| "owner_mismatch"
|
|
11
|
+
/** The user cancelled at Bexio. */
|
|
12
|
+
| "access_denied"
|
|
13
|
+
/** Bad code, nonce or token signature, wrong issuer, or missing claims. */
|
|
14
|
+
| "invalid_callback"
|
|
15
|
+
/** Bexio returned an unexpected error or was unreachable. */
|
|
16
|
+
| "idp_error"
|
|
17
|
+
/** The owner has no grant (for that company / grant id). */
|
|
18
|
+
| "not_connected"
|
|
19
|
+
/** The owner has several grants; pass `companyId` or `grantId`. */
|
|
20
|
+
| "ambiguous_grant"
|
|
21
|
+
/** The refresh token is dead; the user has to reconnect. */
|
|
22
|
+
| "reauth_required"
|
|
23
|
+
/** A stored token can't be decrypted (unknown key id, or it belongs to another grant). */
|
|
24
|
+
| "decryption_failed"
|
|
25
|
+
/** The library was configured incorrectly. */
|
|
26
|
+
| "invalid_config";
|
|
27
|
+
export declare class BexioOAuthError extends Error {
|
|
28
|
+
readonly code: BexioOAuthErrorCode;
|
|
29
|
+
constructor(code: BexioOAuthErrorCode, message: string, options?: {
|
|
30
|
+
cause?: {
|
|
31
|
+
name: string;
|
|
32
|
+
error?: string;
|
|
33
|
+
status?: number;
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export declare function isBexioOAuthError(value: unknown, code?: BexioOAuthErrorCode): value is BexioOAuthError;
|
|
38
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,mBAAmB;AAC7B,+DAA+D;AAC7D,gBAAgB;AAClB,wEAAwE;GACtE,gBAAgB;AAClB,mCAAmC;GACjC,eAAe;AACjB,2EAA2E;GACzE,kBAAkB;AACpB,6DAA6D;GAC3D,WAAW;AACb,4DAA4D;GAC1D,eAAe;AACjB,mEAAmE;GACjE,iBAAiB;AACnB,4DAA4D;GAC1D,iBAAiB;AACnB,0FAA0F;GACxF,mBAAmB;AACrB,8CAA8C;GAC5C,gBAAgB,CAAC;AAErB,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;gBAGjC,IAAI,EAAE,mBAAmB,EACzB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE;CAM1E;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,EACd,IAAI,CAAC,EAAE,mBAAmB,GACzB,KAAK,IAAI,eAAe,CAK1B"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class BexioOAuthError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(code, message, options) {
|
|
4
|
+
super(message, options);
|
|
5
|
+
this.name = "BexioOAuthError";
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function isBexioOAuthError(value, code) {
|
|
10
|
+
return (value instanceof BexioOAuthError &&
|
|
11
|
+
(code === undefined || value.code === code));
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AA2BA,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAC/B,IAAI,CAAsB;IAEnC,YACE,IAAyB,EACzB,OAAe,EACf,OAAuE;QAEvE,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;QAC9B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,MAAM,UAAU,iBAAiB,CAC/B,KAAc,EACd,IAA0B;IAE1B,OAAO,CACL,KAAK,YAAY,eAAe;QAChC,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAC5C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ClientContext } from "./config.js";
|
|
2
|
+
import type { ExternalTokens, Grant } from "./types.js";
|
|
3
|
+
/** `BexioOAuth.saveGrant`: verifies tokens from your auth framework, then stores them. */
|
|
4
|
+
export declare function saveGrant(ctx: ClientContext, input: {
|
|
5
|
+
ownerId: string;
|
|
6
|
+
tokens: ExternalTokens;
|
|
7
|
+
}): Promise<Grant>;
|
|
8
|
+
//# sourceMappingURL=external-tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external-tokens.d.ts","sourceRoot":"","sources":["../src/external-tokens.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAIjD,OAAO,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AA+CxD,0FAA0F;AAC1F,wBAAsB,SAAS,CAC7B,GAAG,EAAE,aAAa,EAClB,KAAK,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,cAAc,CAAA;CAAE,GACjD,OAAO,CAAC,KAAK,CAAC,CAuDhB"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { errors as joseErrors, jwtVerify } from "jose";
|
|
2
|
+
import { BexioOAuthError } from "./errors.js";
|
|
3
|
+
import { saveTokens, toGrant, withUserInfo } from "./grants.js";
|
|
4
|
+
import { describe } from "./oidc.js";
|
|
5
|
+
const SIGNING_ALGORITHMS = ["RS256", "PS256", "ES256"];
|
|
6
|
+
/**
|
|
7
|
+
* Verifies a token's signature against Bexio's keys, plus issuer, expiry and
|
|
8
|
+
* (when given) audience. Not getting the keys is `idp_error`; a token that
|
|
9
|
+
* fails a check is `invalid_callback`.
|
|
10
|
+
*/
|
|
11
|
+
async function verifyBexioJwt(ctx, token, issuer, audience) {
|
|
12
|
+
const keys = await ctx.oidc.keySet();
|
|
13
|
+
try {
|
|
14
|
+
const { payload } = await jwtVerify(token, keys, {
|
|
15
|
+
issuer,
|
|
16
|
+
audience,
|
|
17
|
+
algorithms: SIGNING_ALGORITHMS,
|
|
18
|
+
currentDate: ctx.clock(),
|
|
19
|
+
});
|
|
20
|
+
return payload;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
// Not getting Bexio's keys is an outage, not a bad token.
|
|
24
|
+
// (jose reports a bad JWKS response, e.g. a 503, as its generic error.)
|
|
25
|
+
if (error instanceof joseErrors.JWKSTimeout ||
|
|
26
|
+
error instanceof TypeError ||
|
|
27
|
+
(error instanceof joseErrors.JOSEError &&
|
|
28
|
+
error.code === "ERR_JOSE_GENERIC")) {
|
|
29
|
+
throw new BexioOAuthError("idp_error", "could not load Bexio's signing keys", { cause: describe(error) });
|
|
30
|
+
}
|
|
31
|
+
throw new BexioOAuthError("invalid_callback", "token signature, issuer or expiry check failed", { cause: describe(error) });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** `BexioOAuth.saveGrant`: verifies tokens from your auth framework, then stores them. */
|
|
35
|
+
export async function saveGrant(ctx, input) {
|
|
36
|
+
if (!input?.ownerId)
|
|
37
|
+
throw new BexioOAuthError("invalid_config", "ownerId is required");
|
|
38
|
+
const { tokens } = input;
|
|
39
|
+
if (!tokens?.accessToken)
|
|
40
|
+
throw new BexioOAuthError("invalid_config", "tokens.accessToken is required");
|
|
41
|
+
const issuer = (await ctx.oidc.config()).serverMetadata().issuer;
|
|
42
|
+
const accessClaims = await verifyBexioJwt(ctx, tokens.accessToken, issuer);
|
|
43
|
+
// Must be an access token issued to this app: rejects tokens issued to
|
|
44
|
+
// another client (token substitution) and ID tokens passed as access tokens.
|
|
45
|
+
if (accessClaims.azp !== ctx.clientId) {
|
|
46
|
+
throw new BexioOAuthError("invalid_callback", "access token was not issued to this client");
|
|
47
|
+
}
|
|
48
|
+
if (accessClaims.typ !== undefined && accessClaims.typ !== "Bearer") {
|
|
49
|
+
throw new BexioOAuthError("invalid_callback", "tokens.accessToken is not an access token");
|
|
50
|
+
}
|
|
51
|
+
const idClaims = tokens.idToken
|
|
52
|
+
? await verifyBexioJwt(ctx, tokens.idToken, issuer, ctx.clientId)
|
|
53
|
+
: {};
|
|
54
|
+
if (tokens.idToken && idClaims.sub !== accessClaims.sub) {
|
|
55
|
+
throw new BexioOAuthError("invalid_callback", "ID token and access token belong to different users");
|
|
56
|
+
}
|
|
57
|
+
const claims = await withUserInfo(ctx, { ...accessClaims, ...idClaims }, tokens.accessToken);
|
|
58
|
+
const exp = typeof accessClaims.exp === "number"
|
|
59
|
+
? new Date(accessClaims.exp * 1000)
|
|
60
|
+
: null;
|
|
61
|
+
const record = await saveTokens(ctx, {
|
|
62
|
+
ownerId: input.ownerId,
|
|
63
|
+
claims,
|
|
64
|
+
accessToken: tokens.accessToken,
|
|
65
|
+
refreshToken: tokens.refreshToken ?? null,
|
|
66
|
+
accessExpiresAt: tokens.expiresAt ?? exp,
|
|
67
|
+
refreshExpiresAt: null,
|
|
68
|
+
scope: tokens.scope ??
|
|
69
|
+
(typeof accessClaims.scope === "string" ? accessClaims.scope : null),
|
|
70
|
+
});
|
|
71
|
+
return toGrant(record);
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=external-tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external-tokens.js","sourceRoot":"","sources":["../src/external-tokens.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { JWTPayload } from "jose";
|
|
2
|
+
import type { TokenCipher } from "./crypto.js";
|
|
3
|
+
/** The encrypted, short-lived cookie that carries the Connect flow's state. */
|
|
4
|
+
export declare const FLOW_COOKIE = "bexio_oauth_flow";
|
|
5
|
+
export declare const FLOW_TTL_SECONDS = 600;
|
|
6
|
+
export interface FlowState extends JWTPayload {
|
|
7
|
+
/** state */ s: string;
|
|
8
|
+
/** PKCE verifier */ v: string;
|
|
9
|
+
/** nonce */ n: string;
|
|
10
|
+
/** ownerId */ o: string;
|
|
11
|
+
/** returnTo */ r: string;
|
|
12
|
+
}
|
|
13
|
+
/** The value of cookie `name` in the request's `Cookie` header, or `null`. */
|
|
14
|
+
export declare function readCookie(request: Request, name: string): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* The `Set-Cookie` value for the flow cookie, scoped to the callback path.
|
|
17
|
+
* `Secure` whenever the redirect URI is https. `("", 0)` deletes it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function flowCookie(redirectUri: URL, value: string, maxAge: number): string;
|
|
20
|
+
export declare function sealFlow(cipher: TokenCipher, flow: FlowState, now: Date): Promise<string>;
|
|
21
|
+
/** The request's flow state, or `null` when the cookie is missing, invalid or expired. */
|
|
22
|
+
export declare function readFlow(cipher: TokenCipher, request: Request, now: Date): Promise<FlowState | null>;
|
|
23
|
+
//# sourceMappingURL=flow-cookie.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flow-cookie.d.ts","sourceRoot":"","sources":["../src/flow-cookie.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,+EAA+E;AAC/E,eAAO,MAAM,WAAW,qBAAqB,CAAC;AAC9C,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,MAAM,WAAW,SAAU,SAAQ,UAAU;IAC3C,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC;IACvB,oBAAoB,CAAC,CAAC,EAAE,MAAM,CAAC;IAC/B,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUxE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,WAAW,EAAE,GAAG,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,MAAM,CAUR;AAED,wBAAgB,QAAQ,CACtB,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,SAAS,EACf,GAAG,EAAE,IAAI,GACR,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED,0FAA0F;AAC1F,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,IAAI,GACR,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAI3B"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** The encrypted, short-lived cookie that carries the Connect flow's state. */
|
|
2
|
+
export const FLOW_COOKIE = "bexio_oauth_flow";
|
|
3
|
+
export const FLOW_TTL_SECONDS = 600;
|
|
4
|
+
/** The value of cookie `name` in the request's `Cookie` header, or `null`. */
|
|
5
|
+
export function readCookie(request, name) {
|
|
6
|
+
const header = request.headers.get("cookie");
|
|
7
|
+
if (!header)
|
|
8
|
+
return null;
|
|
9
|
+
for (const part of header.split(";")) {
|
|
10
|
+
const index = part.indexOf("=");
|
|
11
|
+
if (index === -1)
|
|
12
|
+
continue;
|
|
13
|
+
if (part.slice(0, index).trim() === name)
|
|
14
|
+
return part.slice(index + 1).trim();
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The `Set-Cookie` value for the flow cookie, scoped to the callback path.
|
|
20
|
+
* `Secure` whenever the redirect URI is https. `("", 0)` deletes it.
|
|
21
|
+
*/
|
|
22
|
+
export function flowCookie(redirectUri, value, maxAge) {
|
|
23
|
+
const parts = [
|
|
24
|
+
`${FLOW_COOKIE}=${value}`,
|
|
25
|
+
`Path=${redirectUri.pathname}`,
|
|
26
|
+
`Max-Age=${maxAge}`,
|
|
27
|
+
"HttpOnly",
|
|
28
|
+
"SameSite=Lax",
|
|
29
|
+
];
|
|
30
|
+
if (redirectUri.protocol === "https:")
|
|
31
|
+
parts.push("Secure");
|
|
32
|
+
return parts.join("; ");
|
|
33
|
+
}
|
|
34
|
+
export function sealFlow(cipher, flow, now) {
|
|
35
|
+
return cipher.seal(flow, FLOW_TTL_SECONDS, now);
|
|
36
|
+
}
|
|
37
|
+
/** The request's flow state, or `null` when the cookie is missing, invalid or expired. */
|
|
38
|
+
export async function readFlow(cipher, request, now) {
|
|
39
|
+
const sealed = readCookie(request, FLOW_COOKIE);
|
|
40
|
+
if (!sealed)
|
|
41
|
+
return null;
|
|
42
|
+
return (await cipher.unseal(sealed, now));
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=flow-cookie.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flow-cookie.js","sourceRoot":"","sources":["../src/flow-cookie.ts"],"names":[],"mappings":"AAGA,+EAA+E;AAC/E,MAAM,CAAC,MAAM,WAAW,GAAG,kBAAkB,CAAC;AAC9C,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAUpC,8EAA8E;AAC9E,MAAM,UAAU,UAAU,CAAC,OAAgB,EAAE,IAAY;IACvD,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC7C,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,KAAK,KAAK,CAAC,CAAC;YAAE,SAAS;QAC3B,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,KAAK,IAAI;YACtC,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACxC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CACxB,WAAgB,EAChB,KAAa,EACb,MAAc;IAEd,MAAM,KAAK,GAAG;QACZ,GAAG,WAAW,IAAI,KAAK,EAAE;QACzB,QAAQ,WAAW,CAAC,QAAQ,EAAE;QAC9B,WAAW,MAAM,EAAE;QACnB,UAAU;QACV,cAAc;KACf,CAAC;IACF,IAAI,WAAW,CAAC,QAAQ,KAAK,QAAQ;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC5D,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,QAAQ,CACtB,MAAmB,EACnB,IAAe,EACf,GAAS;IAET,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,GAAG,CAAC,CAAC;AAClD,CAAC;AAED,0FAA0F;AAC1F,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,MAAmB,EACnB,OAAgB,EAChB,GAAS;IAET,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IAChD,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,OAAO,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAqB,CAAC;AAChE,CAAC"}
|
package/dist/grants.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ClaimBag } from "./claims.js";
|
|
2
|
+
import type { ClientContext } from "./config.js";
|
|
3
|
+
import type { TokenCipher } from "./crypto.js";
|
|
4
|
+
import type { GrantRecord, GrantStore } from "./store/types.js";
|
|
5
|
+
import type { AccessToken, Grant, GrantSelector } from "./types.js";
|
|
6
|
+
/** The app-facing view of a stored grant: no token material. */
|
|
7
|
+
export declare function toGrant(r: GrantRecord): Grant;
|
|
8
|
+
/** The binding a grant's ciphertexts are encrypted under. */
|
|
9
|
+
export declare function bindingOf(record: GrantRecord): string;
|
|
10
|
+
export declare function decryptFor(cipher: TokenCipher, record: GrantRecord, value: string): Promise<string>;
|
|
11
|
+
/** Decrypts the grant's access token for the caller. */
|
|
12
|
+
export declare function issue(cipher: TokenCipher, record: GrantRecord): Promise<AccessToken>;
|
|
13
|
+
/** Exactly one grant of the owner, or `not_connected` / `ambiguous_grant`. */
|
|
14
|
+
export declare function resolveGrant(store: GrantStore, selector: GrantSelector): Promise<GrantRecord>;
|
|
15
|
+
/** The token's claims if it is a JWT, else none (Bexio access tokens usually are). */
|
|
16
|
+
export declare function decodeIfJwt(token: string): ClaimBag;
|
|
17
|
+
/**
|
|
18
|
+
* Adds userinfo claims, only when the tokens lack the company claims. Token
|
|
19
|
+
* claims win over userinfo; a userinfo failure keeps the token claims.
|
|
20
|
+
*/
|
|
21
|
+
export declare function withUserInfo(ctx: ClientContext, claims: ClaimBag, accessToken: string): Promise<ClaimBag>;
|
|
22
|
+
/** Encrypts the tokens, reads the role and upserts the grant for the identity in `claims`. */
|
|
23
|
+
export declare function saveTokens(ctx: ClientContext, input: {
|
|
24
|
+
ownerId: string;
|
|
25
|
+
claims: ClaimBag;
|
|
26
|
+
accessToken: string;
|
|
27
|
+
refreshToken: string | null;
|
|
28
|
+
accessExpiresAt: Date | null;
|
|
29
|
+
refreshExpiresAt: Date | null;
|
|
30
|
+
scope: string | null;
|
|
31
|
+
}): Promise<GrantRecord>;
|
|
32
|
+
export declare function listGrants(store: GrantStore, query: {
|
|
33
|
+
ownerId: string;
|
|
34
|
+
}): Promise<Grant[]>;
|
|
35
|
+
export declare function disconnect(ctx: ClientContext, selector: GrantSelector): Promise<void>;
|
|
36
|
+
//# sourceMappingURL=grants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grants.d.ts","sourceRoot":"","sources":["../src/grants.ts"],"names":[],"mappings":"AAEA,OAAO,EAA2B,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEpE,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,CAAC,EAAE,WAAW,GAAG,KAAK,CAkB7C;AAED,6DAA6D;AAC7D,wBAAgB,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAErD;AAED,wBAAgB,UAAU,CACxB,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED,wDAAwD;AACxD,wBAAsB,KAAK,CACzB,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,WAAW,CAAC,CAMtB;AAED,8EAA8E;AAC9E,wBAAsB,YAAY,CAChC,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,aAAa,GACtB,OAAO,CAAC,WAAW,CAAC,CAatB;AAED,sFAAsF;AACtF,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAMnD;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,QAAQ,EAChB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,QAAQ,CAAC,CAYnB;AAED,8FAA8F;AAC9F,wBAAsB,UAAU,CAC9B,GAAG,EAAE,aAAa,EAClB,KAAK,EAAE;IACL,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,QAAQ,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,eAAe,EAAE,IAAI,GAAG,IAAI,CAAC;IAC7B,gBAAgB,EAAE,IAAI,GAAG,IAAI,CAAC;IAC9B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,GACA,OAAO,CAAC,WAAW,CAAC,CAuBtB;AAED,wBAAsB,UAAU,CAC9B,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GACzB,OAAO,CAAC,KAAK,EAAE,CAAC,CAIlB;AAED,wBAAsB,UAAU,CAC9B,GAAG,EAAE,aAAa,EAClB,QAAQ,EAAE,aAAa,GACtB,OAAO,CAAC,IAAI,CAAC,CAef"}
|
package/dist/grants.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as oidc from "openid-client";
|
|
2
|
+
import { decodeJwt } from "jose";
|
|
3
|
+
import { grantBinding, mapClaims } from "./claims.js";
|
|
4
|
+
import { BexioOAuthError } from "./errors.js";
|
|
5
|
+
import { fetchRole } from "./permissions.js";
|
|
6
|
+
/** The app-facing view of a stored grant: no token material. */
|
|
7
|
+
export function toGrant(r) {
|
|
8
|
+
return {
|
|
9
|
+
id: r.id,
|
|
10
|
+
ownerId: r.ownerId,
|
|
11
|
+
companyId: r.companyId,
|
|
12
|
+
companyName: r.companyName,
|
|
13
|
+
userId: r.userId,
|
|
14
|
+
userName: r.userName,
|
|
15
|
+
userEmail: r.userEmail,
|
|
16
|
+
companyUserId: r.companyUserId,
|
|
17
|
+
role: r.role,
|
|
18
|
+
status: r.status,
|
|
19
|
+
scope: r.scope,
|
|
20
|
+
accessExpiresAt: r.accessExpiresAt,
|
|
21
|
+
lastRefreshedAt: r.lastRefreshedAt,
|
|
22
|
+
createdAt: r.createdAt,
|
|
23
|
+
updatedAt: r.updatedAt,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** The binding a grant's ciphertexts are encrypted under. */
|
|
27
|
+
export function bindingOf(record) {
|
|
28
|
+
return grantBinding(record.ownerId, record.companyId, record.userId);
|
|
29
|
+
}
|
|
30
|
+
export function decryptFor(cipher, record, value) {
|
|
31
|
+
return cipher.decrypt(value, bindingOf(record));
|
|
32
|
+
}
|
|
33
|
+
/** Decrypts the grant's access token for the caller. */
|
|
34
|
+
export async function issue(cipher, record) {
|
|
35
|
+
return {
|
|
36
|
+
accessToken: await decryptFor(cipher, record, record.accessToken),
|
|
37
|
+
expiresAt: record.accessExpiresAt,
|
|
38
|
+
grant: toGrant(record),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Exactly one grant of the owner, or `not_connected` / `ambiguous_grant`. */
|
|
42
|
+
export async function resolveGrant(store, selector) {
|
|
43
|
+
if (!selector?.ownerId)
|
|
44
|
+
throw new BexioOAuthError("invalid_config", "ownerId is required");
|
|
45
|
+
const rows = await store.findGrants(selector);
|
|
46
|
+
if (rows.length === 0)
|
|
47
|
+
throw new BexioOAuthError("not_connected", "owner has no matching grant");
|
|
48
|
+
if (rows.length > 1) {
|
|
49
|
+
throw new BexioOAuthError("ambiguous_grant", "owner has several grants; pass companyId or grantId");
|
|
50
|
+
}
|
|
51
|
+
return rows[0];
|
|
52
|
+
}
|
|
53
|
+
/** The token's claims if it is a JWT, else none (Bexio access tokens usually are). */
|
|
54
|
+
export function decodeIfJwt(token) {
|
|
55
|
+
try {
|
|
56
|
+
return decodeJwt(token);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Adds userinfo claims, only when the tokens lack the company claims. Token
|
|
64
|
+
* claims win over userinfo; a userinfo failure keeps the token claims.
|
|
65
|
+
*/
|
|
66
|
+
export async function withUserInfo(ctx, claims, accessToken) {
|
|
67
|
+
if (claims.company_id !== undefined)
|
|
68
|
+
return claims;
|
|
69
|
+
try {
|
|
70
|
+
const info = await oidc.fetchUserInfo(await ctx.oidc.config(), accessToken, typeof claims.sub === "string" ? claims.sub : oidc.skipSubjectCheck);
|
|
71
|
+
return { ...info, ...claims };
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return claims;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Encrypts the tokens, reads the role and upserts the grant for the identity in `claims`. */
|
|
78
|
+
export async function saveTokens(ctx, input) {
|
|
79
|
+
const identity = mapClaims(input.claims);
|
|
80
|
+
const binding = grantBinding(input.ownerId, identity.companyId, identity.userId);
|
|
81
|
+
const role = (await fetchRole(ctx.fetchImpl, ctx.apiBaseUrl, input.accessToken)) ??
|
|
82
|
+
"member";
|
|
83
|
+
return ctx.store.upsertGrant({
|
|
84
|
+
ownerId: input.ownerId,
|
|
85
|
+
...identity,
|
|
86
|
+
role,
|
|
87
|
+
accessToken: await ctx.cipher.encrypt(input.accessToken, binding),
|
|
88
|
+
refreshToken: input.refreshToken
|
|
89
|
+
? await ctx.cipher.encrypt(input.refreshToken, binding)
|
|
90
|
+
: null,
|
|
91
|
+
accessExpiresAt: input.accessExpiresAt,
|
|
92
|
+
refreshExpiresAt: input.refreshExpiresAt,
|
|
93
|
+
scope: input.scope,
|
|
94
|
+
now: ctx.clock(),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export async function listGrants(store, query) {
|
|
98
|
+
if (!query?.ownerId)
|
|
99
|
+
throw new BexioOAuthError("invalid_config", "ownerId is required");
|
|
100
|
+
return (await store.findGrants({ ownerId: query.ownerId })).map(toGrant);
|
|
101
|
+
}
|
|
102
|
+
export async function disconnect(ctx, selector) {
|
|
103
|
+
const record = await resolveGrant(ctx.store, selector);
|
|
104
|
+
try {
|
|
105
|
+
const c = await ctx.oidc.config();
|
|
106
|
+
if (record.refreshToken && c.serverMetadata().revocation_endpoint) {
|
|
107
|
+
await oidc.tokenRevocation(c, await decryptFor(ctx.cipher, record, record.refreshToken), { token_type_hint: "refresh_token" });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Revocation is best effort; the grant is deleted either way.
|
|
112
|
+
}
|
|
113
|
+
await ctx.store.deleteGrant(record.id);
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=grants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grants.js","sourceRoot":"","sources":["../src/grants.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ClientContext } from "./config.js";
|
|
2
|
+
import { type Duration } from "./duration.js";
|
|
3
|
+
import type { RefreshIdleResult } from "./types.js";
|
|
4
|
+
export declare const DEFAULT_IDLE_LIMIT = 100;
|
|
5
|
+
/** `BexioOAuth.refreshIdle`: refreshes idle grants one after another. */
|
|
6
|
+
export declare function refreshIdle(ctx: ClientContext, options: {
|
|
7
|
+
idleFor: Duration;
|
|
8
|
+
limit?: number;
|
|
9
|
+
timeBudget?: Duration;
|
|
10
|
+
}): Promise<RefreshIdleResult>;
|
|
11
|
+
//# sourceMappingURL=idle-sweep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idle-sweep.d.ts","sourceRoot":"","sources":["../src/idle-sweep.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAQ,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,yEAAyE;AACzE,wBAAsB,WAAW,CAC/B,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE;IAAE,OAAO,EAAE,QAAQ,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,QAAQ,CAAA;CAAE,GACpE,OAAO,CAAC,iBAAiB,CAAC,CA2D5B"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { toMs } from "./duration.js";
|
|
2
|
+
import { isBexioOAuthError } from "./errors.js";
|
|
3
|
+
import { lockWindow, refreshLocked } from "./refresh.js";
|
|
4
|
+
export const DEFAULT_IDLE_LIMIT = 100;
|
|
5
|
+
/** `BexioOAuth.refreshIdle`: refreshes idle grants one after another. */
|
|
6
|
+
export async function refreshIdle(ctx, options) {
|
|
7
|
+
const { store, clock } = ctx;
|
|
8
|
+
const idleMs = toMs(options?.idleFor, "idleFor");
|
|
9
|
+
const budgetMs = options.timeBudget === undefined
|
|
10
|
+
? Infinity
|
|
11
|
+
: toMs(options.timeBudget, "timeBudget");
|
|
12
|
+
const startedAt = clock().getTime();
|
|
13
|
+
const result = {
|
|
14
|
+
refreshed: 0,
|
|
15
|
+
skipped: 0,
|
|
16
|
+
reauthRequired: 0,
|
|
17
|
+
failed: 0,
|
|
18
|
+
remaining: 0,
|
|
19
|
+
};
|
|
20
|
+
const refreshedBefore = new Date(startedAt - idleMs);
|
|
21
|
+
const idle = await store.listIdleGrants({
|
|
22
|
+
refreshedBefore,
|
|
23
|
+
limit: options.limit ?? DEFAULT_IDLE_LIMIT,
|
|
24
|
+
});
|
|
25
|
+
if (idle.length === 0)
|
|
26
|
+
return result;
|
|
27
|
+
try {
|
|
28
|
+
await ctx.oidc.config();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Bexio unreachable: report, don't throw; the next run retries.
|
|
32
|
+
result.failed = idle.length;
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
for (const [index, grant] of idle.entries()) {
|
|
36
|
+
// Stop before taking the next lock, so a function timeout can't cut a
|
|
37
|
+
// refresh in half. The oldest grants come first, so the next run
|
|
38
|
+
// continues where this one stopped.
|
|
39
|
+
if (clock().getTime() - startedAt >= budgetMs) {
|
|
40
|
+
result.remaining = idle.length - index;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
const window = lockWindow(ctx, clock());
|
|
44
|
+
const locked = await store.acquireRefreshLock({
|
|
45
|
+
id: grant.id,
|
|
46
|
+
...window,
|
|
47
|
+
refreshedBefore,
|
|
48
|
+
});
|
|
49
|
+
if (!locked) {
|
|
50
|
+
result.skipped++;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
if (await refreshLocked(ctx, locked, window.lockToken))
|
|
55
|
+
result.refreshed++;
|
|
56
|
+
else
|
|
57
|
+
result.skipped++;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (isBexioOAuthError(error, "reauth_required")) {
|
|
61
|
+
result.reauthRequired++;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
result.failed++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=idle-sweep.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idle-sweep.js","sourceRoot":"","sources":["../src/idle-sweep.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAiB,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAGzD,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAEtC,yEAAyE;AACzE,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,GAAkB,EAClB,OAAqE;IAErE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,GAAG,CAAC;IAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;IACjD,MAAM,QAAQ,GACZ,OAAO,CAAC,UAAU,KAAK,SAAS;QAC9B,CAAC,CAAC,QAAQ;QACV,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;IAC7C,MAAM,SAAS,GAAG,KAAK,EAAE,CAAC,OAAO,EAAE,CAAC;IACpC,MAAM,MAAM,GAAsB;QAChC,SAAS,EAAE,CAAC;QACZ,OAAO,EAAE,CAAC;QACV,cAAc,EAAE,CAAC;QACjB,MAAM,EAAE,CAAC;QACT,SAAS,EAAE,CAAC;KACb,CAAC;IACF,MAAM,eAAe,GAAG,IAAI,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,cAAc,CAAC;QACtC,eAAe;QACf,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,kBAAkB;KAC3C,CAAC,CAAC;IACH,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC;IACrC,IAAI,CAAC;QACH,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,gEAAgE;QAChE,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC5B,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;QAC5C,sEAAsE;QACtE,iEAAiE;QACjE,oCAAoC;QACpC,IAAI,KAAK,EAAE,CAAC,OAAO,EAAE,GAAG,SAAS,IAAI,QAAQ,EAAE,CAAC;YAC9C,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;YACvC,MAAM;QACR,CAAC;QACD,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;QACxC,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,kBAAkB,CAAC;YAC5C,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,GAAG,MAAM;YACT,eAAe;SAChB,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,CAAC,OAAO,EAAE,CAAC;YACjB,SAAS;QACX,CAAC;QACD,IAAI,CAAC;YACH,IAAI,MAAM,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC;gBACpD,MAAM,CAAC,SAAS,EAAE,CAAC;;gBAChB,MAAM,CAAC,OAAO,EAAE,CAAC;QACxB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,iBAAiB,CAAC,KAAK,EAAE,iBAAiB,CAAC,EAAE,CAAC;gBAChD,MAAM,CAAC,cAAc,EAAE,CAAC;YAC1B,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,MAAM,EAAE,CAAC;YAClB,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|