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.
- package/.claude/settings.local.json +9 -1
- package/AGENTS.md +34 -0
- package/CLAUDE.md +26 -1
- package/CRUSH.md +55 -0
- package/README.md +3 -3
- package/TODO.todo +8 -0
- package/docs/API.md +17 -18
- package/docs/COMPARISON.md +18 -13
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
- package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
- package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
- package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
- package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
- package/docs/GUIDE/index.md +3 -0
- package/docs/QUICKSTART.md +12 -12
- package/docs/index.md +2 -2
- package/docs/mini-guides/file-uploads-complete.md +0 -1
- package/examples/url-override-example.js +137 -0
- package/index.js +7 -1
- package/package.json +5 -9
- package/plugins/core/bulk-operations-plugin.js +2 -0
- package/plugins/core/connectors/express-plugin.js +43 -24
- package/plugins/core/connectors/lib/request-helpers.js +2 -2
- package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
- package/plugins/core/lib/anyapi/schema-utils.js +186 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
- package/plugins/core/lib/dbTablesOperations.js +305 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
- package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
- package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
- package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
- package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
- package/plugins/core/lib/querying/url-helpers.js +119 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
- package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
- package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
- package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
- package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
- package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
- package/plugins/core/lib/storage/storage-adapter.js +160 -0
- package/plugins/core/lib/writing/relationship-processor.js +4 -11
- package/plugins/core/rest-api-access.js +456 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
- package/plugins/core/rest-api-knex-plugin.js +226 -87
- package/plugins/core/rest-api-multihome-plugin.js +89 -27
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
- package/plugins/core/rest-api-plugin-methods/common.js +46 -10
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
- package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
- package/plugins/core/rest-api-plugin-methods/get.js +2 -5
- package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
- package/plugins/core/rest-api-plugin-methods/post.js +23 -11
- package/plugins/core/rest-api-plugin-methods/put.js +18 -21
- package/plugins/core/rest-api-plugin-methods/query.js +3 -4
- package/plugins/core/rest-api-plugin-methods/release.js +13 -0
- package/plugins/core/rest-api-plugin.js +3 -4
- package/plugins/core/rest-api-positioning-plugin.js +253 -76
- package/plugins/core/socketio-plugin.js +436 -502
- package/quickTest.js +2 -2
- package/scripts/debug-anyapi-review-filter.mjs +63 -0
- package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
- package/scripts/inspect-anyapi-descriptor.mjs +19 -0
- package/test-debug.js +59 -0
- package/tests/README.md +7 -1
- package/tests/anyapi-basic.test.js +102 -0
- package/tests/anyapi-cursor-pagination.test.js +144 -0
- package/tests/computed-fields.test.js +27 -12
- package/tests/cors.test.js +5 -6
- package/tests/curl.test.js +1 -1
- package/tests/custom-idproperty-relationships.test.js +37 -29
- package/tests/field-getters.test.js +25 -7
- package/tests/fixtures/api-configs.js +576 -150
- package/tests/helpers/storage-mode.js +65 -0
- package/tests/helpers/test-utils.js +85 -3
- package/tests/include-depth-validation.test.js +35 -1
- package/tests/nested-includes.test.js +130 -103
- package/tests/pagination-cursor-multifield.test.js +17 -13
- package/tests/pagination-enhanced.test.js +19 -24
- package/tests/polymorphic-simple.test.js +9 -9
- package/tests/positioning-advanced.test.js +19 -19
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +62 -62
- package/tests/positioning.test.js +7 -7
- package/tests/queries.test.js +5 -5
- package/tests/query-limits.test.js +77 -92
- package/tests/rest-api-access.test.js +206 -0
- package/tests/searchschema-merge.test.js +182 -0
- package/tests/socketio.test.js +33 -14
- package/tests/virtual-fields.test.js +0 -1
- package/docs/DOCS_ALL.md +0 -21894
- package/plugins/core/jwt-auth-plugin.js +0 -897
- package/plugins/core/lib/jwt-auth-helpers.js +0 -130
- package/tests/jwt-auth.test.js +0 -692
|
@@ -1,155 +1,201 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Authentication and Permissions Guide
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
- [
|
|
7
|
-
- [Quick Start](#quick-start)
|
|
8
|
-
- [Getting
|
|
9
|
-
- [Supabase](#supabase)
|
|
10
|
-
- [Auth0](#auth0)
|
|
11
|
-
- [
|
|
12
|
-
- [
|
|
13
|
-
- [
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
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
|
-
- [
|
|
19
|
-
- [Examples](#examples)
|
|
20
|
-
- [Best Practices](#best-practices)
|
|
17
|
+
- [Migration Guide](#migration-guide)
|
|
21
18
|
|
|
22
|
-
##
|
|
19
|
+
## How It Works - The Architecture
|
|
23
20
|
|
|
24
|
-
The JWT
|
|
21
|
+
The JWT plugin is a **backend/server-side** tool that validates tokens. Here's the complete flow:
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
+
### Step 1: Install the Plugin
|
|
34
55
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
import { createClient } from '@supabase/supabase-js'
|
|
56
|
+
```bash
|
|
57
|
+
npm install jose
|
|
58
|
+
```
|
|
39
59
|
|
|
40
|
-
|
|
60
|
+
### Step 2: Configure Your Backend API
|
|
41
61
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
|
|
67
|
+
// Create your API
|
|
68
|
+
const api = new Api();
|
|
69
|
+
await api.use(RestApiPlugin);
|
|
70
|
+
await api.use(RestApiKnexPlugin, { knex });
|
|
50
71
|
|
|
51
|
-
|
|
52
|
-
```javascript
|
|
53
|
-
// In your API server
|
|
72
|
+
// Add JWT authentication (example with Supabase)
|
|
54
73
|
await api.use(JwtAuthPlugin, {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
rolesField: 'app_metadata.roles'
|
|
58
|
-
})
|
|
59
|
-
```
|
|
74
|
+
jwksUrl: 'https://your-project.supabase.co/auth/v1/.well-known/jwks.json'
|
|
75
|
+
});
|
|
60
76
|
|
|
61
|
-
|
|
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: ['
|
|
70
|
-
delete: ['
|
|
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!
|
|
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
|
-
|
|
124
|
+
### Supabase Setup
|
|
78
125
|
|
|
79
|
-
|
|
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
|
-
|
|
131
|
+
#### 2. Frontend: Email/Password Login
|
|
82
132
|
|
|
83
|
-
#### Step 1: Set up Supabase Auth
|
|
84
133
|
```javascript
|
|
85
|
-
// In your
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
####
|
|
181
|
+
#### 3. Backend Configuration
|
|
182
|
+
|
|
134
183
|
```javascript
|
|
184
|
+
// Your API server
|
|
135
185
|
await api.use(JwtAuthPlugin, {
|
|
136
|
-
|
|
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
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
210
|
+
// Login
|
|
211
|
+
async function login() {
|
|
212
|
+
await auth0.loginWithRedirect()
|
|
213
|
+
}
|
|
169
214
|
|
|
170
|
-
// After redirect
|
|
171
|
-
|
|
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
|
-
####
|
|
222
|
+
#### 3. Backend Configuration
|
|
223
|
+
|
|
175
224
|
```javascript
|
|
225
|
+
// Your API server
|
|
176
226
|
await api.use(JwtAuthPlugin, {
|
|
177
|
-
jwksUrl:
|
|
178
|
-
audience:
|
|
179
|
-
issuer:
|
|
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
|
-
###
|
|
233
|
+
### Google OAuth Login
|
|
187
234
|
|
|
188
|
-
|
|
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
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
277
|
+
##### 3. Backend: Same Configuration!
|
|
278
|
+
|
|
210
279
|
```javascript
|
|
280
|
+
// No change needed - same as email/password
|
|
211
281
|
await api.use(JwtAuthPlugin, {
|
|
212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
288
|
+
## Backend: Validating Tokens
|
|
289
|
+
|
|
290
|
+
### Basic Configuration Options
|
|
224
291
|
|
|
225
|
-
|
|
292
|
+
```javascript
|
|
226
293
|
await api.use(JwtAuthPlugin, {
|
|
227
|
-
//
|
|
228
|
-
|
|
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
|
-
//
|
|
239
|
-
|
|
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
|
-
//
|
|
244
|
-
|
|
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
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
312
|
+
### Understanding JWKS Configuration (Recommended)
|
|
258
313
|
|
|
259
|
-
|
|
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.
|
|
263
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
350
|
-
Anyone can access, no authentication required.
|
|
365
|
+
### What Happens During Validation
|
|
351
366
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
359
|
-
User must be logged in (have a valid token).
|
|
381
|
+
## Using Built-in Checkers
|
|
360
382
|
|
|
361
|
-
|
|
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
|
-
### `
|
|
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
|
-
|
|
373
|
-
|
|
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
|
-
### `
|
|
381
|
-
User must have the 'admin' role.
|
|
394
|
+
### 2. `authenticated` - Must Be Logged In
|
|
382
395
|
|
|
383
396
|
```javascript
|
|
384
397
|
auth: {
|
|
385
|
-
|
|
398
|
+
post: ['authenticated'], // Must have valid JWT token
|
|
399
|
+
patch: ['authenticated']
|
|
386
400
|
}
|
|
387
401
|
```
|
|
388
402
|
|
|
389
|
-
### `
|
|
390
|
-
User must have a specific role.
|
|
403
|
+
### 3. `owns` - Must Own the Resource
|
|
391
404
|
|
|
392
405
|
```javascript
|
|
393
406
|
auth: {
|
|
394
|
-
patch: ['
|
|
395
|
-
delete: ['
|
|
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
|
-
|
|
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
|
-
##
|
|
414
|
+
## Progressive Examples
|
|
410
415
|
|
|
411
|
-
|
|
416
|
+
Let's build up from simple to complex:
|
|
412
417
|
|
|
413
|
-
###
|
|
418
|
+
### Example 1: Public API (No Auth)
|
|
414
419
|
|
|
415
420
|
```javascript
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
480
|
-
|
|
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
|
-
|
|
483
|
-
|
|
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
|
-
###
|
|
492
|
-
|
|
493
|
-
For administrative scripts or background jobs:
|
|
450
|
+
### Example 3: Users Can Only Edit Their Own Content
|
|
494
451
|
|
|
495
452
|
```javascript
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
//
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
###
|
|
522
|
-
|
|
523
|
-
In tests, you can easily simulate different users:
|
|
479
|
+
### Example 4: Adding Admin Override (Custom Checker)
|
|
524
480
|
|
|
525
481
|
```javascript
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
552
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
506
|
+
### Database-Driven Roles
|
|
626
507
|
|
|
627
508
|
```javascript
|
|
628
|
-
//
|
|
629
|
-
api.
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
|
|
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
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
542
|
+
// Premium features
|
|
543
|
+
auth: {
|
|
544
|
+
post: ['plan:pro'] // Need pro plan or higher
|
|
545
|
+
}
|
|
546
|
+
```
|
|
681
547
|
|
|
682
|
-
|
|
548
|
+
### Team Membership
|
|
683
549
|
|
|
684
550
|
```javascript
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
//
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
|
|
710
|
-
|
|
711
|
-
Let's build a complete authenticated book catalog API:
|
|
567
|
+
### Multi-Tenant Permissions
|
|
712
568
|
|
|
713
569
|
```javascript
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
//
|
|
769
|
-
|
|
770
|
-
//
|
|
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
|
-
##
|
|
587
|
+
## Token Management
|
|
778
588
|
|
|
779
|
-
###
|
|
589
|
+
### Accessing Token Data
|
|
780
590
|
|
|
781
591
|
```javascript
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
609
|
+
### Token Revocation / Logout
|
|
810
610
|
|
|
811
611
|
```javascript
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
-
|
|
639
|
+
### Direct API Usage (No HTTP)
|
|
876
640
|
|
|
877
|
-
|
|
641
|
+
For testing or server-side operations:
|
|
878
642
|
|
|
879
|
-
Instead of:
|
|
880
643
|
```javascript
|
|
881
|
-
//
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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
|
-
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
-
|
|
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
|
-
|
|
911
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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
|
-
|
|
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
|