@facilis.dev/auth 0.0.0-stage → 0.1.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/README.md +18 -2
- package/dist/api-clients.d.ts +71 -0
- package/dist/api-clients.js +186 -0
- package/dist/crypto.d.ts +31 -0
- package/dist/crypto.js +76 -0
- package/dist/email-verification.d.ts +45 -0
- package/dist/email-verification.js +72 -0
- package/dist/email.d.ts +7 -0
- package/dist/email.js +9 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/password.d.ts +7 -0
- package/dist/password.js +54 -0
- package/dist/roles.d.ts +40 -0
- package/dist/roles.js +46 -0
- package/dist/seed.d.ts +28 -0
- package/dist/seed.js +115 -0
- package/dist/session.d.ts +66 -0
- package/dist/session.js +161 -0
- package/dist/sso/contracts.d.ts +52 -0
- package/dist/sso/contracts.js +8 -0
- package/dist/sso/errors.d.ts +22 -0
- package/dist/sso/errors.js +45 -0
- package/dist/sso/identity.d.ts +41 -0
- package/dist/sso/identity.js +162 -0
- package/dist/sso/index.d.ts +27 -0
- package/dist/sso/index.js +16 -0
- package/dist/sso/login.d.ts +60 -0
- package/dist/sso/login.js +209 -0
- package/dist/sso/oauth2.d.ts +52 -0
- package/dist/sso/oauth2.js +180 -0
- package/dist/sso/oidc.d.ts +60 -0
- package/dist/sso/oidc.js +201 -0
- package/dist/sso/paths.d.ts +14 -0
- package/dist/sso/paths.js +17 -0
- package/dist/sso/pkce.d.ts +6 -0
- package/dist/sso/pkce.js +21 -0
- package/dist/sso/providers.d.ts +100 -0
- package/dist/sso/providers.js +70 -0
- package/dist/sso/saml.d.ts +44 -0
- package/dist/sso/saml.js +159 -0
- package/dist/sso/state.d.ts +34 -0
- package/dist/sso/state.js +91 -0
- package/dist/sso/tables.d.ts +306 -0
- package/dist/sso/tables.js +59 -0
- package/dist/tables.d.ts +769 -0
- package/dist/tables.js +148 -0
- package/package.json +45 -4
- package/src/api-clients.ts +261 -0
- package/src/crypto.ts +101 -0
- package/src/email-verification.ts +107 -0
- package/src/email.ts +9 -0
- package/src/index.ts +63 -0
- package/src/password.ts +74 -0
- package/src/roles.ts +77 -0
- package/src/seed.ts +146 -0
- package/src/session.ts +209 -0
- package/src/sso/contracts.ts +71 -0
- package/src/sso/errors.ts +53 -0
- package/src/sso/identity.ts +220 -0
- package/src/sso/index.ts +105 -0
- package/src/sso/login.ts +296 -0
- package/src/sso/oauth2.ts +235 -0
- package/src/sso/oidc.ts +285 -0
- package/src/sso/paths.ts +21 -0
- package/src/sso/pkce.ts +30 -0
- package/src/sso/providers.ts +176 -0
- package/src/sso/saml.ts +222 -0
- package/src/sso/state.ts +114 -0
- package/src/sso/tables.ts +77 -0
- package/src/tables.ts +214 -0
- package/src/test-support.ts +149 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { SamlConfig } from "@node-saml/node-saml";
|
|
2
|
+
import type { IdentityClaims } from "./contracts.js";
|
|
3
|
+
import type { SamlProviderConfig } from "./providers.js";
|
|
4
|
+
export interface BuiltSamlRequest {
|
|
5
|
+
readonly requestId: string;
|
|
6
|
+
readonly redirectUrl: string;
|
|
7
|
+
}
|
|
8
|
+
/** The AuthnRequest XML (exported for tests; the IdP only ever sees the DEFLATE+base64 form). */
|
|
9
|
+
export declare function buildAuthnRequestXml(provider: SamlProviderConfig, requestId: string, acsUrl: string, issueInstant: string): string;
|
|
10
|
+
/** Builds the HTTP-Redirect URL carrying the deflated AuthnRequest and the RelayState. */
|
|
11
|
+
export declare function buildSamlRedirect(provider: SamlProviderConfig, acsUrl: string, relayState: string, now?: number): Promise<BuiltSamlRequest>;
|
|
12
|
+
export interface SamlProfileLike {
|
|
13
|
+
readonly nameID?: unknown;
|
|
14
|
+
readonly issuer?: unknown;
|
|
15
|
+
[attribute: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
export interface SamlValidator {
|
|
18
|
+
validatePostResponseAsync(container: Record<string, string>): Promise<{
|
|
19
|
+
profile: SamlProfileLike | null;
|
|
20
|
+
loggedOut: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
export type SamlValidatorFactory = (config: SamlConfig) => SamlValidator | Promise<SamlValidator>;
|
|
24
|
+
export interface ValidateSamlInput {
|
|
25
|
+
readonly provider: SamlProviderConfig;
|
|
26
|
+
/** The IdP signing certificate (PEM or base64 DER). Never logged. */
|
|
27
|
+
readonly certificate: string;
|
|
28
|
+
readonly callbackUrl: string;
|
|
29
|
+
/** The base64 `SAMLResponse` form field. */
|
|
30
|
+
readonly samlResponse: string;
|
|
31
|
+
/** The AuthnRequest id stored in the consumed state row. */
|
|
32
|
+
readonly requestId: string | null;
|
|
33
|
+
/** Test seam: replace the lazily imported validator. */
|
|
34
|
+
readonly validatorFactory?: SamlValidatorFactory;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Validates an ACS POST and returns the asserted identity.
|
|
38
|
+
*
|
|
39
|
+
* Signature, recipient, audience and time-window checks are node-saml's; a rejected response
|
|
40
|
+
* (unsigned, tampered, wrong recipient/audience, expired) becomes `sso_exchange_failed`, never
|
|
41
|
+
* a library message. A signed assertion is the IdP asserting the email, so SAML has no separate
|
|
42
|
+
* `email_verified` flag and treats the address as verified.
|
|
43
|
+
*/
|
|
44
|
+
export declare function validateSamlResponse(input: ValidateSamlInput): Promise<IdentityClaims>;
|
package/dist/sso/saml.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { encodeBase64 } from "../crypto.js";
|
|
2
|
+
import { SsoError } from "./errors.js";
|
|
3
|
+
/**
|
|
4
|
+
* SAML 2.0 SP-initiated Web Browser SSO (D-120).
|
|
5
|
+
*
|
|
6
|
+
* The AuthnRequest is built here (HTTP-Redirect, DEFLATE+base64) so the framework owns the
|
|
7
|
+
* request id, the ACS URL and the RelayState. ACS validation is delegated to
|
|
8
|
+
* `@node-saml/node-saml`, imported LAZILY inside this path: a project without SAML never loads
|
|
9
|
+
* (or bundles) its CJS crypto tree. IdP-initiated assertions are not accepted: there is no
|
|
10
|
+
* unsolicited-response surface, and the single-use RelayState is mandatory.
|
|
11
|
+
*/
|
|
12
|
+
const CLOCK_SKEW_MS = 5000;
|
|
13
|
+
const NAME_ID_FORMAT = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress";
|
|
14
|
+
const PROTOCOL_BINDING_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST";
|
|
15
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+$/;
|
|
16
|
+
function escapeXml(value) {
|
|
17
|
+
return value
|
|
18
|
+
.replaceAll("&", "&")
|
|
19
|
+
.replaceAll("<", "<")
|
|
20
|
+
.replaceAll(">", ">")
|
|
21
|
+
.replaceAll('"', """)
|
|
22
|
+
.replaceAll("'", "'");
|
|
23
|
+
}
|
|
24
|
+
/** The AuthnRequest XML (exported for tests; the IdP only ever sees the DEFLATE+base64 form). */
|
|
25
|
+
export function buildAuthnRequestXml(provider, requestId, acsUrl, issueInstant) {
|
|
26
|
+
return (`<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ` +
|
|
27
|
+
`xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="${escapeXml(requestId)}" ` +
|
|
28
|
+
`Version="2.0" IssueInstant="${issueInstant}" Destination="${escapeXml(provider.entryPoint)}" ` +
|
|
29
|
+
`AssertionConsumerServiceURL="${escapeXml(acsUrl)}" ProtocolBinding="${PROTOCOL_BINDING_POST}">` +
|
|
30
|
+
`<saml:Issuer>${escapeXml(provider.issuer)}</saml:Issuer>` +
|
|
31
|
+
`<samlp:NameIDPolicy AllowCreate="true" Format="${NAME_ID_FORMAT}"/>` +
|
|
32
|
+
`</samlp:AuthnRequest>`);
|
|
33
|
+
}
|
|
34
|
+
async function deflateRaw(bytes) {
|
|
35
|
+
// node:zlib is a server-only builtin and is loaded with the SAML path, never at module load.
|
|
36
|
+
const { deflateRawSync } = await import("node:zlib");
|
|
37
|
+
return new Uint8Array(deflateRawSync(bytes));
|
|
38
|
+
}
|
|
39
|
+
/** Builds the HTTP-Redirect URL carrying the deflated AuthnRequest and the RelayState. */
|
|
40
|
+
export async function buildSamlRedirect(provider, acsUrl, relayState, now = Date.now()) {
|
|
41
|
+
const requestId = `_${globalThis.crypto.randomUUID()}`;
|
|
42
|
+
const xml = buildAuthnRequestXml(provider, requestId, acsUrl, new Date(now).toISOString());
|
|
43
|
+
const compressed = await deflateRaw(new TextEncoder().encode(xml));
|
|
44
|
+
const url = new URL(provider.entryPoint);
|
|
45
|
+
url.searchParams.set("SAMLRequest", encodeBase64(compressed));
|
|
46
|
+
url.searchParams.set("RelayState", relayState);
|
|
47
|
+
return { requestId, redirectUrl: url.toString() };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A cache provider whose only entry is the request id of THIS attempt.
|
|
51
|
+
*
|
|
52
|
+
* `validateInResponseTo: "always"` (D-147) requires the response to carry the InResponseTo of the
|
|
53
|
+
* AuthnRequest this framework issued and to match the single-use state row; a response without
|
|
54
|
+
* it (IdP-initiated / unsolicited, or a replayed assertion) is rejected by node-saml.
|
|
55
|
+
*/
|
|
56
|
+
function singleUseRequestCache(requestId) {
|
|
57
|
+
let remaining = requestId;
|
|
58
|
+
const take = async (key) => {
|
|
59
|
+
if (key === null || remaining === null || key !== remaining)
|
|
60
|
+
return null;
|
|
61
|
+
const value = remaining;
|
|
62
|
+
remaining = null;
|
|
63
|
+
return value;
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
async saveAsync() {
|
|
67
|
+
return null;
|
|
68
|
+
},
|
|
69
|
+
async getAsync(key) {
|
|
70
|
+
return key === remaining ? remaining : null;
|
|
71
|
+
},
|
|
72
|
+
async removeAsync(key) {
|
|
73
|
+
return take(key);
|
|
74
|
+
},
|
|
75
|
+
async consumeAsync(key) {
|
|
76
|
+
return take(key);
|
|
77
|
+
},
|
|
78
|
+
async saveIfAbsentAsync() {
|
|
79
|
+
return false;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function toSamlConfig(provider, certificate, callbackUrl, requestId) {
|
|
84
|
+
return {
|
|
85
|
+
callbackUrl,
|
|
86
|
+
entryPoint: provider.entryPoint,
|
|
87
|
+
issuer: provider.issuer,
|
|
88
|
+
audience: provider.issuer,
|
|
89
|
+
idpCert: certificate,
|
|
90
|
+
wantAssertionsSigned: provider.wantAssertionsSigned,
|
|
91
|
+
wantAuthnResponseSigned: provider.wantResponseSigned,
|
|
92
|
+
acceptedClockSkewMs: CLOCK_SKEW_MS,
|
|
93
|
+
// The library's enum is a runtime value; the string literal is the same member.
|
|
94
|
+
validateInResponseTo: "always",
|
|
95
|
+
cacheProvider: singleUseRequestCache(requestId),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async function defaultSamlFactory(config) {
|
|
99
|
+
const { SAML } = await import("@node-saml/node-saml");
|
|
100
|
+
return new SAML(config);
|
|
101
|
+
}
|
|
102
|
+
function asString(value) {
|
|
103
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
104
|
+
return value;
|
|
105
|
+
if (Array.isArray(value)) {
|
|
106
|
+
const first = value.find((entry) => typeof entry === "string" && entry.length > 0);
|
|
107
|
+
return typeof first === "string" ? first : undefined;
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
/** Case-insensitive attribute lookup; `nameid` means the NameID itself. */
|
|
112
|
+
function readAttribute(profile, attribute) {
|
|
113
|
+
const wanted = attribute.toLowerCase();
|
|
114
|
+
if (wanted === "nameid")
|
|
115
|
+
return asString(profile.nameID);
|
|
116
|
+
for (const [key, value] of Object.entries(profile)) {
|
|
117
|
+
if (key.toLowerCase() !== wanted)
|
|
118
|
+
continue;
|
|
119
|
+
const found = asString(value);
|
|
120
|
+
if (found !== undefined)
|
|
121
|
+
return found;
|
|
122
|
+
}
|
|
123
|
+
for (const fallback of ["email", "mail", "emailaddress"]) {
|
|
124
|
+
const found = asString(profile[fallback]);
|
|
125
|
+
if (found !== undefined)
|
|
126
|
+
return found;
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Validates an ACS POST and returns the asserted identity.
|
|
132
|
+
*
|
|
133
|
+
* Signature, recipient, audience and time-window checks are node-saml's; a rejected response
|
|
134
|
+
* (unsigned, tampered, wrong recipient/audience, expired) becomes `sso_exchange_failed`, never
|
|
135
|
+
* a library message. A signed assertion is the IdP asserting the email, so SAML has no separate
|
|
136
|
+
* `email_verified` flag and treats the address as verified.
|
|
137
|
+
*/
|
|
138
|
+
export async function validateSamlResponse(input) {
|
|
139
|
+
const config = toSamlConfig(input.provider, input.certificate, input.callbackUrl, input.requestId);
|
|
140
|
+
const factory = input.validatorFactory ?? defaultSamlFactory;
|
|
141
|
+
let result;
|
|
142
|
+
try {
|
|
143
|
+
const validator = await factory(config);
|
|
144
|
+
result = await validator.validatePostResponseAsync({ SAMLResponse: input.samlResponse });
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
throw new SsoError("sso_exchange_failed", "SAML response rejected", { cause: error });
|
|
148
|
+
}
|
|
149
|
+
if (result.profile === null || result.loggedOut) {
|
|
150
|
+
throw new SsoError("sso_exchange_failed", "SAML response has no assertion");
|
|
151
|
+
}
|
|
152
|
+
const profile = result.profile;
|
|
153
|
+
const email = readAttribute(profile, input.provider.emailAttribute);
|
|
154
|
+
if (email === undefined || !EMAIL_PATTERN.test(email)) {
|
|
155
|
+
throw new SsoError("sso_identity_invalid", "SAML assertion has no email");
|
|
156
|
+
}
|
|
157
|
+
const subject = asString(profile.nameID) ?? email;
|
|
158
|
+
return { provider: input.provider.id, subject, email, emailVerified: true };
|
|
159
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SystemRunner } from "./contracts.js";
|
|
2
|
+
import { type AuthSsoStateRow } from "./tables.js";
|
|
3
|
+
/** Contract cap on the post-login path (same-origin only; D-120). */
|
|
4
|
+
export declare const MAX_REDIRECT_TO = 2048;
|
|
5
|
+
/**
|
|
6
|
+
* Validates a post-login redirect: a same-origin absolute path only.
|
|
7
|
+
*
|
|
8
|
+
* Rejecting `//host`, a backslash and control characters closes the open-redirect hole: the value
|
|
9
|
+
* becomes the `Location` of a 302 and the user must never leave the application origin.
|
|
10
|
+
*/
|
|
11
|
+
export declare function normalizeRedirectTo(value: unknown): string | null;
|
|
12
|
+
export interface CreateSsoStateInput {
|
|
13
|
+
/** Explicit token (SAML builds the AuthnRequest, whose RelayState is the state, before insert). */
|
|
14
|
+
readonly state?: string;
|
|
15
|
+
readonly provider: string;
|
|
16
|
+
/** `auth.sso.stateTtlMs`. */
|
|
17
|
+
readonly ttlMs: number;
|
|
18
|
+
readonly redirectTo: string | null;
|
|
19
|
+
readonly nonce?: string;
|
|
20
|
+
readonly codeVerifier?: string;
|
|
21
|
+
readonly requestId?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Creates one single-use state row (32 random bytes, base64url) inside the audited system
|
|
25
|
+
* elevation. Expired rows are pruned on the same write, so a start request also cleans up.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createSsoState(system: SystemRunner, input: CreateSsoStateInput, now?: number): Promise<AuthSsoStateRow>;
|
|
28
|
+
/**
|
|
29
|
+
* Consumes a state row atomically (`DELETE ... RETURNING`) and returns it once.
|
|
30
|
+
*
|
|
31
|
+
* A missing row (already used, unknown, or never issued) and an expired row are both
|
|
32
|
+
* `sso_state_invalid`: the caller only learns "this attempt is not valid".
|
|
33
|
+
*/
|
|
34
|
+
export declare function consumeSsoState(system: SystemRunner, state: string, now?: number): Promise<AuthSsoStateRow>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { eq, lt } from "drizzle-orm";
|
|
2
|
+
import { randomToken } from "../crypto.js";
|
|
3
|
+
import { SsoError } from "./errors.js";
|
|
4
|
+
import { authSsoStates } from "./tables.js";
|
|
5
|
+
/** Contract cap on the post-login path (same-origin only; D-120). */
|
|
6
|
+
export const MAX_REDIRECT_TO = 2048;
|
|
7
|
+
const STATE_TOKEN_PATTERN = /^[A-Za-z0-9_-]{20,256}$/;
|
|
8
|
+
const STATE_TABLES = ["auth_sso_states"];
|
|
9
|
+
/**
|
|
10
|
+
* Validates a post-login redirect: a same-origin absolute path only.
|
|
11
|
+
*
|
|
12
|
+
* Rejecting `//host`, a backslash and control characters closes the open-redirect hole: the value
|
|
13
|
+
* becomes the `Location` of a 302 and the user must never leave the application origin.
|
|
14
|
+
*/
|
|
15
|
+
export function normalizeRedirectTo(value) {
|
|
16
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_REDIRECT_TO)
|
|
17
|
+
return null;
|
|
18
|
+
if (!value.startsWith("/") || value.startsWith("//"))
|
|
19
|
+
return null;
|
|
20
|
+
if (value.includes("\\"))
|
|
21
|
+
return null;
|
|
22
|
+
// Whitespace and control characters have no business in an origin-relative path; browsers
|
|
23
|
+
// trim some of them, so refuse instead of relying on normalisation.
|
|
24
|
+
for (const character of value) {
|
|
25
|
+
const code = character.codePointAt(0) ?? 0;
|
|
26
|
+
if (code <= 0x20 || code === 0x7f)
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function stateElevation(operation) {
|
|
32
|
+
return {
|
|
33
|
+
actor: "system:auth:sso",
|
|
34
|
+
operation,
|
|
35
|
+
reason: "The SSO login attempt is framework-owned state written before a session exists.",
|
|
36
|
+
tables: STATE_TABLES,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Creates one single-use state row (32 random bytes, base64url) inside the audited system
|
|
41
|
+
* elevation. Expired rows are pruned on the same write, so a start request also cleans up.
|
|
42
|
+
*/
|
|
43
|
+
export async function createSsoState(system, input, now = Date.now()) {
|
|
44
|
+
const state = input.state ?? randomToken(32);
|
|
45
|
+
if (!STATE_TOKEN_PATTERN.test(state))
|
|
46
|
+
throw new SsoError("sso_internal_error", "bad state token");
|
|
47
|
+
const createdAt = new Date(now);
|
|
48
|
+
const expiresAt = new Date(now + input.ttlMs);
|
|
49
|
+
const row = await system(stateElevation("auth:sso:state:create"), async (db) => {
|
|
50
|
+
await db.delete(authSsoStates).where(lt(authSsoStates.expiresAt, createdAt));
|
|
51
|
+
const inserted = await db
|
|
52
|
+
.insert(authSsoStates)
|
|
53
|
+
.values({
|
|
54
|
+
state,
|
|
55
|
+
provider: input.provider,
|
|
56
|
+
nonce: input.nonce ?? null,
|
|
57
|
+
codeVerifier: input.codeVerifier ?? null,
|
|
58
|
+
requestId: input.requestId ?? null,
|
|
59
|
+
redirectTo: input.redirectTo,
|
|
60
|
+
createdAt,
|
|
61
|
+
expiresAt,
|
|
62
|
+
})
|
|
63
|
+
.returning();
|
|
64
|
+
return inserted[0] ?? null;
|
|
65
|
+
});
|
|
66
|
+
if (row === null)
|
|
67
|
+
throw new SsoError("sso_internal_error");
|
|
68
|
+
return row;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Consumes a state row atomically (`DELETE ... RETURNING`) and returns it once.
|
|
72
|
+
*
|
|
73
|
+
* A missing row (already used, unknown, or never issued) and an expired row are both
|
|
74
|
+
* `sso_state_invalid`: the caller only learns "this attempt is not valid".
|
|
75
|
+
*/
|
|
76
|
+
export async function consumeSsoState(system, state, now = Date.now()) {
|
|
77
|
+
if (!STATE_TOKEN_PATTERN.test(state))
|
|
78
|
+
throw new SsoError("sso_state_invalid");
|
|
79
|
+
const row = await system(stateElevation("auth:sso:state:consume"), async (db) => {
|
|
80
|
+
const deleted = await db
|
|
81
|
+
.delete(authSsoStates)
|
|
82
|
+
.where(eq(authSsoStates.state, state))
|
|
83
|
+
.returning();
|
|
84
|
+
return deleted[0] ?? null;
|
|
85
|
+
});
|
|
86
|
+
if (row === null)
|
|
87
|
+
throw new SsoError("sso_state_invalid");
|
|
88
|
+
if (row.expiresAt.getTime() <= now)
|
|
89
|
+
throw new SsoError("sso_state_invalid");
|
|
90
|
+
return row;
|
|
91
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSO identity links (D-120, SPEC §5.3): one row per `(provider, subject)` pair.
|
|
3
|
+
*
|
|
4
|
+
* The identity key is the IdP's stable subject, never the email: a user who changes their email
|
|
5
|
+
* at the IdP keeps the link. `email` is the address reported at the last sign-in, for operators.
|
|
6
|
+
*
|
|
7
|
+
* RLS matches `users`: restrictive tenant isolation AND-ed with a permissive "tenant scope
|
|
8
|
+
* present" policy, because Postgres denies by default when no permissive policy applies.
|
|
9
|
+
*/
|
|
10
|
+
export declare const authIdentities: Omit<import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
11
|
+
name: "auth_identities";
|
|
12
|
+
schema: undefined;
|
|
13
|
+
columns: {
|
|
14
|
+
provider: import("drizzle-orm/pg-core").PgColumn<{
|
|
15
|
+
name: "provider";
|
|
16
|
+
tableName: "auth_identities";
|
|
17
|
+
dataType: "string";
|
|
18
|
+
columnType: "PgText";
|
|
19
|
+
data: string;
|
|
20
|
+
driverParam: string;
|
|
21
|
+
notNull: true;
|
|
22
|
+
hasDefault: false;
|
|
23
|
+
isPrimaryKey: false;
|
|
24
|
+
isAutoincrement: false;
|
|
25
|
+
hasRuntimeDefault: false;
|
|
26
|
+
enumValues: [string, ...string[]];
|
|
27
|
+
baseColumn: never;
|
|
28
|
+
identity: undefined;
|
|
29
|
+
generated: undefined;
|
|
30
|
+
}, {}, {}>;
|
|
31
|
+
subject: import("drizzle-orm/pg-core").PgColumn<{
|
|
32
|
+
name: "subject";
|
|
33
|
+
tableName: "auth_identities";
|
|
34
|
+
dataType: "string";
|
|
35
|
+
columnType: "PgText";
|
|
36
|
+
data: string;
|
|
37
|
+
driverParam: string;
|
|
38
|
+
notNull: true;
|
|
39
|
+
hasDefault: false;
|
|
40
|
+
isPrimaryKey: false;
|
|
41
|
+
isAutoincrement: false;
|
|
42
|
+
hasRuntimeDefault: false;
|
|
43
|
+
enumValues: [string, ...string[]];
|
|
44
|
+
baseColumn: never;
|
|
45
|
+
identity: undefined;
|
|
46
|
+
generated: undefined;
|
|
47
|
+
}, {}, {}>;
|
|
48
|
+
userId: import("drizzle-orm/pg-core").PgColumn<{
|
|
49
|
+
name: "user_id";
|
|
50
|
+
tableName: "auth_identities";
|
|
51
|
+
dataType: "string";
|
|
52
|
+
columnType: "PgUUID";
|
|
53
|
+
data: string;
|
|
54
|
+
driverParam: string;
|
|
55
|
+
notNull: true;
|
|
56
|
+
hasDefault: false;
|
|
57
|
+
isPrimaryKey: false;
|
|
58
|
+
isAutoincrement: false;
|
|
59
|
+
hasRuntimeDefault: false;
|
|
60
|
+
enumValues: undefined;
|
|
61
|
+
baseColumn: never;
|
|
62
|
+
identity: undefined;
|
|
63
|
+
generated: undefined;
|
|
64
|
+
}, {}, {}>;
|
|
65
|
+
email: import("drizzle-orm/pg-core").PgColumn<{
|
|
66
|
+
name: "email";
|
|
67
|
+
tableName: "auth_identities";
|
|
68
|
+
dataType: "string";
|
|
69
|
+
columnType: "PgText";
|
|
70
|
+
data: string;
|
|
71
|
+
driverParam: string;
|
|
72
|
+
notNull: true;
|
|
73
|
+
hasDefault: false;
|
|
74
|
+
isPrimaryKey: false;
|
|
75
|
+
isAutoincrement: false;
|
|
76
|
+
hasRuntimeDefault: false;
|
|
77
|
+
enumValues: [string, ...string[]];
|
|
78
|
+
baseColumn: never;
|
|
79
|
+
identity: undefined;
|
|
80
|
+
generated: undefined;
|
|
81
|
+
}, {}, {}>;
|
|
82
|
+
tenantId: import("drizzle-orm/pg-core").PgColumn<{
|
|
83
|
+
name: string;
|
|
84
|
+
tableName: "auth_identities";
|
|
85
|
+
dataType: "string";
|
|
86
|
+
columnType: "PgUUID";
|
|
87
|
+
data: string;
|
|
88
|
+
driverParam: string;
|
|
89
|
+
notNull: true;
|
|
90
|
+
hasDefault: true;
|
|
91
|
+
isPrimaryKey: false;
|
|
92
|
+
isAutoincrement: false;
|
|
93
|
+
hasRuntimeDefault: false;
|
|
94
|
+
enumValues: undefined;
|
|
95
|
+
baseColumn: never;
|
|
96
|
+
identity: undefined;
|
|
97
|
+
generated: undefined;
|
|
98
|
+
}, {}, {}>;
|
|
99
|
+
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
100
|
+
name: "created_at";
|
|
101
|
+
tableName: "auth_identities";
|
|
102
|
+
dataType: "date";
|
|
103
|
+
columnType: "PgTimestamp";
|
|
104
|
+
data: Date;
|
|
105
|
+
driverParam: string;
|
|
106
|
+
notNull: true;
|
|
107
|
+
hasDefault: true;
|
|
108
|
+
isPrimaryKey: false;
|
|
109
|
+
isAutoincrement: false;
|
|
110
|
+
hasRuntimeDefault: false;
|
|
111
|
+
enumValues: undefined;
|
|
112
|
+
baseColumn: never;
|
|
113
|
+
identity: undefined;
|
|
114
|
+
generated: undefined;
|
|
115
|
+
}, {}, {}>;
|
|
116
|
+
lastLoginAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
117
|
+
name: "last_login_at";
|
|
118
|
+
tableName: "auth_identities";
|
|
119
|
+
dataType: "date";
|
|
120
|
+
columnType: "PgTimestamp";
|
|
121
|
+
data: Date;
|
|
122
|
+
driverParam: string;
|
|
123
|
+
notNull: true;
|
|
124
|
+
hasDefault: true;
|
|
125
|
+
isPrimaryKey: false;
|
|
126
|
+
isAutoincrement: false;
|
|
127
|
+
hasRuntimeDefault: false;
|
|
128
|
+
enumValues: undefined;
|
|
129
|
+
baseColumn: never;
|
|
130
|
+
identity: undefined;
|
|
131
|
+
generated: undefined;
|
|
132
|
+
}, {}, {}>;
|
|
133
|
+
};
|
|
134
|
+
dialect: 'pg';
|
|
135
|
+
}>, "enableRLS">;
|
|
136
|
+
/**
|
|
137
|
+
* One SSO login attempt (D-120): single-use state plus whatever the protocol needs to finish
|
|
138
|
+
* (`nonce`/`code_verifier` for OIDC, `request_id` for SAML, `redirect_to` for the browser).
|
|
139
|
+
*
|
|
140
|
+
* The row is written and consumed inside the audited `ctx.system` elevation, so RLS is not the
|
|
141
|
+
* load-bearing check here, but every scoped table ships the standard policies (SPEC §8.3).
|
|
142
|
+
*/
|
|
143
|
+
export declare const authSsoStates: Omit<import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
144
|
+
name: "auth_sso_states";
|
|
145
|
+
schema: undefined;
|
|
146
|
+
columns: {
|
|
147
|
+
state: import("drizzle-orm/pg-core").PgColumn<{
|
|
148
|
+
name: "state";
|
|
149
|
+
tableName: "auth_sso_states";
|
|
150
|
+
dataType: "string";
|
|
151
|
+
columnType: "PgText";
|
|
152
|
+
data: string;
|
|
153
|
+
driverParam: string;
|
|
154
|
+
notNull: true;
|
|
155
|
+
hasDefault: false;
|
|
156
|
+
isPrimaryKey: true;
|
|
157
|
+
isAutoincrement: false;
|
|
158
|
+
hasRuntimeDefault: false;
|
|
159
|
+
enumValues: [string, ...string[]];
|
|
160
|
+
baseColumn: never;
|
|
161
|
+
identity: undefined;
|
|
162
|
+
generated: undefined;
|
|
163
|
+
}, {}, {}>;
|
|
164
|
+
provider: import("drizzle-orm/pg-core").PgColumn<{
|
|
165
|
+
name: "provider";
|
|
166
|
+
tableName: "auth_sso_states";
|
|
167
|
+
dataType: "string";
|
|
168
|
+
columnType: "PgText";
|
|
169
|
+
data: string;
|
|
170
|
+
driverParam: string;
|
|
171
|
+
notNull: true;
|
|
172
|
+
hasDefault: false;
|
|
173
|
+
isPrimaryKey: false;
|
|
174
|
+
isAutoincrement: false;
|
|
175
|
+
hasRuntimeDefault: false;
|
|
176
|
+
enumValues: [string, ...string[]];
|
|
177
|
+
baseColumn: never;
|
|
178
|
+
identity: undefined;
|
|
179
|
+
generated: undefined;
|
|
180
|
+
}, {}, {}>;
|
|
181
|
+
nonce: import("drizzle-orm/pg-core").PgColumn<{
|
|
182
|
+
name: "nonce";
|
|
183
|
+
tableName: "auth_sso_states";
|
|
184
|
+
dataType: "string";
|
|
185
|
+
columnType: "PgText";
|
|
186
|
+
data: string;
|
|
187
|
+
driverParam: string;
|
|
188
|
+
notNull: false;
|
|
189
|
+
hasDefault: false;
|
|
190
|
+
isPrimaryKey: false;
|
|
191
|
+
isAutoincrement: false;
|
|
192
|
+
hasRuntimeDefault: false;
|
|
193
|
+
enumValues: [string, ...string[]];
|
|
194
|
+
baseColumn: never;
|
|
195
|
+
identity: undefined;
|
|
196
|
+
generated: undefined;
|
|
197
|
+
}, {}, {}>;
|
|
198
|
+
codeVerifier: import("drizzle-orm/pg-core").PgColumn<{
|
|
199
|
+
name: "code_verifier";
|
|
200
|
+
tableName: "auth_sso_states";
|
|
201
|
+
dataType: "string";
|
|
202
|
+
columnType: "PgText";
|
|
203
|
+
data: string;
|
|
204
|
+
driverParam: string;
|
|
205
|
+
notNull: false;
|
|
206
|
+
hasDefault: false;
|
|
207
|
+
isPrimaryKey: false;
|
|
208
|
+
isAutoincrement: false;
|
|
209
|
+
hasRuntimeDefault: false;
|
|
210
|
+
enumValues: [string, ...string[]];
|
|
211
|
+
baseColumn: never;
|
|
212
|
+
identity: undefined;
|
|
213
|
+
generated: undefined;
|
|
214
|
+
}, {}, {}>;
|
|
215
|
+
requestId: import("drizzle-orm/pg-core").PgColumn<{
|
|
216
|
+
name: "request_id";
|
|
217
|
+
tableName: "auth_sso_states";
|
|
218
|
+
dataType: "string";
|
|
219
|
+
columnType: "PgText";
|
|
220
|
+
data: string;
|
|
221
|
+
driverParam: string;
|
|
222
|
+
notNull: false;
|
|
223
|
+
hasDefault: false;
|
|
224
|
+
isPrimaryKey: false;
|
|
225
|
+
isAutoincrement: false;
|
|
226
|
+
hasRuntimeDefault: false;
|
|
227
|
+
enumValues: [string, ...string[]];
|
|
228
|
+
baseColumn: never;
|
|
229
|
+
identity: undefined;
|
|
230
|
+
generated: undefined;
|
|
231
|
+
}, {}, {}>;
|
|
232
|
+
redirectTo: import("drizzle-orm/pg-core").PgColumn<{
|
|
233
|
+
name: "redirect_to";
|
|
234
|
+
tableName: "auth_sso_states";
|
|
235
|
+
dataType: "string";
|
|
236
|
+
columnType: "PgText";
|
|
237
|
+
data: string;
|
|
238
|
+
driverParam: string;
|
|
239
|
+
notNull: false;
|
|
240
|
+
hasDefault: false;
|
|
241
|
+
isPrimaryKey: false;
|
|
242
|
+
isAutoincrement: false;
|
|
243
|
+
hasRuntimeDefault: false;
|
|
244
|
+
enumValues: [string, ...string[]];
|
|
245
|
+
baseColumn: never;
|
|
246
|
+
identity: undefined;
|
|
247
|
+
generated: undefined;
|
|
248
|
+
}, {}, {}>;
|
|
249
|
+
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
250
|
+
name: "created_at";
|
|
251
|
+
tableName: "auth_sso_states";
|
|
252
|
+
dataType: "date";
|
|
253
|
+
columnType: "PgTimestamp";
|
|
254
|
+
data: Date;
|
|
255
|
+
driverParam: string;
|
|
256
|
+
notNull: true;
|
|
257
|
+
hasDefault: true;
|
|
258
|
+
isPrimaryKey: false;
|
|
259
|
+
isAutoincrement: false;
|
|
260
|
+
hasRuntimeDefault: false;
|
|
261
|
+
enumValues: undefined;
|
|
262
|
+
baseColumn: never;
|
|
263
|
+
identity: undefined;
|
|
264
|
+
generated: undefined;
|
|
265
|
+
}, {}, {}>;
|
|
266
|
+
expiresAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
267
|
+
name: "expires_at";
|
|
268
|
+
tableName: "auth_sso_states";
|
|
269
|
+
dataType: "date";
|
|
270
|
+
columnType: "PgTimestamp";
|
|
271
|
+
data: Date;
|
|
272
|
+
driverParam: string;
|
|
273
|
+
notNull: true;
|
|
274
|
+
hasDefault: false;
|
|
275
|
+
isPrimaryKey: false;
|
|
276
|
+
isAutoincrement: false;
|
|
277
|
+
hasRuntimeDefault: false;
|
|
278
|
+
enumValues: undefined;
|
|
279
|
+
baseColumn: never;
|
|
280
|
+
identity: undefined;
|
|
281
|
+
generated: undefined;
|
|
282
|
+
}, {}, {}>;
|
|
283
|
+
tenantId: import("drizzle-orm/pg-core").PgColumn<{
|
|
284
|
+
name: string;
|
|
285
|
+
tableName: "auth_sso_states";
|
|
286
|
+
dataType: "string";
|
|
287
|
+
columnType: "PgUUID";
|
|
288
|
+
data: string;
|
|
289
|
+
driverParam: string;
|
|
290
|
+
notNull: true;
|
|
291
|
+
hasDefault: true;
|
|
292
|
+
isPrimaryKey: false;
|
|
293
|
+
isAutoincrement: false;
|
|
294
|
+
hasRuntimeDefault: false;
|
|
295
|
+
enumValues: undefined;
|
|
296
|
+
baseColumn: never;
|
|
297
|
+
identity: undefined;
|
|
298
|
+
generated: undefined;
|
|
299
|
+
}, {}, {}>;
|
|
300
|
+
};
|
|
301
|
+
dialect: 'pg';
|
|
302
|
+
}>, "enableRLS">;
|
|
303
|
+
export type AuthIdentityRow = typeof authIdentities.$inferSelect;
|
|
304
|
+
export type NewAuthIdentityRow = typeof authIdentities.$inferInsert;
|
|
305
|
+
export type AuthSsoStateRow = typeof authSsoStates.$inferSelect;
|
|
306
|
+
export type NewAuthSsoStateRow = typeof authSsoStates.$inferInsert;
|