@rudderjs/passport 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/boost/guidelines.md +190 -0
- package/package.json +7 -6
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# @rudderjs/passport
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
OAuth 2 server package — issues JWT access tokens, refresh tokens, and personal access tokens. Ships four grants (authorization code + PKCE, client credentials, refresh token, device code), a `HasApiTokens` mixin for user models, and `RequireBearer` + `scope` middleware for protecting API routes. JWTs are RS256-signed, so third parties can verify them without calling the server.
|
|
6
|
+
|
|
7
|
+
## When to Use Passport vs Auth
|
|
8
|
+
|
|
9
|
+
`@rudderjs/auth` covers **session-based web auth** — login forms, cookies, password reset, email verification. `@rudderjs/passport` covers **token-based API auth** — OAuth flows for third-party integrations, M2M service auth, personal access tokens.
|
|
10
|
+
|
|
11
|
+
Most apps need both:
|
|
12
|
+
|
|
13
|
+
- **Web routes** (`m.web` group): `AuthMiddleware` runs automatically — read `req.user` directly.
|
|
14
|
+
- **API routes** (`m.api` group): stateless by default. Opt in per-route with `RequireBearer()` + `scope(...)`, or mount `AuthMiddleware('api')` + `RequireAuth('api')` with a token guard.
|
|
15
|
+
|
|
16
|
+
**Don't** mount `AuthMiddleware` globally via `m.use(...)`. API routes must stay stateless so they don't depend on session ALS context.
|
|
17
|
+
|
|
18
|
+
## Key Patterns
|
|
19
|
+
|
|
20
|
+
### Protecting API Routes
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { RequireBearer, scope } from '@rudderjs/passport'
|
|
24
|
+
|
|
25
|
+
router.get('/api/user', [RequireBearer()], (req) => req.user)
|
|
26
|
+
router.get('/api/posts', [RequireBearer(), scope('read')], listPosts)
|
|
27
|
+
router.post('/api/posts', [RequireBearer(), scope('write')], createPost)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`RequireBearer()` validates the JWT signature, checks expiration, and confirms the token hasn't been revoked in the DB. A valid token attaches the user to `req.user` (same shape as session-based routes).
|
|
31
|
+
|
|
32
|
+
`scope(...)` must run **after** `RequireBearer()` — it reads token scopes from request state set by the bearer middleware. Wildcard `*` grants everything.
|
|
33
|
+
|
|
34
|
+
### Personal Access Tokens (HasApiTokens)
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { Model } from '@rudderjs/orm'
|
|
38
|
+
import { HasApiTokens } from '@rudderjs/passport'
|
|
39
|
+
|
|
40
|
+
export class User extends HasApiTokens(Model) {
|
|
41
|
+
static table = 'user'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Issue — plain-text JWT is shown ONCE
|
|
45
|
+
const { plainTextToken, token } = await user.createToken('my-cli', ['read', 'write'])
|
|
46
|
+
|
|
47
|
+
// Manage
|
|
48
|
+
await user.tokens() // all tokens for this user
|
|
49
|
+
await user.revokeAllTokens() // revokes all, returns count
|
|
50
|
+
user.tokenCan('admin') // checks current-request token's scope (inside RequireBearer route)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Personal access tokens are issued against an internal `__personal_access__` OAuth client that Passport auto-creates on first use.
|
|
54
|
+
|
|
55
|
+
### Route Registration
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
// routes/api.ts
|
|
59
|
+
import { registerPassportRoutes } from '@rudderjs/passport'
|
|
60
|
+
|
|
61
|
+
export default (router) => {
|
|
62
|
+
registerPassportRoutes(router) // mounts /oauth/* endpoints
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Or selectively skip groups:
|
|
66
|
+
registerPassportRoutes(router, {
|
|
67
|
+
except: ['authorize', 'scopes'], // mount custom consent + scopes endpoints
|
|
68
|
+
prefix: '/api/oauth', // default is '/oauth'
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Available groups: `authorize`, `token`, `revoke`, `scopes`, `device`.
|
|
73
|
+
|
|
74
|
+
### Customization Hooks
|
|
75
|
+
|
|
76
|
+
All hooks live on the `Passport` static singleton. Call them from a provider's `boot()` method, before routes register:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { Passport, OAuthClient } from '@rudderjs/passport'
|
|
80
|
+
import { view } from '@rudderjs/view'
|
|
81
|
+
|
|
82
|
+
// Custom consent screen (default returns JSON)
|
|
83
|
+
Passport.authorizationView((ctx) => {
|
|
84
|
+
return view('oauth.authorize', {
|
|
85
|
+
client: ctx.client,
|
|
86
|
+
scopes: ctx.scopes,
|
|
87
|
+
redirectUri: ctx.redirectUri,
|
|
88
|
+
state: ctx.state,
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// Swap any model (add columns, override behavior)
|
|
93
|
+
class CustomOAuthClient extends OAuthClient { /* ... */ }
|
|
94
|
+
Passport.useClientModel(CustomOAuthClient)
|
|
95
|
+
// Also: useTokenModel, useRefreshTokenModel, useAuthCodeModel, useDeviceCodeModel
|
|
96
|
+
|
|
97
|
+
// Disable automatic route registration entirely
|
|
98
|
+
Passport.ignoreRoutes() // registerPassportRoutes() becomes a no-op
|
|
99
|
+
|
|
100
|
+
// Scopes can also be defined here instead of config
|
|
101
|
+
Passport.tokensCan({ read: 'Read access', write: 'Write access' })
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Config Shape
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
// config/passport.ts
|
|
108
|
+
import type { PassportConfig } from '@rudderjs/passport'
|
|
109
|
+
|
|
110
|
+
export default {
|
|
111
|
+
scopes: { read: 'Read', write: 'Write', admin: 'Admin' },
|
|
112
|
+
|
|
113
|
+
// Keys — prefer env vars in production
|
|
114
|
+
privateKey: process.env.PASSPORT_PRIVATE_KEY,
|
|
115
|
+
publicKey: process.env.PASSPORT_PUBLIC_KEY,
|
|
116
|
+
// OR filesystem:
|
|
117
|
+
keyPath: 'storage', // reads storage/oauth-{private,public}.key
|
|
118
|
+
|
|
119
|
+
// Lifetimes (ms)
|
|
120
|
+
tokensExpireIn: 15 * 24 * 60 * 60 * 1000,
|
|
121
|
+
refreshTokensExpireIn: 30 * 24 * 60 * 60 * 1000,
|
|
122
|
+
personalAccessTokensExpireIn: 6 * 30 * 24 * 60 * 60 * 1000,
|
|
123
|
+
} satisfies PassportConfig
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### CLI Commands
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
pnpm rudder passport:keys [--force] # generate RSA keypair
|
|
130
|
+
pnpm rudder passport:client "App Name" [--public|--client-credentials|--device|--personal]
|
|
131
|
+
pnpm rudder passport:purge # remove expired/revoked records
|
|
132
|
+
pnpm rudder make:passport-client # scaffold a client seeder
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Common Pitfalls
|
|
136
|
+
|
|
137
|
+
- **Missing RSA keys** — run `pnpm rudder passport:keys` before issuing tokens, or set `PASSPORT_PRIVATE_KEY`/`PASSPORT_PUBLIC_KEY` env vars. Without keys, `passport.token()` throws.
|
|
138
|
+
- **Prisma schema not copied** — `@rudderjs/passport` ships 5 Prisma models in `schema/passport.prisma`. Copy that file into the app's multi-file Prisma schema directory and run `prisma db push`. The provider does not migrate for you.
|
|
139
|
+
- **Mounting `AuthMiddleware` globally breaks API routes** — `@rudderjs/auth`'s `AuthMiddleware` auto-installs on the `web` group only. API routes stay stateless; opt into auth per-route with `RequireBearer()`. Never call `m.use(AuthMiddleware())` — it reintroduces the old global-install problem.
|
|
140
|
+
- **Scope middleware before bearer** — `scope('read')` must come after `RequireBearer()` in the middleware array; it reads the token scopes the bearer middleware attaches to the request.
|
|
141
|
+
- **PKCE required for public clients** — public clients (created with `--public`) must send `code_challenge` + `code_challenge_method=S256`. Missing PKCE → `invalid_request`.
|
|
142
|
+
- **Refresh token reuse** — rotation revokes the old refresh token atomically. Retrying with the old one returns `invalid_grant`.
|
|
143
|
+
- **ORM returns records, not Model instances** — `AccessToken.where(...).first()` returns a plain data object. Prototype methods don't work on query results. Use `@rudderjs/passport`'s `models/helpers.ts` helpers (e.g. `accessTokenHelpers.can(token, scope)`) rather than calling methods on the record.
|
|
144
|
+
- **Custom model `static table`** — use the Prisma delegate name (camelCase, e.g. `oauthClient`), NOT the `@@map`'d SQL name (`oauth_clients`). Wrong table name → `[RudderJS ORM] Prisma has no delegate for table "oauth_clients"`.
|
|
145
|
+
- **Consent screen needs session** — `POST /oauth/authorize` and `POST /oauth/device/approve` both require `req.user`. If you mount OAuth routes on the `api` group, these two routes will 401. Either keep consent + device-approve on the `web` group, or mount `SessionMiddleware()` + `AuthMiddleware()` per-route.
|
|
146
|
+
- **Personal access client cache** — `_personalClientId` is cached module-level. `resetPersonalAccessClient()` is test-only; don't call it in production code.
|
|
147
|
+
- **Don't store plain-text JWTs** — `user.createToken()` returns `plainTextToken` once. The DB stores only the record (used for revocation lookup via `jti`). Show the JWT to the user; they must save it themselves.
|
|
148
|
+
|
|
149
|
+
## Key Imports
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// Middleware
|
|
153
|
+
import { RequireBearer, BearerMiddleware, scope } from '@rudderjs/passport'
|
|
154
|
+
|
|
155
|
+
// Personal access tokens (user model mixin)
|
|
156
|
+
import { HasApiTokens } from '@rudderjs/passport'
|
|
157
|
+
|
|
158
|
+
// Customization
|
|
159
|
+
import { Passport } from '@rudderjs/passport'
|
|
160
|
+
|
|
161
|
+
// Route registration
|
|
162
|
+
import { registerPassportRoutes } from '@rudderjs/passport'
|
|
163
|
+
import type { PassportRouteOptions, PassportRouteGroup } from '@rudderjs/passport'
|
|
164
|
+
|
|
165
|
+
// Grant primitives (for custom route handlers)
|
|
166
|
+
import {
|
|
167
|
+
validateAuthorizationRequest,
|
|
168
|
+
issueAuthCode,
|
|
169
|
+
exchangeAuthCode,
|
|
170
|
+
clientCredentialsGrant,
|
|
171
|
+
refreshTokenGrant,
|
|
172
|
+
requestDeviceCode,
|
|
173
|
+
pollDeviceCode,
|
|
174
|
+
approveDeviceCode,
|
|
175
|
+
OAuthError,
|
|
176
|
+
} from '@rudderjs/passport'
|
|
177
|
+
|
|
178
|
+
// Models
|
|
179
|
+
import { OAuthClient, AccessToken, RefreshToken, AuthCode, DeviceCode } from '@rudderjs/passport'
|
|
180
|
+
|
|
181
|
+
// JWT primitives
|
|
182
|
+
import { createToken, verifyToken, unsafeDecodeToken } from '@rudderjs/passport'
|
|
183
|
+
// `decodeToken` is kept as a deprecated alias for `unsafeDecodeToken`. The
|
|
184
|
+
// `unsafe` prefix is intentional — the function does NOT verify the
|
|
185
|
+
// signature, so its output cannot be trusted for auth decisions. Use
|
|
186
|
+
// `verifyToken` whenever you need an authenticated payload.
|
|
187
|
+
|
|
188
|
+
// Types
|
|
189
|
+
import type { PassportConfig, PassportScope, NewPersonalAccessToken } from '@rudderjs/passport'
|
|
190
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rudderjs/passport",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"rudderjs": {
|
|
5
5
|
"provider": "PassportProvider",
|
|
6
6
|
"stage": "infrastructure",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"type": "module",
|
|
19
19
|
"files": [
|
|
20
20
|
"dist",
|
|
21
|
-
"schema"
|
|
21
|
+
"schema",
|
|
22
|
+
"boost"
|
|
22
23
|
],
|
|
23
24
|
"main": "./dist/index.js",
|
|
24
25
|
"types": "./dist/index.d.ts",
|
|
@@ -33,15 +34,15 @@
|
|
|
33
34
|
}
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {
|
|
36
|
-
"@rudderjs/core": "^1.1.
|
|
37
|
-
"@rudderjs/contracts": "^1.
|
|
38
|
-
"@rudderjs/orm": "^1.8.
|
|
37
|
+
"@rudderjs/core": "^1.1.3",
|
|
38
|
+
"@rudderjs/contracts": "^1.4.0",
|
|
39
|
+
"@rudderjs/orm": "^1.8.1"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"@types/node": "^20.0.0",
|
|
42
43
|
"typescript": "^5.4.0",
|
|
43
44
|
"tsx": "^4.0.0",
|
|
44
|
-
"@rudderjs/console": "^1.0.
|
|
45
|
+
"@rudderjs/console": "^1.0.1"
|
|
45
46
|
},
|
|
46
47
|
"author": "Suleiman Shahbari",
|
|
47
48
|
"scripts": {
|