@meistrari/auth-nuxt 2.0.1 → 2.1.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/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/auth-nuxt",
3
3
  "configKey": "telaAuth",
4
- "version": "1.0.0",
4
+ "version": "2.1.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { defineNuxtModule, createResolver, addImports, addPlugin, extendPages, addComponent, addRouteMiddleware, addServerImportsDir, addServerHandler } from '@nuxt/kit';
1
+ import { defineNuxtModule, createResolver, addImports, addPlugin, extendPages, addComponent, addServerImportsDir, addServerHandler } from '@nuxt/kit';
2
2
 
3
3
  const module$1 = defineNuxtModule({
4
4
  meta: {
@@ -39,12 +39,13 @@ const module$1 = defineNuxtModule({
39
39
  });
40
40
  addPlugin(resolver.resolve("./runtime/plugins/auth-guard"));
41
41
  addPlugin(resolver.resolve("./runtime/plugins/directives"));
42
- addRouteMiddleware({
43
- name: "tela-auth",
44
- path: resolver.resolve("./runtime/middleware/require-auth"),
45
- global: true
46
- });
47
42
  addServerImportsDir(resolver.resolve("./runtime/server/utils"));
43
+ if (!options.skipServerMiddleware) {
44
+ addServerHandler({
45
+ route: "",
46
+ handler: resolver.resolve("./runtime/server/middleware/application-auth")
47
+ });
48
+ }
48
49
  return;
49
50
  }
50
51
  if (!options.skipServerMiddleware) {
@@ -36,6 +36,8 @@ export declare function useTelaApplicationAuth(): {
36
36
  login: () => Promise<void>;
37
37
  logout: () => Promise<void>;
38
38
  initSession: () => Promise<void>;
39
+ getAvailableOrganizations: () => Promise<import("@meistrari/auth-core").Organization[]>;
40
+ switchOrganization: (organizationId: string) => Promise<void>;
39
41
  user: import("vue").Ref<{
40
42
  id: string;
41
43
  createdAt: Date;
@@ -1,7 +1,7 @@
1
1
  import { navigateTo, useCookie, useRoute, useRuntimeConfig } from "#app";
2
+ import { AuthorizationFlowError, isTokenExpired, RefreshTokenExpiredError } from "@meistrari/auth-core";
2
3
  import { createNuxtAuthClient } from "../shared.js";
3
4
  import { useApplicationSessionState } from "./state.js";
4
- import { AuthorizationFlowError, isTokenExpired, RefreshTokenExpiredError } from "@meistrari/auth-core";
5
5
  const SEVEN_DAYS = 60 * 60 * 24 * 7;
6
6
  const FIFTEEN_MINUTES = 60 * 15;
7
7
  const ONE_MINUTE = 60 * 1e3;
@@ -162,10 +162,23 @@ export function useTelaApplicationAuth() {
162
162
  await refreshToken();
163
163
  }
164
164
  }
165
+ async function getAvailableOrganizations() {
166
+ const { organizations } = await authClient.application.listCandidateOrganizations(applicationId);
167
+ return organizations;
168
+ }
169
+ async function switchOrganization(organizationId) {
170
+ const { accessToken, refreshToken: refreshToken2, user, organization } = await authClient.application.switchOrganization(organizationId);
171
+ accessTokenCookie.value = accessToken;
172
+ refreshTokenCookie.value = refreshToken2;
173
+ state.user.value = user;
174
+ state.activeOrganization.value = mapOrganization(organization);
175
+ }
165
176
  return {
166
177
  ...state,
167
178
  login,
168
179
  logout,
169
- initSession
180
+ initSession,
181
+ getAvailableOrganizations,
182
+ switchOrganization
170
183
  };
171
184
  }
@@ -1,11 +1,22 @@
1
1
  <script setup>
2
- import { navigateTo } from "#app";
2
+ import { navigateTo, useRuntimeConfig } from "#app";
3
3
  import { onMounted } from "vue";
4
4
  import { useTelaApplicationAuth } from "../composables/application-auth";
5
5
  const { initSession } = useTelaApplicationAuth();
6
+ const config = useRuntimeConfig();
7
+ const authConfig = config.public.telaAuth;
8
+ const loginPath = authConfig.application?.loginPath ?? "/login";
6
9
  onMounted(async () => {
7
- await initSession();
8
- navigateTo("/");
10
+ try {
11
+ await initSession();
12
+ navigateTo("/");
13
+ } catch (error) {
14
+ console.error("Session initialization failed:", error);
15
+ navigateTo({
16
+ path: loginPath,
17
+ query: { error: "session_failed" }
18
+ });
19
+ }
9
20
  });
10
21
  </script>
11
22
 
@@ -1,2 +1,22 @@
1
+ /**
2
+ * Client-side authentication guard plugin
3
+ *
4
+ * This plugin provides UX-level route protection by checking JWT tokens client-side
5
+ * and redirecting users before they navigate to protected routes. This creates a smooth
6
+ * user experience by avoiding unnecessary page loads.
7
+ *
8
+ * SECURITY NOTE: This is NOT the primary security layer. The client-side JWT decoding
9
+ * is for UX purposes only. Actual cryptographic verification happens server-side in:
10
+ * - application-auth.ts: Server middleware that verifies JWTs for all /api routes
11
+ * - require-auth.ts: Server utility for protecting individual API route handlers
12
+ *
13
+ * An attacker could bypass this client-side check, but they cannot bypass the server-side
14
+ * verification which cryptographically validates the JWT signature using JWKS.
15
+ *
16
+ * This follows a defense-in-depth approach:
17
+ * 1. Client-side (this file): Fast UX-level routing decisions
18
+ * 2. Server middleware: Automatic JWT verification for all API routes
19
+ * 3. Route handlers: Explicit role checks in sensitive endpoints
20
+ */
1
21
  declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
2
22
  export default _default;
@@ -23,7 +23,7 @@ export default defineNuxtPlugin((nuxtApp) => {
23
23
  const isComment = el.nodeType === 8;
24
24
  if (shouldRender && isComment) {
25
25
  const originalElement = el.__vIfRoleElement;
26
- if (originalElement) {
26
+ if (originalElement && originalElement.parentNode === null) {
27
27
  el.parentNode?.replaceChild(originalElement, el);
28
28
  }
29
29
  } else if (!shouldRender && !isComment) {
@@ -31,6 +31,11 @@ export default defineNuxtPlugin((nuxtApp) => {
31
31
  el.parentNode?.replaceChild(comment, el);
32
32
  comment.__vIfRoleElement = el;
33
33
  }
34
+ },
35
+ unmounted(el) {
36
+ if (el.nodeType === 8) {
37
+ delete el.__vIfRoleElement;
38
+ }
34
39
  }
35
40
  });
36
41
  nuxtApp.vueApp.directive("show-role", {
@@ -0,0 +1,4 @@
1
+ import type { AuthenticatedH3Event } from '../types/h3.js';
2
+ export declare function meistrariApplicationAuthMiddleware(callback: (event: AuthenticatedH3Event) => void | Promise<void>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
3
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
4
+ export default _default;
@@ -0,0 +1,49 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ import { defineEventHandler, getCookie } from "h3";
3
+ import { createRemoteJWKSet, jwtVerify } from "jose";
4
+ async function setApplicationAuthContext(event) {
5
+ event.context.auth = {
6
+ user: null,
7
+ workspace: null,
8
+ token: void 0
9
+ };
10
+ const token = getCookie(event, "tela-access-token");
11
+ if (!token) {
12
+ return;
13
+ }
14
+ const authConfig = useRuntimeConfig(event).public.telaAuth;
15
+ const { apiUrl, application } = authConfig;
16
+ const { applicationId } = application ?? {};
17
+ if (!applicationId || !apiUrl) {
18
+ return;
19
+ }
20
+ try {
21
+ const payload = await jwtVerify(
22
+ token,
23
+ createRemoteJWKSet(new URL("/api/auth/jwks", apiUrl)),
24
+ {
25
+ issuer: apiUrl,
26
+ audience: applicationId,
27
+ algorithms: ["RS256"]
28
+ }
29
+ ).then(({ payload: payload2 }) => payload2);
30
+ event.context.auth = {
31
+ user: { ...payload.user, email: payload.email },
32
+ workspace: payload.workspace,
33
+ token
34
+ };
35
+ } catch {
36
+ }
37
+ }
38
+ export function meistrariApplicationAuthMiddleware(callback) {
39
+ return defineEventHandler(async (event) => {
40
+ await setApplicationAuthContext(event);
41
+ await callback(event);
42
+ });
43
+ }
44
+ export default defineEventHandler(async (event) => {
45
+ if (!event.path.startsWith("/api")) {
46
+ return;
47
+ }
48
+ await setApplicationAuthContext(event);
49
+ });
@@ -2,7 +2,14 @@ import type { EventHandlerRequest } from 'h3';
2
2
  import type { AuthenticatedH3Event } from '../types/h3.js';
3
3
  /**
4
4
  * Wraps an event handler to require authentication.
5
- * Throws 401 if no valid token is present.
5
+ * Throws 401 if no valid token is present, 403 if user lacks required role.
6
+ *
7
+ * This utility works in two modes:
8
+ * 1. If application-auth middleware has already verified the token, reuses that context
9
+ * 2. If middleware is skipped (skipServerMiddleware: true), performs JWT verification
10
+ *
11
+ * Use this when you need explicit role-based access control on specific API routes,
12
+ * or when you've disabled the global server middleware for performance reasons.
6
13
  *
7
14
  * @example
8
15
  * export default requireAuth(async (event) => {
@@ -12,7 +19,6 @@ import type { AuthenticatedH3Event } from '../types/h3.js';
12
19
  *
13
20
  * @example With roles
14
21
  * export default requireAuth(async (event) => {
15
- * // TODO: Validate roles from token
16
22
  * return { data: 'admin only' }
17
23
  * }, { roles: ['admin'] })
18
24
  */
@@ -1,8 +1,19 @@
1
- import { useRuntimeConfig } from "#build/types/nitro-imports";
1
+ import { useRuntimeConfig } from "nitropack/runtime";
2
2
  import { createError, defineEventHandler, getCookie } from "h3";
3
3
  import { createRemoteJWKSet, jwtVerify } from "jose";
4
4
  export function requireAuth(handler, options) {
5
5
  return defineEventHandler(async (event) => {
6
+ if (event.context.auth?.user && event.context.auth?.token) {
7
+ const user = event.context.auth.user;
8
+ if (options?.roles && !options.roles.includes(user.role ?? "")) {
9
+ throw createError({
10
+ statusCode: 403,
11
+ statusMessage: "Forbidden",
12
+ message: "User is not authorized to access this resource"
13
+ });
14
+ }
15
+ return handler(event);
16
+ }
6
17
  const token = getCookie(event, "tela-access-token");
7
18
  const authConfig = useRuntimeConfig(event).public.telaAuth;
8
19
  const { apiUrl, application } = authConfig;
@@ -28,23 +39,31 @@ export function requireAuth(handler, options) {
28
39
  message: "Authentication required"
29
40
  });
30
41
  }
31
- const payload = await jwtVerify(token, createRemoteJWKSet(new URL("/.well-known/jwks.json", apiUrl)), {
32
- issuer: apiUrl,
33
- audience: applicationId,
34
- algorithms: ["RS256"]
35
- }).then(({ payload: payload2 }) => payload2);
36
- const user = payload.user;
37
- if (options?.roles && !options.roles.includes(user.role ?? "")) {
42
+ try {
43
+ const payload = await jwtVerify(token, createRemoteJWKSet(new URL("/api/auth/jwks", apiUrl)), {
44
+ issuer: apiUrl,
45
+ audience: applicationId,
46
+ algorithms: ["RS256"]
47
+ }).then(({ payload: payload2 }) => payload2);
48
+ const user = payload.user;
49
+ if (options?.roles && !options.roles.includes(user.role ?? "")) {
50
+ throw createError({
51
+ statusCode: 403,
52
+ statusMessage: "Forbidden",
53
+ message: "User is not authorized to access this resource"
54
+ });
55
+ }
56
+ event.context.auth = { user: { ...payload.user, email: payload.email }, workspace: payload.workspace, token };
57
+ return handler(event);
58
+ } catch (error) {
59
+ if (error && typeof error === "object" && "statusCode" in error) {
60
+ throw error;
61
+ }
38
62
  throw createError({
39
- statusCode: 403,
40
- statusMessage: "Forbidden",
41
- message: "User is not authorized to access this resource"
63
+ statusCode: 401,
64
+ statusMessage: "Unauthorized",
65
+ message: "Invalid or expired token"
42
66
  });
43
67
  }
44
- if (!event.context.auth) {
45
- event.context.auth = { user: { ...payload.user, email: payload.email }, workspace: payload.workspace, token };
46
- }
47
- event.context.auth.token = token;
48
- return handler(event);
49
68
  });
50
69
  }
package/package.json CHANGED
@@ -1,56 +1,56 @@
1
1
  {
2
- "name": "@meistrari/auth-nuxt",
3
- "version": "2.0.1",
4
- "type": "module",
5
- "exports": {
6
- ".": {
7
- "types": "./dist/types.d.mts",
8
- "import": "./dist/module.mjs"
9
- },
10
- "./server/middleware/auth": {
11
- "types": "./dist/runtime/server/middleware/auth.d.ts",
12
- "import": "./dist/runtime/server/middleware/auth.js"
13
- },
14
- "./core": {
15
- "types": "./dist/core.d.mts",
16
- "import": "./dist/core.mjs"
17
- }
2
+ "name": "@meistrari/auth-nuxt",
3
+ "version": "2.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/types.d.mts",
8
+ "import": "./dist/module.mjs"
18
9
  },
19
- "main": "./dist/module.mjs",
20
- "typesVersions": {
21
- "*": {
22
- ".": [
23
- "./dist/types.d.mts"
24
- ]
25
- }
10
+ "./server/middleware/auth": {
11
+ "types": "./dist/runtime/server/middleware/auth.d.ts",
12
+ "import": "./dist/runtime/server/middleware/auth.js"
26
13
  },
27
- "files": [
28
- "dist"
29
- ],
30
- "scripts": {
31
- "build": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt-module-build build"
32
- },
33
- "dependencies": {
34
- "@meistrari/auth-core": "1.4.0",
35
- "jose": "6.1.3"
36
- },
37
- "peerDependencies": {
38
- "nuxt": "^3.0.0 || ^4.0.0",
39
- "vue": "^3.0.0"
40
- },
41
- "devDependencies": {
42
- "@nuxt/devtools": "2.6.3",
43
- "@nuxt/eslint-config": "1.9.0",
44
- "@nuxt/kit": "4.0.3",
45
- "@nuxt/module-builder": "1.0.2",
46
- "@nuxt/schema": "4.0.3",
47
- "@nuxt/test-utils": "3.19.2",
48
- "@types/node": "latest",
49
- "changelogen": "0.6.2",
50
- "nuxt": "4.0.3",
51
- "typescript": "5.9.2",
52
- "unbuild": "3.6.1",
53
- "vitest": "3.2.4",
54
- "vue-tsc": "3.0.6"
14
+ "./core": {
15
+ "types": "./dist/core.d.mts",
16
+ "import": "./dist/core.mjs"
17
+ }
18
+ },
19
+ "main": "./dist/module.mjs",
20
+ "typesVersions": {
21
+ "*": {
22
+ ".": [
23
+ "./dist/types.d.mts"
24
+ ]
55
25
  }
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "scripts": {
31
+ "build": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt-module-build build"
32
+ },
33
+ "dependencies": {
34
+ "@meistrari/auth-core": "1.7.0",
35
+ "jose": "6.1.3"
36
+ },
37
+ "peerDependencies": {
38
+ "nuxt": "^3.0.0 || ^4.0.0",
39
+ "vue": "^3.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "@nuxt/devtools": "2.6.3",
43
+ "@nuxt/eslint-config": "1.9.0",
44
+ "@nuxt/kit": "4.0.3",
45
+ "@nuxt/module-builder": "1.0.2",
46
+ "@nuxt/schema": "4.0.3",
47
+ "@nuxt/test-utils": "3.19.2",
48
+ "@types/node": "latest",
49
+ "changelogen": "0.6.2",
50
+ "nuxt": "4.0.3",
51
+ "typescript": "5.9.2",
52
+ "unbuild": "3.6.1",
53
+ "vitest": "3.2.4",
54
+ "vue-tsc": "3.0.6"
55
+ }
56
56
  }
@@ -1,2 +0,0 @@
1
- declare const _default: import("#app").RouteMiddleware;
2
- export default _default;
@@ -1,42 +0,0 @@
1
- import { defineNuxtRouteMiddleware, navigateTo, useCookie, useRuntimeConfig } from "#app";
2
- import { createRemoteJWKSet, jwtVerify } from "jose";
3
- export default defineNuxtRouteMiddleware(async (to) => {
4
- const authMeta = to.meta?.auth;
5
- if (!authMeta || authMeta.required !== true) {
6
- return;
7
- }
8
- const config = useRuntimeConfig();
9
- const authConfig = config.public.telaAuth;
10
- const { apiUrl, application } = authConfig;
11
- const { applicationId, loginPath = "/login", unauthorizedPath = "/unauthorized" } = application ?? {};
12
- const token = useCookie("tela-access-token");
13
- if (!token.value) {
14
- return navigateTo(loginPath);
15
- }
16
- if (!applicationId || !apiUrl) {
17
- console.error("Application ID or API URL is not configured");
18
- return;
19
- }
20
- try {
21
- const { payload } = await jwtVerify(
22
- token.value,
23
- createRemoteJWKSet(new URL("/.well-known/jwks.json", apiUrl)),
24
- {
25
- issuer: apiUrl,
26
- audience: applicationId,
27
- algorithms: ["RS256"]
28
- }
29
- );
30
- const tokenPayload = payload;
31
- const user = tokenPayload.user;
32
- if (authMeta.roles && authMeta.roles.length > 0) {
33
- const userRole = user.role ?? "";
34
- if (!authMeta.roles.includes(userRole)) {
35
- return navigateTo(unauthorizedPath);
36
- }
37
- }
38
- } catch (error) {
39
- console.error("Token validation failed:", error);
40
- return navigateTo(loginPath);
41
- }
42
- });