@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.
Files changed (2) hide show
  1. package/boost/guidelines.md +190 -0
  2. 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.0",
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.2",
37
- "@rudderjs/contracts": "^1.3.0",
38
- "@rudderjs/orm": "^1.8.0"
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.0"
45
+ "@rudderjs/console": "^1.0.1"
45
46
  },
46
47
  "author": "Suleiman Shahbari",
47
48
  "scripts": {