nuxt-bearer-auth 0.1.6 → 0.1.8

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.
Files changed (38) hide show
  1. package/README.md +141 -0
  2. package/dist/module.d.mts +128 -144
  3. package/dist/module.d.ts +128 -144
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +58 -12
  6. package/dist/runtime/components/Can.vue +23 -0
  7. package/dist/runtime/components/Cannot.vue +24 -0
  8. package/dist/runtime/components/useAbilityRequirement.d.ts +40 -0
  9. package/dist/runtime/components/useAbilityRequirement.js +22 -0
  10. package/dist/runtime/composables/useBearerAuth.d.ts +9 -11
  11. package/dist/runtime/composables/useBearerAuth.js +80 -31
  12. package/dist/runtime/middleware/bearer-auth.global.d.ts +1 -1
  13. package/dist/runtime/middleware/bearer-auth.global.js +9 -0
  14. package/dist/runtime/plugins/bearer-auth.server.d.ts +1 -1
  15. package/dist/runtime/plugins/bearer-auth.server.js +5 -2
  16. package/dist/runtime/server/api/auth/login.post.d.ts +2 -1
  17. package/dist/runtime/server/api/auth/login.post.js +5 -1
  18. package/dist/runtime/server/api/auth/me.get.d.ts +1 -0
  19. package/dist/runtime/server/api/auth/me.get.js +10 -3
  20. package/dist/runtime/server/api/auth/otp-verification.post.d.ts +3 -2
  21. package/dist/runtime/server/api/auth/otp-verification.post.js +5 -1
  22. package/dist/runtime/server/api/auth/refresh.post.d.ts +2 -1
  23. package/dist/runtime/server/api/auth/refresh.post.js +9 -2
  24. package/dist/runtime/server/api/auth/register.post.d.ts +3 -2
  25. package/dist/runtime/server/api/auth/register.post.js +5 -1
  26. package/dist/runtime/server/api/auth/social-login.post.d.ts +2 -1
  27. package/dist/runtime/server/api/auth/social-login.post.js +5 -1
  28. package/dist/runtime/server/middleware/auth.js +9 -0
  29. package/dist/runtime/server/utils/authorization.d.ts +26 -0
  30. package/dist/runtime/server/utils/authorization.js +97 -0
  31. package/dist/runtime/server/utils/config.d.ts +2 -1
  32. package/dist/runtime/server/utils/sessions.d.ts +3 -2
  33. package/dist/runtime/server/utils/sessions.js +6 -1
  34. package/dist/runtime/types/auth.d.ts +6 -0
  35. package/dist/runtime/types/h3.d.ts +2 -1
  36. package/dist/runtime/utils/abilities.d.ts +26 -0
  37. package/dist/runtime/utils/abilities.js +16 -0
  38. package/package.json +14 -4
package/README.md CHANGED
@@ -21,6 +21,10 @@ Reusable Nuxt authentication for APIs that issue bearer tokens. It is designed f
21
21
  - Session revocation
22
22
  - SSR auth hydration
23
23
  - Global route middleware
24
+ - Optional authorization with abilities normalization (disabled by default)
25
+ - Route/page authorization via `definePageMeta` metadata (`all` / `any` modes)
26
+ - Declarative `<Can>` / `<Cannot>` UI authorization components (advisory rendering only)
27
+ - Server-side `requireAbility()` guard via the `nuxt-bearer-auth/server` export
24
28
  - `useBearerAuth()` composable
25
29
  - `useAuth()` alias for convenience
26
30
 
@@ -180,8 +184,145 @@ Available composable state and methods:
180
184
  - `forgotPassword`
181
185
  - `resetPassword`
182
186
  - `resendOtp`
187
+ - `abilities`
188
+ - `can(ability)`
189
+ - `cannot(ability)`
183
190
  - `clearAuthState`
184
191
 
192
+ ## Authorization
193
+
194
+ Authorization is opt-in and disabled by default. Backend roles, permissions, and direct abilities are normalized into a sorted `string[]`. Roles use the `role:` prefix by default.
195
+
196
+ ```ts
197
+ export default defineNuxtConfig({
198
+ bearerAuth: {
199
+ authorization: {
200
+ enabled: true,
201
+ source: "session",
202
+ responsePaths: {
203
+ abilities: ["abilities", "data.abilities"],
204
+ roles: ["roles", "data.roles"],
205
+ permissions: ["permissions", "data.permissions"],
206
+ },
207
+ rolePrefix: "role:",
208
+ },
209
+ redirects: {
210
+ unauthorized: "/auth/not-allowed",
211
+ },
212
+ },
213
+ });
214
+ ```
215
+
216
+ ```ts
217
+ const auth = useBearerAuth();
218
+
219
+ await auth.login(credentials);
220
+ auth.can("campaign.create");
221
+ auth.cannot("campaign.delete");
222
+ auth.abilities.value;
223
+ ```
224
+
225
+ Abilities are persisted with the server session and hydrated during SSR. Login, social login, registration, OTP verification, refresh, and `me` responses can update them. An omitted authorization field preserves the current list; logout and failed authentication clear it. Tokens, passwords, OTPs, and raw backend responses are never authorization state.
226
+
227
+ ### Client Checks Are Not Security
228
+
229
+ `auth.can()` and `auth.cannot()` are advisory UI helpers for showing or hiding controls. They do not secure APIs. A user who bypasses your UI can still call any endpoint directly, so your backend must authorize every request it receives.
230
+
231
+ ### UI Authorization Components
232
+
233
+ `<Can>` and `<Cannot>` provide the same advisory checks declaratively. Both are auto-imported, evaluate the existing `auth.abilities` state reactively through one shared evaluator, and match route/server semantics exactly (exact strings, `mode` defaults to `"all"`, empty `abilities` means unrestricted, malformed props or state fail closed):
234
+
235
+ ```vue
236
+ <template>
237
+ <!-- single ability -->
238
+ <Can ability="users.delete">
239
+ Delete
240
+ </Can>
241
+
242
+ <!-- every listed ability (default) / at least one -->
243
+ <Can :abilities="['users.view', 'users.edit']">Edit</Can>
244
+ <Can :abilities="['reports.view', 'reports.export']" mode="any">
245
+ Export
246
+ </Can>
247
+
248
+ <!-- inverse rendering -->
249
+ <Cannot ability="users.delete">
250
+ <template #fallback>Request access</template>
251
+ </Cannot>
252
+ </template>
253
+ ```
254
+
255
+ Unauthorized content is removed from the DOM; an optional `#fallback` slot renders instead. Disabled authorization behaves like `auth.can()` — ability state stays `null`, so `<Can>` renders fallback/nothing. SSR output matches hydration because evaluation is synchronous over the transferred state.
256
+
257
+ As with all client checks: **UI authorization controls rendering only. It never secures API requests. Your backend must authorize every request it receives.**
258
+
259
+ ### Route Authorization
260
+
261
+ Pages opt in by declaring required abilities in `definePageMeta`. The global `bearer-auth` middleware enforces them when authorization is enabled:
262
+
263
+ ```vue
264
+ <script setup lang="ts">
265
+ definePageMeta({
266
+ authorization: {
267
+ abilities: ["users.view"],
268
+ // optional — defaults to "all"
269
+ mode: "all",
270
+ },
271
+ });
272
+ </script>
273
+ ```
274
+
275
+ Semantics:
276
+
277
+ - `mode` defaults to `"all"`: every listed ability must exist on the session.
278
+ - `mode: "any"`: at least one listed ability must exist.
279
+ - Matching is exact string equality. `users.*`, prefixes, and hierarchy are not supported.
280
+ - Routes without metadata behave exactly as before. When `authorization.enabled` is `false`, metadata is ignored.
281
+ - Unauthenticated visitors follow the normal login flow (`?redirect=` preserved). Authenticated users without the required abilities are redirected to `redirects.unauthorized` — not to login.
282
+
283
+ Route authorization protects navigation inside your Nuxt app only. It never authorizes external API requests.
284
+
285
+ ### Server Enforcement with requireAbility()
286
+
287
+ Nitro server routes use the dedicated server-only export:
288
+
289
+ ```ts
290
+ import { requireAbility } from "nuxt-bearer-auth/server";
291
+
292
+ export default defineEventHandler((event) => {
293
+ const session = requireAbility(event, "users.delete");
294
+
295
+ // every ability required (default):
296
+ requireAbility(event, ["users.view", "users.export"]);
297
+ // at least one required:
298
+ requireAbility(event, ["reports.view", "reports.export"], "any");
299
+
300
+ return { ok: true };
301
+ });
302
+ ```
303
+
304
+ Behavior:
305
+
306
+ - Throws `401 Unauthenticated` when there is no authenticated session and `403 Authorization required` when the session lacks the ability.
307
+ - Reads abilities only from `event.context.auth` — the Redis-backed server session. It never reads client state (`useState("bearer-auth-abilities")`), headers, query parameters, or request bodies.
308
+ - On success it sets `event.context.authorization = { abilities, source: "session" }` (normalized strings only, no tokens) and returns the session.
309
+ - Fails closed: if the session has no abilities, every requirement is rejected.
310
+ - The helper lives behind the `nuxt-bearer-auth/server` subpath so it is never bundled into client code.
311
+
312
+ ### Security Boundary
313
+
314
+ Authorization is layered, and the lower layer is always authoritative:
315
+
316
+ ```text
317
+ UI checks (auth.can) → convenience only
318
+ Nuxt enforcement (meta/403) → protects pages and Nuxt routes
319
+ Laravel backend → authoritative for its own endpoints
320
+ ```
321
+
322
+ `requireAbility(event, "campaign.delete")` stops your Nuxt route from running, but if that route calls `DELETE /campaigns/123`, Laravel must still authorize `campaign.delete`. This package never claims to replace backend authorization.
323
+
324
+ Declarative `<Can>` / `<Cannot>` components ship with the package — see [UI Authorization Components](#ui-authorization-components). Directives such as `v-can` are intentionally not provided.
325
+
185
326
  ## Redis Sessions
186
327
 
187
328
  The backend token is stored server-side in Redis. The browser only receives a secure HTTP-only session id cookie. Nuxt server routes can read the hydrated session from:
package/dist/module.d.mts CHANGED
@@ -1,146 +1,130 @@
1
- declare const module = defineNuxtModule({
2
- meta: {
3
- name: "nuxt-bearer-auth",
4
- configKey: "bearerAuth",
5
- compatibility: {
6
- nuxt: "^3.12.0 || ^4.0.0"
7
- }
8
- },
9
- defaults: defaultOptions,
10
- async setup(moduleOptions, nuxt) {
11
- const resolver = createResolver(import.meta.url);
12
- const options = defu(moduleOptions, defaultOptions);
13
- nuxt.options.runtimeConfig.bearerAuth = defu(
14
- nuxt.options.runtimeConfig.bearerAuth,
15
- {
16
- apiBaseUrl: options.apiBaseUrl,
17
- redisUrl: options.redisUrl,
18
- sessionSecret: options.sessionSecret,
19
- appEnv: options.appEnv,
20
- endpoints: options.endpoints,
21
- responsePaths: options.responsePaths,
22
- sessionCookie: options.sessionCookie,
23
- verificationRequiredActions: options.verificationRequiredActions,
24
- twoFactorRequiredActions: options.twoFactorRequiredActions
25
- }
26
- );
27
- nuxt.options.runtimeConfig.public.bearerAuth = defu(
28
- nuxt.options.runtimeConfig.public.bearerAuth,
29
- {
30
- redirects: options.redirects,
31
- routes: options.routes
32
- }
33
- );
34
- if (options.installCsurf && options.csrf?.enabled) {
35
- await installModule("nuxt-csurf", {
36
- https: options.csrf.https,
37
- cookieKey: options.appEnv === "local" || options.appEnv === "development" ? options.csrf.devCookieKey : options.csrf.cookieKey,
38
- cookie: options.csrf.cookie,
39
- methods: options.csrf.methods,
40
- methodsToProtect: options.csrf.methodsToProtect,
41
- encryptAlgorithm: "aes-256-cbc",
42
- addCsrfTokenToEventCtx: true,
43
- headerName: options.csrf.headerName
44
- });
45
- const prefix2 = options.routes?.localApiPrefix || "/api/auth";
46
- nuxt.options.routeRules = {
47
- ...nuxt.options.routeRules,
48
- [`${prefix2}/login`]: { csurf: false, ...nuxt.options.routeRules?.[`${prefix2}/login`] },
49
- [`${prefix2}/register`]: { csurf: false, ...nuxt.options.routeRules?.[`${prefix2}/register`] },
50
- [`${prefix2}/social-login`]: { csurf: false, ...nuxt.options.routeRules?.[`${prefix2}/social-login`] },
51
- [`${prefix2}/forgot-password`]: { csurf: false, ...nuxt.options.routeRules?.[`${prefix2}/forgot-password`] },
52
- [`${prefix2}/reset-password`]: { csurf: false, ...nuxt.options.routeRules?.[`${prefix2}/reset-password`] },
53
- [`${prefix2}/otp-verification`]: { csurf: false, ...nuxt.options.routeRules?.[`${prefix2}/otp-verification`] },
54
- [`${prefix2}/resend-otp/**`]: { csurf: false, ...nuxt.options.routeRules?.[`${prefix2}/resend-otp/**`] },
55
- "/api/_csrf": { csurf: false, ...nuxt.options.routeRules?.["/api/_csrf"] }
56
- };
57
- }
58
- addImports([
59
- {
60
- name: "useBearerAuth",
61
- from: resolver.resolve("runtime/composables/useBearerAuth")
62
- },
63
- {
64
- name: "useBearerAuth",
65
- as: "useAuth",
66
- from: resolver.resolve("runtime/composables/useBearerAuth")
67
- }
68
- ]);
69
- addPlugin(resolver.resolve("runtime/plugins/bearer-auth.server"));
70
- if (options.routes?.middleware !== false) {
71
- addRouteMiddleware({
72
- name: "bearer-auth",
73
- path: resolver.resolve("runtime/middleware/bearer-auth.global"),
74
- global: true
75
- });
76
- }
77
- addServerPlugin(resolver.resolve("runtime/server/plugins/redis"));
78
- addServerHandler({
79
- middleware: true,
80
- handler: resolver.resolve("runtime/server/middleware/auth")
81
- });
82
- const prefix = options.routes?.localApiPrefix || "/api/auth";
83
- addServerHandler({
84
- route: `${prefix}/login`,
85
- method: "post",
86
- handler: resolver.resolve("runtime/server/api/auth/login.post")
87
- });
88
- addServerHandler({
89
- route: `${prefix}/social-login`,
90
- method: "post",
91
- handler: resolver.resolve("runtime/server/api/auth/social-login.post")
92
- });
93
- addServerHandler({
94
- route: `${prefix}/logout`,
95
- method: "post",
96
- handler: resolver.resolve("runtime/server/api/auth/logout.post")
97
- });
98
- addServerHandler({
99
- route: `${prefix}/me`,
100
- method: "get",
101
- handler: resolver.resolve("runtime/server/api/auth/me.get")
102
- });
103
- addServerHandler({
104
- route: `${prefix}/refresh`,
105
- method: "post",
106
- handler: resolver.resolve("runtime/server/api/auth/refresh.post")
107
- });
108
- addServerHandler({
109
- route: `${prefix}/forgot-password`,
110
- method: "post",
111
- handler: resolver.resolve("runtime/server/api/auth/forgot-password.post")
112
- });
113
- addServerHandler({
114
- route: `${prefix}/reset-password`,
115
- method: "post",
116
- handler: resolver.resolve("runtime/server/api/auth/reset-password.post")
117
- });
118
- addServerHandler({
119
- route: `${prefix}/otp-verification`,
120
- method: "post",
121
- handler: resolver.resolve("runtime/server/api/auth/otp-verification.post")
122
- });
123
- addServerHandler({
124
- route: `${prefix}/resend-otp/:identifier`,
125
- method: "post",
126
- handler: resolver.resolve("runtime/server/api/auth/resend-otp/[identifier].post")
127
- });
128
- addServerHandler({
129
- route: `${prefix}/register`,
130
- method: "post",
131
- handler: resolver.resolve("runtime/server/api/auth/register.post")
132
- });
133
- addServerHandler({
134
- route: `${prefix}/sessions`,
135
- method: "get",
136
- handler: resolver.resolve("runtime/server/api/auth/sessions.get")
137
- });
138
- addServerHandler({
139
- route: `${prefix}/sessions/:id`,
140
- method: "delete",
141
- handler: resolver.resolve("runtime/server/api/auth/sessions/[id].delete")
142
- });
143
- }
144
- });
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+ export * from '../dist/runtime/types/auth.js';
3
+
4
+ type SameSite = true | false | "lax" | "strict" | "none";
5
+ interface BearerAuthEndpointOptions {
6
+ login: string;
7
+ socialLogin: string;
8
+ logout: string;
9
+ me: string;
10
+ refresh: string;
11
+ forgotPassword: string;
12
+ resetPassword: string;
13
+ verifyOtp: string;
14
+ resendOtp: string;
15
+ register: string;
16
+ }
17
+ interface BearerAuthResponsePaths {
18
+ token: string[];
19
+ refreshToken: string[];
20
+ user: string[];
21
+ userId: string[];
22
+ message: string[];
23
+ success: string[];
24
+ code: string[];
25
+ nextAction: string[];
26
+ }
27
+ interface BearerAuthRedirectOptions {
28
+ login: string;
29
+ authenticated: string;
30
+ logout: string;
31
+ unauthorized: string;
32
+ }
33
+ interface BearerAuthRouteOptions {
34
+ localApiPrefix: string;
35
+ public: string[];
36
+ authPages: string[];
37
+ protectedApiPrefixes: string[];
38
+ publicApiPrefixes: string[];
39
+ middleware: boolean;
40
+ }
41
+ interface BearerAuthCookieOptions {
42
+ name: string;
43
+ devName: string;
44
+ maxAge: number;
45
+ sameSite: SameSite;
46
+ secure?: boolean;
47
+ domain?: string;
48
+ path: string;
49
+ }
50
+ interface BearerAuthCsrfOptions {
51
+ enabled: boolean;
52
+ https: boolean;
53
+ cookieKey: string;
54
+ devCookieKey: string;
55
+ headerName: string;
56
+ methods: string[];
57
+ methodsToProtect: string[];
58
+ cookie: {
59
+ path: string;
60
+ httpOnly: boolean;
61
+ sameSite: SameSite;
62
+ secure?: boolean;
63
+ };
64
+ }
65
+ type Ability = string;
66
+ type AuthorizationSource = "session" | "endpoint";
67
+ type AuthorizationMatchMode = "all" | "any";
68
+ interface AuthorizationRouteRequirement {
69
+ abilities: string[];
70
+ mode?: AuthorizationMatchMode;
71
+ }
72
+ interface AuthorizationResponsePaths {
73
+ roles?: string[];
74
+ permissions?: string[];
75
+ abilities?: string[];
76
+ }
77
+ interface BearerAuthAuthorizationConfig {
78
+ enabled: boolean;
79
+ source: AuthorizationSource;
80
+ endpoint?: string;
81
+ responsePaths: AuthorizationResponsePaths;
82
+ rolePrefix: string;
83
+ }
84
+ /**
85
+ * Shape of the private (server-side) `runtimeConfig.bearerAuth` namespace
86
+ * populated by the module. Never place functions or secrets beyond what the
87
+ * server requires here; runtime configuration must stay serializable.
88
+ */
89
+ interface BearerAuthPrivateRuntimeConfig {
90
+ apiBaseUrl: string;
91
+ redisUrl: string;
92
+ sessionSecret: string;
93
+ appEnv: string;
94
+ endpoints: BearerAuthEndpointOptions;
95
+ responsePaths: BearerAuthResponsePaths;
96
+ sessionCookie: BearerAuthCookieOptions;
97
+ authorization: BearerAuthAuthorizationConfig;
98
+ verificationRequiredActions: string[];
99
+ twoFactorRequiredActions: string[];
100
+ }
101
+ /**
102
+ * Shape of the public `runtimeConfig.public.bearerAuth` namespace.
103
+ * Only serializable, browser-safe values are allowed here.
104
+ */
105
+ interface BearerAuthPublicRuntimeConfig {
106
+ redirects: Required<BearerAuthRedirectOptions>;
107
+ routes: Required<BearerAuthRouteOptions>;
108
+ authorizationEnabled: boolean;
109
+ }
110
+ interface BearerAuthModuleOptions {
111
+ apiBaseUrl?: string;
112
+ redisUrl?: string;
113
+ sessionSecret?: string;
114
+ appEnv?: string;
115
+ installCsurf?: boolean;
116
+ endpoints?: Partial<BearerAuthEndpointOptions>;
117
+ responsePaths?: Partial<BearerAuthResponsePaths>;
118
+ redirects?: Partial<BearerAuthRedirectOptions>;
119
+ routes?: Partial<BearerAuthRouteOptions>;
120
+ sessionCookie?: Partial<BearerAuthCookieOptions>;
121
+ csrf?: Partial<BearerAuthCsrfOptions>;
122
+ authorization?: Partial<BearerAuthAuthorizationConfig>;
123
+ verificationRequiredActions?: string[];
124
+ twoFactorRequiredActions?: string[];
125
+ }
126
+
127
+ declare const module: _nuxt_schema.NuxtModule<BearerAuthModuleOptions, BearerAuthModuleOptions, false>;
145
128
 
146
129
  export { module as default };
130
+ export type { Ability, AuthorizationMatchMode, AuthorizationResponsePaths, AuthorizationRouteRequirement, AuthorizationSource, BearerAuthAuthorizationConfig, BearerAuthCookieOptions, BearerAuthCsrfOptions, BearerAuthEndpointOptions, BearerAuthModuleOptions, BearerAuthPrivateRuntimeConfig, BearerAuthPublicRuntimeConfig, BearerAuthRedirectOptions, BearerAuthResponsePaths, BearerAuthRouteOptions, SameSite };