@fluixi/oauth2 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/api-guard.cjs +185 -0
- package/dist/api-guard.d.ts +94 -0
- package/dist/api-guard.d.ts.map +1 -0
- package/dist/api-guard.js +127 -0
- package/dist/api-guard.mjs +141 -0
- package/dist/browser-FDUOEUM2.mjs +3544 -0
- package/dist/browser-ONMHX3Z3.mjs +3545 -0
- package/dist/browser.cjs +735 -0
- package/dist/browser.d.ts +3 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +448 -0
- package/dist/browser.mjs +704 -0
- package/dist/callback.cjs +125 -0
- package/dist/callback.d.ts +113 -0
- package/dist/callback.d.ts.map +1 -0
- package/dist/callback.js +183 -0
- package/dist/callback.mjs +106 -0
- package/dist/chunk-25WE6UWI.mjs +10 -0
- package/dist/chunk-4NSOGPLM.mjs +15 -0
- package/dist/chunk-7P6ASYW6.mjs +9 -0
- package/dist/chunk-OGSFNTKA.mjs +13 -0
- package/dist/client-auth.cjs +115 -0
- package/dist/client-auth.d.ts +43 -0
- package/dist/client-auth.d.ts.map +1 -0
- package/dist/client-auth.js +112 -0
- package/dist/client-auth.mjs +84 -0
- package/dist/discovery.cjs +86 -0
- package/dist/discovery.d.ts +15 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +51 -0
- package/dist/discovery.mjs +63 -0
- package/dist/dpop-LVPWWADA.mjs +80 -0
- package/dist/dpop-NS7NKOWM.mjs +79 -0
- package/dist/dpop-store.cjs +152 -0
- package/dist/dpop-store.d.ts +37 -0
- package/dist/dpop-store.d.ts.map +1 -0
- package/dist/dpop-store.js +119 -0
- package/dist/dpop-store.mjs +121 -0
- package/dist/dpop.cjs +117 -0
- package/dist/dpop.d.ts +72 -0
- package/dist/dpop.d.ts.map +1 -0
- package/dist/dpop.js +114 -0
- package/dist/dpop.mjs +86 -0
- package/dist/env-config.cjs +366 -0
- package/dist/env-config.d.ts +103 -0
- package/dist/env-config.d.ts.map +1 -0
- package/dist/env-config.js +184 -0
- package/dist/env-config.mjs +343 -0
- package/dist/env-credentials.cjs +82 -0
- package/dist/env-credentials.d.ts +68 -0
- package/dist/env-credentials.d.ts.map +1 -0
- package/dist/env-credentials.js +76 -0
- package/dist/env-credentials.mjs +51 -0
- package/dist/index.cjs +940 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +101 -0
- package/dist/index.mjs +909 -0
- package/dist/pkce.cjs +87 -0
- package/dist/pkce.d.ts +58 -0
- package/dist/pkce.d.ts.map +1 -0
- package/dist/pkce.js +112 -0
- package/dist/pkce.mjs +66 -0
- package/dist/presets.cjs +240 -0
- package/dist/presets.d.ts +140 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +182 -0
- package/dist/presets.mjs +217 -0
- package/dist/return-to.cjs +46 -0
- package/dist/return-to.d.ts +49 -0
- package/dist/return-to.d.ts.map +1 -0
- package/dist/return-to.js +71 -0
- package/dist/return-to.mjs +25 -0
- package/dist/roles.cjs +56 -0
- package/dist/roles.d.ts +35 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/roles.js +37 -0
- package/dist/roles.mjs +35 -0
- package/dist/server-client.cjs +134 -0
- package/dist/server-client.d.ts +12 -0
- package/dist/server-client.d.ts.map +1 -0
- package/dist/server-client.js +107 -0
- package/dist/server-client.mjs +111 -0
- package/dist/server.cjs +470 -0
- package/dist/server.d.ts +89 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +360 -0
- package/dist/server.mjs +441 -0
- package/dist/service-client.cjs +244 -0
- package/dist/service-client.d.ts +89 -0
- package/dist/service-client.d.ts.map +1 -0
- package/dist/service-client.js +112 -0
- package/dist/service-client.mjs +213 -0
- package/dist/service.cjs +903 -0
- package/dist/service.d.ts +96 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +129 -0
- package/dist/service.mjs +874 -0
- package/dist/tokens.cjs +29 -0
- package/dist/tokens.d.ts +13 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +21 -0
- package/dist/tokens.mjs +8 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/types.cjs +33 -0
- package/dist/types.d.ts +239 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +23 -0
- package/dist/types.mjs +12 -0
- package/dist/verify.cjs +724 -0
- package/dist/verify.d.ts +205 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +371 -0
- package/dist/verify.mjs +590 -0
- package/package.json +121 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a completed login is allowed to land.
|
|
3
|
+
*
|
|
4
|
+
* A login flow carries a destination so a visitor resumes where they were interrupted,
|
|
5
|
+
* and that destination arrives from outside: a query parameter, a caller's argument, a
|
|
6
|
+
* value read back out of storage. Redirecting to it unchecked is an open redirect, and a
|
|
7
|
+
* login is the worst place to have one. The visitor has just authenticated, so the page
|
|
8
|
+
* they land on is trusted by them and reached with a live session.
|
|
9
|
+
*
|
|
10
|
+
* Sealing the value does not help. A signed cookie cannot be tampered with, and an
|
|
11
|
+
* attacker putting `?returnTo=https://evil.example/` on a link has no need to tamper: the
|
|
12
|
+
* application seals the attacker's value itself and honours it afterwards.
|
|
13
|
+
*
|
|
14
|
+
* So the rule is what the value is allowed to be, rather than where it came from. A path
|
|
15
|
+
* on this origin, and nothing else.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The destination to use, which is `candidate` when it is safe and the fallback when not.
|
|
19
|
+
*
|
|
20
|
+
* Refused, with reasons:
|
|
21
|
+
*
|
|
22
|
+
* - an absolute URL on another origin, which is the open redirect itself
|
|
23
|
+
* - `//host`, which a browser reads as protocol-relative and therefore another origin
|
|
24
|
+
* - `\\host`, which some browsers normalise to the same thing
|
|
25
|
+
* - a scheme of any kind, so `javascript:` and `data:` cannot be reached
|
|
26
|
+
* - a control character, which can be used to smuggle a newline past a naive check
|
|
27
|
+
* - anything that is not a path, since a bare `dashboard` resolves differently depending
|
|
28
|
+
* on where it is used
|
|
29
|
+
*/
|
|
30
|
+
export function safeReturnTo(candidate, options = {}) {
|
|
31
|
+
const fallback = options.fallback ?? '/';
|
|
32
|
+
if (!candidate || typeof candidate !== 'string')
|
|
33
|
+
return fallback;
|
|
34
|
+
const value = candidate.trim();
|
|
35
|
+
if (!value)
|
|
36
|
+
return fallback;
|
|
37
|
+
// A newline or a NUL can split a header or end a string early in whatever reads it next.
|
|
38
|
+
if (/[\u0000-\u001f\u007f]/.test(value))
|
|
39
|
+
return fallback;
|
|
40
|
+
// Protocol-relative in both spellings. `//evil.test` is another origin, and a backslash
|
|
41
|
+
// is normalised to a slash by enough browsers to count.
|
|
42
|
+
if (/^[/\\]{2}/.test(value))
|
|
43
|
+
return fallback;
|
|
44
|
+
// A path on this origin, which is the ordinary case and the only one needing no origin
|
|
45
|
+
// to compare against.
|
|
46
|
+
if (value.startsWith('/'))
|
|
47
|
+
return value;
|
|
48
|
+
// Anything else has to be an absolute URL on an origin this application accepts.
|
|
49
|
+
const here = options.origin ?? (typeof location === 'undefined' ? undefined : location.origin);
|
|
50
|
+
const allowed = [here, ...(options.allowedOrigins ?? [])].filter(Boolean);
|
|
51
|
+
if (!allowed.length)
|
|
52
|
+
return fallback;
|
|
53
|
+
let url;
|
|
54
|
+
try {
|
|
55
|
+
url = new URL(value);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Not a URL and not a path: a bare word, which resolves differently depending on the
|
|
59
|
+
// page it is used from.
|
|
60
|
+
return fallback;
|
|
61
|
+
}
|
|
62
|
+
// Compared as whole origins. A prefix test would accept
|
|
63
|
+
// `https://app.example.com.evil.test`, which is a different site.
|
|
64
|
+
if (!allowed.includes(url.origin))
|
|
65
|
+
return fallback;
|
|
66
|
+
// Only http and https, whatever the origin list says: an allowed origin cannot make
|
|
67
|
+
// `javascript:` safe, and `new URL('javascript:...')` has an origin of `null` anyway.
|
|
68
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
69
|
+
return fallback;
|
|
70
|
+
return url.toString();
|
|
71
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/return-to.ts
|
|
2
|
+
function safeReturnTo(candidate, options = {}) {
|
|
3
|
+
const fallback = options.fallback ?? "/";
|
|
4
|
+
if (!candidate || typeof candidate !== "string") return fallback;
|
|
5
|
+
const value = candidate.trim();
|
|
6
|
+
if (!value) return fallback;
|
|
7
|
+
if (/[\u0000-\u001f\u007f]/.test(value)) return fallback;
|
|
8
|
+
if (/^[/\\]{2}/.test(value)) return fallback;
|
|
9
|
+
if (value.startsWith("/")) return value;
|
|
10
|
+
const here = options.origin ?? (typeof location === "undefined" ? void 0 : location.origin);
|
|
11
|
+
const allowed = [here, ...options.allowedOrigins ?? []].filter(Boolean);
|
|
12
|
+
if (!allowed.length) return fallback;
|
|
13
|
+
let url;
|
|
14
|
+
try {
|
|
15
|
+
url = new URL(value);
|
|
16
|
+
} catch {
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
if (!allowed.includes(url.origin)) return fallback;
|
|
20
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return fallback;
|
|
21
|
+
return url.toString();
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
safeReturnTo
|
|
25
|
+
};
|
package/dist/roles.cjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
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/roles.ts
|
|
21
|
+
var roles_exports = {};
|
|
22
|
+
__export(roles_exports, {
|
|
23
|
+
rolesFrom: () => rolesFrom
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(roles_exports);
|
|
26
|
+
var rolesFrom = {
|
|
27
|
+
/** Keycloak, realm roles. Where it puts them unless a client mapper is configured. */
|
|
28
|
+
keycloakRealm: (claims) => {
|
|
29
|
+
const realm = claims.realm_access;
|
|
30
|
+
return realm?.roles ?? [];
|
|
31
|
+
},
|
|
32
|
+
/** Keycloak, roles scoped to one client rather than the realm. */
|
|
33
|
+
keycloakClient: (clientId) => (claims) => {
|
|
34
|
+
const all = claims.resource_access;
|
|
35
|
+
return all?.[clientId]?.roles ?? [];
|
|
36
|
+
},
|
|
37
|
+
/** Okta, and anything else using a groups claim. Needs a groups claim on the token. */
|
|
38
|
+
groups: (claims) => Array.isArray(claims.groups) ? claims.groups : [],
|
|
39
|
+
/** Cognito, which namespaces the claim. */
|
|
40
|
+
cognito: (claims) => {
|
|
41
|
+
const groups = claims["cognito:groups"];
|
|
42
|
+
return Array.isArray(groups) ? groups : [];
|
|
43
|
+
},
|
|
44
|
+
/**
|
|
45
|
+
* Auth0, which requires a namespaced custom claim added by an action.
|
|
46
|
+
*
|
|
47
|
+
* `rolesFrom.namespaced('https://adafri.com/roles')`. Auth0 silently drops a custom claim
|
|
48
|
+
* that is not a URI, so an empty list here usually means the namespace, not the action.
|
|
49
|
+
*/
|
|
50
|
+
namespaced: (claim) => (claims) => {
|
|
51
|
+
const value = claims[claim];
|
|
52
|
+
return Array.isArray(value) ? value : [];
|
|
53
|
+
},
|
|
54
|
+
/** The `scope` claim read as a role list, for a provider issuing no roles at all. */
|
|
55
|
+
scope: (claims) => typeof claims.scope === "string" ? claims.scope.split(" ") : []
|
|
56
|
+
};
|
package/dist/roles.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading roles off a token's claims.
|
|
3
|
+
*
|
|
4
|
+
* Its own module because it is pure data access and a browser bundle wants it too: presets
|
|
5
|
+
* name one per provider, and an app's shared config picks one. Kept here, clear of
|
|
6
|
+
* `verify.ts`, so importing it never drags the verifier and its `node:` imports into a
|
|
7
|
+
* client build.
|
|
8
|
+
*/
|
|
9
|
+
import type { TokenClaims } from './verify.js';
|
|
10
|
+
/**
|
|
11
|
+
* Where each provider puts roles.
|
|
12
|
+
*
|
|
13
|
+
* There is no default because they genuinely disagree, and guessing wrong means a guard that
|
|
14
|
+
* reads an empty list and refuses everyone. Each of these was read off a real token.
|
|
15
|
+
*/
|
|
16
|
+
export declare const rolesFrom: {
|
|
17
|
+
/** Keycloak, realm roles. Where it puts them unless a client mapper is configured. */
|
|
18
|
+
keycloakRealm: (claims: TokenClaims) => string[];
|
|
19
|
+
/** Keycloak, roles scoped to one client rather than the realm. */
|
|
20
|
+
keycloakClient: (clientId: string) => (claims: TokenClaims) => string[];
|
|
21
|
+
/** Okta, and anything else using a groups claim. Needs a groups claim on the token. */
|
|
22
|
+
groups: (claims: TokenClaims) => string[];
|
|
23
|
+
/** Cognito, which namespaces the claim. */
|
|
24
|
+
cognito: (claims: TokenClaims) => string[];
|
|
25
|
+
/**
|
|
26
|
+
* Auth0, which requires a namespaced custom claim added by an action.
|
|
27
|
+
*
|
|
28
|
+
* `rolesFrom.namespaced('https://adafri.com/roles')`. Auth0 silently drops a custom claim
|
|
29
|
+
* that is not a URI, so an empty list here usually means the namespace, not the action.
|
|
30
|
+
*/
|
|
31
|
+
namespaced: (claim: string) => (claims: TokenClaims) => string[];
|
|
32
|
+
/** The `scope` claim read as a role list, for a provider issuing no roles at all. */
|
|
33
|
+
scope: (claims: TokenClaims) => string[];
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=roles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roles.d.ts","sourceRoot":"","sources":["../src/roles.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;;;GAKG;AACH,eAAO,MAAM,SAAS;IACpB,sFAAsF;4BAC9D,WAAW,KAAG,MAAM,EAAE;IAK9C,kEAAkE;+BAErD,MAAM,MAChB,QAAQ,WAAW,KAAG,MAAM,EAAE;IAKjC,uFAAuF;qBACtE,WAAW,KAAG,MAAM,EAAE;IAGvC,2CAA2C;sBACzB,WAAW,KAAG,MAAM,EAAE;IAKxC;;;;;OAKG;wBAEO,MAAM,MACb,QAAQ,WAAW,KAAG,MAAM,EAAE;IAKjC,qFAAqF;oBACrE,WAAW,KAAG,MAAM,EAAE;CAEvC,CAAC"}
|
package/dist/roles.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where each provider puts roles.
|
|
3
|
+
*
|
|
4
|
+
* There is no default because they genuinely disagree, and guessing wrong means a guard that
|
|
5
|
+
* reads an empty list and refuses everyone. Each of these was read off a real token.
|
|
6
|
+
*/
|
|
7
|
+
export const rolesFrom = {
|
|
8
|
+
/** Keycloak, realm roles. Where it puts them unless a client mapper is configured. */
|
|
9
|
+
keycloakRealm: (claims) => {
|
|
10
|
+
const realm = claims.realm_access;
|
|
11
|
+
return realm?.roles ?? [];
|
|
12
|
+
},
|
|
13
|
+
/** Keycloak, roles scoped to one client rather than the realm. */
|
|
14
|
+
keycloakClient: (clientId) => (claims) => {
|
|
15
|
+
const all = claims.resource_access;
|
|
16
|
+
return all?.[clientId]?.roles ?? [];
|
|
17
|
+
},
|
|
18
|
+
/** Okta, and anything else using a groups claim. Needs a groups claim on the token. */
|
|
19
|
+
groups: (claims) => Array.isArray(claims.groups) ? claims.groups : [],
|
|
20
|
+
/** Cognito, which namespaces the claim. */
|
|
21
|
+
cognito: (claims) => {
|
|
22
|
+
const groups = claims['cognito:groups'];
|
|
23
|
+
return Array.isArray(groups) ? groups : [];
|
|
24
|
+
},
|
|
25
|
+
/**
|
|
26
|
+
* Auth0, which requires a namespaced custom claim added by an action.
|
|
27
|
+
*
|
|
28
|
+
* `rolesFrom.namespaced('https://adafri.com/roles')`. Auth0 silently drops a custom claim
|
|
29
|
+
* that is not a URI, so an empty list here usually means the namespace, not the action.
|
|
30
|
+
*/
|
|
31
|
+
namespaced: (claim) => (claims) => {
|
|
32
|
+
const value = claims[claim];
|
|
33
|
+
return Array.isArray(value) ? value : [];
|
|
34
|
+
},
|
|
35
|
+
/** The `scope` claim read as a role list, for a provider issuing no roles at all. */
|
|
36
|
+
scope: (claims) => typeof claims.scope === 'string' ? claims.scope.split(' ') : [],
|
|
37
|
+
};
|
package/dist/roles.mjs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/roles.ts
|
|
2
|
+
var rolesFrom = {
|
|
3
|
+
/** Keycloak, realm roles. Where it puts them unless a client mapper is configured. */
|
|
4
|
+
keycloakRealm: (claims) => {
|
|
5
|
+
const realm = claims.realm_access;
|
|
6
|
+
return realm?.roles ?? [];
|
|
7
|
+
},
|
|
8
|
+
/** Keycloak, roles scoped to one client rather than the realm. */
|
|
9
|
+
keycloakClient: (clientId) => (claims) => {
|
|
10
|
+
const all = claims.resource_access;
|
|
11
|
+
return all?.[clientId]?.roles ?? [];
|
|
12
|
+
},
|
|
13
|
+
/** Okta, and anything else using a groups claim. Needs a groups claim on the token. */
|
|
14
|
+
groups: (claims) => Array.isArray(claims.groups) ? claims.groups : [],
|
|
15
|
+
/** Cognito, which namespaces the claim. */
|
|
16
|
+
cognito: (claims) => {
|
|
17
|
+
const groups = claims["cognito:groups"];
|
|
18
|
+
return Array.isArray(groups) ? groups : [];
|
|
19
|
+
},
|
|
20
|
+
/**
|
|
21
|
+
* Auth0, which requires a namespaced custom claim added by an action.
|
|
22
|
+
*
|
|
23
|
+
* `rolesFrom.namespaced('https://adafri.com/roles')`. Auth0 silently drops a custom claim
|
|
24
|
+
* that is not a URI, so an empty list here usually means the namespace, not the action.
|
|
25
|
+
*/
|
|
26
|
+
namespaced: (claim) => (claims) => {
|
|
27
|
+
const value = claims[claim];
|
|
28
|
+
return Array.isArray(value) ? value : [];
|
|
29
|
+
},
|
|
30
|
+
/** The `scope` claim read as a role list, for a provider issuing no roles at all. */
|
|
31
|
+
scope: (claims) => typeof claims.scope === "string" ? claims.scope.split(" ") : []
|
|
32
|
+
};
|
|
33
|
+
export {
|
|
34
|
+
rolesFrom
|
|
35
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
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/server-client.ts
|
|
21
|
+
var server_client_exports = {};
|
|
22
|
+
__export(server_client_exports, {
|
|
23
|
+
COOKIE_SESSION: () => COOKIE_SESSION,
|
|
24
|
+
createServerClient: () => createServerClient
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(server_client_exports);
|
|
27
|
+
|
|
28
|
+
// src/return-to.ts
|
|
29
|
+
function safeReturnTo(candidate, options = {}) {
|
|
30
|
+
const fallback = options.fallback ?? "/";
|
|
31
|
+
if (!candidate || typeof candidate !== "string") return fallback;
|
|
32
|
+
const value = candidate.trim();
|
|
33
|
+
if (!value) return fallback;
|
|
34
|
+
if (/[\u0000-\u001f\u007f]/.test(value)) return fallback;
|
|
35
|
+
if (/^[/\\]{2}/.test(value)) return fallback;
|
|
36
|
+
if (value.startsWith("/")) return value;
|
|
37
|
+
const here = options.origin ?? (typeof location === "undefined" ? void 0 : location.origin);
|
|
38
|
+
const allowed = [here, ...options.allowedOrigins ?? []].filter(Boolean);
|
|
39
|
+
if (!allowed.length) return fallback;
|
|
40
|
+
let url;
|
|
41
|
+
try {
|
|
42
|
+
url = new URL(value);
|
|
43
|
+
} catch {
|
|
44
|
+
return fallback;
|
|
45
|
+
}
|
|
46
|
+
if (!allowed.includes(url.origin)) return fallback;
|
|
47
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return fallback;
|
|
48
|
+
return url.toString();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/types.ts
|
|
52
|
+
var OAuthError = class extends Error {
|
|
53
|
+
constructor(message, code, description) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.code = code;
|
|
56
|
+
this.description = description;
|
|
57
|
+
this.name = "OAuthError";
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/server-client.ts
|
|
62
|
+
var COOKIE_SESSION = "cookie";
|
|
63
|
+
function createServerClient(config, fetcher = fetch) {
|
|
64
|
+
const base = (config.basePath ?? "/auth").replace(/\/$/, "");
|
|
65
|
+
const url = (path) => `${base}${path}`;
|
|
66
|
+
const call = (path, init = {}) => fetcher(url(path), { credentials: "same-origin", ...init });
|
|
67
|
+
return {
|
|
68
|
+
async login(options = {}) {
|
|
69
|
+
const target = new URL(url("/login"), location.origin);
|
|
70
|
+
if (options.returnTo) target.searchParams.set("returnTo", options.returnTo);
|
|
71
|
+
for (const [key, value] of Object.entries(options.params ?? {})) {
|
|
72
|
+
target.searchParams.set(key, value);
|
|
73
|
+
}
|
|
74
|
+
location.assign(target.toString());
|
|
75
|
+
},
|
|
76
|
+
async handleCallback(params) {
|
|
77
|
+
const response = await call("/callback", {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "content-type": "application/json" },
|
|
80
|
+
body: JSON.stringify(params)
|
|
81
|
+
});
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
const body = await response.json().catch(() => ({}));
|
|
84
|
+
throw new OAuthError(
|
|
85
|
+
String(body.error ?? `Callback failed: ${response.status}`),
|
|
86
|
+
String(body.error ?? "callback_failed")
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return { accessToken: COOKIE_SESSION };
|
|
90
|
+
},
|
|
91
|
+
async refresh() {
|
|
92
|
+
const response = await call("/refresh", { method: "POST" });
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
throw new OAuthError("The server could not extend the session.", "refresh_failed");
|
|
95
|
+
}
|
|
96
|
+
return { accessToken: COOKIE_SESSION };
|
|
97
|
+
},
|
|
98
|
+
async fetchUser() {
|
|
99
|
+
const custom = config.userInfo;
|
|
100
|
+
const response = custom?.url ? await fetcher(custom.url, {
|
|
101
|
+
method: custom.method ?? "GET",
|
|
102
|
+
credentials: custom.credentials ?? "same-origin",
|
|
103
|
+
headers: { accept: "application/json", ...custom.headers }
|
|
104
|
+
}) : await call("/me");
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new OAuthError(`The server refused the user request: ${response.status}`, "me_failed");
|
|
107
|
+
}
|
|
108
|
+
const raw = await response.json();
|
|
109
|
+
return custom?.map ? await custom.map(raw, COOKIE_SESSION) : raw;
|
|
110
|
+
},
|
|
111
|
+
/**
|
|
112
|
+
* Retire the session's tokens, which only the server can do here.
|
|
113
|
+
*
|
|
114
|
+
* The browser holds a cookie and no token, so it has nothing to send to a revocation
|
|
115
|
+
* endpoint. This asks the server's own route, which opens the cookie and revokes what
|
|
116
|
+
* is inside.
|
|
117
|
+
*/
|
|
118
|
+
async revoke(_token, hint = "refresh_token") {
|
|
119
|
+
await call(`/revoke?hint=${hint}`, { method: "POST" }).catch(() => void 0);
|
|
120
|
+
},
|
|
121
|
+
async logout(options = {}) {
|
|
122
|
+
await call("/logout", { method: "POST" }).catch(() => void 0);
|
|
123
|
+
const target = options.returnTo ? safeReturnTo(options.returnTo, {
|
|
124
|
+
fallback: config.postLogoutRedirectUri ?? "/",
|
|
125
|
+
allowedOrigins: config.allowedReturnOrigins
|
|
126
|
+
}) : config.postLogoutRedirectUri ?? "/";
|
|
127
|
+
if (typeof location !== "undefined") location.assign(target);
|
|
128
|
+
},
|
|
129
|
+
async restore() {
|
|
130
|
+
const response = await call("/me");
|
|
131
|
+
return response.ok ? { accessToken: COOKIE_SESSION } : null;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AuthClient, ServerConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Stands in for the access token the caller never sees.
|
|
4
|
+
*
|
|
5
|
+
* `@fluixi/session` is built around a token string, and here the credential is a cookie
|
|
6
|
+
* the browser attaches on its own. A constant keeps the session's state machine working
|
|
7
|
+
* without inventing a token, and nothing ever sends it anywhere: requests carry the
|
|
8
|
+
* cookie.
|
|
9
|
+
*/
|
|
10
|
+
export declare const COOKIE_SESSION = "cookie";
|
|
11
|
+
export declare function createServerClient(config: ServerConfig, fetcher?: typeof fetch): AuthClient;
|
|
12
|
+
//# sourceMappingURL=server-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-client.d.ts","sourceRoot":"","sources":["../src/server-client.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,UAAU,EAAgC,YAAY,EAAU,MAAM,YAAY,CAAC;AAEjG;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,WAAW,CAAC;AAEvC,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,OAAO,KAAa,GAC5B,UAAU,CAgGZ"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser half of a server-held session.
|
|
3
|
+
*
|
|
4
|
+
* Every method is a call to a route the application's own server exposes, which is what
|
|
5
|
+
* `@fluixi/oauth2/server` provides. No token reaches this file: the server sets an
|
|
6
|
+
* httpOnly cookie, so a script on the page has nothing to read and an XSS cannot lift a
|
|
7
|
+
* session.
|
|
8
|
+
*
|
|
9
|
+
* The shape is the same `AuthClient` the browser implementation satisfies, so the
|
|
10
|
+
* service above does not know which one it has.
|
|
11
|
+
*/
|
|
12
|
+
import { safeReturnTo } from './return-to.js';
|
|
13
|
+
import { OAuthError } from './types.js';
|
|
14
|
+
/**
|
|
15
|
+
* Stands in for the access token the caller never sees.
|
|
16
|
+
*
|
|
17
|
+
* `@fluixi/session` is built around a token string, and here the credential is a cookie
|
|
18
|
+
* the browser attaches on its own. A constant keeps the session's state machine working
|
|
19
|
+
* without inventing a token, and nothing ever sends it anywhere: requests carry the
|
|
20
|
+
* cookie.
|
|
21
|
+
*/
|
|
22
|
+
export const COOKIE_SESSION = 'cookie';
|
|
23
|
+
export function createServerClient(config, fetcher = fetch) {
|
|
24
|
+
const base = (config.basePath ?? '/auth').replace(/\/$/, '');
|
|
25
|
+
const url = (path) => `${base}${path}`;
|
|
26
|
+
/** Same credentials rule for every call: the cookie, and never a bearer token. */
|
|
27
|
+
const call = (path, init = {}) => fetcher(url(path), { credentials: 'same-origin', ...init });
|
|
28
|
+
return {
|
|
29
|
+
async login(options = {}) {
|
|
30
|
+
// The server builds the authorization URL, because it holds the PKCE verifier and
|
|
31
|
+
// the client secret. The browser only navigates.
|
|
32
|
+
const target = new URL(url('/login'), location.origin);
|
|
33
|
+
if (options.returnTo)
|
|
34
|
+
target.searchParams.set('returnTo', options.returnTo);
|
|
35
|
+
for (const [key, value] of Object.entries(options.params ?? {})) {
|
|
36
|
+
target.searchParams.set(key, value);
|
|
37
|
+
}
|
|
38
|
+
location.assign(target.toString());
|
|
39
|
+
},
|
|
40
|
+
async handleCallback(params) {
|
|
41
|
+
// The server owns the exchange; the browser is telling it the callback arrived.
|
|
42
|
+
// In the usual deployment the redirect URI points at the server route directly and
|
|
43
|
+
// this is never called, which is why it is tolerant about what it gets back.
|
|
44
|
+
const response = await call('/callback', {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'content-type': 'application/json' },
|
|
47
|
+
body: JSON.stringify(params),
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const body = (await response.json().catch(() => ({})));
|
|
51
|
+
throw new OAuthError(String(body.error ?? `Callback failed: ${response.status}`), String(body.error ?? 'callback_failed'));
|
|
52
|
+
}
|
|
53
|
+
return { accessToken: COOKIE_SESSION };
|
|
54
|
+
},
|
|
55
|
+
async refresh() {
|
|
56
|
+
const response = await call('/refresh', { method: 'POST' });
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
throw new OAuthError('The server could not extend the session.', 'refresh_failed');
|
|
59
|
+
}
|
|
60
|
+
return { accessToken: COOKIE_SESSION };
|
|
61
|
+
},
|
|
62
|
+
async fetchUser() {
|
|
63
|
+
const custom = config.userInfo;
|
|
64
|
+
// A URL here points at the app's own API rather than the session route. The cookie
|
|
65
|
+
// still authenticates it, which is why `credentials` stays on either way.
|
|
66
|
+
const response = custom?.url
|
|
67
|
+
? await fetcher(custom.url, {
|
|
68
|
+
method: custom.method ?? 'GET',
|
|
69
|
+
credentials: custom.credentials ?? 'same-origin',
|
|
70
|
+
headers: { accept: 'application/json', ...custom.headers },
|
|
71
|
+
})
|
|
72
|
+
: await call('/me');
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new OAuthError(`The server refused the user request: ${response.status}`, 'me_failed');
|
|
75
|
+
}
|
|
76
|
+
const raw = (await response.json());
|
|
77
|
+
return custom?.map ? (await custom.map(raw, COOKIE_SESSION)) : raw;
|
|
78
|
+
},
|
|
79
|
+
/**
|
|
80
|
+
* Retire the session's tokens, which only the server can do here.
|
|
81
|
+
*
|
|
82
|
+
* The browser holds a cookie and no token, so it has nothing to send to a revocation
|
|
83
|
+
* endpoint. This asks the server's own route, which opens the cookie and revokes what
|
|
84
|
+
* is inside.
|
|
85
|
+
*/
|
|
86
|
+
async revoke(_token, hint = 'refresh_token') {
|
|
87
|
+
await call(`/revoke?hint=${hint}`, { method: 'POST' }).catch(() => undefined);
|
|
88
|
+
},
|
|
89
|
+
async logout(options = {}) {
|
|
90
|
+
await call('/logout', { method: 'POST' }).catch(() => undefined);
|
|
91
|
+
const target = options.returnTo
|
|
92
|
+
? safeReturnTo(options.returnTo, {
|
|
93
|
+
fallback: config.postLogoutRedirectUri ?? '/',
|
|
94
|
+
allowedOrigins: config.allowedReturnOrigins,
|
|
95
|
+
})
|
|
96
|
+
: (config.postLogoutRedirectUri ?? '/');
|
|
97
|
+
if (typeof location !== 'undefined')
|
|
98
|
+
location.assign(target);
|
|
99
|
+
},
|
|
100
|
+
async restore() {
|
|
101
|
+
// The cookie is either valid or it is not, and only the server knows. A successful
|
|
102
|
+
// `/me` is the answer.
|
|
103
|
+
const response = await call('/me');
|
|
104
|
+
return response.ok ? { accessToken: COOKIE_SESSION } : null;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// src/return-to.ts
|
|
2
|
+
function safeReturnTo(candidate, options = {}) {
|
|
3
|
+
const fallback = options.fallback ?? "/";
|
|
4
|
+
if (!candidate || typeof candidate !== "string") return fallback;
|
|
5
|
+
const value = candidate.trim();
|
|
6
|
+
if (!value) return fallback;
|
|
7
|
+
if (/[\u0000-\u001f\u007f]/.test(value)) return fallback;
|
|
8
|
+
if (/^[/\\]{2}/.test(value)) return fallback;
|
|
9
|
+
if (value.startsWith("/")) return value;
|
|
10
|
+
const here = options.origin ?? (typeof location === "undefined" ? void 0 : location.origin);
|
|
11
|
+
const allowed = [here, ...options.allowedOrigins ?? []].filter(Boolean);
|
|
12
|
+
if (!allowed.length) return fallback;
|
|
13
|
+
let url;
|
|
14
|
+
try {
|
|
15
|
+
url = new URL(value);
|
|
16
|
+
} catch {
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
if (!allowed.includes(url.origin)) return fallback;
|
|
20
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return fallback;
|
|
21
|
+
return url.toString();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/types.ts
|
|
25
|
+
var OAuthError = class extends Error {
|
|
26
|
+
constructor(message, code, description) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.code = code;
|
|
29
|
+
this.description = description;
|
|
30
|
+
this.name = "OAuthError";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// src/server-client.ts
|
|
35
|
+
var COOKIE_SESSION = "cookie";
|
|
36
|
+
function createServerClient(config, fetcher = fetch) {
|
|
37
|
+
const base = (config.basePath ?? "/auth").replace(/\/$/, "");
|
|
38
|
+
const url = (path) => `${base}${path}`;
|
|
39
|
+
const call = (path, init = {}) => fetcher(url(path), { credentials: "same-origin", ...init });
|
|
40
|
+
return {
|
|
41
|
+
async login(options = {}) {
|
|
42
|
+
const target = new URL(url("/login"), location.origin);
|
|
43
|
+
if (options.returnTo) target.searchParams.set("returnTo", options.returnTo);
|
|
44
|
+
for (const [key, value] of Object.entries(options.params ?? {})) {
|
|
45
|
+
target.searchParams.set(key, value);
|
|
46
|
+
}
|
|
47
|
+
location.assign(target.toString());
|
|
48
|
+
},
|
|
49
|
+
async handleCallback(params) {
|
|
50
|
+
const response = await call("/callback", {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "content-type": "application/json" },
|
|
53
|
+
body: JSON.stringify(params)
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const body = await response.json().catch(() => ({}));
|
|
57
|
+
throw new OAuthError(
|
|
58
|
+
String(body.error ?? `Callback failed: ${response.status}`),
|
|
59
|
+
String(body.error ?? "callback_failed")
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return { accessToken: COOKIE_SESSION };
|
|
63
|
+
},
|
|
64
|
+
async refresh() {
|
|
65
|
+
const response = await call("/refresh", { method: "POST" });
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new OAuthError("The server could not extend the session.", "refresh_failed");
|
|
68
|
+
}
|
|
69
|
+
return { accessToken: COOKIE_SESSION };
|
|
70
|
+
},
|
|
71
|
+
async fetchUser() {
|
|
72
|
+
const custom = config.userInfo;
|
|
73
|
+
const response = custom?.url ? await fetcher(custom.url, {
|
|
74
|
+
method: custom.method ?? "GET",
|
|
75
|
+
credentials: custom.credentials ?? "same-origin",
|
|
76
|
+
headers: { accept: "application/json", ...custom.headers }
|
|
77
|
+
}) : await call("/me");
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new OAuthError(`The server refused the user request: ${response.status}`, "me_failed");
|
|
80
|
+
}
|
|
81
|
+
const raw = await response.json();
|
|
82
|
+
return custom?.map ? await custom.map(raw, COOKIE_SESSION) : raw;
|
|
83
|
+
},
|
|
84
|
+
/**
|
|
85
|
+
* Retire the session's tokens, which only the server can do here.
|
|
86
|
+
*
|
|
87
|
+
* The browser holds a cookie and no token, so it has nothing to send to a revocation
|
|
88
|
+
* endpoint. This asks the server's own route, which opens the cookie and revokes what
|
|
89
|
+
* is inside.
|
|
90
|
+
*/
|
|
91
|
+
async revoke(_token, hint = "refresh_token") {
|
|
92
|
+
await call(`/revoke?hint=${hint}`, { method: "POST" }).catch(() => void 0);
|
|
93
|
+
},
|
|
94
|
+
async logout(options = {}) {
|
|
95
|
+
await call("/logout", { method: "POST" }).catch(() => void 0);
|
|
96
|
+
const target = options.returnTo ? safeReturnTo(options.returnTo, {
|
|
97
|
+
fallback: config.postLogoutRedirectUri ?? "/",
|
|
98
|
+
allowedOrigins: config.allowedReturnOrigins
|
|
99
|
+
}) : config.postLogoutRedirectUri ?? "/";
|
|
100
|
+
if (typeof location !== "undefined") location.assign(target);
|
|
101
|
+
},
|
|
102
|
+
async restore() {
|
|
103
|
+
const response = await call("/me");
|
|
104
|
+
return response.ok ? { accessToken: COOKIE_SESSION } : null;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export {
|
|
109
|
+
COOKIE_SESSION,
|
|
110
|
+
createServerClient
|
|
111
|
+
};
|