@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 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
@@ -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
- export declare const createAuthContext: <UserType>({ agentAuth, authSessionStore, authorization, emit, seedSource }: {
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) => Promise<import("elysia").ElysiaStatus<"Bad Request", "Cookies are missing", 400> | import("elysia").ElysiaStatus<"Unauthorized", "User is not authenticated", 401> | AuthReturn | NonNullable<AuthFailReturn>>;
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) => Promise<import("elysia").ElysiaStatus<"Bad Request", "Cookies are missing", 400> | import("elysia").ElysiaStatus<"Unauthorized", "User is not authenticated", 401> | AuthReturn | NonNullable<AuthFailReturn>>;
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) => Promise<import("elysia").ElysiaStatus<"Bad Request", "Cookies are missing", 400> | import("elysia").ElysiaStatus<"Unauthorized", "User is not authenticated", 401> | AuthReturn | NonNullable<AuthFailReturn>>;
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: {};
@@ -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, sql2, log) => {
3224
- await client.query(sql2);
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, sql2, applied, result, log) => {
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, sql2, log);
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=03868D61AE219DE664756E2164756E21
3638
+ //# debugId=3B381D4CCB9C631A64756E2164756E21
3610
3639
  //# sourceMappingURL=migrate.js.map