@micacms/auth-oidc 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/admission.d.ts +5 -4
- package/dist/admission.d.ts.map +1 -1
- package/dist/admission.js +22 -15
- package/dist/admission.js.map +1 -1
- package/dist/config.d.ts +8 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +41 -4
- package/dist/config.js.map +1 -1
- package/dist/gate.d.ts +3 -4
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +12 -11
- package/dist/gate.js.map +1 -1
- package/dist/index.d.ts +4 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -9
- package/dist/index.js.map +1 -1
- package/dist/routes.d.ts +0 -2
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +8 -2
- package/dist/routes.js.map +1 -1
- package/dist/session.d.ts +42 -26
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +119 -72
- package/dist/session.js.map +1 -1
- package/package.json +3 -3
- package/dist/sessions-repository.d.ts +0 -56
- package/dist/sessions-repository.d.ts.map +0 -1
- package/dist/sessions-repository.js +0 -96
- package/dist/sessions-repository.js.map +0 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **Experimental.** Pre-1.0: the API can still change without notice.
|
|
4
4
|
|
|
5
5
|
The server-only OIDC gate for Mica: the authorization code flow with PKCE,
|
|
6
|
-
sessions in
|
|
6
|
+
sessions in an encrypted cookie, admission rules over provider claims, and the middleware
|
|
7
7
|
gate the runtime mounts in front of the admin API.
|
|
8
8
|
|
|
9
9
|
It is a dependency of `@micacms/self-hosted` and of nothing else. In
|
package/dist/admission.d.ts
CHANGED
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
*
|
|
12
12
|
* MICA_ADMIT_GROUP=mica-editors a claim the provider sends
|
|
13
13
|
* MICA_ADMIT_DOMAIN=example.com every verified address there
|
|
14
|
+
* MICA_ADMIT_EMAILS=alice@gmail.com these verified addresses
|
|
14
15
|
* MICA_ADMIT_SUBJECTS=https://id.example.com|a1b2c3
|
|
15
16
|
*
|
|
16
17
|
* The group rule is the one to grow into. Adding an editor is then a group
|
|
17
18
|
* change in the identity provider — where the operator already manages
|
|
18
19
|
* joiners and leavers — and Mica is not touched, not restarted, and holds no
|
|
19
|
-
* copy of the list to go stale. The
|
|
20
|
-
* groups,
|
|
20
|
+
* copy of the list to go stale. The email list exists for a provider with no
|
|
21
|
+
* groups, such as Google; the subject list for one that verifies no addresses.
|
|
21
22
|
*
|
|
22
23
|
* What this costs, and it is a real cost: removing somebody from a group at
|
|
23
24
|
* the provider does not take effect until their next sign-in, because the
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
* decision in docs/decisions.md.
|
|
28
29
|
*/
|
|
29
30
|
import type { AuthConfig } from "./config.js";
|
|
30
|
-
import type { SessionIdentity } from "./
|
|
31
|
+
import type { SessionIdentity } from "./session.js";
|
|
31
32
|
export type AdmissionResult = {
|
|
32
33
|
admitted: true;
|
|
33
34
|
} | {
|
|
@@ -39,5 +40,5 @@ export interface AdmissionPolicy {
|
|
|
39
40
|
readonly summary: string;
|
|
40
41
|
admits(identity: SessionIdentity): AdmissionResult;
|
|
41
42
|
}
|
|
42
|
-
export declare function createAdmissionPolicy(config: Pick<AuthConfig, "admitGroups" | "admitDomains" | "admitSubjects">): AdmissionPolicy;
|
|
43
|
+
export declare function createAdmissionPolicy(config: Pick<AuthConfig, "admitGroups" | "admitDomains" | "admitEmails" | "admitSubjects">): AdmissionPolicy;
|
|
43
44
|
//# sourceMappingURL=admission.d.ts.map
|
package/dist/admission.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admission.d.ts","sourceRoot":"","sources":["../src/admission.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"admission.d.ts","sourceRoot":"","sources":["../src/admission.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,MAAM,MAAM,eAAe,GAAG;IAAE,QAAQ,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,QAAQ,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvF,MAAM,WAAW,eAAe;IAC9B,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,eAAe,CAAC;CACpD;AAsCD,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,aAAa,GAAG,cAAc,GAAG,aAAa,GAAG,eAAe,CAAC,GACzF,eAAe,CAsDjB"}
|
package/dist/admission.js
CHANGED
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
*
|
|
12
12
|
* MICA_ADMIT_GROUP=mica-editors a claim the provider sends
|
|
13
13
|
* MICA_ADMIT_DOMAIN=example.com every verified address there
|
|
14
|
+
* MICA_ADMIT_EMAILS=alice@gmail.com these verified addresses
|
|
14
15
|
* MICA_ADMIT_SUBJECTS=https://id.example.com|a1b2c3
|
|
15
16
|
*
|
|
16
17
|
* The group rule is the one to grow into. Adding an editor is then a group
|
|
17
18
|
* change in the identity provider — where the operator already manages
|
|
18
19
|
* joiners and leavers — and Mica is not touched, not restarted, and holds no
|
|
19
|
-
* copy of the list to go stale. The
|
|
20
|
-
* groups,
|
|
20
|
+
* copy of the list to go stale. The email list exists for a provider with no
|
|
21
|
+
* groups, such as Google; the subject list for one that verifies no addresses.
|
|
21
22
|
*
|
|
22
23
|
* What this costs, and it is a real cost: removing somebody from a group at
|
|
23
24
|
* the provider does not take effect until their next sign-in, because the
|
|
@@ -27,17 +28,16 @@
|
|
|
27
28
|
* decision in docs/decisions.md.
|
|
28
29
|
*/
|
|
29
30
|
/**
|
|
30
|
-
* The
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* hold one in turn. Keying admission on an address is therefore a standing
|
|
34
|
-
* offer to whoever holds it next, which is a well-trodden route to account
|
|
35
|
-
* takeover. The issuer is part of the key because a subject is only unique
|
|
36
|
-
* within the provider that issued it.
|
|
31
|
+
* The strictest rule: the subject never changes and is never reassigned. The
|
|
32
|
+
* issuer is part of the key because a subject is only unique within the
|
|
33
|
+
* provider that issued it.
|
|
37
34
|
*/
|
|
38
35
|
function matchesSubject(identity, entries) {
|
|
39
36
|
return entries.some((entry) => entry.issuer === identity.issuer && entry.subject === identity.subject);
|
|
40
37
|
}
|
|
38
|
+
function matchesEmail(identity, emails) {
|
|
39
|
+
return emails.includes(identity.email.toLowerCase());
|
|
40
|
+
}
|
|
41
41
|
function matchesDomain(identity, domains) {
|
|
42
42
|
const at = identity.email.lastIndexOf("@");
|
|
43
43
|
if (at < 0)
|
|
@@ -56,10 +56,11 @@ function matchesGroup(identity, groups) {
|
|
|
56
56
|
return groups.some((group) => held.has(group.replace(/^\//, "")));
|
|
57
57
|
}
|
|
58
58
|
export function createAdmissionPolicy(config) {
|
|
59
|
-
const { admitGroups, admitDomains, admitSubjects } = config;
|
|
59
|
+
const { admitGroups, admitDomains, admitEmails, admitSubjects } = config;
|
|
60
60
|
const summary = [
|
|
61
61
|
admitGroups.length > 0 ? `group=${admitGroups.join("|")}` : null,
|
|
62
62
|
admitDomains.length > 0 ? `domain=${admitDomains.join("|")}` : null,
|
|
63
|
+
admitEmails.length > 0 ? `emails=${admitEmails.length}` : null,
|
|
63
64
|
admitSubjects.length > 0 ? `subjects=${admitSubjects.length}` : null,
|
|
64
65
|
]
|
|
65
66
|
.filter(Boolean)
|
|
@@ -73,22 +74,28 @@ export function createAdmissionPolicy(config) {
|
|
|
73
74
|
return { admitted: true };
|
|
74
75
|
if (matchesGroup(identity, admitGroups))
|
|
75
76
|
return { admitted: true };
|
|
76
|
-
|
|
77
|
+
const byEmail = matchesEmail(identity, admitEmails);
|
|
78
|
+
if (byEmail || matchesDomain(identity, admitDomains)) {
|
|
77
79
|
/**
|
|
78
|
-
*
|
|
80
|
+
* An email or domain rule trusts the provider to have checked the address.
|
|
79
81
|
*
|
|
80
82
|
* Where a provider allows self-registration, an unverified address is
|
|
81
83
|
* a claim the person typed. Admitting on that would let anybody enter
|
|
82
|
-
*
|
|
84
|
+
* an address they do not hold, so the rule refuses and says why — the
|
|
83
85
|
* operator can turn verification on, or use a group or subject rule
|
|
84
86
|
* instead.
|
|
87
|
+
*
|
|
88
|
+
* What remains is reassignment: an organisation that gives a former
|
|
89
|
+
* editor's address to somebody new gives them this site too. Public
|
|
90
|
+
* providers such as Gmail never reuse an address; the trade is
|
|
91
|
+
* recorded in docs/decisions.md.
|
|
85
92
|
*/
|
|
86
93
|
if (identity.emailVerified)
|
|
87
94
|
return { admitted: true };
|
|
88
95
|
return {
|
|
89
96
|
admitted: false,
|
|
90
|
-
reason: `${identity.email} is
|
|
91
|
-
"does not report the address as verified.",
|
|
97
|
+
reason: `${identity.email} is ${byEmail ? "an admitted address" : "in an admitted domain"}, ` +
|
|
98
|
+
"but the identity provider does not report the address as verified.",
|
|
92
99
|
};
|
|
93
100
|
}
|
|
94
101
|
return {
|
package/dist/admission.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admission.js","sourceRoot":"","sources":["../src/admission.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"admission.js","sourceRoot":"","sources":["../src/admission.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
CHANGED
|
@@ -28,11 +28,13 @@ export declare const authEnv: {
|
|
|
28
28
|
/**
|
|
29
29
|
* Who may edit this site. Authentication says who somebody is; these say
|
|
30
30
|
* whether that person gets in. At least one is required — the composition
|
|
31
|
-
* refuses to start without one. Comma-separated, all
|
|
31
|
+
* refuses to start without one. Comma-separated, all four.
|
|
32
32
|
*/
|
|
33
33
|
MICA_ADMIT_GROUP: z.ZodOptional<z.ZodString>;
|
|
34
34
|
MICA_ADMIT_DOMAIN: z.ZodOptional<z.ZodString>;
|
|
35
|
-
/**
|
|
35
|
+
/** Verified addresses — the list for a provider with no groups, such as Google. */
|
|
36
|
+
MICA_ADMIT_EMAILS: z.ZodOptional<z.ZodString>;
|
|
37
|
+
/** `issuer|subject` pairs — see admission.ts. */
|
|
36
38
|
MICA_ADMIT_SUBJECTS: z.ZodOptional<z.ZodString>;
|
|
37
39
|
/** Signs session cookies. Rotating it invalidates every session. */
|
|
38
40
|
SESSION_SECRET: z.ZodOptional<z.ZodString>;
|
|
@@ -55,6 +57,7 @@ declare const authSchema: z.ZodObject<{
|
|
|
55
57
|
MICA_OIDC_GROUPS_CLAIM: z.ZodDefault<z.ZodString>;
|
|
56
58
|
MICA_ADMIT_GROUP: z.ZodOptional<z.ZodString>;
|
|
57
59
|
MICA_ADMIT_DOMAIN: z.ZodOptional<z.ZodString>;
|
|
60
|
+
MICA_ADMIT_EMAILS: z.ZodOptional<z.ZodString>;
|
|
58
61
|
MICA_ADMIT_SUBJECTS: z.ZodOptional<z.ZodString>;
|
|
59
62
|
SESSION_SECRET: z.ZodOptional<z.ZodString>;
|
|
60
63
|
SESSION_TTL_HOURS: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
@@ -75,6 +78,7 @@ export declare function deriveAuth<T extends AuthEnv & {
|
|
|
75
78
|
oidcAllowInsecure: boolean;
|
|
76
79
|
admitGroups: string[];
|
|
77
80
|
admitDomains: string[];
|
|
81
|
+
admitEmails: string[];
|
|
78
82
|
admitSubjects: {
|
|
79
83
|
issuer: string;
|
|
80
84
|
subject: string;
|
|
@@ -93,6 +97,7 @@ export interface AuthConfig {
|
|
|
93
97
|
oidcAllowInsecure: boolean;
|
|
94
98
|
admitGroups: string[];
|
|
95
99
|
admitDomains: string[];
|
|
100
|
+
admitEmails: string[];
|
|
96
101
|
admitSubjects: Array<{
|
|
97
102
|
issuer: string;
|
|
98
103
|
subject: string;
|
|
@@ -117,6 +122,7 @@ export declare function requireAuthConfig(config: {
|
|
|
117
122
|
MICA_OIDC_CLIENT_ID?: string | undefined;
|
|
118
123
|
admitGroups: string[];
|
|
119
124
|
admitDomains: string[];
|
|
125
|
+
admitEmails: string[];
|
|
120
126
|
admitSubjects: Array<{
|
|
121
127
|
issuer: string;
|
|
122
128
|
subject: string;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,OAAO;IAClB;;;;OAIG;;;IAGH,iDAAiD;;;IAGjD;;;;;OAKG;;IAGH;;;;OAIG;;;IAGH,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,OAAO;IAClB;;;;OAIG;;;IAGH,iDAAiD;;;IAGjD;;;;;OAKG;;IAGH;;;;OAIG;;;IAGH,mFAAmF;;IAEnF,iDAAiD;;IAGjD,oEAAoE;;IAEpE;;;;;OAKG;;;;;CAUJ,CAAC;AAEF,QAAA,MAAM,UAAU;;;;;;;;;;;;;;;iBAAoB,CAAC;AACrC,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEjD,2EAA2E;AAC3E,wBAAgB,UAAU,CAAC,CAAC,SAAS,OAAO,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAA;CAAE,EAC1F,GAAG,EAAE,CAAC;IAIJ;;;OAGG;;;;;;;;;;EAsBN;AAED,0DAA0D;AAC1D,MAAM,WAAW,UAAU;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,OAAO,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,mBAAmB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,uBAAuB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7C,eAAe,EAAE,MAAM,CAAC;IACxB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,aAAa,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1D,8EAA8E;IAC9E,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;CACzB;AA0BD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,mBAAmB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,aAAa,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC3D,GAAG,IAAI,CA4DP"}
|
package/dist/config.js
CHANGED
|
@@ -29,11 +29,13 @@ export const authEnv = {
|
|
|
29
29
|
/**
|
|
30
30
|
* Who may edit this site. Authentication says who somebody is; these say
|
|
31
31
|
* whether that person gets in. At least one is required — the composition
|
|
32
|
-
* refuses to start without one. Comma-separated, all
|
|
32
|
+
* refuses to start without one. Comma-separated, all four.
|
|
33
33
|
*/
|
|
34
34
|
MICA_ADMIT_GROUP: z.string().optional(),
|
|
35
35
|
MICA_ADMIT_DOMAIN: z.string().optional(),
|
|
36
|
-
/**
|
|
36
|
+
/** Verified addresses — the list for a provider with no groups, such as Google. */
|
|
37
|
+
MICA_ADMIT_EMAILS: z.string().optional(),
|
|
38
|
+
/** `issuer|subject` pairs — see admission.ts. */
|
|
37
39
|
MICA_ADMIT_SUBJECTS: z.string().optional(),
|
|
38
40
|
/** Signs session cookies. Rotating it invalidates every session. */
|
|
39
41
|
SESSION_SECRET: z.string().min(32).optional(),
|
|
@@ -67,6 +69,7 @@ export function deriveAuth(env) {
|
|
|
67
69
|
// Lowercased: a domain is case-insensitive, and an operator who writes
|
|
68
70
|
// `Example.com` means the same thing the provider spells `example.com`.
|
|
69
71
|
admitDomains: list(env.MICA_ADMIT_DOMAIN).map((domain) => domain.toLowerCase().replace(/^@/, "")),
|
|
72
|
+
admitEmails: list(env.MICA_ADMIT_EMAILS).map((email) => email.toLowerCase()),
|
|
70
73
|
admitSubjects: list(env.MICA_ADMIT_SUBJECTS).map((entry) => {
|
|
71
74
|
const separator = entry.indexOf("|");
|
|
72
75
|
return {
|
|
@@ -79,6 +82,29 @@ export function deriveAuth(env) {
|
|
|
79
82
|
cookieSecure: env.MICA_PUBLIC_URL.startsWith("https://"),
|
|
80
83
|
};
|
|
81
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Domains where anybody can register an address. A domain rule naming one
|
|
87
|
+
* admits the public, so it is refused; name the addresses instead.
|
|
88
|
+
*/
|
|
89
|
+
const PUBLIC_EMAIL_DOMAINS = new Set([
|
|
90
|
+
"gmail.com",
|
|
91
|
+
"googlemail.com",
|
|
92
|
+
"outlook.com",
|
|
93
|
+
"hotmail.com",
|
|
94
|
+
"live.com",
|
|
95
|
+
"msn.com",
|
|
96
|
+
"yahoo.com",
|
|
97
|
+
"icloud.com",
|
|
98
|
+
"me.com",
|
|
99
|
+
"aol.com",
|
|
100
|
+
"proton.me",
|
|
101
|
+
"protonmail.com",
|
|
102
|
+
"gmx.com",
|
|
103
|
+
"gmx.net",
|
|
104
|
+
"gmx.de",
|
|
105
|
+
"gmx.ch",
|
|
106
|
+
"web.de",
|
|
107
|
+
]);
|
|
82
108
|
/**
|
|
83
109
|
* The refusals that make a deployment honest before it serves a request.
|
|
84
110
|
*
|
|
@@ -96,23 +122,34 @@ export function requireAuthConfig(config) {
|
|
|
96
122
|
}
|
|
97
123
|
if (config.admitGroups.length === 0 &&
|
|
98
124
|
config.admitDomains.length === 0 &&
|
|
125
|
+
config.admitEmails.length === 0 &&
|
|
99
126
|
config.admitSubjects.length === 0) {
|
|
100
127
|
throw new ConfigError("Nothing says who may edit this site.\n" +
|
|
101
128
|
" Authentication is not admission: your provider will happily vouch for\n" +
|
|
102
129
|
" everybody it knows. Set at least one of these:\n\n" +
|
|
103
130
|
" MICA_ADMIT_GROUP=mica-editors a claim your provider sends\n" +
|
|
104
131
|
" MICA_ADMIT_DOMAIN=example.com every verified address there\n" +
|
|
132
|
+
" MICA_ADMIT_EMAILS=alice@gmail.com these verified addresses\n" +
|
|
105
133
|
" MICA_ADMIT_SUBJECTS=https://id.example.com|a1b2c3\n\n" +
|
|
106
134
|
" The group rule is the one to grow into: adding an editor is then a\n" +
|
|
107
135
|
" change in the identity provider, and Mica is not touched.");
|
|
108
136
|
}
|
|
137
|
+
const open = config.admitDomains.filter((domain) => PUBLIC_EMAIL_DOMAINS.has(domain));
|
|
138
|
+
if (open.length > 0) {
|
|
139
|
+
throw new ConfigError(`MICA_ADMIT_DOMAIN names ${open.join(", ")}, where anybody can register an address.\n` +
|
|
140
|
+
" That rule would let the public edit this site. Name the people instead:\n" +
|
|
141
|
+
" MICA_ADMIT_EMAILS=alice@gmail.com,bob@gmail.com");
|
|
142
|
+
}
|
|
143
|
+
const malformed = config.admitEmails.filter((email) => !/^[^@\s]+@[^@\s]+$/.test(email));
|
|
144
|
+
if (malformed.length > 0) {
|
|
145
|
+
throw new ConfigError(`MICA_ADMIT_EMAILS entries are email addresses, and these are not: ${malformed.join(", ")}`);
|
|
146
|
+
}
|
|
109
147
|
const wrong = config.admitSubjects.filter((entry) => !entry.issuer || !entry.subject);
|
|
110
148
|
if (wrong.length > 0) {
|
|
111
149
|
throw new ConfigError("MICA_ADMIT_SUBJECTS entries are `issuer|subject` pairs, and one is not:\n" +
|
|
112
150
|
` ${wrong.map((entry) => entry.subject || "(empty)").join(", ")}\n\n` +
|
|
113
151
|
" The issuer is part of the key because a subject is only unique within\n" +
|
|
114
|
-
" the provider that issued it.
|
|
115
|
-
" let people change those, and two people can hold one in turn.\n" +
|
|
152
|
+
" the provider that issued it. To admit an address, use MICA_ADMIT_EMAILS.\n" +
|
|
116
153
|
` MICA_ADMIT_SUBJECTS=${config.MICA_OIDC_ISSUER ?? "https://id.example.com"}|a1b2c3`);
|
|
117
154
|
}
|
|
118
155
|
}
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB;;;;OAIG;IACH,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IAC7C,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC1C,iDAAiD;IACjD,uBAAuB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC9C,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,sBAAsB,CAAC;IAC3D;;;;;OAKG;IACH,sBAAsB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC;IAEpD;;;;OAIG;IACH,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACvC,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACxC,
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/gate.d.ts
CHANGED
|
@@ -3,19 +3,18 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Two layers, in order. `requireAccess` turns a session cookie into an
|
|
5
5
|
* `AccessContext` — or a 401 — re-checking admission on every request so a
|
|
6
|
-
* configuration change is immediate
|
|
6
|
+
* configuration change is immediate, and re-issuing the cookie while the
|
|
7
|
+
* session is in use. `csrfProtection` is the double-submit
|
|
7
8
|
* token over the Origin check the runtime already applies: cross-origin
|
|
8
9
|
* JavaScript can neither read the cookie nor set the header.
|
|
9
10
|
*/
|
|
10
11
|
import type { Context, MiddlewareHandler } from "hono";
|
|
11
12
|
import { type Env } from "@micacms/runtime";
|
|
12
13
|
import type { AdmissionPolicy } from "./admission.js";
|
|
13
|
-
import { type SessionService } from "./session.js";
|
|
14
|
-
import type { SessionRecord, SessionRepository } from "./sessions-repository.js";
|
|
14
|
+
import { type SessionRecord, type SessionService } from "./session.js";
|
|
15
15
|
export declare function sessionOf(c: Context<Env>): SessionRecord;
|
|
16
16
|
export interface GateDeps {
|
|
17
17
|
sessions: SessionService;
|
|
18
|
-
sessionsRepo: SessionRepository;
|
|
19
18
|
admission: AdmissionPolicy;
|
|
20
19
|
}
|
|
21
20
|
/**
|
package/dist/gate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAEvD,OAAO,EAAY,KAAK,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,cAAc,CAAC;AAQtB,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,aAAa,CAGxD;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,cAAc,CAAC;IACzB,SAAS,EAAE,eAAe,CAAC;CAC5B;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAiCpE;AAID;;;;;;GAMG;AACH,wBAAgB,cAAc,IAAI,iBAAiB,CAAC,GAAG,CAAC,CAiBvD"}
|
package/dist/gate.js
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Two layers, in order. `requireAccess` turns a session cookie into an
|
|
5
5
|
* `AccessContext` — or a 401 — re-checking admission on every request so a
|
|
6
|
-
* configuration change is immediate
|
|
6
|
+
* configuration change is immediate, and re-issuing the cookie while the
|
|
7
|
+
* session is in use. `csrfProtection` is the double-submit
|
|
7
8
|
* token over the Origin check the runtime already applies: cross-origin
|
|
8
9
|
* JavaScript can neither read the cookie nor set the header.
|
|
9
10
|
*/
|
|
10
|
-
import { getCookie } from "hono/cookie";
|
|
11
|
+
import { getCookie, setCookie } from "hono/cookie";
|
|
11
12
|
import { ApiError } from "@micacms/runtime";
|
|
12
|
-
import { CSRF_COOKIE, CSRF_HEADER, safeEqual, SESSION_COOKIE } from "./session.js";
|
|
13
|
+
import { CSRF_COOKIE, CSRF_HEADER, safeEqual, SESSION_COOKIE, } from "./session.js";
|
|
13
14
|
/**
|
|
14
15
|
* The session behind the current request. Auth's own context variable —
|
|
15
16
|
* the runtime knows only the `access` it derives from it.
|
|
@@ -31,18 +32,11 @@ export function sessionOf(c) {
|
|
|
31
32
|
*/
|
|
32
33
|
export function requireAccess(deps) {
|
|
33
34
|
return async (c, next) => {
|
|
34
|
-
|
|
35
|
+
let session = deps.sessions.resolve(getCookie(c, SESSION_COOKIE));
|
|
35
36
|
if (!session) {
|
|
36
37
|
throw new ApiError("unauthenticated", "Your session has expired. Please sign in again.");
|
|
37
38
|
}
|
|
38
39
|
const { identity } = session;
|
|
39
|
-
if (!identity) {
|
|
40
|
-
// A row from a pre-split local-mode run of the same database. It
|
|
41
|
-
// authenticates nobody, so it is not a session for an authenticated
|
|
42
|
-
// deployment.
|
|
43
|
-
deps.sessionsRepo.delete(session.id);
|
|
44
|
-
throw new ApiError("unauthenticated", "Please sign in again.");
|
|
45
|
-
}
|
|
46
40
|
const verdict = deps.admission.admits(identity);
|
|
47
41
|
if (!verdict.admitted) {
|
|
48
42
|
c.get("logger").warn("Session no longer admitted", {
|
|
@@ -51,11 +45,18 @@ export function requireAccess(deps) {
|
|
|
51
45
|
});
|
|
52
46
|
throw new ApiError("forbidden", "You no longer have access to this site.");
|
|
53
47
|
}
|
|
48
|
+
const refreshed = deps.sessions.refresh(session);
|
|
49
|
+
if (refreshed)
|
|
50
|
+
session = refreshed.record;
|
|
54
51
|
c.set(SESSION_VARIABLE, session);
|
|
55
52
|
c.set("access", {
|
|
56
53
|
identity: { subject: identity.subject, email: identity.email, name: identity.name },
|
|
57
54
|
});
|
|
58
55
|
await next();
|
|
56
|
+
// After the handler, so the cookie lands on whatever response it built.
|
|
57
|
+
if (refreshed) {
|
|
58
|
+
setCookie(c, SESSION_COOKIE, refreshed.cookie, deps.sessions.sessionCookieOptions());
|
|
59
|
+
}
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
62
|
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
package/dist/gate.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate.js","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"gate.js","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @micacms/auth-oidc — the server-only OIDC gate around @micacms/runtime.
|
|
3
3
|
*
|
|
4
|
-
* One factory: hand it the finished configuration
|
|
5
|
-
*
|
|
4
|
+
* One factory: hand it the finished configuration and a logger, and it
|
|
5
|
+
* returns the `AuthSurface` the runtime mounts — sign-in
|
|
6
6
|
* routes at `/api/auth`, and the session-plus-CSRF gate in front of the admin
|
|
7
7
|
* API — together with the services the composing app reports on.
|
|
8
8
|
*
|
|
@@ -10,24 +10,20 @@
|
|
|
10
10
|
* The local editor must never gain a path to it: that absence is the product
|
|
11
11
|
* boundary that keeps `npx @micacms/editor` free of authentication code.
|
|
12
12
|
*/
|
|
13
|
-
import type { AuthSurface, Logger
|
|
13
|
+
import type { AuthSurface, Logger } from "@micacms/runtime";
|
|
14
14
|
import { type RateLimiter } from "@micacms/runtime";
|
|
15
15
|
import { type AdmissionPolicy } from "./admission.js";
|
|
16
16
|
import type { AuthConfig } from "./config.js";
|
|
17
17
|
import { type OidcService } from "./oidc.js";
|
|
18
18
|
import { type SessionService } from "./session.js";
|
|
19
|
-
import { type SessionRepository } from "./sessions-repository.js";
|
|
20
19
|
export interface OidcAuthDeps {
|
|
21
20
|
config: AuthConfig;
|
|
22
|
-
/** The database sessions live in. Owned and opened by the composing app. */
|
|
23
|
-
db: SqlDatabase;
|
|
24
21
|
logger: Logger;
|
|
25
22
|
}
|
|
26
23
|
export interface OidcAuth {
|
|
27
24
|
/** What `createApp` mounts: routes at /api/auth, the gate before /api/admin. */
|
|
28
25
|
surface: AuthSurface;
|
|
29
26
|
sessions: SessionService;
|
|
30
|
-
sessionsRepo: SessionRepository;
|
|
31
27
|
admission: AdmissionPolicy;
|
|
32
28
|
oidc: OidcService;
|
|
33
29
|
loginLimiter: RateLimiter;
|
|
@@ -37,6 +33,5 @@ export { createAdmissionPolicy, type AdmissionPolicy, type AdmissionResult } fro
|
|
|
37
33
|
export { authEnv, deriveAuth, requireAuthConfig, type AuthConfig, type AuthEnv, } from "./config.js";
|
|
38
34
|
export { csrfProtection, requireAccess, sessionOf } from "./gate.js";
|
|
39
35
|
export { createOidcService, type OidcService } from "./oidc.js";
|
|
40
|
-
export { createSessionService, CSRF_COOKIE, CSRF_HEADER, SESSION_COOKIE, type SessionService, } from "./session.js";
|
|
41
|
-
export { createSessionRepository, type SessionIdentity, type SessionRecord, type SessionRepository, } from "./sessions-repository.js";
|
|
36
|
+
export { createSessionService, CSRF_COOKIE, CSRF_HEADER, SESSION_COOKIE, type SessionIdentity, type SessionRecord, type SessionService, } from "./session.js";
|
|
42
37
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEvE,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC7E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAEhE,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAEzE,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,QAAQ;IACvB,gFAAgF;IAChF,OAAO,EAAE,WAAW,CAAC;IACrB,QAAQ,EAAE,cAAc,CAAC;IACzB,SAAS,EAAE,eAAe,CAAC;IAC3B,IAAI,EAAE,WAAW,CAAC;IAClB,YAAY,EAAE,WAAW,CAAC;CAC3B;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,CAuB3D;AAED,OAAO,EAAE,qBAAqB,EAAE,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACnG,OAAO,EACL,OAAO,EACP,UAAU,EACV,iBAAiB,EACjB,KAAK,UAAU,EACf,KAAK,OAAO,GACb,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,WAAW,EACX,cAAc,EACd,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,cAAc,GACpB,MAAM,cAAc,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @micacms/auth-oidc — the server-only OIDC gate around @micacms/runtime.
|
|
3
3
|
*
|
|
4
|
-
* One factory: hand it the finished configuration
|
|
5
|
-
*
|
|
4
|
+
* One factory: hand it the finished configuration and a logger, and it
|
|
5
|
+
* returns the `AuthSurface` the runtime mounts — sign-in
|
|
6
6
|
* routes at `/api/auth`, and the session-plus-CSRF gate in front of the admin
|
|
7
7
|
* API — together with the services the composing app reports on.
|
|
8
8
|
*
|
|
@@ -16,25 +16,22 @@ import { csrfProtection, requireAccess } from "./gate.js";
|
|
|
16
16
|
import { createOidcService } from "./oidc.js";
|
|
17
17
|
import { authRoutes } from "./routes.js";
|
|
18
18
|
import { createSessionService } from "./session.js";
|
|
19
|
-
import { createSessionRepository } from "./sessions-repository.js";
|
|
20
19
|
export function createOidcAuth(deps) {
|
|
21
20
|
const { config, logger } = deps;
|
|
22
|
-
const
|
|
23
|
-
const sessions = createSessionService(sessionsRepo, config);
|
|
21
|
+
const sessions = createSessionService(config);
|
|
24
22
|
const admission = createAdmissionPolicy(config);
|
|
25
23
|
const oidc = createOidcService({ config, logger });
|
|
26
24
|
const loginLimiter = createRateLimiter({
|
|
27
25
|
limit: config.LOGIN_MAX_ATTEMPTS,
|
|
28
26
|
windowMs: config.LOGIN_WINDOW_MS,
|
|
29
27
|
});
|
|
30
|
-
const gate = { sessions,
|
|
28
|
+
const gate = { sessions, admission };
|
|
31
29
|
return {
|
|
32
30
|
surface: {
|
|
33
|
-
routes: authRoutes({ config, sessions,
|
|
31
|
+
routes: authRoutes({ config, sessions, oidc, admission, loginLimiter }),
|
|
34
32
|
gate: [requireAccess(gate), csrfProtection()],
|
|
35
33
|
},
|
|
36
34
|
sessions,
|
|
37
|
-
sessionsRepo,
|
|
38
35
|
admission,
|
|
39
36
|
oidc,
|
|
40
37
|
loginLimiter,
|
|
@@ -45,5 +42,4 @@ export { authEnv, deriveAuth, requireAuthConfig, } from "./config.js";
|
|
|
45
42
|
export { csrfProtection, requireAccess, sessionOf } from "./gate.js";
|
|
46
43
|
export { createOidcService } from "./oidc.js";
|
|
47
44
|
export { createSessionService, CSRF_COOKIE, CSRF_HEADER, SESSION_COOKIE, } from "./session.js";
|
|
48
|
-
export { createSessionRepository, } from "./sessions-repository.js";
|
|
49
45
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,iBAAiB,EAAoB,MAAM,kBAAkB,CAAC;AAEvE,OAAO,EAAE,qBAAqB,EAAwB,MAAM,gBAAgB,CAAC;AAE7E,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAoB,MAAM,WAAW,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAuB,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,iBAAiB,EAAoB,MAAM,kBAAkB,CAAC;AAEvE,OAAO,EAAE,qBAAqB,EAAwB,MAAM,gBAAgB,CAAC;AAE7E,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAoB,MAAM,WAAW,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAuB,MAAM,cAAc,CAAC;AAgBzE,MAAM,UAAU,cAAc,CAAC,IAAkB;IAC/C,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAEhC,MAAM,QAAQ,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC9C,MAAM,SAAS,GAAG,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAChD,MAAM,IAAI,GAAG,iBAAiB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACnD,MAAM,YAAY,GAAG,iBAAiB,CAAC;QACrC,KAAK,EAAE,MAAM,CAAC,kBAAkB;QAChC,QAAQ,EAAE,MAAM,CAAC,eAAe;KACjC,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;IAErC,OAAO;QACL,OAAO,EAAE;YACP,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;YACvE,IAAI,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,cAAc,EAAE,CAAC;SAC9C;QACD,QAAQ;QACR,SAAS;QACT,IAAI;QACJ,YAAY;KACb,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,qBAAqB,EAA8C,MAAM,gBAAgB,CAAC;AACnG,OAAO,EACL,OAAO,EACP,UAAU,EACV,iBAAiB,GAGlB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAoB,MAAM,WAAW,CAAC;AAChE,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,WAAW,EACX,cAAc,GAIf,MAAM,cAAc,CAAC"}
|
package/dist/routes.d.ts
CHANGED
|
@@ -15,12 +15,10 @@ import { type Env, type RateLimiter } from "@micacms/runtime";
|
|
|
15
15
|
import type { AdmissionPolicy } from "./admission.js";
|
|
16
16
|
import type { AuthConfig } from "./config.js";
|
|
17
17
|
import { type SessionService } from "./session.js";
|
|
18
|
-
import type { SessionRepository } from "./sessions-repository.js";
|
|
19
18
|
import { type OidcService } from "./oidc.js";
|
|
20
19
|
export interface AuthRoutesDeps {
|
|
21
20
|
config: AuthConfig;
|
|
22
21
|
sessions: SessionService;
|
|
23
|
-
sessionsRepo: SessionRepository;
|
|
24
22
|
oidc: OidcService;
|
|
25
23
|
admission: AdmissionPolicy;
|
|
26
24
|
loginLimiter: RateLimiter;
|
package/dist/routes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAAsB,KAAK,GAAG,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAElF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAAsB,KAAK,GAAG,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAElF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,EAKL,KAAK,cAAc,EACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EAKL,KAAK,WAAW,EACjB,MAAM,WAAW,CAAC;AAiBnB,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,cAAc,CAAC;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,EAAE,eAAe,CAAC;IAC3B,YAAY,EAAE,WAAW,CAAC;CAC3B;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,CAsI1D"}
|
package/dist/routes.js
CHANGED
|
@@ -108,12 +108,18 @@ export function authRoutes(deps) {
|
|
|
108
108
|
return refuse(`You are signed in as ${claims.email}, but that account has no access to ` +
|
|
109
109
|
"this site. Ask whoever runs it to add you.");
|
|
110
110
|
}
|
|
111
|
-
|
|
111
|
+
try {
|
|
112
|
+
establishSession(c, sessions, identity);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
logger.error("Could not start a session", { subject: identity.subject, error });
|
|
116
|
+
return refuse("Sign-in could not be completed: your account details are too large.");
|
|
117
|
+
}
|
|
112
118
|
logger.info("Signed in", { subject: identity.subject, issuer: identity.issuer });
|
|
113
119
|
return c.redirect(transaction.returnTo);
|
|
114
120
|
});
|
|
121
|
+
// There is no server-side session to end; clearing the cookies is sign-out.
|
|
115
122
|
app.post("/logout", async (c) => {
|
|
116
|
-
sessions.destroy(getCookie(c, SESSION_COOKIE));
|
|
117
123
|
deleteCookie(c, SESSION_COOKIE, sessions.clearedCookieOptions());
|
|
118
124
|
deleteCookie(c, CSRF_COOKIE, sessions.clearedCookieOptions());
|
|
119
125
|
// Offered, not followed: ending the provider's session is the provider's
|
package/dist/routes.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAEjE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAA8B,MAAM,kBAAkB,CAAC;AAIlF,OAAO,EAAE,aAAa,EAAE,SAAS,EAAiB,MAAM,WAAW,CAAC;AACpE,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,cAAc,
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
|
package/dist/session.d.ts
CHANGED
|
@@ -1,48 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session management.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The session is the cookie: who the provider said this is, a CSRF token and
|
|
5
|
+
* an expiry, encrypted with a key derived from SESSION_SECRET. There is no
|
|
6
|
+
* session table, so Mica stores nothing about people at all.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* - AES-256-GCM, so the cookie can be neither read nor altered. Encrypted and
|
|
9
|
+
* not only signed, because it carries an email address and groups.
|
|
10
|
+
* - A fresh random IV per cookie, and a new cookie at every sign-in, so a
|
|
11
|
+
* client-supplied value is never adopted (session fixation).
|
|
12
|
+
* - The idle window slides: an active session is re-issued every few
|
|
13
|
+
* minutes with a later expiry, capped by the absolute lifetime.
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
* a
|
|
15
|
+
* What this gives up is server-side revocation of one cookie. Sign-out clears
|
|
16
|
+
* it in the browser; a copy taken before then stays valid until it expires.
|
|
17
|
+
* Rotating SESSION_SECRET ends every session at once. The admission rules are
|
|
18
|
+
* still re-read on every request — see gate.ts and docs/decisions.md.
|
|
16
19
|
*/
|
|
17
20
|
import type { Context } from "hono";
|
|
18
21
|
import { type Env } from "@micacms/runtime";
|
|
19
22
|
import type { AuthConfig } from "./config.js";
|
|
20
|
-
import type { SessionIdentity, SessionRecord, SessionRepository } from "./sessions-repository.js";
|
|
21
23
|
export declare const SESSION_COOKIE = "mica_session";
|
|
22
24
|
export declare const CSRF_COOKIE = "mica_csrf";
|
|
23
25
|
export declare const CSRF_HEADER = "x-csrf-token";
|
|
26
|
+
/** What the identity provider stated about the person holding this session. */
|
|
27
|
+
export interface SessionIdentity {
|
|
28
|
+
issuer: string;
|
|
29
|
+
/** Opaque and stable at that issuer. */
|
|
30
|
+
subject: string;
|
|
31
|
+
email: string;
|
|
32
|
+
/** The provider's own `email_verified`. Absent counts as false. */
|
|
33
|
+
emailVerified: boolean;
|
|
34
|
+
name: string;
|
|
35
|
+
/** The groups claim, reduced at sign-in to the groups the rules name. */
|
|
36
|
+
groups: string[];
|
|
37
|
+
}
|
|
38
|
+
export interface SessionRecord {
|
|
39
|
+
identity: SessionIdentity;
|
|
40
|
+
csrfToken: string;
|
|
41
|
+
createdAt: string;
|
|
42
|
+
/** When the session was last issued; drives the sliding idle window. */
|
|
43
|
+
refreshedAt: string;
|
|
44
|
+
expiresAt: string;
|
|
45
|
+
}
|
|
24
46
|
export interface SessionService {
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
* the row exists to carry a CSRF token and an expiry, and nothing else.
|
|
28
|
-
*/
|
|
29
|
-
create(input: {
|
|
30
|
-
identity: SessionIdentity | null;
|
|
31
|
-
ip?: string | null;
|
|
32
|
-
userAgent?: string | null;
|
|
33
|
-
}): {
|
|
47
|
+
/** Seals a new session. Throws when the identity cannot fit in a cookie. */
|
|
48
|
+
create(identity: SessionIdentity): {
|
|
34
49
|
record: SessionRecord;
|
|
35
50
|
cookie: string;
|
|
36
|
-
csrfToken: string;
|
|
37
51
|
};
|
|
52
|
+
/** The live session in a cookie, or null for a missing, forged or expired one. */
|
|
38
53
|
resolve(cookieValue: string | undefined): SessionRecord | null;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
/** A later-expiring copy of an active session, or null while it is fresh. */
|
|
55
|
+
refresh(record: SessionRecord): {
|
|
56
|
+
record: SessionRecord;
|
|
57
|
+
cookie: string;
|
|
58
|
+
} | null;
|
|
42
59
|
sessionCookieOptions(): CookieOptions;
|
|
43
60
|
csrfCookieOptions(): CookieOptions;
|
|
44
61
|
clearedCookieOptions(): CookieOptions;
|
|
45
|
-
purgeExpired(): number;
|
|
46
62
|
}
|
|
47
63
|
export interface CookieOptions {
|
|
48
64
|
path: string;
|
|
@@ -53,7 +69,7 @@ export interface CookieOptions {
|
|
|
53
69
|
}
|
|
54
70
|
/** Constant-time string comparison that tolerates differing lengths. */
|
|
55
71
|
export declare function safeEqual(a: string, b: string): boolean;
|
|
56
|
-
export declare function createSessionService(
|
|
72
|
+
export declare function createSessionService(config: AuthConfig): SessionService;
|
|
57
73
|
/** Issues the session cookies for a browser that has just been identified. */
|
|
58
74
|
export declare function establishSession(c: Context<Env>, sessions: SessionService, identity: SessionIdentity): void;
|
|
59
75
|
//# sourceMappingURL=session.d.ts.map
|
package/dist/session.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAGpC,OAAO,EAAe,KAAK,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAEzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,eAAO,MAAM,cAAc,iBAAiB,CAAC;AAC7C,eAAO,MAAM,WAAW,cAAc,CAAC;AACvC,eAAO,MAAM,WAAW,iBAAiB,CAAC;AAW1C,+EAA+E;AAC/E,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,aAAa,EAAE,OAAO,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,eAAe,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG;QAAE,MAAM,EAAE,aAAa,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7E,kFAAkF;IAClF,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa,GAAG,IAAI,CAAC;IAC/D,6EAA6E;IAC7E,OAAO,CAAC,MAAM,EAAE,aAAa,GAAG;QAAE,MAAM,EAAE,aAAa,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACjF,oBAAoB,IAAI,aAAa,CAAC;IACtC,iBAAiB,IAAI,aAAa,CAAC;IACnC,oBAAoB,IAAI,aAAa,CAAC;CACvC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CASvD;AA6CD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,UAAU,GAAG,cAAc,CAoHvE;AAMD,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAC9B,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,EACf,QAAQ,EAAE,cAAc,EACxB,QAAQ,EAAE,eAAe,GACxB,IAAI,CAKN"}
|
package/dist/session.js
CHANGED
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session management.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The session is the cookie: who the provider said this is, a CSRF token and
|
|
5
|
+
* an expiry, encrypted with a key derived from SESSION_SECRET. There is no
|
|
6
|
+
* session table, so Mica stores nothing about people at all.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* - AES-256-GCM, so the cookie can be neither read nor altered. Encrypted and
|
|
9
|
+
* not only signed, because it carries an email address and groups.
|
|
10
|
+
* - A fresh random IV per cookie, and a new cookie at every sign-in, so a
|
|
11
|
+
* client-supplied value is never adopted (session fixation).
|
|
12
|
+
* - The idle window slides: an active session is re-issued every few
|
|
13
|
+
* minutes with a later expiry, capped by the absolute lifetime.
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
* a
|
|
15
|
+
* What this gives up is server-side revocation of one cookie. Sign-out clears
|
|
16
|
+
* it in the browser; a copy taken before then stays valid until it expires.
|
|
17
|
+
* Rotating SESSION_SECRET ends every session at once. The admission rules are
|
|
18
|
+
* still re-read on every request — see gate.ts and docs/decisions.md.
|
|
16
19
|
*/
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
+
import { createCipheriv, createDecipheriv, hkdfSync, randomBytes, timingSafeEqual } from "node:crypto";
|
|
21
|
+
import { setCookie } from "hono/cookie";
|
|
22
|
+
import { randomToken } from "@micacms/runtime";
|
|
20
23
|
export const SESSION_COOKIE = "mica_session";
|
|
21
24
|
export const CSRF_COOKIE = "mica_csrf";
|
|
22
25
|
export const CSRF_HEADER = "x-csrf-token";
|
|
23
|
-
/**
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
/** How often an active session is re-issued with a later idle expiry. */
|
|
27
|
+
const REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
28
|
+
/** Browsers drop cookies over 4096 bytes; refuse rather than sign in to nothing. */
|
|
29
|
+
const MAX_COOKIE_BYTES = 4000;
|
|
30
|
+
const IV_BYTES = 12;
|
|
31
|
+
const TAG_BYTES = 16;
|
|
29
32
|
/** Constant-time string comparison that tolerates differing lengths. */
|
|
30
33
|
export function safeEqual(a, b) {
|
|
31
34
|
const left = Buffer.from(a);
|
|
@@ -37,64 +40,117 @@ export function safeEqual(a, b) {
|
|
|
37
40
|
}
|
|
38
41
|
return timingSafeEqual(left, right);
|
|
39
42
|
}
|
|
40
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Keycloak sends `/mica-editors` where the rule says `mica-editors`; both
|
|
45
|
+
* spellings are the same group — see admission.ts.
|
|
46
|
+
*/
|
|
47
|
+
const groupName = (group) => group.replace(/^\//, "");
|
|
48
|
+
/** Defensive read of a decrypted payload: it was written by us, but checked anyway. */
|
|
49
|
+
function parseRecord(json) {
|
|
50
|
+
try {
|
|
51
|
+
const value = JSON.parse(json);
|
|
52
|
+
const identity = value.identity;
|
|
53
|
+
if (!identity ||
|
|
54
|
+
typeof identity.issuer !== "string" ||
|
|
55
|
+
typeof identity.subject !== "string" ||
|
|
56
|
+
typeof value.csrfToken !== "string" ||
|
|
57
|
+
typeof value.createdAt !== "string" ||
|
|
58
|
+
typeof value.refreshedAt !== "string" ||
|
|
59
|
+
typeof value.expiresAt !== "string") {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
identity: {
|
|
64
|
+
issuer: identity.issuer,
|
|
65
|
+
subject: identity.subject,
|
|
66
|
+
email: typeof identity.email === "string" ? identity.email : "",
|
|
67
|
+
emailVerified: identity.emailVerified === true,
|
|
68
|
+
name: typeof identity.name === "string" ? identity.name : "",
|
|
69
|
+
groups: Array.isArray(identity.groups)
|
|
70
|
+
? identity.groups.filter((group) => typeof group === "string")
|
|
71
|
+
: [],
|
|
72
|
+
},
|
|
73
|
+
csrfToken: value.csrfToken,
|
|
74
|
+
createdAt: value.createdAt,
|
|
75
|
+
refreshedAt: value.refreshedAt,
|
|
76
|
+
expiresAt: value.expiresAt,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function createSessionService(config) {
|
|
41
84
|
const ttlMs = config.SESSION_TTL_HOURS * 3600_000;
|
|
42
85
|
const idleMs = config.SESSION_IDLE_HOURS * 3600_000;
|
|
86
|
+
const key = Buffer.from(hkdfSync("sha256", config.sessionSecret, "", "mica session cookie v1", 32));
|
|
87
|
+
// Only the groups a rule names are kept: an Entra token can carry hundreds,
|
|
88
|
+
// and a cookie holds 4 KB. A group added to the rules later counts from the
|
|
89
|
+
// next sign-in.
|
|
90
|
+
const admitted = new Set(config.admitGroups.map(groupName));
|
|
43
91
|
/** Expiry is the earlier of the idle window and the absolute lifetime. */
|
|
44
92
|
const nextExpiry = (createdAt, now) => new Date(Math.min(now + idleMs, createdAt + ttlMs)).toISOString();
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
93
|
+
const seal = (record) => {
|
|
94
|
+
const iv = randomBytes(IV_BYTES);
|
|
95
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
96
|
+
const body = Buffer.concat([cipher.update(JSON.stringify(record), "utf8"), cipher.final()]);
|
|
97
|
+
const cookie = Buffer.concat([iv, body, cipher.getAuthTag()]).toString("base64url");
|
|
98
|
+
if (cookie.length > MAX_COOKIE_BYTES) {
|
|
99
|
+
throw new Error("The session does not fit in a cookie; the identity is too large.");
|
|
100
|
+
}
|
|
101
|
+
return cookie;
|
|
102
|
+
};
|
|
103
|
+
const open = (cookieValue) => {
|
|
104
|
+
const raw = Buffer.from(cookieValue, "base64url");
|
|
105
|
+
if (raw.length <= IV_BYTES + TAG_BYTES)
|
|
50
106
|
return null;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
107
|
+
try {
|
|
108
|
+
const decipher = createDecipheriv("aes-256-gcm", key, raw.subarray(0, IV_BYTES));
|
|
109
|
+
decipher.setAuthTag(raw.subarray(raw.length - TAG_BYTES));
|
|
110
|
+
const json = Buffer.concat([
|
|
111
|
+
decipher.update(raw.subarray(IV_BYTES, raw.length - TAG_BYTES)),
|
|
112
|
+
decipher.final(),
|
|
113
|
+
]).toString("utf8");
|
|
114
|
+
return parseRecord(json);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Forged, truncated, or sealed with a secret that has since rotated.
|
|
54
118
|
return null;
|
|
55
|
-
|
|
119
|
+
}
|
|
56
120
|
};
|
|
57
121
|
return {
|
|
58
|
-
create(
|
|
59
|
-
const token = randomToken(32);
|
|
60
|
-
const csrfToken = randomToken(32);
|
|
122
|
+
create(identity) {
|
|
61
123
|
const now = Date.now();
|
|
62
|
-
const record =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
124
|
+
const record = {
|
|
125
|
+
identity: {
|
|
126
|
+
...identity,
|
|
127
|
+
groups: identity.groups.filter((group) => admitted.has(groupName(group))),
|
|
128
|
+
},
|
|
129
|
+
csrfToken: randomToken(32),
|
|
130
|
+
createdAt: new Date(now).toISOString(),
|
|
131
|
+
refreshedAt: new Date(now).toISOString(),
|
|
66
132
|
expiresAt: nextExpiry(now, now),
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
});
|
|
70
|
-
return { record, cookie: `${token}.${sign(token, config.sessionSecret)}`, csrfToken };
|
|
133
|
+
};
|
|
134
|
+
return { record, cookie: seal(record) };
|
|
71
135
|
},
|
|
72
136
|
resolve(cookieValue) {
|
|
73
|
-
|
|
74
|
-
if (!token)
|
|
75
|
-
return null;
|
|
76
|
-
const record = sessions.find(sha256(token));
|
|
77
|
-
if (!record)
|
|
137
|
+
if (!cookieValue)
|
|
78
138
|
return null;
|
|
79
|
-
const
|
|
80
|
-
if (Date.parse(record.expiresAt) <= now)
|
|
81
|
-
sessions.delete(record.id);
|
|
139
|
+
const record = open(cookieValue);
|
|
140
|
+
if (!record || Date.parse(record.expiresAt) <= Date.now())
|
|
82
141
|
return null;
|
|
83
|
-
}
|
|
84
|
-
if (now - Date.parse(record.lastSeenAt) > TOUCH_INTERVAL_MS) {
|
|
85
|
-
const expiresAt = nextExpiry(Date.parse(record.createdAt), now);
|
|
86
|
-
sessions.touch(record.id, new Date(now).toISOString(), expiresAt);
|
|
87
|
-
return { ...record, lastSeenAt: new Date(now).toISOString(), expiresAt };
|
|
88
|
-
}
|
|
89
142
|
return record;
|
|
90
143
|
},
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
144
|
+
refresh(record) {
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
if (now - Date.parse(record.refreshedAt) < REFRESH_INTERVAL_MS)
|
|
147
|
+
return null;
|
|
148
|
+
const next = {
|
|
149
|
+
...record,
|
|
150
|
+
refreshedAt: new Date(now).toISOString(),
|
|
151
|
+
expiresAt: nextExpiry(Date.parse(record.createdAt), now),
|
|
152
|
+
};
|
|
153
|
+
return { record: next, cookie: seal(next) };
|
|
98
154
|
},
|
|
99
155
|
sessionCookieOptions() {
|
|
100
156
|
return {
|
|
@@ -127,9 +183,6 @@ export function createSessionService(sessions, config) {
|
|
|
127
183
|
maxAge: 0,
|
|
128
184
|
};
|
|
129
185
|
},
|
|
130
|
-
purgeExpired() {
|
|
131
|
-
return sessions.deleteExpired();
|
|
132
|
-
},
|
|
133
186
|
};
|
|
134
187
|
}
|
|
135
188
|
/* ------------------------------------------------------------------ *
|
|
@@ -137,14 +190,8 @@ export function createSessionService(sessions, config) {
|
|
|
137
190
|
* ------------------------------------------------------------------ */
|
|
138
191
|
/** Issues the session cookies for a browser that has just been identified. */
|
|
139
192
|
export function establishSession(c, sessions, identity) {
|
|
140
|
-
|
|
141
|
-
sessions.destroy(getCookie(c, SESSION_COOKIE));
|
|
142
|
-
const { cookie, csrfToken } = sessions.create({
|
|
143
|
-
identity,
|
|
144
|
-
ip: clientIp(c),
|
|
145
|
-
userAgent: c.req.header("user-agent") ?? null,
|
|
146
|
-
});
|
|
193
|
+
const { record, cookie } = sessions.create(identity);
|
|
147
194
|
setCookie(c, SESSION_COOKIE, cookie, sessions.sessionCookieOptions());
|
|
148
|
-
setCookie(c, CSRF_COOKIE, csrfToken, sessions.csrfCookieOptions());
|
|
195
|
+
setCookie(c, CSRF_COOKIE, record.csrfToken, sessions.csrfCookieOptions());
|
|
149
196
|
}
|
|
150
197
|
//# sourceMappingURL=session.js.map
|
package/dist/session.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@micacms/auth-oidc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The server-only OIDC gate for Mica: sign-in, sessions and admission around @micacms/runtime. Never enters a browser bundle, and never a dependency of the local editor.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"hono": "^4.6.20",
|
|
29
29
|
"openid-client": "^6.8.5",
|
|
30
30
|
"zod": "^4.3.6",
|
|
31
|
-
"@micacms/
|
|
32
|
-
"@micacms/
|
|
31
|
+
"@micacms/runtime": "0.3.0",
|
|
32
|
+
"@micacms/core": "0.3.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "^22.20.1",
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session repository.
|
|
3
|
-
*
|
|
4
|
-
* The primary key is the SHA-256 of the cookie token, never the token itself:
|
|
5
|
-
* a leaked database backup therefore does not hand an attacker usable sessions.
|
|
6
|
-
*
|
|
7
|
-
* A row carries who the provider said this is, because there is no user table
|
|
8
|
-
* to look it up in. That is the whole of Mica's identity storage.
|
|
9
|
-
*/
|
|
10
|
-
import type { SqlDatabase } from "@micacms/runtime";
|
|
11
|
-
/**
|
|
12
|
-
* What the identity provider stated about the person holding this session.
|
|
13
|
-
*
|
|
14
|
-
* `null` where there is no provider: local mode has no user, and the row is
|
|
15
|
-
* then a CSRF token with an expiry.
|
|
16
|
-
*/
|
|
17
|
-
export interface SessionIdentity {
|
|
18
|
-
issuer: string;
|
|
19
|
-
/** Opaque and stable at that issuer. The account key, and never the email. */
|
|
20
|
-
subject: string;
|
|
21
|
-
email: string;
|
|
22
|
-
/** The provider's own `email_verified`. Absent counts as false. */
|
|
23
|
-
emailVerified: boolean;
|
|
24
|
-
name: string;
|
|
25
|
-
/** The groups claim as it arrived, or empty when the provider sends none. */
|
|
26
|
-
groups: string[];
|
|
27
|
-
}
|
|
28
|
-
export interface SessionRecord {
|
|
29
|
-
id: string;
|
|
30
|
-
identity: SessionIdentity | null;
|
|
31
|
-
csrfToken: string;
|
|
32
|
-
createdAt: string;
|
|
33
|
-
lastSeenAt: string;
|
|
34
|
-
expiresAt: string;
|
|
35
|
-
ip: string | null;
|
|
36
|
-
userAgent: string | null;
|
|
37
|
-
}
|
|
38
|
-
export declare function createSessionRepository(db: SqlDatabase): {
|
|
39
|
-
create(input: {
|
|
40
|
-
id: string;
|
|
41
|
-
identity: SessionIdentity | null;
|
|
42
|
-
csrfToken: string;
|
|
43
|
-
expiresAt: string;
|
|
44
|
-
ip?: string | null;
|
|
45
|
-
userAgent?: string | null;
|
|
46
|
-
}): SessionRecord;
|
|
47
|
-
find(id: string): SessionRecord | undefined;
|
|
48
|
-
touch(id: string, lastSeenAt: string, expiresAt: string): void;
|
|
49
|
-
delete(id: string): boolean;
|
|
50
|
-
/** Every session this person holds, on every device. */
|
|
51
|
-
deleteForIdentity(issuer: string, subject: string): number;
|
|
52
|
-
deleteExpired(now?: string): number;
|
|
53
|
-
count(): number;
|
|
54
|
-
};
|
|
55
|
-
export type SessionRepository = ReturnType<typeof createSessionRepository>;
|
|
56
|
-
//# sourceMappingURL=sessions-repository.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sessions-repository.d.ts","sourceRoot":"","sources":["../src/sessions-repository.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,aAAa,EAAE,OAAO,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AA2DD,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,WAAW;kBAErC;QACZ,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;QACjC,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC3B,GAAG,aAAa;aAwBR,MAAM,GAAG,aAAa,GAAG,SAAS;cAKjC,MAAM,cAAc,MAAM,aAAa,MAAM,GAAG,IAAI;eAQnD,MAAM,GAAG,OAAO;IAI3B,wDAAwD;8BAC9B,MAAM,WAAW,MAAM,GAAG,MAAM;iCAKX,MAAM;aAI5C,MAAM;EAIlB;AAED,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC"}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session repository.
|
|
3
|
-
*
|
|
4
|
-
* The primary key is the SHA-256 of the cookie token, never the token itself:
|
|
5
|
-
* a leaked database backup therefore does not hand an attacker usable sessions.
|
|
6
|
-
*
|
|
7
|
-
* A row carries who the provider said this is, because there is no user table
|
|
8
|
-
* to look it up in. That is the whole of Mica's identity storage.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* The groups column holds whatever the provider sent, serialised. It is read
|
|
12
|
-
* back defensively — a claim that was a string, or an object, or absent, must
|
|
13
|
-
* not throw four weeks later on a request that has nothing to do with it.
|
|
14
|
-
*/
|
|
15
|
-
function parseGroups(value) {
|
|
16
|
-
try {
|
|
17
|
-
const parsed = JSON.parse(value);
|
|
18
|
-
return Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === "string") : [];
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function toRecord(row) {
|
|
25
|
-
return {
|
|
26
|
-
id: row.id,
|
|
27
|
-
identity: row.issuer && row.subject
|
|
28
|
-
? {
|
|
29
|
-
issuer: row.issuer,
|
|
30
|
-
subject: row.subject,
|
|
31
|
-
email: row.email ?? "",
|
|
32
|
-
emailVerified: row.email_verified === 1,
|
|
33
|
-
name: row.name ?? "",
|
|
34
|
-
groups: parseGroups(row.groups),
|
|
35
|
-
}
|
|
36
|
-
: null,
|
|
37
|
-
csrfToken: row.csrf_token,
|
|
38
|
-
createdAt: row.created_at,
|
|
39
|
-
lastSeenAt: row.last_seen_at,
|
|
40
|
-
expiresAt: row.expires_at,
|
|
41
|
-
ip: row.ip,
|
|
42
|
-
userAgent: row.user_agent,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
const COLUMNS = "id, issuer, subject, email, email_verified, name, groups, csrf_token, " +
|
|
46
|
-
"created_at, last_seen_at, expires_at, ip, user_agent";
|
|
47
|
-
export function createSessionRepository(db) {
|
|
48
|
-
return {
|
|
49
|
-
create(input) {
|
|
50
|
-
const now = new Date().toISOString();
|
|
51
|
-
db.run(`INSERT INTO sessions
|
|
52
|
-
(${COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
53
|
-
input.id,
|
|
54
|
-
input.identity?.issuer ?? null,
|
|
55
|
-
input.identity?.subject ?? null,
|
|
56
|
-
input.identity?.email ?? null,
|
|
57
|
-
input.identity?.emailVerified ? 1 : 0,
|
|
58
|
-
input.identity?.name ?? null,
|
|
59
|
-
JSON.stringify(input.identity?.groups ?? []),
|
|
60
|
-
input.csrfToken,
|
|
61
|
-
now,
|
|
62
|
-
now,
|
|
63
|
-
input.expiresAt,
|
|
64
|
-
input.ip ?? null,
|
|
65
|
-
input.userAgent ?? null,
|
|
66
|
-
]);
|
|
67
|
-
return this.find(input.id);
|
|
68
|
-
},
|
|
69
|
-
find(id) {
|
|
70
|
-
const row = db.get(`SELECT ${COLUMNS} FROM sessions WHERE id = ?`, [id]);
|
|
71
|
-
return row && toRecord(row);
|
|
72
|
-
},
|
|
73
|
-
touch(id, lastSeenAt, expiresAt) {
|
|
74
|
-
db.run("UPDATE sessions SET last_seen_at = ?, expires_at = ? WHERE id = ?", [
|
|
75
|
-
lastSeenAt,
|
|
76
|
-
expiresAt,
|
|
77
|
-
id,
|
|
78
|
-
]);
|
|
79
|
-
},
|
|
80
|
-
delete(id) {
|
|
81
|
-
return db.run("DELETE FROM sessions WHERE id = ?", [id]).changes > 0;
|
|
82
|
-
},
|
|
83
|
-
/** Every session this person holds, on every device. */
|
|
84
|
-
deleteForIdentity(issuer, subject) {
|
|
85
|
-
return db.run("DELETE FROM sessions WHERE issuer = ? AND subject = ?", [issuer, subject])
|
|
86
|
-
.changes;
|
|
87
|
-
},
|
|
88
|
-
deleteExpired(now = new Date().toISOString()) {
|
|
89
|
-
return db.run("DELETE FROM sessions WHERE expires_at <= ?", [now]).changes;
|
|
90
|
-
},
|
|
91
|
-
count() {
|
|
92
|
-
return db.get("SELECT COUNT(*) AS count FROM sessions")?.count ?? 0;
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
//# sourceMappingURL=sessions-repository.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sessions-repository.js","sourceRoot":"","sources":["../src/sessions-repository.ts"],"names":[],"mappings":"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"}
|