remix 3.0.0-beta.0 → 3.0.0-beta.10

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 (258) hide show
  1. package/README.md +4 -2
  2. package/dist/assets/types/hmr.d.ts +2 -0
  3. package/dist/cli-entry.js +1 -1
  4. package/dist/data-table/cli.d.ts +2 -0
  5. package/dist/data-table/cli.d.ts.map +1 -0
  6. package/dist/{ui/scroll-lock.js → data-table/cli.js} +1 -1
  7. package/dist/fetch-router.d.ts +7 -0
  8. package/dist/fetch-router.d.ts.map +1 -1
  9. package/dist/headers/accept-encoding.d.ts +2 -0
  10. package/dist/headers/accept-encoding.d.ts.map +1 -0
  11. package/dist/headers/accept-encoding.js +2 -0
  12. package/dist/headers/accept-language.d.ts +2 -0
  13. package/dist/headers/accept-language.d.ts.map +1 -0
  14. package/dist/headers/accept-language.js +2 -0
  15. package/dist/headers/accept.d.ts +2 -0
  16. package/dist/headers/accept.d.ts.map +1 -0
  17. package/dist/headers/accept.js +2 -0
  18. package/dist/headers/cache-control.d.ts +2 -0
  19. package/dist/headers/cache-control.d.ts.map +1 -0
  20. package/dist/headers/cache-control.js +2 -0
  21. package/dist/headers/content-disposition.d.ts +2 -0
  22. package/dist/headers/content-disposition.d.ts.map +1 -0
  23. package/dist/headers/content-disposition.js +2 -0
  24. package/dist/headers/content-range.d.ts +2 -0
  25. package/dist/headers/content-range.d.ts.map +1 -0
  26. package/dist/headers/content-range.js +2 -0
  27. package/dist/headers/content-type.d.ts +2 -0
  28. package/dist/headers/content-type.d.ts.map +1 -0
  29. package/dist/headers/content-type.js +2 -0
  30. package/dist/headers/cookie.d.ts +2 -0
  31. package/dist/headers/cookie.d.ts.map +1 -0
  32. package/dist/headers/cookie.js +2 -0
  33. package/dist/headers/if-match.d.ts +2 -0
  34. package/dist/headers/if-match.d.ts.map +1 -0
  35. package/dist/headers/if-match.js +2 -0
  36. package/dist/headers/if-none-match.d.ts +2 -0
  37. package/dist/headers/if-none-match.d.ts.map +1 -0
  38. package/dist/headers/if-none-match.js +2 -0
  39. package/dist/headers/if-range.d.ts +2 -0
  40. package/dist/headers/if-range.d.ts.map +1 -0
  41. package/dist/headers/if-range.js +2 -0
  42. package/dist/headers/range.d.ts +2 -0
  43. package/dist/headers/range.d.ts.map +1 -0
  44. package/dist/headers/range.js +2 -0
  45. package/dist/headers/raw-headers.d.ts +2 -0
  46. package/dist/headers/raw-headers.d.ts.map +1 -0
  47. package/dist/headers/raw-headers.js +2 -0
  48. package/dist/headers/set-cookie.d.ts +2 -0
  49. package/dist/headers/set-cookie.d.ts.map +1 -0
  50. package/dist/headers/set-cookie.js +2 -0
  51. package/dist/headers/vary.d.ts +2 -0
  52. package/dist/headers/vary.d.ts.map +1 -0
  53. package/dist/{ui/separator.js → headers/vary.js} +1 -1
  54. package/dist/node-hmr/runtime.d.ts +2 -0
  55. package/dist/node-hmr/runtime.d.ts.map +1 -0
  56. package/dist/node-hmr/runtime.js +2 -0
  57. package/dist/node-hmr/types.d.ts +2 -0
  58. package/dist/node-hmr.d.ts +2 -0
  59. package/dist/node-hmr.d.ts.map +1 -0
  60. package/dist/{ui/glyph.js → node-hmr.js} +1 -1
  61. package/dist/node-tsx/load-module.d.ts +2 -0
  62. package/dist/node-tsx/load-module.d.ts.map +1 -0
  63. package/dist/node-tsx/load-module.js +2 -0
  64. package/dist/node-tsx.d.ts +3 -0
  65. package/dist/node-tsx.d.ts.map +1 -0
  66. package/dist/node-tsx.js +3 -0
  67. package/dist/render-middleware.d.ts +2 -0
  68. package/dist/render-middleware.d.ts.map +1 -0
  69. package/dist/render-middleware.js +2 -0
  70. package/dist/route-pattern/href.d.ts +2 -0
  71. package/dist/route-pattern/href.d.ts.map +1 -0
  72. package/dist/route-pattern/href.js +2 -0
  73. package/dist/route-pattern/join.d.ts +2 -0
  74. package/dist/route-pattern/join.d.ts.map +1 -0
  75. package/dist/route-pattern/join.js +2 -0
  76. package/dist/route-pattern/match.d.ts +2 -0
  77. package/dist/route-pattern/match.d.ts.map +1 -0
  78. package/dist/route-pattern/match.js +2 -0
  79. package/dist/ui/accordion/primitives.d.ts +2 -0
  80. package/dist/ui/accordion/primitives.d.ts.map +1 -0
  81. package/dist/ui/accordion/primitives.js +2 -0
  82. package/dist/ui/button.d.ts +1 -0
  83. package/dist/ui/button.d.ts.map +1 -1
  84. package/dist/ui/button.js +1 -0
  85. package/dist/ui/checkbox.d.ts +3 -0
  86. package/dist/ui/checkbox.d.ts.map +1 -0
  87. package/dist/ui/checkbox.js +3 -0
  88. package/dist/ui/combobox/primitives.d.ts +2 -0
  89. package/dist/ui/combobox/primitives.d.ts.map +1 -0
  90. package/dist/ui/combobox/primitives.js +2 -0
  91. package/dist/ui/dev/refresh.d.ts +2 -0
  92. package/dist/ui/dev/refresh.d.ts.map +1 -0
  93. package/dist/ui/dev/refresh.js +2 -0
  94. package/dist/ui/input.d.ts +3 -0
  95. package/dist/ui/input.d.ts.map +1 -0
  96. package/dist/ui/input.js +3 -0
  97. package/dist/ui/menu/primitives.d.ts +2 -0
  98. package/dist/ui/menu/primitives.d.ts.map +1 -0
  99. package/dist/ui/menu/primitives.js +2 -0
  100. package/dist/ui/radio.d.ts +3 -0
  101. package/dist/ui/radio.d.ts.map +1 -0
  102. package/dist/ui/radio.js +3 -0
  103. package/dist/ui/select/primitives.d.ts +2 -0
  104. package/dist/ui/select/primitives.d.ts.map +1 -0
  105. package/dist/ui/select/primitives.js +2 -0
  106. package/dist/ui/tabs/primitives.d.ts +2 -0
  107. package/dist/ui/tabs/primitives.d.ts.map +1 -0
  108. package/dist/ui/tabs/primitives.js +2 -0
  109. package/dist/ui/tabs.d.ts +2 -0
  110. package/dist/ui/tabs.d.ts.map +1 -0
  111. package/{src/ui/theme.ts → dist/ui/tabs.js} +1 -1
  112. package/dist/ui/toggle/primitives.d.ts +2 -0
  113. package/dist/ui/toggle/primitives.d.ts.map +1 -0
  114. package/dist/ui/toggle/primitives.js +2 -0
  115. package/dist/ui/toggle.d.ts +3 -0
  116. package/dist/ui/toggle.d.ts.map +1 -0
  117. package/dist/ui/toggle.js +3 -0
  118. package/dist/ui-hmr/assets.d.ts +2 -0
  119. package/dist/ui-hmr/assets.d.ts.map +1 -0
  120. package/dist/ui-hmr/assets.js +2 -0
  121. package/dist/ui-hmr/node.d.ts +3 -0
  122. package/dist/ui-hmr/node.d.ts.map +1 -0
  123. package/dist/ui-hmr/node.js +3 -0
  124. package/dist/ui-hmr/runtime/browser.d.ts +2 -0
  125. package/dist/ui-hmr/runtime/browser.d.ts.map +1 -0
  126. package/dist/ui-hmr/runtime/browser.js +2 -0
  127. package/dist/ui-hmr/runtime/server.d.ts +2 -0
  128. package/dist/ui-hmr/runtime/server.d.ts.map +1 -0
  129. package/dist/ui-hmr/runtime/server.js +2 -0
  130. package/dist/ui-hmr.d.ts +2 -0
  131. package/dist/ui-hmr.d.ts.map +1 -0
  132. package/{src/ui/glyph.ts → dist/ui-hmr.js} +1 -1
  133. package/package.json +284 -129
  134. package/src/assert/README.md +116 -0
  135. package/src/assets/README.md +805 -0
  136. package/src/assets/types/hmr.d.ts +2 -0
  137. package/src/async-context-middleware/README.md +100 -0
  138. package/src/auth/README.md +445 -0
  139. package/src/auth-middleware/README.md +246 -0
  140. package/src/cli/README.md +182 -0
  141. package/src/compression-middleware/README.md +176 -0
  142. package/src/cookie/README.md +106 -0
  143. package/src/cop-middleware/README.md +117 -0
  144. package/src/cors-middleware/README.md +174 -0
  145. package/src/csrf-middleware/README.md +92 -0
  146. package/src/data-schema/README.md +416 -0
  147. package/src/data-table/README.md +678 -0
  148. package/src/data-table/cli.ts +2 -0
  149. package/src/data-table-mysql/README.md +120 -0
  150. package/src/data-table-postgres/README.md +98 -0
  151. package/src/data-table-sqlite/README.md +100 -0
  152. package/src/fetch-proxy/README.md +70 -0
  153. package/src/fetch-router/README.md +982 -0
  154. package/src/fetch-router.ts +7 -0
  155. package/src/file-storage/README.md +57 -0
  156. package/src/file-storage-s3/README.md +46 -0
  157. package/src/form-data-middleware/README.md +108 -0
  158. package/src/form-data-parser/README.md +158 -0
  159. package/src/fs/README.md +60 -0
  160. package/src/headers/README.md +639 -0
  161. package/src/headers/accept-encoding.ts +2 -0
  162. package/src/headers/accept-language.ts +2 -0
  163. package/src/headers/accept.ts +2 -0
  164. package/src/headers/cache-control.ts +2 -0
  165. package/src/headers/content-disposition.ts +2 -0
  166. package/src/headers/content-range.ts +2 -0
  167. package/src/headers/content-type.ts +2 -0
  168. package/src/headers/cookie.ts +2 -0
  169. package/src/headers/if-match.ts +2 -0
  170. package/src/headers/if-none-match.ts +2 -0
  171. package/src/headers/if-range.ts +2 -0
  172. package/src/headers/range.ts +2 -0
  173. package/src/headers/raw-headers.ts +2 -0
  174. package/src/headers/set-cookie.ts +2 -0
  175. package/src/{node-serve.ts → headers/vary.ts} +1 -1
  176. package/src/html-template/README.md +101 -0
  177. package/src/lazy-file/README.md +109 -0
  178. package/src/logger-middleware/README.md +132 -0
  179. package/src/method-override-middleware/README.md +71 -0
  180. package/src/mime/README.md +117 -0
  181. package/src/multipart-parser/README.md +241 -0
  182. package/src/node-fetch-server/README.md +377 -0
  183. package/src/node-hmr/README.md +307 -0
  184. package/src/node-hmr/runtime.ts +2 -0
  185. package/src/node-hmr/types.d.ts +2 -0
  186. package/{dist/ui/theme.js → src/node-hmr.ts} +1 -1
  187. package/src/node-tsx/README.md +74 -0
  188. package/src/node-tsx/load-module.ts +2 -0
  189. package/{dist/node-serve.js → src/node-tsx.ts} +2 -1
  190. package/src/render-middleware/README.md +99 -0
  191. package/src/render-middleware.ts +2 -0
  192. package/src/route-pattern/README.md +424 -0
  193. package/src/route-pattern/href.ts +2 -0
  194. package/src/route-pattern/join.ts +2 -0
  195. package/src/route-pattern/match.ts +2 -0
  196. package/src/session/README.md +171 -0
  197. package/src/session-middleware/README.md +111 -0
  198. package/src/session-storage-memcache/README.md +37 -0
  199. package/src/session-storage-redis/README.md +36 -0
  200. package/src/static-middleware/README.md +89 -0
  201. package/src/tar-parser/README.md +74 -0
  202. package/src/terminal/README.md +92 -0
  203. package/src/test/README.md +500 -0
  204. package/src/ui/README.md +216 -0
  205. package/src/ui/accordion/README.md +202 -0
  206. package/src/ui/accordion/primitives/README.md +202 -0
  207. package/src/ui/accordion/primitives.ts +2 -0
  208. package/src/ui/anchor/README.md +196 -0
  209. package/src/ui/animation/README.md +316 -0
  210. package/src/ui/breadcrumbs/README.md +55 -0
  211. package/src/ui/button/README.md +44 -0
  212. package/src/ui/button.ts +1 -0
  213. package/src/ui/checkbox/README.md +59 -0
  214. package/src/ui/checkbox.ts +3 -0
  215. package/src/ui/combobox/README.md +194 -0
  216. package/src/ui/combobox/primitives/README.md +194 -0
  217. package/src/ui/combobox/primitives.ts +2 -0
  218. package/src/ui/dev/refresh.ts +2 -0
  219. package/src/ui/input/README.md +52 -0
  220. package/src/ui/input.ts +3 -0
  221. package/src/ui/listbox/README.md +83 -0
  222. package/src/ui/menu/README.md +161 -0
  223. package/src/ui/menu/primitives/README.md +161 -0
  224. package/src/ui/menu/primitives.ts +2 -0
  225. package/src/ui/popover/README.md +103 -0
  226. package/src/ui/radio/README.md +53 -0
  227. package/src/ui/radio.ts +3 -0
  228. package/src/ui/select/README.md +117 -0
  229. package/src/ui/select/primitives/README.md +117 -0
  230. package/src/ui/select/primitives.ts +2 -0
  231. package/src/ui/server/README.md +103 -0
  232. package/src/ui/tabs/README.md +141 -0
  233. package/src/ui/tabs/primitives/README.md +141 -0
  234. package/src/ui/tabs/primitives.ts +2 -0
  235. package/src/ui/tabs.ts +2 -0
  236. package/src/ui/test/README.md +198 -0
  237. package/src/ui/toggle/README.md +56 -0
  238. package/src/ui/toggle/primitives/README.md +56 -0
  239. package/src/ui/toggle/primitives.ts +2 -0
  240. package/src/ui/toggle.ts +3 -0
  241. package/src/ui-hmr/README.md +119 -0
  242. package/src/ui-hmr/assets.ts +2 -0
  243. package/src/ui-hmr/node.ts +3 -0
  244. package/src/ui-hmr/runtime/browser.ts +2 -0
  245. package/src/ui-hmr/runtime/server.ts +2 -0
  246. package/src/ui-hmr.ts +2 -0
  247. package/dist/node-serve.d.ts +0 -2
  248. package/dist/node-serve.d.ts.map +0 -1
  249. package/dist/ui/glyph.d.ts +0 -2
  250. package/dist/ui/glyph.d.ts.map +0 -1
  251. package/dist/ui/scroll-lock.d.ts +0 -2
  252. package/dist/ui/scroll-lock.d.ts.map +0 -1
  253. package/dist/ui/separator.d.ts +0 -2
  254. package/dist/ui/separator.d.ts.map +0 -1
  255. package/dist/ui/theme.d.ts +0 -2
  256. package/dist/ui/theme.d.ts.map +0 -1
  257. package/src/ui/scroll-lock.ts +0 -2
  258. package/src/ui/separator.ts +0 -2
@@ -0,0 +1,2 @@
1
+ // IMPORTANT: This file is auto-generated, please do not edit manually.
2
+ export type * from '@remix-run/assets/types/hmr'
@@ -0,0 +1,100 @@
1
+ # async-context-middleware
2
+
3
+ Request-scoped async context middleware for Remix. It stores each request context in [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) so utilities can access it anywhere in the same async call stack.
4
+
5
+ ## Features
6
+
7
+ - **Request context access** - Read the current `RequestContext` from anywhere in the same async execution flow
8
+ - **App-typed `getContext()`** - Reuses your fetch-router `RouterTypes.context` by default
9
+ - **Simple router integration** - Add a single middleware at the router level
10
+ - **Node async hooks** - Built on `node:async_hooks` `AsyncLocalStorage`
11
+
12
+ ## Installation
13
+
14
+ ```sh
15
+ npm i remix
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ Use `asyncContext()` at the router level to make the current request context available to helpers deeper in the same async call stack.
21
+
22
+ ```ts
23
+ import { createRouter } from 'remix/router'
24
+ import { asyncContext, getContext } from 'remix/middleware/async-context'
25
+
26
+ let router = createRouter({
27
+ middleware: [asyncContext()],
28
+ })
29
+
30
+ async function loadCurrentUser() {
31
+ let context = getContext()
32
+ let userId = context.params.id
33
+
34
+ return users.getById(userId)
35
+ }
36
+
37
+ router.get('/users/:id', async () => {
38
+ let user = await loadCurrentUser()
39
+ return Response.json(user)
40
+ })
41
+ ```
42
+
43
+ This middleware requires support for `node:async_hooks`, so it is intended for Node.js runtimes.
44
+
45
+ ## Typed `getContext()`
46
+
47
+ `getContext()` is global and out-of-band, so it reuses your fetch-router `RouterTypes.context` by default.
48
+
49
+ ```ts
50
+ import { requireAuth } from 'remix/middleware/auth'
51
+ import type { AnyParams, ContextWithParams, MiddlewareContext } from 'remix/router'
52
+ import { loadAuth } from './middleware/auth.ts'
53
+ import { loadSession } from './middleware/session.ts'
54
+
55
+ export type RootMiddleware = [ReturnType<typeof loadSession>, ReturnType<typeof loadAuth>]
56
+ export const authenticatedMiddleware = [requireAuth<{ id: string }>()] as const
57
+
58
+ export type AppContext<params extends AnyParams = {}> = ContextWithParams<
59
+ MiddlewareContext<RootMiddleware>,
60
+ params
61
+ >
62
+
63
+ export type AuthenticatedAppContext<params extends AnyParams = {}> = ContextWithParams<
64
+ MiddlewareContext<typeof authenticatedMiddleware, AppContext>,
65
+ params
66
+ >
67
+
68
+ declare module 'remix/router' {
69
+ interface RouterTypes {
70
+ context: AppContext
71
+ }
72
+ }
73
+ ```
74
+
75
+ After that augmentation, `getContext()` returns your app context values everywhere in the app, with route params typed broadly as `AnyParams`.
76
+
77
+ ```ts
78
+ import { Auth } from 'remix/middleware/auth'
79
+ import { getContext } from 'remix/middleware/async-context'
80
+
81
+ function getCurrentAuth() {
82
+ return getContext().get(Auth)
83
+ }
84
+ ```
85
+
86
+ Route handlers themselves can still use more precise route-specific params in their own `RequestContext` types.
87
+
88
+ ## Related Packages
89
+
90
+ - [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router and request context contracts for Remix
91
+ - [`auth-middleware`](https://github.com/remix-run/remix/tree/main/packages/auth-middleware) - Request-time auth state and protected route middleware
92
+ - [`session-middleware`](https://github.com/remix-run/remix/tree/main/packages/session-middleware) - Session loading middleware often paired with async request context
93
+
94
+ ## Related Work
95
+
96
+ - [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage)
97
+
98
+ ## License
99
+
100
+ See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
@@ -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)