json-rest-api 1.0.10 → 1.0.11

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 (117) hide show
  1. package/.claude/settings.local.json +9 -1
  2. package/AGENTS.md +34 -0
  3. package/CLAUDE.md +26 -1
  4. package/CRUSH.md +55 -0
  5. package/README.md +3 -3
  6. package/TODO.todo +8 -0
  7. package/docs/API.md +17 -18
  8. package/docs/COMPARISON.md +18 -13
  9. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
  10. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
  11. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
  12. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
  13. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
  14. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
  15. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
  16. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
  17. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
  18. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
  19. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
  20. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
  21. package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
  22. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
  23. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
  24. package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
  25. package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
  26. package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
  27. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
  28. package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
  29. package/docs/GUIDE/index.md +3 -0
  30. package/docs/QUICKSTART.md +12 -12
  31. package/docs/index.md +2 -2
  32. package/docs/mini-guides/file-uploads-complete.md +0 -1
  33. package/examples/url-override-example.js +137 -0
  34. package/index.js +7 -1
  35. package/package.json +5 -9
  36. package/plugins/core/bulk-operations-plugin.js +2 -0
  37. package/plugins/core/connectors/express-plugin.js +43 -24
  38. package/plugins/core/connectors/lib/request-helpers.js +2 -2
  39. package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
  40. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
  41. package/plugins/core/lib/anyapi/schema-utils.js +186 -0
  42. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
  43. package/plugins/core/lib/dbTablesOperations.js +305 -0
  44. package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
  45. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
  46. package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
  47. package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
  48. package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
  49. package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
  50. package/plugins/core/lib/querying/url-helpers.js +119 -0
  51. package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
  52. package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
  53. package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
  54. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
  55. package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
  56. package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
  57. package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
  58. package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
  59. package/plugins/core/lib/storage/storage-adapter.js +160 -0
  60. package/plugins/core/lib/writing/relationship-processor.js +4 -11
  61. package/plugins/core/rest-api-access.js +456 -0
  62. package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
  63. package/plugins/core/rest-api-knex-plugin.js +226 -87
  64. package/plugins/core/rest-api-multihome-plugin.js +89 -27
  65. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
  66. package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
  67. package/plugins/core/rest-api-plugin-methods/common.js +46 -10
  68. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
  69. package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
  70. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
  71. package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
  72. package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
  73. package/plugins/core/rest-api-plugin-methods/get.js +2 -5
  74. package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
  75. package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
  76. package/plugins/core/rest-api-plugin-methods/post.js +23 -11
  77. package/plugins/core/rest-api-plugin-methods/put.js +18 -21
  78. package/plugins/core/rest-api-plugin-methods/query.js +3 -4
  79. package/plugins/core/rest-api-plugin-methods/release.js +13 -0
  80. package/plugins/core/rest-api-plugin.js +3 -4
  81. package/plugins/core/rest-api-positioning-plugin.js +253 -76
  82. package/plugins/core/socketio-plugin.js +436 -502
  83. package/quickTest.js +2 -2
  84. package/scripts/debug-anyapi-review-filter.mjs +63 -0
  85. package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
  86. package/scripts/inspect-anyapi-descriptor.mjs +19 -0
  87. package/test-debug.js +59 -0
  88. package/tests/README.md +7 -1
  89. package/tests/anyapi-basic.test.js +102 -0
  90. package/tests/anyapi-cursor-pagination.test.js +144 -0
  91. package/tests/computed-fields.test.js +27 -12
  92. package/tests/cors.test.js +5 -6
  93. package/tests/curl.test.js +1 -1
  94. package/tests/custom-idproperty-relationships.test.js +37 -29
  95. package/tests/field-getters.test.js +25 -7
  96. package/tests/fixtures/api-configs.js +576 -150
  97. package/tests/helpers/storage-mode.js +65 -0
  98. package/tests/helpers/test-utils.js +85 -3
  99. package/tests/include-depth-validation.test.js +35 -1
  100. package/tests/nested-includes.test.js +130 -103
  101. package/tests/pagination-cursor-multifield.test.js +17 -13
  102. package/tests/pagination-enhanced.test.js +19 -24
  103. package/tests/polymorphic-simple.test.js +9 -9
  104. package/tests/positioning-advanced.test.js +19 -19
  105. package/tests/positioning-extended.test.js +25 -25
  106. package/tests/positioning-multifilter.test.js +62 -62
  107. package/tests/positioning.test.js +7 -7
  108. package/tests/queries.test.js +5 -5
  109. package/tests/query-limits.test.js +77 -92
  110. package/tests/rest-api-access.test.js +206 -0
  111. package/tests/searchschema-merge.test.js +182 -0
  112. package/tests/socketio.test.js +33 -14
  113. package/tests/virtual-fields.test.js +0 -1
  114. package/docs/DOCS_ALL.md +0 -21894
  115. package/plugins/core/jwt-auth-plugin.js +0 -897
  116. package/plugins/core/lib/jwt-auth-helpers.js +0 -130
  117. package/tests/jwt-auth.test.js +0 -692
@@ -1,155 +1,201 @@
1
- # Permissions and Authentication Guide
1
+ # Authentication and Permissions Guide
2
2
 
3
- The JWT Authentication Plugin provides a powerful declarative permission system that makes it easy to secure your REST API resources. This guide shows you how to use authentication and define permissions using the book catalog example.
3
+ This guide shows you how to add authentication to your REST API using the JWT Authentication Plugin. The plugin validates JWT tokens from any auth provider (Supabase, Auth0, Firebase, etc.) and enforces permissions on your resources.
4
4
 
5
5
  ## Table of Contents
6
- - [Overview](#overview)
7
- - [Quick Start](#quick-start)
8
- - [Getting Tokens from Auth Providers](#getting-tokens-from-auth-providers)
9
- - [Supabase](#supabase)
10
- - [Auth0](#auth0)
11
- - [Custom JWT](#custom-jwt)
12
- - [Installation and Setup](#installation-and-setup)
13
- - [Declarative Permissions](#declarative-permissions)
14
- - [Built-in Auth Checkers](#built-in-auth-checkers)
15
- - [Making Authenticated API Calls Directly](#making-authenticated-api-calls-directly)
16
- - [Using Auth Helpers](#using-auth-helpers)
6
+ - [How It Works - The Architecture](#how-it-works---the-architecture)
7
+ - [Quick Start - Get Running in 5 Minutes](#quick-start---get-running-in-5-minutes)
8
+ - [Frontend: Getting Auth Tokens](#frontend-getting-auth-tokens)
9
+ - [Supabase Setup](#supabase-setup)
10
+ - [Auth0 Setup](#auth0-setup)
11
+ - [Google OAuth Login](#google-oauth-login)
12
+ - [Backend: Validating Tokens](#backend-validating-tokens)
13
+ - [Using Built-in Checkers](#using-built-in-checkers)
14
+ - [Progressive Examples](#progressive-examples)
15
+ - [Advanced: Custom Authorization](#advanced-custom-authorization)
17
16
  - [Token Management](#token-management)
18
- - [Custom Auth Checkers](#custom-auth-checkers)
19
- - [Examples](#examples)
20
- - [Best Practices](#best-practices)
17
+ - [Migration Guide](#migration-guide)
21
18
 
22
- ## Overview
19
+ ## How It Works - The Architecture
23
20
 
24
- The JWT Auth Plugin provides two main features:
21
+ The JWT plugin is a **backend/server-side** tool that validates tokens. Here's the complete flow:
25
22
 
26
- 1. **Authentication** - Validates JWT tokens and populates `context.auth`
27
- 2. **Authorization** - Declarative permission rules on resources
23
+ ```
24
+ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐
25
+ │ Browser │ │ Supabase/ │ │ Your API │
26
+ │ (Frontend) │ ───> │ Auth0 │ │ (Backend) │
27
+ └─────────────┘ └─────────────┘ └──────────────┘
28
+ │ │ │
29
+ │ 1. Login with │ │
30
+ │ email/Google │ │
31
+ │ ─────────────────> │ │
32
+ │ │ │
33
+ │ 2. Get JWT token │ │
34
+ │ <───────────────── │ │
35
+ │ │ │
36
+ │ 3. API request with Bearer token │
37
+ │ ──────────────────────────────────────> │
38
+ │ │
39
+ │ 4. JWT Plugin validates
40
+ │ token with Supabase
41
+ │ │
42
+ │ 5. Response (allowed or denied) │
43
+ │ <────────────────────────────────────── │
44
+ ```
28
45
 
29
- The plugin validates tokens from any JWT provider (Supabase, Auth0, your own auth server) and enforces permissions you define on your resources.
46
+ **Key Points:**
47
+ - **Frontend**: Handles user login (email/password, Google, etc.) and gets JWT token
48
+ - **Auth Provider**: Issues and manages JWT tokens (Supabase, Auth0, etc.)
49
+ - **Backend (Your API)**: Uses JWT plugin to validate tokens and enforce permissions
50
+ - **JWT Plugin**: Does NOT handle login/signup - only validates existing tokens
30
51
 
31
- ## Quick Start
52
+ ## Quick Start - Get Running in 5 Minutes
32
53
 
33
- Here's how to get authentication working in 3 steps:
54
+ ### Step 1: Install the Plugin
34
55
 
35
- ### 1. Get JWT tokens from your auth provider
36
- ```javascript
37
- // Example with Supabase (in your frontend)
38
- import { createClient } from '@supabase/supabase-js'
56
+ ```bash
57
+ npm install jose
58
+ ```
39
59
 
40
- const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
60
+ ### Step 2: Configure Your Backend API
41
61
 
42
- // User login
43
- const { data: { session } } = await supabase.auth.signInWithPassword({
44
- email: 'user@example.com',
45
- password: 'password'
46
- })
62
+ ```javascript
63
+ import { Api } from 'hooked-api';
64
+ import { RestApiPlugin, RestApiKnexPlugin } from 'json-rest-api';
65
+ import { JwtAuthPlugin } from 'json-rest-api/plugins/core/jwt-auth-plugin.js';
47
66
 
48
- const token = session.access_token // This is your JWT token!
49
- ```
67
+ // Create your API
68
+ const api = new Api();
69
+ await api.use(RestApiPlugin);
70
+ await api.use(RestApiKnexPlugin, { knex });
50
71
 
51
- ### 2. Configure the plugin in your API
52
- ```javascript
53
- // In your API server
72
+ // Add JWT authentication (example with Supabase)
54
73
  await api.use(JwtAuthPlugin, {
55
- // For Supabase
56
- jwksUrl: `${SUPABASE_URL}/auth/v1/.well-known/jwks.json`,
57
- rolesField: 'app_metadata.roles'
58
- })
59
- ```
74
+ jwksUrl: 'https://your-project.supabase.co/auth/v1/.well-known/jwks.json'
75
+ });
60
76
 
61
- ### 3. Define permissions on resources
62
- ```javascript
77
+ // Add a protected resource
63
78
  await api.addResource('posts', {
64
- schema: { /* ... */ },
79
+ schema: {
80
+ id: { type: 'id' },
81
+ title: { type: 'string', required: true },
82
+ content: { type: 'text' },
83
+ user_id: { type: 'string' } // Automatically set to current user
84
+ },
65
85
 
86
+ // Simple auth rules using built-in checkers
66
87
  auth: {
67
- query: ['public'], // Anyone can read
68
- post: ['authenticated'], // Must be logged in
69
- patch: ['is_owner'], // Must own the post
70
- delete: ['is_owner', 'admin'] // Owner or admin
88
+ query: ['public'], // Anyone can read posts
89
+ post: ['authenticated'], // Must be logged in to create
90
+ patch: ['owns'], // Can only edit your own posts
91
+ delete: ['owns'] // Can only delete your own posts
71
92
  }
72
- })
93
+ });
94
+ ```
95
+
96
+ ### Step 3: Test It
97
+
98
+ ```javascript
99
+ // From your frontend (after user logs in)
100
+ const response = await fetch('http://localhost:3000/api/posts', {
101
+ method: 'POST',
102
+ headers: {
103
+ 'Authorization': `Bearer ${token}`, // Token from Supabase/Auth0
104
+ 'Content-Type': 'application/json'
105
+ },
106
+ body: JSON.stringify({
107
+ data: {
108
+ type: 'posts',
109
+ attributes: {
110
+ title: 'My First Post',
111
+ content: 'Hello world!'
112
+ }
113
+ }
114
+ })
115
+ });
73
116
  ```
74
117
 
75
- That's it! Your API now requires authentication and enforces permissions.
118
+ That's it! You now have authenticated API endpoints.
119
+
120
+ ## Frontend: Getting Auth Tokens
121
+
122
+ The JWT plugin validates tokens from any provider. Here's how to get tokens from popular services:
76
123
 
77
- ## Getting Tokens from Auth Providers
124
+ ### Supabase Setup
78
125
 
79
- The plugin doesn't generate tokens - it validates them. Here's how to get tokens from popular providers:
126
+ #### 1. Create a Supabase Project
127
+ Go to [supabase.com](https://supabase.com) and create a new project. You'll get:
128
+ - Project URL: `https://your-project.supabase.co`
129
+ - Anon Key: `eyJhbGc...` (public key for frontend)
80
130
 
81
- ### Supabase
131
+ #### 2. Frontend: Email/Password Login
82
132
 
83
- #### Step 1: Set up Supabase Auth
84
133
  ```javascript
85
- // In your frontend app
134
+ // In your React/Vue/etc app
86
135
  import { createClient } from '@supabase/supabase-js'
87
136
 
88
137
  const supabase = createClient(
89
138
  'https://your-project.supabase.co',
90
139
  'your-anon-key'
91
140
  )
92
- ```
93
141
 
94
- #### Step 2: User Registration/Login
95
- ```javascript
96
142
  // Sign up new user
97
- const { data, error } = await supabase.auth.signUp({
98
- email: 'user@example.com',
99
- password: 'secure-password'
100
- })
143
+ async function signUp(email, password) {
144
+ const { data, error } = await supabase.auth.signUp({
145
+ email,
146
+ password
147
+ })
148
+ if (error) throw error
149
+ return data.session.access_token // This is your JWT!
150
+ }
101
151
 
102
152
  // Sign in existing user
103
- const { data: { session } } = await supabase.auth.signInWithPassword({
104
- email: 'user@example.com',
105
- password: 'secure-password'
106
- })
107
-
108
- // The JWT token is in session.access_token
109
- const token = session.access_token
110
- ```
153
+ async function signIn(email, password) {
154
+ const { data, error } = await supabase.auth.signInWithPassword({
155
+ email,
156
+ password
157
+ })
158
+ if (error) throw error
159
+ return data.session.access_token // This is your JWT!
160
+ }
111
161
 
112
- #### Step 3: Use token with your API
113
- ```javascript
114
- // Make authenticated requests to your API
115
- const response = await fetch('https://your-api.com/api/posts', {
116
- method: 'POST',
117
- headers: {
118
- 'Authorization': `Bearer ${token}`,
119
- 'Content-Type': 'application/json'
120
- },
121
- body: JSON.stringify({
122
- data: {
123
- type: 'posts',
124
- attributes: {
125
- title: 'My Post',
126
- content: 'Hello world'
162
+ // Use the token for API calls
163
+ async function createPost(token, title, content) {
164
+ const response = await fetch('http://localhost:3000/api/posts', {
165
+ method: 'POST',
166
+ headers: {
167
+ 'Authorization': `Bearer ${token}`,
168
+ 'Content-Type': 'application/json'
169
+ },
170
+ body: JSON.stringify({
171
+ data: {
172
+ type: 'posts',
173
+ attributes: { title, content }
127
174
  }
128
- }
175
+ })
129
176
  })
130
- })
177
+ return response.json()
178
+ }
131
179
  ```
132
180
 
133
- #### Step 4: Configure plugin for Supabase
181
+ #### 3. Backend Configuration
182
+
134
183
  ```javascript
184
+ // Your API server
135
185
  await api.use(JwtAuthPlugin, {
136
- // Supabase JWKS URL - replace with your project URL
137
- jwksUrl: `${process.env.SUPABASE_URL}/auth/v1/.well-known/jwks.json`,
138
-
139
- // Supabase stores roles in app_metadata
140
- rolesField: 'app_metadata.roles',
141
-
142
- // Optional: Add these for extra security
143
- audience: 'authenticated',
144
- issuer: process.env.SUPABASE_URL
186
+ jwksUrl: 'https://your-project.supabase.co/auth/v1/.well-known/jwks.json'
145
187
  })
146
188
  ```
147
189
 
148
- ### Auth0
190
+ ### Auth0 Setup
191
+
192
+ #### 1. Create an Auth0 Application
193
+ Go to [auth0.com](https://auth0.com) and create a new Single Page Application.
194
+
195
+ #### 2. Frontend: Auth0 Login
149
196
 
150
- #### Step 1: Set up Auth0
151
197
  ```javascript
152
- // In your frontend
198
+ // In your React/Vue/etc app
153
199
  import { createAuth0Client } from '@auth0/auth0-spa-js'
154
200
 
155
201
  const auth0 = await createAuth0Client({
@@ -157,808 +203,505 @@ const auth0 = await createAuth0Client({
157
203
  clientId: 'your-client-id',
158
204
  authorizationParams: {
159
205
  redirect_uri: window.location.origin,
160
- audience: 'https://your-api.com'
206
+ audience: 'https://your-api.com' // Your API identifier
161
207
  }
162
208
  })
163
- ```
164
209
 
165
- #### Step 2: User Login
166
- ```javascript
167
- // Redirect to Auth0 login
168
- await auth0.loginWithRedirect()
210
+ // Login
211
+ async function login() {
212
+ await auth0.loginWithRedirect()
213
+ }
169
214
 
170
- // After redirect back, get token
171
- const token = await auth0.getAccessTokenSilently()
215
+ // After redirect, get token
216
+ async function getToken() {
217
+ const token = await auth0.getAccessTokenSilently()
218
+ return token // Your JWT!
219
+ }
172
220
  ```
173
221
 
174
- #### Step 3: Configure plugin for Auth0
222
+ #### 3. Backend Configuration
223
+
175
224
  ```javascript
225
+ // Your API server
176
226
  await api.use(JwtAuthPlugin, {
177
- jwksUrl: `https://${AUTH0_DOMAIN}/.well-known/jwks.json`,
178
- audience: process.env.AUTH0_AUDIENCE,
179
- issuer: `https://${AUTH0_DOMAIN}/`,
180
-
181
- // Auth0 uses custom claims
182
- rolesField: 'https://your-app.com/roles'
227
+ jwksUrl: 'https://your-domain.auth0.com/.well-known/jwks.json',
228
+ audience: 'https://your-api.com',
229
+ issuer: 'https://your-domain.auth0.com/'
183
230
  })
184
231
  ```
185
232
 
186
- ### Custom JWT
233
+ ### Google OAuth Login
187
234
 
188
- If you're generating your own JWTs:
235
+ Google login works through your auth provider (Supabase/Auth0), not directly. The user logs in with Google, but you still get a JWT from Supabase/Auth0.
236
+
237
+ #### Supabase + Google
238
+
239
+ ##### 1. Enable Google in Supabase Dashboard
240
+ - Go to Authentication → Providers → Google
241
+ - Add your Google Client ID and Secret (from Google Cloud Console)
242
+
243
+ ##### 2. Frontend: Google Login Button
189
244
 
190
- #### Step 1: Generate tokens in your auth server
191
245
  ```javascript
192
- // In your auth server
193
- import jwt from 'jsonwebtoken'
194
-
195
- const token = jwt.sign(
196
- {
197
- sub: user.id, // User ID
198
- email: user.email,
199
- roles: ['user', 'editor'],
200
- jti: generateUniqueId(), // For revocation
201
- iat: Math.floor(Date.now() / 1000),
202
- exp: Math.floor(Date.now() / 1000) + 3600 // 1 hour
203
- },
204
- process.env.JWT_SECRET,
205
- { algorithm: 'HS256' }
206
- )
246
+ // React component example
247
+ function GoogleLoginButton() {
248
+ const handleGoogleLogin = async () => {
249
+ const { data, error } = await supabase.auth.signInWithOAuth({
250
+ provider: 'google',
251
+ options: {
252
+ redirectTo: 'http://localhost:3000/auth/callback'
253
+ }
254
+ })
255
+ if (error) throw error
256
+ // User will be redirected to Google, then back to your app
257
+ }
258
+
259
+ return (
260
+ <button onClick={handleGoogleLogin}>
261
+ Sign in with Google
262
+ </button>
263
+ )
264
+ }
265
+
266
+ // After redirect, get the session
267
+ useEffect(() => {
268
+ supabase.auth.getSession().then(({ data: { session } }) => {
269
+ if (session) {
270
+ const token = session.access_token // JWT from Supabase!
271
+ // Use this token for API calls
272
+ }
273
+ })
274
+ }, [])
207
275
  ```
208
276
 
209
- #### Step 2: Configure plugin with secret
277
+ ##### 3. Backend: Same Configuration!
278
+
210
279
  ```javascript
280
+ // No change needed - same as email/password
211
281
  await api.use(JwtAuthPlugin, {
212
- secret: process.env.JWT_SECRET,
213
-
214
- // Your token structure
215
- userIdField: 'sub',
216
- rolesField: 'roles'
282
+ jwksUrl: 'https://your-project.supabase.co/auth/v1/.well-known/jwks.json'
217
283
  })
218
284
  ```
219
285
 
220
- ## Installation and Setup
286
+ The beauty is that your backend doesn't care HOW users logged in (email, Google, Facebook) - it just validates the JWT from Supabase.
221
287
 
222
- ```javascript
223
- import { JwtAuthPlugin } from 'json-rest-api/plugins/core/jwt-auth-plugin.js';
288
+ ## Backend: Validating Tokens
289
+
290
+ ### Basic Configuration Options
224
291
 
225
- // Install the plugin
292
+ ```javascript
226
293
  await api.use(JwtAuthPlugin, {
227
- // Required: Choose one authentication method
228
- secret: 'your-secret-key', // For HS256 tokens
229
- // OR
230
- publicKey: '-----BEGIN PUBLIC KEY...', // For RS256 tokens
231
- // OR
232
- jwksUrl: 'https://your-auth-provider.com/.well-known/jwks.json', // For external auth
233
-
234
- // Optional: Token configuration
235
- audience: 'your-api-audience',
236
- issuer: 'https://your-auth-provider.com',
294
+ // Option 1: For external auth providers (Supabase, Auth0)
295
+ jwksUrl: 'https://.../.well-known/jwks.json',
237
296
 
238
- // Optional: Field mappings
239
- userIdField: 'sub', // Where to find user ID in token (default: 'sub')
240
- rolesField: 'roles', // Where to find roles (default: 'roles')
241
- ownershipField: 'user_id', // Field in resources for ownership (default: 'user_id')
297
+ // Option 2: For symmetric key (your own auth)
298
+ secret: process.env.JWT_SECRET,
242
299
 
243
- // Optional: Revocation settings
244
- revocation: {
245
- enabled: true, // Enable token revocation (default: true)
246
- storage: 'database', // 'database' or 'memory' (default: 'database')
247
- },
300
+ // Option 3: For asymmetric key (your own auth)
301
+ publicKey: '-----BEGIN PUBLIC KEY-----...',
248
302
 
249
- // Optional: Endpoints
250
- endpoints: {
251
- logout: '/auth/logout', // Add logout endpoint
252
- session: '/auth/session' // Add session check endpoint
253
- }
254
- });
303
+ // Optional settings
304
+ audience: 'your-api-audience', // Required for Auth0
305
+ issuer: 'https://issuer.com', // Validate token issuer
306
+ userIdField: 'sub', // Where to find user ID (default: 'sub')
307
+ emailField: 'email', // Where to find email (default: 'email')
308
+ ownershipField: 'user_id' // Field for ownership checks (default: 'user_id')
309
+ })
255
310
  ```
256
311
 
257
- ## How Auth Rules Work
312
+ ### Understanding JWKS Configuration (Recommended)
258
313
 
259
- **Important**: Auth rules must be defined in the same configuration object as your schema when calling `addResource`:
314
+ When you use `jwksUrl` (the recommended approach for Supabase/Auth0), here's what actually happens behind the scenes:
260
315
 
316
+ #### What is JWKS?
317
+ JWKS (JSON Web Key Set) is a URL that provides public keys for verifying JWT signatures. Instead of hardcoding keys, your API fetches them dynamically.
318
+
319
+ #### The Automatic Process
261
320
  ```javascript
262
- await api.addResource('resource-name', {
263
- schema: { ... }, // Your field definitions
264
- relationships: { ... }, // Optional relationships
265
- auth: { // Permission rules go here!
266
- query: ['public'],
267
- get: ['public'],
268
- post: ['authenticated'],
269
- patch: ['is_owner', 'has_role:editor', 'admin'],
270
- delete: ['is_owner', 'has_role:moderator', 'admin']
271
- }
321
+ await api.use(JwtAuthPlugin, {
322
+ jwksUrl: 'https://your-project.supabase.co/auth/v1/.well-known/jwks.json'
272
323
  });
273
324
  ```
274
325
 
275
- The JWT plugin will automatically extract these rules and enforce them on all operations.
276
-
277
- ### Common Permission Patterns
278
-
279
- ```javascript
280
- // Public read, authenticated write
281
- auth: {
282
- query: ['public'],
283
- get: ['public'],
284
- post: ['authenticated'],
285
- patch: ['authenticated'],
286
- delete: ['admin']
287
- }
288
-
289
- // Private resource with role-based access
290
- auth: {
291
- query: ['authenticated'],
292
- get: ['authenticated'],
293
- post: ['has_role:author', 'has_role:editor'],
294
- patch: ['is_owner', 'has_role:editor'],
295
- delete: ['is_owner', 'has_role:moderator', 'admin']
296
- }
297
-
298
- // Admin-only resource
299
- auth: {
300
- query: ['admin'],
301
- get: ['admin'],
302
- post: ['admin'],
303
- patch: ['admin'],
304
- delete: ['admin']
305
- }
306
-
307
- // User profiles (self-service)
308
- auth: {
309
- query: ['admin'], // Only admins can list all users
310
- get: ['is_owner', 'admin'], // Users can see their own profile
311
- post: ['admin'], // Only admins create users
312
- patch: ['is_owner', 'admin'], // Users can edit their own profile
313
- delete: ['admin'] // Only admins can delete users
326
+ This single line sets up:
327
+
328
+ 1. **Automatic Key Fetching**: On first request, downloads Supabase's current public keys
329
+ 2. **Smart Caching**: Caches keys for 10 minutes to avoid repeated downloads
330
+ 3. **Key Rotation Handling**: When Supabase rotates keys, your API automatically gets the new ones
331
+ 4. **Multiple Key Support**: Handles multiple keys during rotation periods
332
+
333
+ #### What the JWKS URL Returns
334
+ ```json
335
+ {
336
+ "keys": [
337
+ {
338
+ "kid": "abc123", // Key ID - matches 'kid' in JWT header
339
+ "kty": "RSA", // Key type
340
+ "use": "sig", // Usage: signature verification
341
+ "n": "xGOr-H7A...", // RSA public key modulus
342
+ "e": "AQAB" // RSA public key exponent
343
+ },
344
+ {
345
+ "kid": "def456", // Another key (during rotation)
346
+ "n": "yKPq-J8B...",
347
+ "e": "AQAB"
348
+ }
349
+ ]
314
350
  }
315
351
  ```
316
352
 
317
- ## Declarative Permissions
318
-
319
- Instead of writing permission checks in hooks, you declare permissions directly on your resources:
320
-
321
- ```javascript
322
- await api.addResource('books', {
323
- schema: {
324
- id: { type: 'id' },
325
- title: { type: 'string', required: true },
326
- content: { type: 'text' },
327
- user_id: { type: 'string' },
328
- country_id: { type: 'number', belongsTo: 'countries', as: 'country' },
329
- published: { type: 'boolean', default: false }
330
- },
331
-
332
- // Declare permissions for each operation
333
- auth: {
334
- query: ['public'], // Anyone can list books
335
- get: ['public'], // Anyone can read a book
336
- post: ['authenticated'], // Must be logged in to create
337
- patch: ['is_owner', 'has_role:editor', 'admin'], // Owner, editor, or admin
338
- delete: ['is_owner', 'admin'] // Only owner or admin
339
- }
340
- });
341
- ```
342
-
343
- The permission rules are checked automatically - no manual hook writing needed!
344
-
345
- ## Built-in Auth Checkers
353
+ #### Performance Impact
354
+ - **First request**: ~50-100ms (downloads keys from Supabase)
355
+ - **Next 10 minutes**: ~1-2ms (uses cached keys)
356
+ - **After cache expires**: Re-downloads automatically
357
+ - **On verification failure**: Attempts to refresh keys
346
358
 
347
- The plugin includes these auth checkers out of the box:
359
+ #### Why This is Superior to Hardcoded Keys
360
+ - **Zero Maintenance**: Keys update automatically when provider rotates them
361
+ - **Always Secure**: You never handle or store private keys
362
+ - **No Downtime**: Key rotation happens seamlessly
363
+ - **No Code Changes**: Provider can change keys without you knowing
348
364
 
349
- ### `public`
350
- Anyone can access, no authentication required.
365
+ ### What Happens During Validation
351
366
 
352
- ```javascript
353
- auth: {
354
- query: ['public'] // Anyone can list resources
355
- }
356
- ```
367
+ 1. **Token Extraction**: Plugin extracts token from `Authorization: Bearer <token>` header
368
+ 2. **Key Selection**: Matches token's `kid` (key ID) with cached JWKS keys
369
+ 3. **Signature Verification**: Validates JWT signature using the matching public key
370
+ 4. **Claims Validation**: Checks audience, issuer, expiration
371
+ 5. **Context Population**: Sets `context.auth`:
372
+ ```javascript
373
+ context.auth = {
374
+ userId: '123', // From 'sub' claim
375
+ email: 'user@example.com', // From 'email' claim
376
+ token: { /* full JWT */ }, // All token data
377
+ tokenId: 'jti-123' // For revocation
378
+ }
379
+ ```
357
380
 
358
- ### `authenticated`
359
- User must be logged in (have a valid token).
381
+ ## Using Built-in Checkers
360
382
 
361
- ```javascript
362
- auth: {
363
- post: ['authenticated'] // Must be logged in to create
364
- }
365
- ```
383
+ The plugin provides only THREE built-in authorization checkers:
366
384
 
367
- ### `is_owner`
368
- User must own the resource (their ID matches the ownership field).
385
+ ### 1. `public` - No Authentication Required
369
386
 
370
387
  ```javascript
371
388
  auth: {
372
- patch: ['is_owner'], // Only owner can update
373
- delete: ['is_owner'] // Only owner can delete
389
+ query: ['public'], // Anyone can read, even without token
390
+ get: ['public']
374
391
  }
375
-
376
- // The plugin checks: record.user_id === context.auth.userId
377
- // The ownership field is configurable (default: 'user_id')
378
392
  ```
379
393
 
380
- ### `admin`
381
- User must have the 'admin' role.
394
+ ### 2. `authenticated` - Must Be Logged In
382
395
 
383
396
  ```javascript
384
397
  auth: {
385
- delete: ['admin'] // Only admins can delete
398
+ post: ['authenticated'], // Must have valid JWT token
399
+ patch: ['authenticated']
386
400
  }
387
401
  ```
388
402
 
389
- ### `has_role:X`
390
- User must have a specific role.
403
+ ### 3. `owns` - Must Own the Resource
391
404
 
392
405
  ```javascript
393
406
  auth: {
394
- patch: ['has_role:editor'], // Must be editor
395
- delete: ['has_role:moderator'] // Must be moderator
407
+ patch: ['owns'], // Can only edit records where user_id matches
408
+ delete: ['owns'] // Can only delete your own records
396
409
  }
397
410
  ```
398
411
 
399
- ### `has_permission:X`
400
- User must have a specific permission (for fine-grained control).
401
-
402
- ```javascript
403
- auth: {
404
- patch: ['has_permission:posts:write'],
405
- delete: ['has_permission:posts:delete']
406
- }
407
- ```
412
+ The `owns` checker compares the `user_id` field in the database record with the current user's ID from the token.
408
413
 
409
- ## Making Authenticated API Calls Directly
414
+ ## Progressive Examples
410
415
 
411
- When using the API programmatically (not through HTTP), you can pass authentication context as the second parameter to any API method:
416
+ Let's build up from simple to complex:
412
417
 
413
- ### Direct API Usage
418
+ ### Example 1: Public API (No Auth)
414
419
 
415
420
  ```javascript
416
- // Import your configured API
417
- import { api } from './your-api-setup.js';
418
-
419
- // Make authenticated calls by passing auth context as second parameter
420
- const authContext = {
421
- auth: {
422
- userId: 'user-123',
423
- email: 'user@example.com',
424
- role: 'admin',
425
- // Any other auth data your app needs
426
- }
427
- };
428
-
429
- // Query with auth
430
- const books = await api.resources.books.query({
431
- filters: { published: true },
432
- include: ['author'],
433
- page: { size: 10 }
434
- }, authContext);
435
-
436
- // Get single resource with auth
437
- const book = await api.resources.books.get({
438
- id: 123
439
- }, authContext);
440
-
441
- // Create with auth
442
- const newBook = await api.resources.books.post({
443
- inputRecord: {
444
- data: {
445
- type: 'books',
446
- attributes: {
447
- title: 'My New Book',
448
- isbn: '978-3-16-148410-0'
449
- }
450
- }
451
- }
452
- }, authContext);
453
-
454
- // Update with auth
455
- const updated = await api.resources.books.patch({
456
- id: 123,
457
- inputRecord: {
458
- data: {
459
- type: 'books',
460
- id: '123',
461
- attributes: {
462
- title: 'Updated Title'
463
- }
464
- }
421
+ await api.addResource('articles', {
422
+ schema: {
423
+ title: { type: 'string', required: true },
424
+ content: { type: 'text' }
465
425
  }
466
- }, authContext);
467
-
468
- // Delete with auth
469
- await api.resources.books.delete({
470
- id: 123
471
- }, authContext);
426
+ // No auth property = completely open
427
+ })
472
428
  ```
473
429
 
474
- ### Multi-tenancy Example
475
-
476
- If using the MultiHome plugin for multi-tenancy:
430
+ ### Example 2: Read-Only Public, Write Requires Login
477
431
 
478
432
  ```javascript
479
- // Tenant-specific context
480
- const tenantContext = {
433
+ await api.addResource('articles', {
434
+ schema: {
435
+ title: { type: 'string', required: true },
436
+ content: { type: 'text' },
437
+ user_id: { type: 'string' }
438
+ },
439
+
481
440
  auth: {
482
- userId: 'user-123',
483
- multihome_id: 'tenant-a' // Required for multihome
441
+ query: ['public'], // Anyone can read
442
+ get: ['public'],
443
+ post: ['authenticated'], // Must be logged in to create
444
+ patch: ['authenticated'],
445
+ delete: ['authenticated']
484
446
  }
485
- };
486
-
487
- // All operations will be scoped to tenant-a
488
- const tenantProjects = await api.resources.projects.query({}, tenantContext);
447
+ })
489
448
  ```
490
449
 
491
- ### Script/Admin Usage
492
-
493
- For administrative scripts or background jobs:
450
+ ### Example 3: Users Can Only Edit Their Own Content
494
451
 
495
452
  ```javascript
496
- // Admin context with elevated privileges
497
- const adminContext = {
453
+ await api.addResource('articles', {
454
+ schema: {
455
+ title: { type: 'string', required: true },
456
+ content: { type: 'text' },
457
+ user_id: { type: 'string' } // Auto-set on create
458
+ },
459
+
498
460
  auth: {
499
- userId: 'system',
500
- role: 'superadmin',
501
- isSystem: true
461
+ query: ['public'],
462
+ get: ['public'],
463
+ post: ['authenticated'],
464
+ patch: ['owns'], // Only edit your own
465
+ delete: ['owns'] // Only delete your own
502
466
  }
503
- };
467
+ })
504
468
 
505
- // Batch operations
506
- async function processAllBooks() {
507
- const books = await api.resources.books.query({
508
- page: { size: 100 }
509
- }, adminContext);
510
-
511
- for (const book of books.data) {
512
- // Process each book with admin privileges
513
- await api.resources.books.patch({
514
- id: book.id,
515
- inputRecord: { /* ... */ }
516
- }, adminContext);
469
+ // Also enable auto-ownership
470
+ await api.use(JwtAuthPlugin, {
471
+ jwksUrl: '...',
472
+ autoOwnership: {
473
+ enabled: true, // Auto-set user_id on create
474
+ filterByOwner: true // Users only see their own records in queries
517
475
  }
518
- }
476
+ })
519
477
  ```
520
478
 
521
- ### Testing Example
522
-
523
- In tests, you can easily simulate different users:
479
+ ### Example 4: Adding Admin Override (Custom Checker)
524
480
 
525
481
  ```javascript
526
- // Test different permission scenarios
527
- const contexts = {
528
- anonymous: {}, // No auth
529
- regular: { auth: { userId: 'user-1', role: 'member' } },
530
- editor: { auth: { userId: 'user-2', role: 'editor' } },
531
- admin: { auth: { userId: 'user-3', role: 'admin' } }
532
- };
533
-
534
- // Test that regular users can't delete
535
- await assert.rejects(
536
- api.resources.books.delete({ id: 1 }, contexts.regular),
537
- /Forbidden/
538
- );
539
-
540
- // Test that editors can update
541
- await api.resources.books.patch({
542
- id: 1,
543
- inputRecord: { /* ... */ }
544
- }, contexts.editor);
545
- ```
546
-
547
- ## Using Auth Helpers
548
-
549
- While declarative permissions handle most cases, you can also use auth helpers in custom hooks:
482
+ // Register a custom role checker
483
+ api.helpers.auth.registerChecker('role', async (context, { param }) => {
484
+ // In real app, query your users table
485
+ const user = await api.resources.users.get(context.auth.userId)
486
+ return user.role === param
487
+ })
550
488
 
551
- ```javascript
552
- // In any hook, you have access to helpers.auth
553
- api.addHook('beforeCreate', async ({ context, inputRecord, helpers }) => {
554
- // Require authentication
555
- helpers.auth.requireAuth(context);
556
-
557
- // Require specific roles
558
- helpers.auth.requireRoles(context, ['editor', 'admin']);
559
-
560
- // Check ownership (multiple ways)
561
- helpers.auth.requireOwnership(context); // Uses context.existingRecord
562
- helpers.auth.requireOwnership(context, record); // Pass record
563
- helpers.auth.requireOwnership(context, '123'); // Pass user ID
489
+ await api.addResource('articles', {
490
+ schema: { /* ... */ },
564
491
 
565
- // Set owner on new records
566
- inputRecord.user_id = context.auth.userId;
567
- });
568
- ```
569
-
570
- ## Token Management
571
-
572
- ### Context Population
573
-
574
- When a valid JWT token is provided, the plugin populates `context.auth`:
575
-
576
- ```javascript
577
- context.auth = {
578
- userId: '123', // From token 'sub' claim
579
- email: 'user@example.com', // From token 'email' claim
580
- roles: ['user', 'editor'], // From token 'roles' claim
581
- permissions: ['posts:write'], // From token 'permissions' claim
582
- token: { /* full JWT payload */ },
583
- tokenId: 'jti-value' // For revocation
584
- }
585
- ```
586
-
587
- ### Token Revocation and Logout
588
-
589
- The plugin supports token revocation for logout and security. Here's how it works:
590
-
591
- #### Frontend Logout Flow
592
- ```javascript
593
- // 1. Call your API's logout endpoint
594
- await fetch('/api/auth/logout', {
595
- method: 'POST',
596
- headers: {
597
- 'Authorization': `Bearer ${token}`
492
+ auth: {
493
+ query: ['public'],
494
+ get: ['public'],
495
+ post: ['authenticated'],
496
+ patch: ['owns', 'role:admin'], // Owner OR admin
497
+ delete: ['owns', 'role:admin'] // Owner OR admin
598
498
  }
599
- });
600
-
601
- // 2. Clear local storage
602
- localStorage.removeItem('supabase.auth.token');
603
-
604
- // 3. Sign out from Supabase (optional but recommended)
605
- await supabase.auth.signOut();
499
+ })
606
500
  ```
607
501
 
608
- #### API-Side Token Management
609
- ```javascript
610
- // The plugin provides these methods:
611
-
612
- // In your custom endpoints
613
- await helpers.auth.logout(context); // Revokes current token
614
-
615
- // Revoke specific token (e.g., from webhook)
616
- await helpers.auth.revokeToken(jti, userId, expiresAt);
502
+ ## Advanced: Custom Authorization
617
503
 
618
- // Check current session
619
- GET /api/auth/session
620
- // Returns: { authenticated: true/false, user: {...} }
621
- ```
622
-
623
- #### Handling Auth Provider Webhooks
504
+ The plugin is designed to be minimal. For complex authorization, you create custom checkers:
624
505
 
625
- If your auth provider supports webhooks, you can sync logouts:
506
+ ### Database-Driven Roles
626
507
 
627
508
  ```javascript
628
- // Handle Supabase auth events
629
- api.addRoute('POST', '/webhooks/supabase-auth', async ({ body }) => {
630
- if (body.event === 'SIGNED_OUT') {
631
- // Revoke the token in your API too
632
- await helpers.auth.revokeToken(
633
- body.logout_token_id,
634
- body.user_id,
635
- body.token_exp
636
- );
509
+ // Store roles in your database
510
+ await api.addResource('user_roles', {
511
+ schema: {
512
+ user_id: { type: 'string', required: true },
513
+ role: { type: 'string', required: true }
637
514
  }
638
- });
639
- ```
640
-
641
- ### Working with Different Auth Providers
515
+ })
642
516
 
643
- The plugin works with any JWT provider. Here are the common configurations:
517
+ // Create role checker
518
+ api.helpers.auth.registerChecker('role', async (context, { param }) => {
519
+ const roles = await api.resources.user_roles.query({
520
+ filter: { user_id: context.auth.userId }
521
+ })
522
+ return roles.some(r => r.role === param)
523
+ })
644
524
 
645
- #### Supabase Configuration
646
- ```javascript
647
- await api.use(JwtAuthPlugin, {
648
- jwksUrl: `${SUPABASE_URL}/auth/v1/.well-known/jwks.json`,
649
- rolesField: 'app_metadata.roles', // Supabase stores custom data here
650
-
651
- // Add roles in Supabase Dashboard:
652
- // Authentication > Users > Select User > Edit User Metadata
653
- // Add to app_metadata: { "roles": ["admin", "editor"] }
654
- });
525
+ // Use it
526
+ auth: {
527
+ delete: ['role:admin'] // Must have admin role in database
528
+ }
655
529
  ```
656
530
 
657
- #### Auth0 Configuration
658
- ```javascript
659
- await api.use(JwtAuthPlugin, {
660
- jwksUrl: `https://${AUTH0_DOMAIN}/.well-known/jwks.json`,
661
- audience: AUTH0_AUDIENCE,
662
- issuer: `https://${AUTH0_DOMAIN}/`,
663
- rolesField: 'https://your-app.com/roles', // Auth0 uses namespaced claims
664
-
665
- // Add roles in Auth0:
666
- // Create a Rule or Action that adds roles to the token
667
- });
668
- ```
531
+ ### Subscription Tiers
669
532
 
670
- #### Firebase Auth Configuration
671
533
  ```javascript
672
- await api.use(JwtAuthPlugin, {
673
- jwksUrl: 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com',
674
- audience: process.env.FIREBASE_PROJECT_ID,
675
- issuer: `https://securetoken.google.com/${process.env.FIREBASE_PROJECT_ID}`,
676
- rolesField: 'custom_claims.roles'
677
- });
678
- ```
534
+ api.helpers.auth.registerChecker('plan', async (context, { param }) => {
535
+ const user = await api.resources.users.get(context.auth.userId)
536
+ const plans = ['free', 'basic', 'pro', 'enterprise']
537
+ const requiredLevel = plans.indexOf(param)
538
+ const userLevel = plans.indexOf(user.subscription_plan)
539
+ return userLevel >= requiredLevel
540
+ })
679
541
 
680
- ## Custom Auth Checkers
542
+ // Premium features
543
+ auth: {
544
+ post: ['plan:pro'] // Need pro plan or higher
545
+ }
546
+ ```
681
547
 
682
- You can create domain-specific auth checkers:
548
+ ### Team Membership
683
549
 
684
550
  ```javascript
685
- // Register a custom checker
686
- helpers.auth.registerChecker('is_team_member', async (context, { existingRecord }) => {
687
- if (!context.auth?.userId) return false;
688
-
689
- // Check if user is part of the team
690
- const team = await api.resources.teams.get({
691
- id: existingRecord.team_id
692
- });
693
-
694
- return team.member_ids.includes(context.auth.userId);
695
- });
551
+ api.helpers.auth.registerChecker('team', async (context, { param }) => {
552
+ const membership = await api.resources.team_members.query({
553
+ filter: {
554
+ user_id: context.auth.userId,
555
+ team_id: param
556
+ }
557
+ })
558
+ return membership.length > 0
559
+ })
696
560
 
697
- // Use in a resource
698
- await api.addResource('team_documents', {
699
- schema: { /* ... */ },
700
-
701
- auth: {
702
- query: ['is_team_member'],
703
- get: ['is_team_member'],
704
- patch: ['is_team_member', 'admin']
705
- }
706
- });
561
+ // Team resources
562
+ auth: {
563
+ query: ['team:engineering'] // Must be on engineering team
564
+ }
707
565
  ```
708
566
 
709
- ## Complete Example: Book Catalog API
710
-
711
- Let's build a complete authenticated book catalog API:
567
+ ### Multi-Tenant Permissions
712
568
 
713
569
  ```javascript
714
- import { Api } from 'hooked-api';
715
- import { RestApiPlugin, RestApiKnexPlugin, HttpPlugin } from 'json-rest-api';
716
- import { JwtAuthPlugin } from 'json-rest-api/plugins/core/jwt-auth-plugin.js';
717
-
718
- // 1. Create and configure API
719
- const api = new Api();
720
-
721
- await api.use(RestApiPlugin);
722
- await api.use(RestApiKnexPlugin, { knex });
723
-
724
- // 2. Configure JWT Auth for Supabase
725
- await api.use(JwtAuthPlugin, {
726
- jwksUrl: `${process.env.SUPABASE_URL}/auth/v1/.well-known/jwks.json`,
727
- rolesField: 'app_metadata.roles',
728
- ownershipField: 'user_id'
729
- });
730
-
731
- // 3. Define authenticated resources
732
- await api.addResource('books', {
733
- schema: {
734
- id: { type: 'id' },
735
- title: { type: 'string', required: true },
736
- isbn: { type: 'string' },
737
- user_id: { type: 'string' }, // Owner
738
- country_id: { type: 'number', belongsTo: 'countries' },
739
- published: { type: 'boolean', default: false }
740
- },
741
-
742
- auth: {
743
- query: ['public'], // Anyone can browse
744
- get: ['public'], // Anyone can read
745
- post: ['authenticated'], // Must be logged in
746
- patch: ['is_owner', 'has_role:librarian'], // Owner or librarian
747
- delete: ['is_owner', 'admin'] // Owner or admin
748
- }
749
- });
750
-
751
- // 4. Auto-set ownership on create
752
- api.addHook('beforeCreate', async ({ context, inputRecord, scopeName }) => {
753
- if (scopeName === 'books' && context.auth) {
754
- inputRecord.user_id = context.auth.userId;
755
- }
756
- });
757
-
758
- // 5. Filter unpublished books for anonymous users
759
- api.addHook('beforeQuery', async ({ context, queryParams, scopeName }) => {
760
- if (scopeName === 'books' && !context.auth) {
761
- queryParams.filter = { ...queryParams.filter, published: true };
762
- }
763
- });
764
-
765
- // 6. Start HTTP server
766
- await api.use(HttpPlugin, { port: 3000, basePath: '/api' });
570
+ api.helpers.auth.registerChecker('tenant_role', async (context, { param }) => {
571
+ const tenantId = context.request.headers['x-tenant-id']
572
+ const membership = await api.resources.tenant_members.query({
573
+ filter: {
574
+ tenant_id: tenantId,
575
+ user_id: context.auth.userId
576
+ }
577
+ })
578
+ return membership[0]?.role === param
579
+ })
767
580
 
768
- // Frontend usage:
769
- // const { data: { session } } = await supabase.auth.signIn(...)
770
- // const token = session.access_token
771
- //
772
- // fetch('/api/books', {
773
- // headers: { 'Authorization': `Bearer ${token}` }
774
- // })
581
+ // Tenant-specific permissions
582
+ auth: {
583
+ delete: ['tenant_role:admin'] // Admin in current tenant
584
+ }
775
585
  ```
776
586
 
777
- ## More Examples
587
+ ## Token Management
778
588
 
779
- ### Public Blog with Private Drafts
589
+ ### Accessing Token Data
780
590
 
781
591
  ```javascript
782
- await api.addResource('posts', {
783
- schema: {
784
- id: { type: 'id' },
785
- title: { type: 'string', required: true },
786
- content: { type: 'text' },
787
- user_id: { type: 'string' },
788
- published: { type: 'boolean', default: false }
592
+ // After validation, you have access to:
593
+ context.auth = {
594
+ userId: '123', // From 'sub' claim
595
+ email: 'user@example.com', // From 'email' claim
596
+ token: { // Full JWT payload
597
+ sub: '123',
598
+ email: 'user@example.com',
599
+ iat: 1234567890,
600
+ exp: 1234567890,
601
+ // Any custom claims from your auth provider
602
+ app_metadata: { /* ... */ },
603
+ user_metadata: { /* ... */ }
789
604
  },
790
-
791
- auth: {
792
- query: ['public'], // Anyone can list
793
- get: ['public'], // Anyone can read
794
- post: ['authenticated'], // Must be logged in to create
795
- patch: ['is_owner'], // Only owner can edit
796
- delete: ['is_owner', 'admin'] // Owner or admin can delete
797
- }
798
- });
799
-
800
- // Add custom filtering for drafts
801
- api.addHook('beforeQuery', async ({ context, queryParams }) => {
802
- // Non-owners only see published posts
803
- if (!context.auth || context.auth.userId !== queryParams.filter?.user_id) {
804
- queryParams.filter = { ...queryParams.filter, published: true };
805
- }
806
- });
605
+ tokenId: 'jti-123' // For revocation
606
+ }
807
607
  ```
808
608
 
809
- ### Multi-Author Books
609
+ ### Token Revocation / Logout
810
610
 
811
611
  ```javascript
812
- await api.addResource('books', {
813
- schema: {
814
- id: { type: 'id' },
815
- title: { type: 'string', required: true },
816
- publisher_id: { type: 'number', belongsTo: 'publishers' }
817
- },
818
-
819
- relationships: {
820
- authors: { hasMany: 'authors', through: 'book_authors' }
612
+ // Enable revocation
613
+ await api.use(JwtAuthPlugin, {
614
+ jwksUrl: '...',
615
+ revocation: {
616
+ enabled: true,
617
+ storage: 'database' // Survives restarts
821
618
  },
822
-
823
- auth: {
824
- query: ['public'],
825
- get: ['public'],
826
- post: ['has_role:author', 'has_role:editor'],
827
- patch: ['is_book_author', 'has_role:editor'],
828
- delete: ['admin']
619
+ endpoints: {
620
+ logout: '/auth/logout' // Add logout endpoint
829
621
  }
830
- });
622
+ })
831
623
 
832
- // Custom checker for multi-author books
833
- helpers.auth.registerChecker('is_book_author', async (context, { existingRecord }) => {
834
- if (!context.auth?.userId || !existingRecord) return false;
835
-
836
- const bookAuthors = await api.resources.book_authors.query({
837
- queryParams: {
838
- filter: {
839
- book_id: existingRecord.id,
840
- author_id: context.auth.userId
841
- }
624
+ // Frontend logout
625
+ async function logout(token) {
626
+ // 1. Revoke token on your API
627
+ await fetch('http://localhost:3000/auth/logout', {
628
+ method: 'POST',
629
+ headers: {
630
+ 'Authorization': `Bearer ${token}`
842
631
  }
843
- });
844
-
845
- return bookAuthors.length > 0;
846
- });
847
- ```
848
-
849
- ### Admin Panel
850
-
851
- ```javascript
852
- await api.addResource('users', {
853
- schema: {
854
- id: { type: 'id' },
855
- email: { type: 'string', required: true },
856
- role: { type: 'string' },
857
- banned: { type: 'boolean', default: false }
858
- },
632
+ })
859
633
 
860
- auth: {
861
- query: ['admin', 'has_role:user_manager'],
862
- get: ['admin', 'has_role:user_manager', 'is_self'],
863
- post: ['admin'],
864
- patch: ['admin', 'has_role:user_manager', 'is_self'],
865
- delete: ['admin']
866
- }
867
- });
868
-
869
- // Users can view/edit their own profile
870
- helpers.auth.registerChecker('is_self', (context, { existingRecord }) => {
871
- return context.auth?.userId === existingRecord?.id;
872
- });
634
+ // 2. Clear Supabase session
635
+ await supabase.auth.signOut()
636
+ }
873
637
  ```
874
638
 
875
- ## Best Practices
639
+ ### Direct API Usage (No HTTP)
876
640
 
877
- ### 1. Use Declarative Permissions
641
+ For testing or server-side operations:
878
642
 
879
- Instead of:
880
643
  ```javascript
881
- // ❌ Manual permission checks in hooks
882
- api.addHook('checkPermissions', async ({ context, operation }) => {
883
- if (operation === 'post' && !context.auth) {
884
- throw new Error('Must be authenticated');
644
+ // Create auth context manually
645
+ const authContext = {
646
+ auth: {
647
+ userId: 'user-123',
648
+ email: 'user@example.com'
885
649
  }
886
- });
887
- ```
888
-
889
- Do this:
890
- ```javascript
891
- // ✅ Declarative permissions
892
- auth: {
893
- post: ['authenticated']
894
650
  }
895
- ```
896
651
 
897
- ### 2. Combine Rules with OR Logic
652
+ // Use API methods directly
653
+ const posts = await api.resources.posts.query({
654
+ filter: { published: true }
655
+ }, authContext) // Pass context as second parameter
898
656
 
899
- Multiple rules in an array work as OR conditions:
900
- ```javascript
901
- auth: {
902
- patch: ['is_owner', 'has_role:moderator', 'admin']
903
- // Can update if: owner OR moderator OR admin
904
- }
657
+ const newPost = await api.resources.posts.post({
658
+ inputRecord: { /* ... */ }
659
+ }, authContext)
905
660
  ```
906
661
 
907
- ### 3. Set Ownership on Create
662
+ ## Common Issues and Solutions
908
663
 
664
+ ### Issue: "Access denied" on all requests
665
+ **Solution**: Check that your token is being sent correctly:
909
666
  ```javascript
910
- api.addHook('beforeCreate', async ({ context, inputRecord, scopeName }) => {
911
- // Set owner for user-owned resources
912
- if (scopeName === 'posts' && context.auth) {
913
- inputRecord.user_id = context.auth.userId;
914
- }
915
- });
916
- ```
917
-
918
- ### 4. Use Appropriate Checkers
919
-
920
- - `public` - For truly public data
921
- - `authenticated` - When you just need a logged-in user
922
- - `is_owner` - For user-owned resources
923
- - `has_role:X` - For role-based access
924
- - `admin` - For administrative functions
667
+ // Correct
668
+ headers: { 'Authorization': `Bearer ${token}` }
925
669
 
926
- ### 5. Handle Unauthenticated Users Gracefully
670
+ // Wrong
671
+ headers: { 'Authorization': token } // Missing "Bearer "
672
+ ```
927
673
 
674
+ ### Issue: Token validation fails
675
+ **Solution**: Ensure your JWKS URL matches your auth provider:
928
676
  ```javascript
929
- // Filter data for unauthenticated users instead of denying access
930
- api.addHook('beforeQuery', async ({ context, queryParams, scopeName }) => {
931
- if (scopeName === 'posts' && !context.auth) {
932
- // Only show published posts to anonymous users
933
- queryParams.filter = { ...queryParams.filter, published: true };
934
- }
935
- });
677
+ // Supabase: Check your project URL
678
+ jwksUrl: 'https://YOUR-PROJECT.supabase.co/auth/v1/.well-known/jwks.json'
679
+
680
+ // Auth0: Check your domain
681
+ jwksUrl: 'https://YOUR-DOMAIN.auth0.com/.well-known/jwks.json'
936
682
  ```
937
683
 
938
- ### 6. Create Semantic Custom Checkers
684
+ ### Issue: Google login token not working
685
+ **Solution**: Remember that Google login still goes through your auth provider:
686
+ - User logs in with Google → Supabase/Auth0 → Get Supabase/Auth0 JWT
687
+ - Your API validates the Supabase/Auth0 JWT, not Google's token
939
688
 
689
+ ### Issue: Ownership not working
690
+ **Solution**: Ensure your schema has a `user_id` field:
940
691
  ```javascript
941
- // ✅ Good: Semantic name that explains the permission
942
- helpers.auth.registerChecker('can_moderate_content', (context) => {
943
- return context.auth?.roles?.includes('moderator') ||
944
- context.auth?.roles?.includes('admin');
945
- });
946
-
947
- // ❌ Bad: Technical implementation detail
948
- helpers.auth.registerChecker('has_mod_or_admin', (context) => {
949
- // Same logic but less clear intent
950
- });
692
+ schema: {
693
+ user_id: { type: 'string' } // Required for 'owns' checker
694
+ }
951
695
  ```
952
696
 
953
697
  ## Summary
954
698
 
955
- The JWT Auth Plugin provides a clean, declarative way to handle authentication and authorization in your REST API. By defining permissions directly on resources, you eliminate boilerplate code and create a more maintainable, secure API.
956
-
957
- Key benefits:
958
- - **No manual hook writing** for common permission patterns
959
- - **Clear, readable** permission declarations
960
- - **Flexible** enough for complex scenarios
961
- - **Secure by default** with deny-by-default behavior
962
- - **Extensible** with custom checkers
699
+ The JWT Authentication Plugin provides:
700
+ 1. **Token validation** from any JWT provider (Supabase, Auth0, etc.)
701
+ 2. **Three built-in checkers**: `public`, `authenticated`, `owns`
702
+ 3. **Framework for custom authorization** via `checker:parameter` pattern
963
703
 
964
- Whether you're building a simple blog or a complex multi-tenant application, the declarative permission system scales with your needs while keeping your code clean and maintainable.
704
+ Remember:
705
+ - **Frontend**: Handles login (email, Google, etc.) and gets JWT
706
+ - **Backend**: Validates JWT and enforces permissions
707
+ - **Start simple**: Use built-in checkers, add custom logic as needed