@absolutejs/auth 0.68.2 → 0.69.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/README.md +69 -0
- package/dist/authContext.d.ts +9 -4
- package/dist/cli/migrate.js +37 -8
- package/dist/cli/migrate.js.map +4 -4
- package/dist/client/createAuthClient.d.ts +28 -2
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +542 -8
- package/dist/client/index.js.map +5 -4
- package/dist/client/mobile.d.ts +84 -0
- package/dist/client/mobile.js +572 -0
- package/dist/client/mobile.js.map +10 -0
- package/dist/htmx/configuredRoutes.d.ts +16 -28
- package/dist/htmx/routes.d.ts +16 -28
- package/dist/index.d.ts +221 -34
- package/dist/index.js +1855 -1401
- package/dist/index.js.map +16 -12
- package/dist/oidc/config.d.ts +17 -2
- package/dist/oidc/inMemoryStores.d.ts +2 -1
- package/dist/oidc/index.d.ts +4 -2
- package/dist/oidc/index.js +589 -6
- package/dist/oidc/index.js.map +8 -4
- package/dist/oidc/nativeClients.d.ts +11 -0
- package/dist/oidc/postgresStores.d.ts +145 -1
- package/dist/oidc/socketTicketRoutes.d.ts +7 -0
- package/dist/oidc/socketTickets.d.ts +21 -0
- package/dist/oidc/types.d.ts +20 -0
- package/dist/principal.d.ts +39 -0
- package/dist/routes/protectRoute.d.ts +7 -3
- package/dist/routes/requireAuth.d.ts +4 -1
- package/dist/server.js +1843 -1400
- package/dist/server.js.map +16 -12
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -66,6 +66,75 @@ installSessionExpiryGuard({
|
|
|
66
66
|
});
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
### Installed-app authentication
|
|
70
|
+
|
|
71
|
+
`createMobileAuthClient` keeps the public auth surface provider-neutral while
|
|
72
|
+
using the installed-app security model: system-browser Authorization Code with
|
|
73
|
+
S256 PKCE, exact state/issuer/redirect validation, rotating refresh credentials
|
|
74
|
+
in native secure storage, in-memory access tokens, serialized refresh, and an
|
|
75
|
+
origin allowlist for bearer requests. Passwords are entered in the external
|
|
76
|
+
authorization UI and never posted through the app WebView.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import {
|
|
80
|
+
createMobileAuthClient,
|
|
81
|
+
createMobileAuthTransport,
|
|
82
|
+
createAuthClient
|
|
83
|
+
} from '@absolutejs/auth/client';
|
|
84
|
+
import { lifecycle, links, secureStorage } from '@absolutejs/devices';
|
|
85
|
+
|
|
86
|
+
const mobile = createMobileAuthClient({
|
|
87
|
+
clientId: 'com.example.app',
|
|
88
|
+
issuer: 'https://app.example',
|
|
89
|
+
lifecycle,
|
|
90
|
+
links,
|
|
91
|
+
redirectUri: 'com.example.app:/oauth/callback',
|
|
92
|
+
storage: secureStorage
|
|
93
|
+
});
|
|
94
|
+
const authClient = createAuthClient({
|
|
95
|
+
transport: createMobileAuthTransport(mobile)
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The OIDC client registration must be public (no client secret), include the
|
|
100
|
+
exact redirect URI, permit the requested scopes/resource, and require PKCE.
|
|
101
|
+
Browser applications continue using HTTP-only session cookies.
|
|
102
|
+
|
|
103
|
+
AbsoluteJS mobile builds provision that public client automatically when the
|
|
104
|
+
application declares `@absolutejs/auth`. The CLI passes a strict
|
|
105
|
+
`ABSOLUTE_AUTH_NATIVE_CLIENTS` deployment declaration into the server runtime;
|
|
106
|
+
Auth layers matching issuer clients over `oidc.clientStore` without writing to
|
|
107
|
+
the consumer's database. An explicitly stored client with the same ID remains
|
|
108
|
+
authoritative. Applications that use Auth on mobile must mount the OIDC
|
|
109
|
+
provider; the mobile build fails with an actionable error when it is absent.
|
|
110
|
+
`mobile.fetchOptional()` is intended for application-shell/page-envelope
|
|
111
|
+
requests: it sends a bearer token when a renewable session exists and otherwise
|
|
112
|
+
performs a credential-free request so public pages still load before sign-in.
|
|
113
|
+
|
|
114
|
+
For WebSocket/Sync authentication, enable a ticket store on the provider. A
|
|
115
|
+
valid audience-bound access token can then obtain a 30-second, hashed-at-rest,
|
|
116
|
+
single-use ticket from `/oauth2/socket-ticket`:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const socketTicketStore = createPostgresSocketTicketStore(db);
|
|
120
|
+
|
|
121
|
+
await auth({
|
|
122
|
+
oidc: {
|
|
123
|
+
// ...normal provider configuration
|
|
124
|
+
socketTicketStore
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const ticket = await mobile.socketTicket('https://app.example/sync');
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Run the `oidc` migration block after upgrading; migration
|
|
132
|
+
`0004_socket_tickets` creates the ticket table. Resource servers may use
|
|
133
|
+
`requireAuthPlugin({ accessTokens: { getUser, oidc } })` to resolve cookie
|
|
134
|
+
sessions and bearer access tokens into the same typed `authPrincipal`. DPoP-
|
|
135
|
+
bound bearer tokens currently fail closed until resource-proof verification is
|
|
136
|
+
enabled.
|
|
137
|
+
|
|
69
138
|
The defaults use `/oauth2/status`, `/signin`, `reason=session_expired`, and a
|
|
70
139
|
`returnUrl` query parameter. Use `onExpired` when a router or application shell
|
|
71
140
|
should own navigation. The returned guard exposes `check()` for an immediate
|
package/dist/authContext.d.ts
CHANGED
|
@@ -3,8 +3,10 @@ import type { AgentAuthConfig } from './agents/config';
|
|
|
3
3
|
import type { AuditEmitter } from './audit/config';
|
|
4
4
|
import type { AuthorizationConfig } from './authorization/config';
|
|
5
5
|
import type { AuthSessionStore } from './session/types';
|
|
6
|
-
|
|
6
|
+
import type { AccessTokenPrincipalConfig } from './principal';
|
|
7
|
+
export declare const createAuthContext: <UserType>({ agentAuth, accessTokens, authSessionStore, authorization, emit, seedSource }: {
|
|
7
8
|
agentAuth?: AgentAuthConfig;
|
|
9
|
+
accessTokens?: AccessTokenPrincipalConfig<UserType>;
|
|
8
10
|
authSessionStore?: AuthSessionStore<UserType>;
|
|
9
11
|
authorization?: AuthorizationConfig<UserType>;
|
|
10
12
|
emit?: AuditEmitter;
|
|
@@ -16,13 +18,14 @@ export declare const createAuthContext: <UserType>({ agentAuth, authSessionStore
|
|
|
16
18
|
unregisteredSession: import("./types").UnregisteredSessionRecord;
|
|
17
19
|
};
|
|
18
20
|
derive: ({
|
|
21
|
+
readonly authPrincipal: import("./principal").AuthPrincipal<UserType> | undefined;
|
|
19
22
|
readonly protectRoute: <AuthReturn, AuthFailReturn = never>(handleAuth: (user: UserType) => AuthReturn | Promise<AuthReturn>, handleAuthFail?: ((error: {
|
|
20
23
|
readonly code: "Bad Request";
|
|
21
24
|
readonly message: "Cookies are missing";
|
|
22
25
|
} | {
|
|
23
26
|
readonly code: "Unauthorized";
|
|
24
27
|
readonly message: "User is not authenticated";
|
|
25
|
-
}) => AuthFailReturn) | undefined) =>
|
|
28
|
+
}) => AuthFailReturn) | undefined) => import("elysia").ElysiaStatus<"Bad Request", "Cookies are missing", 400> | import("elysia").ElysiaStatus<"Unauthorized", "User is not authenticated", 401> | AuthReturn | Promise<AuthReturn> | NonNullable<AuthFailReturn>;
|
|
26
29
|
} & {
|
|
27
30
|
readonly requireRecentAuth: <AuthReturn, AuthFailReturn_1>(maxAgeMs: number, handleAuth: (user: UserType) => AuthReturn | Promise<AuthReturn>, handleAuthFail?: ((error: {
|
|
28
31
|
readonly code: "Unauthorized";
|
|
@@ -34,13 +37,14 @@ export declare const createAuthContext: <UserType>({ agentAuth, authSessionStore
|
|
|
34
37
|
message: "Agent is not authenticated" | "Insufficient agent scopes";
|
|
35
38
|
}) => AuthFailReturn_2 | Promise<AuthFailReturn_2>) => Promise<Response | AuthReturn | NonNullable<Awaited<AuthFailReturn_2>>>;
|
|
36
39
|
}) | ({
|
|
40
|
+
readonly authPrincipal: import("./principal").AuthPrincipal<UserType> | undefined;
|
|
37
41
|
readonly protectRoute: <AuthReturn, AuthFailReturn = never>(handleAuth: (user: UserType) => AuthReturn | Promise<AuthReturn>, handleAuthFail?: ((error: {
|
|
38
42
|
readonly code: "Bad Request";
|
|
39
43
|
readonly message: "Cookies are missing";
|
|
40
44
|
} | {
|
|
41
45
|
readonly code: "Unauthorized";
|
|
42
46
|
readonly message: "User is not authenticated";
|
|
43
|
-
}) => AuthFailReturn) | undefined) =>
|
|
47
|
+
}) => AuthFailReturn) | undefined) => import("elysia").ElysiaStatus<"Bad Request", "Cookies are missing", 400> | import("elysia").ElysiaStatus<"Unauthorized", "User is not authenticated", 401> | AuthReturn | Promise<AuthReturn> | NonNullable<AuthFailReturn>;
|
|
44
48
|
} & {
|
|
45
49
|
readonly requireRecentAuth: <AuthReturn, AuthFailReturn_1>(maxAgeMs: number, handleAuth: (user: UserType) => AuthReturn | Promise<AuthReturn>, handleAuthFail?: ((error: {
|
|
46
50
|
readonly code: "Unauthorized";
|
|
@@ -78,13 +82,14 @@ export declare const createAuthContext: <UserType>({ agentAuth, authSessionStore
|
|
|
78
82
|
macroFn: {};
|
|
79
83
|
parser: {};
|
|
80
84
|
response: import("elysia/types").ExtractErrorFromHandle<{
|
|
85
|
+
readonly authPrincipal: import("./principal").AuthPrincipal<UserType> | undefined;
|
|
81
86
|
readonly protectRoute: <AuthReturn, AuthFailReturn = never>(handleAuth: (user: UserType) => AuthReturn | Promise<AuthReturn>, handleAuthFail?: ((error: {
|
|
82
87
|
readonly code: "Bad Request";
|
|
83
88
|
readonly message: "Cookies are missing";
|
|
84
89
|
} | {
|
|
85
90
|
readonly code: "Unauthorized";
|
|
86
91
|
readonly message: "User is not authenticated";
|
|
87
|
-
}) => AuthFailReturn) | undefined) =>
|
|
92
|
+
}) => AuthFailReturn) | undefined) => import("elysia").ElysiaStatus<"Bad Request", "Cookies are missing", 400> | import("elysia").ElysiaStatus<"Unauthorized", "User is not authenticated", 401> | AuthReturn | Promise<AuthReturn> | NonNullable<AuthFailReturn>;
|
|
88
93
|
}>;
|
|
89
94
|
} & {
|
|
90
95
|
schema: {};
|
package/dist/cli/migrate.js
CHANGED
|
@@ -2748,7 +2748,7 @@ var mfaEnrollmentsTable = pgTable9("auth_mfa_enrollments", {
|
|
|
2748
2748
|
});
|
|
2749
2749
|
|
|
2750
2750
|
// src/oidc/postgresStores.ts
|
|
2751
|
-
import { and as and5, desc as desc6, eq as eq10, gt as gt2, lt as lt3 } from "drizzle-orm";
|
|
2751
|
+
import { and as and5, desc as desc6, eq as eq10, gt as gt2, isNull as isNull3, lt as lt3, or as or3, sql as sql2 } from "drizzle-orm";
|
|
2752
2752
|
import {
|
|
2753
2753
|
bigint as bigint8,
|
|
2754
2754
|
boolean as boolean4,
|
|
@@ -2869,13 +2869,24 @@ var oauthRefreshTokensTable = pgTable10("auth_oauth_refresh_tokens", {
|
|
|
2869
2869
|
audience: varchar10("audience", { length: URL_LENGTH }),
|
|
2870
2870
|
claims_json: jsonb5("claims_json").$type(),
|
|
2871
2871
|
client_id: varchar10("client_id", { length: ID_LENGTH8 }).notNull(),
|
|
2872
|
+
consumed_token_hashes: text5("consumed_token_hashes").array().notNull().default([]),
|
|
2872
2873
|
created_at_ms: bigint8("created_at_ms", { mode: "number" }).notNull(),
|
|
2873
2874
|
dpop_jkt: varchar10("dpop_jkt", { length: ID_LENGTH8 }),
|
|
2874
2875
|
expires_at_ms: bigint8("expires_at_ms", { mode: "number" }).notNull(),
|
|
2876
|
+
family_id: varchar10("family_id", { length: ID_LENGTH8 }).notNull(),
|
|
2877
|
+
revoked_at_ms: bigint8("revoked_at_ms", { mode: "number" }),
|
|
2875
2878
|
scopes: text5("scopes").array().notNull(),
|
|
2876
2879
|
token_hash: varchar10("token_hash", { length: ID_LENGTH8 }).primaryKey(),
|
|
2877
2880
|
user_id: varchar10("user_id", { length: ID_LENGTH8 }).notNull()
|
|
2878
2881
|
});
|
|
2882
|
+
var oauthSocketTicketsTable = pgTable10("auth_oauth_socket_tickets", {
|
|
2883
|
+
audience: varchar10("audience", { length: URL_LENGTH }).notNull(),
|
|
2884
|
+
client_id: varchar10("client_id", { length: ID_LENGTH8 }).notNull(),
|
|
2885
|
+
expires_at_ms: bigint8("expires_at_ms", { mode: "number" }).notNull(),
|
|
2886
|
+
scopes: text5("scopes").array().notNull(),
|
|
2887
|
+
subject: varchar10("subject", { length: ID_LENGTH8 }).notNull(),
|
|
2888
|
+
ticket_hash: varchar10("ticket_hash", { length: ID_LENGTH8 }).primaryKey()
|
|
2889
|
+
});
|
|
2879
2890
|
|
|
2880
2891
|
// src/organizations/postgresOrganizationStore.ts
|
|
2881
2892
|
import { and as and6, desc as desc7, eq as eq11 } from "drizzle-orm";
|
|
@@ -3220,18 +3231,18 @@ var JOURNAL_DDL = `CREATE TABLE IF NOT EXISTS "auth_migrations" (
|
|
|
3220
3231
|
var isJournalRow = (value) => typeof value === "object" && value !== null && typeof Reflect.get(value, "id") === "string";
|
|
3221
3232
|
var isBlockName = (value) => Object.hasOwn(blockMigrations, value);
|
|
3222
3233
|
var allBlockNames = () => Object.keys(blockMigrations).filter(isBlockName);
|
|
3223
|
-
var applyOne = async (client, id,
|
|
3224
|
-
await client.query(
|
|
3234
|
+
var applyOne = async (client, id, sql3, log) => {
|
|
3235
|
+
await client.query(sql3);
|
|
3225
3236
|
await client.query(`INSERT INTO "auth_migrations" ("id", "applied_at_ms") VALUES ($1, $2)`, [id, Date.now()]);
|
|
3226
3237
|
log(`apply ${id}`);
|
|
3227
3238
|
};
|
|
3228
|
-
var runOne = async (client, id,
|
|
3239
|
+
var runOne = async (client, id, sql3, applied, result, log) => {
|
|
3229
3240
|
if (applied.has(id)) {
|
|
3230
3241
|
result.skipped.push(id);
|
|
3231
3242
|
log(`skip ${id}`);
|
|
3232
3243
|
return;
|
|
3233
3244
|
}
|
|
3234
|
-
await applyOne(client, id,
|
|
3245
|
+
await applyOne(client, id, sql3, log);
|
|
3235
3246
|
result.applied.push(id);
|
|
3236
3247
|
};
|
|
3237
3248
|
var runMigrations = async ({
|
|
@@ -3315,6 +3326,21 @@ var oidcResourceAudienceMigration = {
|
|
|
3315
3326
|
].join(`
|
|
3316
3327
|
`)
|
|
3317
3328
|
};
|
|
3329
|
+
var oidcRefreshTokenFamiliesMigration = {
|
|
3330
|
+
id: "0003_refresh_token_families",
|
|
3331
|
+
sql: [
|
|
3332
|
+
'ALTER TABLE "auth_oauth_refresh_tokens" ADD COLUMN IF NOT EXISTS "family_id" varchar(255);',
|
|
3333
|
+
'UPDATE "auth_oauth_refresh_tokens" SET "family_id" = "token_hash" WHERE "family_id" IS NULL;',
|
|
3334
|
+
'ALTER TABLE "auth_oauth_refresh_tokens" ALTER COLUMN "family_id" SET NOT NULL;',
|
|
3335
|
+
'ALTER TABLE "auth_oauth_refresh_tokens" ADD COLUMN IF NOT EXISTS "consumed_token_hashes" text[] NOT NULL DEFAULT ARRAY[]::text[];',
|
|
3336
|
+
'ALTER TABLE "auth_oauth_refresh_tokens" ADD COLUMN IF NOT EXISTS "revoked_at_ms" bigint;'
|
|
3337
|
+
].join(`
|
|
3338
|
+
`)
|
|
3339
|
+
};
|
|
3340
|
+
var oidcSocketTicketsMigration = {
|
|
3341
|
+
id: "0004_socket_tickets",
|
|
3342
|
+
sql: tablesToInitSql([oauthSocketTicketsTable])
|
|
3343
|
+
};
|
|
3318
3344
|
var sessionOAuthSubjectMigration = {
|
|
3319
3345
|
id: "0002_oauth_subject",
|
|
3320
3346
|
sql: [
|
|
@@ -3385,9 +3411,12 @@ var blockMigrations = {
|
|
|
3385
3411
|
oauthInitialAccessTokensTable,
|
|
3386
3412
|
oauthLogoutDeliveriesTable,
|
|
3387
3413
|
oauthPushedAuthorizationRequestsTable,
|
|
3388
|
-
oauthRefreshTokensTable
|
|
3414
|
+
oauthRefreshTokensTable,
|
|
3415
|
+
oauthSocketTicketsTable
|
|
3389
3416
|
]).migrations,
|
|
3390
|
-
oidcResourceAudienceMigration
|
|
3417
|
+
oidcResourceAudienceMigration,
|
|
3418
|
+
oidcRefreshTokenFamiliesMigration,
|
|
3419
|
+
oidcSocketTicketsMigration
|
|
3391
3420
|
]
|
|
3392
3421
|
},
|
|
3393
3422
|
organizations: initMigration("organizations", [
|
|
@@ -3606,5 +3635,5 @@ var main = async () => {
|
|
|
3606
3635
|
};
|
|
3607
3636
|
await main();
|
|
3608
3637
|
|
|
3609
|
-
//# debugId=
|
|
3638
|
+
//# debugId=3B381D4CCB9C631A64756E2164756E21
|
|
3610
3639
|
//# sourceMappingURL=migrate.js.map
|