remix 3.0.0-beta.0 → 3.0.0-beta.2

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 (88) hide show
  1. package/dist/fetch-router.d.ts +7 -0
  2. package/dist/fetch-router.d.ts.map +1 -1
  3. package/dist/node-tsx/load-module.d.ts +2 -0
  4. package/dist/node-tsx/load-module.d.ts.map +1 -0
  5. package/dist/node-tsx/load-module.js +2 -0
  6. package/dist/node-tsx.d.ts +3 -0
  7. package/dist/node-tsx.d.ts.map +1 -0
  8. package/{src/node-serve.ts → dist/node-tsx.js} +2 -1
  9. package/dist/render-middleware.d.ts +2 -0
  10. package/dist/render-middleware.d.ts.map +1 -0
  11. package/dist/render-middleware.js +2 -0
  12. package/dist/route-pattern/href.d.ts +2 -0
  13. package/dist/route-pattern/href.d.ts.map +1 -0
  14. package/dist/route-pattern/href.js +2 -0
  15. package/dist/route-pattern/join.d.ts +2 -0
  16. package/dist/route-pattern/join.d.ts.map +1 -0
  17. package/dist/route-pattern/join.js +2 -0
  18. package/dist/route-pattern/match.d.ts +2 -0
  19. package/dist/route-pattern/match.d.ts.map +1 -0
  20. package/dist/route-pattern/match.js +2 -0
  21. package/package.json +158 -44
  22. package/src/assert/README.md +109 -0
  23. package/src/assets/README.md +539 -0
  24. package/src/async-context-middleware/README.md +100 -0
  25. package/src/auth/README.md +445 -0
  26. package/src/auth-middleware/README.md +246 -0
  27. package/src/cli/README.md +78 -0
  28. package/src/compression-middleware/README.md +176 -0
  29. package/src/cookie/README.md +106 -0
  30. package/src/cop-middleware/README.md +117 -0
  31. package/src/cors-middleware/README.md +174 -0
  32. package/src/csrf-middleware/README.md +99 -0
  33. package/src/data-schema/README.md +422 -0
  34. package/src/data-table/README.md +552 -0
  35. package/src/data-table-mysql/README.md +97 -0
  36. package/src/data-table-postgres/README.md +74 -0
  37. package/src/data-table-sqlite/README.md +84 -0
  38. package/src/fetch-proxy/README.md +46 -0
  39. package/src/fetch-router/README.md +902 -0
  40. package/src/fetch-router.ts +7 -0
  41. package/src/file-storage/README.md +57 -0
  42. package/src/file-storage-s3/README.md +47 -0
  43. package/src/form-data-middleware/README.md +109 -0
  44. package/src/form-data-parser/README.md +160 -0
  45. package/src/fs/README.md +60 -0
  46. package/src/headers/README.md +629 -0
  47. package/src/html-template/README.md +101 -0
  48. package/src/lazy-file/README.md +109 -0
  49. package/src/logger-middleware/README.md +132 -0
  50. package/src/method-override-middleware/README.md +71 -0
  51. package/src/mime/README.md +110 -0
  52. package/src/multipart-parser/README.md +241 -0
  53. package/src/node-fetch-server/README.md +352 -0
  54. package/src/node-tsx/README.md +79 -0
  55. package/src/node-tsx/load-module.ts +2 -0
  56. package/{dist/node-serve.js → src/node-tsx.ts} +2 -1
  57. package/src/render-middleware/README.md +99 -0
  58. package/src/render-middleware.ts +2 -0
  59. package/src/route-pattern/README.md +291 -0
  60. package/src/route-pattern/href.ts +2 -0
  61. package/src/route-pattern/join.ts +2 -0
  62. package/src/route-pattern/match.ts +2 -0
  63. package/src/session/README.md +171 -0
  64. package/src/session-middleware/README.md +109 -0
  65. package/src/session-storage-memcache/README.md +37 -0
  66. package/src/session-storage-redis/README.md +37 -0
  67. package/src/static-middleware/README.md +89 -0
  68. package/src/tar-parser/README.md +74 -0
  69. package/src/terminal/README.md +92 -0
  70. package/src/test/README.md +430 -0
  71. package/src/ui/README.md +219 -0
  72. package/src/ui/accordion/README.md +166 -0
  73. package/src/ui/anchor/README.md +153 -0
  74. package/src/ui/animation/README.md +316 -0
  75. package/src/ui/breadcrumbs/README.md +55 -0
  76. package/src/ui/button/README.md +44 -0
  77. package/src/ui/combobox/README.md +145 -0
  78. package/src/ui/glyph/README.md +72 -0
  79. package/src/ui/listbox/README.md +115 -0
  80. package/src/ui/menu/README.md +96 -0
  81. package/src/ui/popover/README.md +122 -0
  82. package/src/ui/scroll-lock/README.md +33 -0
  83. package/src/ui/select/README.md +107 -0
  84. package/src/ui/server/README.md +90 -0
  85. package/src/ui/test/README.md +107 -0
  86. package/src/ui/theme/README.md +103 -0
  87. package/dist/node-serve.d.ts +0 -2
  88. package/dist/node-serve.d.ts.map +0 -1
@@ -0,0 +1,445 @@
1
+ # auth
2
+
3
+ Composable browser authentication primitives for Remix. Use this package to verify credentials on your own server, start external OAuth or OIDC redirects, finish provider callbacks, and write an app-owned auth record into the session. Pair it with [`remix/middleware/auth`](https://github.com/remix-run/remix/tree/main/packages/auth-middleware) when later requests need to resolve that session data into the current user and protect routes.
4
+
5
+ ## Features
6
+
7
+ - Small, composable primitives: `verifyCredentials()`, `startExternalAuth()`, `finishExternalAuth()`, `refreshExternalAuth()`, and `completeAuth()`
8
+ - Built-in provider support for Google, Microsoft, Okta, Auth0, GitHub, Facebook, X, and Atmosphere
9
+ - Module-scope provider configuration for boot-time validation and stable callback URLs
10
+ - App-owned session records so you decide what auth data to persist
11
+ - Shared session completion for credentials and external auth flows
12
+ - Designed to pair with `remix/middleware/auth` for request-time auth resolution and route protection
13
+
14
+ ## Installation
15
+
16
+ ```sh
17
+ npm i remix
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ `remix/auth` exposes five primitives:
23
+
24
+ - `verifyCredentials(provider, context)` parses submitted credentials and returns the authenticated result or `null`
25
+ - `startExternalAuth(provider, context, options?)` stores the in-progress OAuth transaction in the session and returns the provider redirect response
26
+ - `finishExternalAuth(provider, context, options?)` validates the callback, clears the stored transaction, and returns `{ result, returnTo? }`, including any provider tokens in `result.tokens`
27
+ - `refreshExternalAuth(provider, tokens)` exchanges a previously stored `refreshToken` for a fresh provider token bundle when the provider runtime supports refresh
28
+ - `completeAuth(context)` rotates the current session id and returns the session for auth writes
29
+
30
+ The route owns redirects, flashes, and other app-specific behavior. `remix/auth` owns the protocol work.
31
+
32
+ ## Credentials Auth
33
+
34
+ Use `createCredentialsAuthProvider()` when your own server can verify submitted credentials directly, such as email/password logins.
35
+
36
+ ```ts
37
+ import { auth, Auth, createSessionAuthScheme, requireAuth } from 'remix/middleware/auth'
38
+ import { completeAuth, createCredentialsAuthProvider, verifyCredentials } from 'remix/auth'
39
+ import { createCookie } from 'remix/cookie'
40
+ import { createRouter } from 'remix/router'
41
+ import { formData } from 'remix/middleware/form-data'
42
+ import { form, route } from 'remix/routes'
43
+ import type { GoodAuth } from 'remix/middleware/auth'
44
+ import { redirect } from 'remix/response/redirect'
45
+ import { Session } from 'remix/session'
46
+ import { session } from 'remix/middleware/session'
47
+ import { createCookieSessionStorage } from 'remix/session-storage/cookie'
48
+
49
+ let sessionCookie = createCookie('__session', {
50
+ secrets: [env.SESSION_SECRET],
51
+ httpOnly: true,
52
+ secure: true,
53
+ sameSite: 'lax',
54
+ path: '/',
55
+ })
56
+
57
+ let sessionStorage = createCookieSessionStorage()
58
+
59
+ let routes = route({
60
+ auth: {
61
+ session: {
62
+ login: form('/login'),
63
+ logout: { method: 'POST', pattern: '/logout' },
64
+ },
65
+ },
66
+ app: {
67
+ dashboard: '/dashboard',
68
+ },
69
+ })
70
+
71
+ let passwordProvider = createCredentialsAuthProvider({
72
+ parse(context) {
73
+ let formData = context.get(FormData)
74
+ if (formData == null) {
75
+ throw new Error('Expected formData() middleware before verifyCredentials()')
76
+ }
77
+
78
+ return {
79
+ email: String(formData.get('email') ?? ''),
80
+ password: String(formData.get('password') ?? ''),
81
+ }
82
+ },
83
+ async verify({ email, password }) {
84
+ return users.verifyPassword(email, password)
85
+ },
86
+ })
87
+
88
+ let router = createRouter({
89
+ middleware: [
90
+ session(sessionCookie, sessionStorage),
91
+ formData(),
92
+ auth({
93
+ schemes: [
94
+ createSessionAuthScheme({
95
+ read(session) {
96
+ return session.get('auth') as { userId: string } | null
97
+ },
98
+ verify(value) {
99
+ return users.getById(value.userId)
100
+ },
101
+ invalidate(session) {
102
+ session.unset('auth')
103
+ },
104
+ }),
105
+ ],
106
+ }),
107
+ ],
108
+ })
109
+
110
+ router.get(routes.auth.session.login.index, () => new Response('Login page'))
111
+
112
+ router.post(routes.auth.session.login.action, async (context) => {
113
+ let user = await verifyCredentials(passwordProvider, context)
114
+
115
+ if (user == null) {
116
+ return redirect(routes.auth.session.login.index.href())
117
+ }
118
+
119
+ let session = completeAuth(context)
120
+ session.set('auth', { userId: user.id })
121
+
122
+ return redirect(routes.app.dashboard.href())
123
+ })
124
+
125
+ router.post(routes.auth.session.logout, ({ get }) => {
126
+ let session = get(Session)
127
+ session.unset('auth')
128
+ session.regenerateId(true)
129
+ return redirect(routes.auth.session.login.index.href())
130
+ })
131
+
132
+ router.get(routes.app.dashboard, {
133
+ middleware: [requireAuth()],
134
+ handler(context) {
135
+ let auth = context.get(Auth) as GoodAuth<{ id: string; email: string }>
136
+
137
+ return Response.json({
138
+ id: auth.identity.id,
139
+ email: auth.identity.email,
140
+ method: auth.method,
141
+ })
142
+ },
143
+ })
144
+ ```
145
+
146
+ ## External Auth
147
+
148
+ Starting from the same `session()`, `auth()`, and `createSessionAuthScheme()` setup as the credentials example above, you can add a Google login flow like this. The provider is created once at module scope, and the routes compose `startExternalAuth()`, `finishExternalAuth()`, and `completeAuth()` directly.
149
+
150
+ ```ts
151
+ import { auth, Auth, createSessionAuthScheme, requireAuth } from 'remix/middleware/auth'
152
+ import {
153
+ completeAuth,
154
+ createGoogleAuthProvider,
155
+ finishExternalAuth,
156
+ refreshExternalAuth,
157
+ startExternalAuth,
158
+ } from 'remix/auth'
159
+ import { createCookie } from 'remix/cookie'
160
+ import { createRouter } from 'remix/router'
161
+ import { route } from 'remix/routes'
162
+ import type { GoodAuth } from 'remix/middleware/auth'
163
+ import { redirect } from 'remix/response/redirect'
164
+ import { session } from 'remix/middleware/session'
165
+ import { createCookieSessionStorage } from 'remix/session-storage/cookie'
166
+
167
+ let sessionCookie = createCookie('__session', {
168
+ secrets: [env.SESSION_SECRET],
169
+ httpOnly: true,
170
+ secure: true,
171
+ sameSite: 'lax',
172
+ path: '/',
173
+ })
174
+
175
+ let sessionStorage = createCookieSessionStorage()
176
+
177
+ let routes = route({
178
+ auth: {
179
+ session: {
180
+ login: '/login',
181
+ },
182
+ google: {
183
+ login: '/login/google',
184
+ callback: '/auth/google/callback',
185
+ },
186
+ },
187
+ app: {
188
+ dashboard: '/dashboard',
189
+ },
190
+ })
191
+
192
+ let googleProvider = createGoogleAuthProvider({
193
+ clientId: env.GOOGLE_CLIENT_ID,
194
+ clientSecret: env.GOOGLE_CLIENT_SECRET,
195
+ redirectUri: new URL(routes.auth.google.callback.href(), env.APP_ORIGIN),
196
+ authorizationParams: {
197
+ access_type: 'offline',
198
+ prompt: 'consent',
199
+ },
200
+ })
201
+
202
+ let router = createRouter({
203
+ middleware: [
204
+ session(sessionCookie, sessionStorage),
205
+ auth({
206
+ schemes: [
207
+ createSessionAuthScheme({
208
+ read(session) {
209
+ return session.get('auth') as { userId: string } | null
210
+ },
211
+ verify(value) {
212
+ return users.getById(value.userId)
213
+ },
214
+ invalidate(session) {
215
+ session.unset('auth')
216
+ },
217
+ }),
218
+ ],
219
+ }),
220
+ ],
221
+ })
222
+
223
+ router.get(routes.auth.session.login, () => {
224
+ return new Response(`<a href="${routes.auth.google.login.href()}">Login with Google</a>`, {
225
+ headers: {
226
+ 'Content-Type': 'text/html; charset=utf-8',
227
+ },
228
+ })
229
+ })
230
+
231
+ router.get(routes.auth.google.login, (context) =>
232
+ startExternalAuth(googleProvider, context, {
233
+ returnTo: context.url.searchParams.get('returnTo'),
234
+ }),
235
+ )
236
+
237
+ router.get(routes.auth.google.callback, async (context) => {
238
+ let { result, returnTo } = await finishExternalAuth(googleProvider, context)
239
+
240
+ let user = await users.upsertFromGoogle(result.profile)
241
+ await persistProviderTokens(user.id, result.tokens)
242
+
243
+ let session = completeAuth(context)
244
+ session.set('auth', { userId: user.id })
245
+
246
+ return redirect(returnTo ?? routes.app.dashboard.href())
247
+ })
248
+
249
+ async function getGoogleAccessToken(userId: string) {
250
+ let tokens = await readStoredProviderTokens(userId)
251
+ if (tokens == null) {
252
+ return null
253
+ }
254
+
255
+ if (tokens.expiresAt != null && tokens.expiresAt.getTime() <= Date.now()) {
256
+ tokens = (await refreshExternalAuth(googleProvider, tokens)).tokens
257
+ await persistProviderTokens(userId, tokens)
258
+ }
259
+
260
+ return tokens.accessToken
261
+ }
262
+
263
+ router.get(routes.app.dashboard, {
264
+ middleware: [requireAuth()],
265
+ handler(context) {
266
+ let auth = context.get(Auth) as GoodAuth<{ id: string; email: string | null }>
267
+
268
+ return Response.json({
269
+ id: auth.identity.id,
270
+ email: auth.identity.email,
271
+ method: auth.method,
272
+ })
273
+ },
274
+ })
275
+ ```
276
+
277
+ A typical external auth flow looks like this:
278
+
279
+ 1. Create the provider once at module scope. For Atmosphere, call `provider.prepare(handleOrDid)` only when starting the login flow.
280
+ 2. Call `startExternalAuth()` from the login route.
281
+ 3. Call `finishExternalAuth()` from the callback route.
282
+ 4. Persist any provider tokens you want to reuse later.
283
+ 5. Call `completeAuth(context)` and write your auth record into the returned session.
284
+ 6. On a later follow-up request, load the stored provider tokens, refresh them with `refreshExternalAuth()` only if needed, then save the refreshed bundle back to storage.
285
+ 7. Return your own redirect or other response.
286
+
287
+ ## Built-in External Auth Providers
288
+
289
+ When one of the built-in providers matches your auth provider, start there. Google, Microsoft, Okta, and Auth0 use the shared OIDC runtime. GitHub, Facebook, X, and Atmosphere use built-in custom OAuth flows.
290
+
291
+ ```ts
292
+ import {
293
+ createAuth0AuthProvider,
294
+ createAtmosphereAuthProvider,
295
+ createFacebookAuthProvider,
296
+ createGitHubAuthProvider,
297
+ createGoogleAuthProvider,
298
+ createMicrosoftAuthProvider,
299
+ createOktaAuthProvider,
300
+ createXAuthProvider,
301
+ } from 'remix/auth'
302
+
303
+ let auth0Provider = createAuth0AuthProvider({
304
+ domain: env.AUTH0_DOMAIN,
305
+ clientId: env.AUTH0_CLIENT_ID,
306
+ clientSecret: env.AUTH0_CLIENT_SECRET,
307
+ redirectUri: new URL('/auth/auth0/callback', env.APP_ORIGIN),
308
+ })
309
+
310
+ let atmosphereProvider = createAtmosphereAuthProvider({
311
+ clientId: new URL('/oauth/client-metadata.json', env.APP_ORIGIN),
312
+ redirectUri: new URL('/auth/atmosphere/callback', env.APP_ORIGIN),
313
+ sessionSecret: env.SESSION_SECRET,
314
+ })
315
+
316
+ let facebookProvider = createFacebookAuthProvider({
317
+ clientId: env.FACEBOOK_CLIENT_ID,
318
+ clientSecret: env.FACEBOOK_CLIENT_SECRET,
319
+ redirectUri: new URL('/auth/facebook/callback', env.APP_ORIGIN),
320
+ })
321
+
322
+ let githubProvider = createGitHubAuthProvider({
323
+ clientId: env.GITHUB_CLIENT_ID,
324
+ clientSecret: env.GITHUB_CLIENT_SECRET,
325
+ redirectUri: new URL('/auth/github/callback', env.APP_ORIGIN),
326
+ })
327
+
328
+ let googleProvider = createGoogleAuthProvider({
329
+ clientId: env.GOOGLE_CLIENT_ID,
330
+ clientSecret: env.GOOGLE_CLIENT_SECRET,
331
+ redirectUri: new URL('/auth/google/callback', env.APP_ORIGIN),
332
+ })
333
+
334
+ let microsoftProvider = createMicrosoftAuthProvider({
335
+ tenant: 'organizations',
336
+ clientId: env.MICROSOFT_CLIENT_ID,
337
+ clientSecret: env.MICROSOFT_CLIENT_SECRET,
338
+ redirectUri: new URL('/auth/microsoft/callback', env.APP_ORIGIN),
339
+ })
340
+
341
+ let oktaProvider = createOktaAuthProvider({
342
+ issuer: env.OKTA_ISSUER,
343
+ clientId: env.OKTA_CLIENT_ID,
344
+ clientSecret: env.OKTA_CLIENT_SECRET,
345
+ redirectUri: new URL('/auth/okta/callback', env.APP_ORIGIN),
346
+ })
347
+
348
+ let xProvider = createXAuthProvider({
349
+ clientId: env.X_CLIENT_ID,
350
+ clientSecret: env.X_CLIENT_SECRET,
351
+ redirectUri: new URL('/auth/x/callback', env.APP_ORIGIN),
352
+ })
353
+ ```
354
+
355
+ Notes:
356
+
357
+ - OIDC providers use discovery by default at `/.well-known/openid-configuration`
358
+ - Pass `metadata` when you want to skip discovery or `discoveryUrl` when the metadata document lives elsewhere
359
+ - Default OIDC scopes are `openid profile email`
360
+ - `createGoogleAuthProvider()` uses the same OIDC runtime with Google's published endpoints wired in directly, so it does not need a discovery request
361
+ - `createMicrosoftAuthProvider()` adds the `tenant` option and builds the issuer from it
362
+ - `createOktaAuthProvider()` expects the full Okta issuer URL, usually something like `https://example.okta.com/oauth2/default`
363
+ - `createAuth0AuthProvider()` expects your Auth0 domain and derives the issuer URL for you
364
+ - `createAtmosphereAuthProvider()` returns a module-scope provider with `prepare(handleOrDid)` for request-time atproto account discovery before `startExternalAuth()`
365
+ - Atmosphere callback routes pass the module-scope provider directly to `finishExternalAuth()`; the original handle or DID is stored in the sealed OAuth transaction state
366
+ - `createAtmosphereAuthProvider()` requires `sessionSecret` and seals the in-flight account, authorization server, nonce, and DPoP key state into the existing OAuth transaction stored in your app session, so you do not need a separate file or database store for the redirect step
367
+ - `createAtmosphereAuthProvider()` returns DPoP-bound token material in `result.tokens`, including `accessToken`, `refreshToken`, authorization server refresh details, and `dpop` JWK state for follow-up DPoP-signed requests
368
+ - `refreshExternalAuth()` supports built-in OIDC providers, X, and Atmosphere when the stored token bundle includes a refresh token
369
+ - Providers only return refresh tokens when configured to request offline access, such as `authorizationParams: { access_type: 'offline' }` for Google or adding `offline.access` to X scopes
370
+ - Use `mapProfile()` with `createOIDCAuthProvider()` when you want `result.profile` to have an app-specific type before it reaches your route code
371
+
372
+ Default scopes for OAuth providers that don't use OIDC discovery:
373
+
374
+ - GitHub: `read:user user:email`
375
+ - Facebook: `public_profile email`
376
+ - X: `tweet.read users.read`
377
+
378
+ Pass `scopes` if you need a different set for a provider.
379
+
380
+ ## Custom Auth Providers
381
+
382
+ Use `createOIDCAuthProvider()` directly for custom external auth providers. This is the extension point for providers that support OpenID Connect discovery, authorization code flow, and a userinfo endpoint. Reach for a custom OAuth provider implementation only when the provider does not support OIDC.
383
+
384
+ ```ts
385
+ import {
386
+ completeAuth,
387
+ createOIDCAuthProvider,
388
+ finishExternalAuth,
389
+ startExternalAuth,
390
+ } from 'remix/auth'
391
+ import { redirect } from 'remix/response/redirect'
392
+
393
+ let companyProvider = createOIDCAuthProvider({
394
+ name: 'company',
395
+ issuer: 'https://sso.acme.com',
396
+ clientId: 'acme-web',
397
+ clientSecret: 'acme-web-secret',
398
+ redirectUri: new URL('/auth/company/callback', 'https://app.acme.com'),
399
+ authorizationParams: {
400
+ prompt: 'login',
401
+ },
402
+ mapProfile({ claims }) {
403
+ return {
404
+ id: claims.sub,
405
+ email: claims.email ?? null,
406
+ name: claims.name ?? claims.preferred_username ?? 'Unknown user',
407
+ }
408
+ },
409
+ })
410
+
411
+ router.get('/login/company', (context) =>
412
+ startExternalAuth(companyProvider, context, {
413
+ returnTo: context.url.searchParams.get('returnTo'),
414
+ }),
415
+ )
416
+
417
+ router.get('/auth/company/callback', async (context) => {
418
+ let { result, returnTo } = await finishExternalAuth(companyProvider, context)
419
+
420
+ let user = await users.upsertFromCompanySSO(result.profile)
421
+ let session = completeAuth(context)
422
+ session.set('auth', { userId: user.id })
423
+
424
+ return redirect(returnTo ?? '/dashboard')
425
+ })
426
+ ```
427
+
428
+ ## Related Packages
429
+
430
+ - [`auth-middleware`](https://github.com/remix-run/remix/tree/main/packages/auth-middleware) - Request authentication and route protection helpers
431
+ - [`form-data-middleware`](https://github.com/remix-run/remix/tree/main/packages/form-data-middleware) - Form body parsing for `createCredentialsAuthProvider()` routes
432
+ - [`session-middleware`](https://github.com/remix-run/remix/tree/main/packages/session-middleware) - Request-scoped session loading and persistence
433
+ - [`session`](https://github.com/remix-run/remix/tree/main/packages/session) - Session data model and storage backends
434
+ - [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router and middleware runtime
435
+
436
+ ## Related Work
437
+
438
+ - [OAuth 2.0](https://oauth.net/2/)
439
+ - [RFC 7636: PKCE](https://datatracker.ietf.org/doc/html/rfc7636)
440
+ - [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html)
441
+ - [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html)
442
+
443
+ ## License
444
+
445
+ See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
@@ -0,0 +1,246 @@
1
+ # auth-middleware
2
+
3
+ Request-time authentication and route protection for Remix. Use this package to resolve identity into `context.auth` from sessions, bearer tokens, API keys, or your own schemes. Pair it with [`remix/auth`](https://github.com/remix-run/remix/tree/main/packages/auth) when you need browser login routes that call `verifyCredentials()` or `finishExternalAuth()`, then rotate the session id with `completeAuth()` before writing the auth record.
4
+
5
+ ## Features
6
+
7
+ - Request auth resolution without mutating request objects
8
+ - Route protection with `requireAuth()` and configurable failure behavior
9
+ - Built-in auth schemes for sessions, bearer tokens, and API keys
10
+ - Ordered fallback across multiple auth schemes
11
+ - Public and private route support with the same resolved auth state
12
+ - Read auth state with `context.auth` (or `context.get(Auth)`)
13
+ - Designed to pair with browser login flows that persist session auth records earlier in the request lifecycle
14
+
15
+ ## Installation
16
+
17
+ ```sh
18
+ npm i remix
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ The following example shows the request-time half of a session-backed browser login flow:
24
+
25
+ - another part of the app has already called `completeAuth()` and written `{ userId }` into the returned session
26
+ - `remix/middleware/auth` reads that value, resolves the current user, and protects the dashboard route
27
+
28
+ ```ts
29
+ import { auth, createSessionAuthScheme, requireAuth } from 'remix/middleware/auth'
30
+ import { createRouter } from 'remix/router'
31
+ import { route } from 'remix/routes'
32
+ import { session } from 'remix/middleware/session'
33
+
34
+ let routes = route({
35
+ app: {
36
+ dashboard: '/dashboard',
37
+ },
38
+ })
39
+
40
+ type User = { id: string; email: string }
41
+
42
+ let router = createRouter({
43
+ middleware: [
44
+ session(sessionCookie, sessionStorage),
45
+ auth({
46
+ schemes: [
47
+ createSessionAuthScheme<User, { userId: string }>({
48
+ read(session) {
49
+ return session.get('auth') as { userId: string } | null
50
+ },
51
+ verify(value) {
52
+ return users.getById(value.userId)
53
+ },
54
+ invalidate(session) {
55
+ session.unset('auth')
56
+ },
57
+ }),
58
+ ],
59
+ }),
60
+ ],
61
+ })
62
+
63
+ router.get(routes.app.dashboard, {
64
+ middleware: [requireAuth<User>()],
65
+ handler(context) {
66
+ let auth = context.auth
67
+ if (!auth.ok) {
68
+ return new Response('Unauthorized', { status: 401 })
69
+ }
70
+
71
+ return Response.json({
72
+ id: auth.identity.id,
73
+ email: auth.identity.email,
74
+ method: auth.method,
75
+ })
76
+ },
77
+ })
78
+ ```
79
+
80
+ In this example, `createSessionAuthScheme()` turns a persisted session auth record back into request auth state, `auth()` stores that state at `context.auth` (or `context.get(Auth)`), and `requireAuth()` rejects anonymous requests.
81
+
82
+ If you need to create the login route, start an OAuth redirect, finish a provider callback, or write the session auth record in the first place, use [`remix/auth`](https://github.com/remix-run/remix/tree/main/packages/auth):
83
+
84
+ - `verifyCredentials()` for direct credentials flows
85
+ - `startExternalAuth()` and `finishExternalAuth()` for OAuth and OIDC flows
86
+ - `completeAuth()` to rotate the session id before writing the auth record that this package reads later
87
+
88
+ ## Route Protection
89
+
90
+ This package includes two middlewares:
91
+
92
+ - `auth()` to resolve auth state and store it in `context.auth` (or `context.get(Auth)`)
93
+ - `requireAuth()` to reject requests that aren't authenticated
94
+
95
+ That separation is intentional so the same auth resolution can support public routes, API routes, and browser routes with different failure behavior.
96
+
97
+ `auth()` resolves auth state and stores either `{ ok: true, identity, method }` or `{ ok: false, error? }` in `context.auth`.
98
+
99
+ Use `requireAuth()` after `auth()` when a route must be authenticated. If `auth()` did not run first, `requireAuth()` throws. Otherwise it returns `401 Unauthorized` by default, or you can replace that with `onFailure(context, auth)` to return JSON, redirects, or any other custom response.
100
+
101
+ Handlers whose context contract includes `auth()` and `requireAuth<Identity>()` can read `context.auth.identity` without a manual lookup or cast. You can also use `context.get(Auth)`.
102
+
103
+ Auth challenges are forwarded to `WWW-Authenticate` automatically when the auth failure included a `challenge`, so clients that honor those challenges can react without custom header handling.
104
+
105
+ ## Auth Schemes
106
+
107
+ An `AuthScheme` is any object with a `name` and an `authenticate(context)` method. The `auth()` middleware tries each scheme in order until one returns a success or failure result. If no scheme returns success or failure, the request is treated as anonymous.
108
+
109
+ This package ships with three built-in auth schemes:
110
+
111
+ - `createBearerTokenAuthScheme()` for bearer tokens in the [HTTP `Authorization: Bearer <token>` header](https://datatracker.ietf.org/doc/html/rfc6750#section-2.1)
112
+ - `createAPIAuthScheme()` for API keys in a custom request header
113
+ - `createSessionAuthScheme()` for session-backed auth loaded by [a `session()` middleware](https://github.com/remix-run/remix/tree/main/packages/session-middleware)
114
+
115
+ ## Custom Auth Schemes
116
+
117
+ If none of the built-in auth schemes match your environment, you can create your own auth scheme easily. A custom scheme usually wraps one auth mechanism behind a small `create*` factory function and returns an `AuthScheme`. For example, apps behind a trusted access proxy can authenticate requests from forwarded identity headers instead of sessions or bearer tokens.
118
+
119
+ ```ts
120
+ import type { RequestContext } from 'remix/router'
121
+ import type { AuthScheme } from 'remix/middleware/auth'
122
+
123
+ type User = {
124
+ id: string
125
+ role: 'admin' | 'user'
126
+ }
127
+
128
+ function createTrustedProxyAuthScheme(): AuthScheme<User> {
129
+ return {
130
+ name: 'trusted-proxy',
131
+ async authenticate(context: RequestContext) {
132
+ let email = context.headers.get('X-Forwarded-Email')
133
+
134
+ if (email == null) {
135
+ return
136
+ }
137
+
138
+ let user = await users.getByEmail(email)
139
+
140
+ if (user == null) {
141
+ return {
142
+ status: 'failure',
143
+ code: 'invalid_credentials',
144
+ message: 'Unknown forwarded user',
145
+ }
146
+ }
147
+
148
+ return {
149
+ status: 'success',
150
+ identity: user,
151
+ }
152
+ },
153
+ }
154
+ }
155
+ ```
156
+
157
+ Only use a scheme like this when the app is reachable exclusively through infrastructure you trust to set the headers you rely on. In this case, the `X-Forwarded-Email` header.
158
+
159
+ `authenticate(context)` can return:
160
+
161
+ - `null`, `undefined`, or no return value to skip this scheme
162
+ - `{ status: 'success', identity }` to authenticate the request
163
+ - `{ status: 'failure', code?, message?, challenge? }` to stop with an auth error
164
+
165
+ The scheme `name` becomes `auth.method` when authentication succeeds.
166
+
167
+ ## Simple Auth Cookies
168
+
169
+ If your app already has an auth cookie and you do not need a session-backed identity lookup, you can use a small custom auth scheme and still rely on `requireAuth()` for route protection.
170
+
171
+ ```ts
172
+ import { auth, requireAuth } from 'remix/middleware/auth'
173
+ import type { AuthScheme } from 'remix/middleware/auth'
174
+ import { createCookie } from 'remix/cookie'
175
+ import { createRouter } from 'remix/router'
176
+ import { redirect } from 'remix/response/redirect'
177
+
178
+ let authCookie = createCookie('__auth', {
179
+ httpOnly: true,
180
+ sameSite: 'lax',
181
+ path: '/',
182
+ })
183
+
184
+ let authCookieScheme: AuthScheme<'demo-user'> = {
185
+ name: 'auth-cookie',
186
+ async authenticate(context) {
187
+ let value = await authCookie.parse(context.headers.get('Cookie'))
188
+ if (value !== '1') {
189
+ return
190
+ }
191
+
192
+ return {
193
+ status: 'success',
194
+ identity: 'demo-user',
195
+ }
196
+ },
197
+ }
198
+
199
+ let requireAuthCookie = requireAuth<'demo-user'>({
200
+ onFailure(context) {
201
+ let isFrameRequest = context.request.headers.get('X-Remix-Frame') === 'true'
202
+ if (isFrameRequest) {
203
+ return new Response('<p>Not authorized</p>', {
204
+ status: 401,
205
+ headers: {
206
+ 'Content-Type': 'text/html; charset=utf-8',
207
+ },
208
+ })
209
+ }
210
+
211
+ return redirect('/login')
212
+ },
213
+ })
214
+
215
+ let router = createRouter({
216
+ middleware: [
217
+ auth({
218
+ schemes: [authCookieScheme],
219
+ }),
220
+ ],
221
+ })
222
+
223
+ router.get('/dashboard', {
224
+ middleware: [requireAuthCookie],
225
+ handler() {
226
+ return new Response('ok')
227
+ },
228
+ })
229
+ ```
230
+
231
+ This pattern keeps the auth check app-owned. Use [`remix/middleware/session`](https://github.com/remix-run/remix/tree/main/packages/session-middleware) and [`remix/auth`](https://github.com/remix-run/remix/tree/main/packages/auth) when you need server-managed session data, credential verification helpers, or OAuth/OIDC flows.
232
+
233
+ ## Related Packages
234
+
235
+ - [`auth`](https://github.com/remix-run/remix/tree/main/packages/auth) - Browser auth primitives for credentials, OAuth, and OIDC flows
236
+ - [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router and middleware runtime
237
+ - [`response`](https://github.com/remix-run/remix/tree/main/packages/response) - Response helpers like redirects
238
+
239
+ ## Related Work
240
+
241
+ - [HTTP Authentication Framework](https://datatracker.ietf.org/doc/html/rfc7235)
242
+ - [OAuth 2.0 Bearer Token Usage](https://datatracker.ietf.org/doc/html/rfc6750)
243
+
244
+ ## License
245
+
246
+ See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)