Skip to content

Commit eb33c9d

Browse files
refactor: decouple Next.js from core (#2857)
* refactor: decouple Next.js from core (WIP) * refactor: use `base` instead of `baseUrl`+`basePath` * fix: signout route * refactor(ts): convert files to TS * fix: imports * refactor: convert callback route * fix: add `next` files to package * chore(dev): alias npm email * refactor: do not merge req with user options * refactor: rename userOptions to options * refactor: use native `URL` in `parseUrl` * refactor: move Next.js specific code to `next` module * refactor(ts): return `OutgoingResponse` on all routes * fix: change `base` to `url` * feat: introduce `getServerSession` * refactor: move main logic to `handler` file * chore(dev): showcase `getServerSession` * feat: extract `sessionToken` from Authorization header * fix: pass headers to getServerSession * refactor: rename `server` to `core` * refactor: re-export `next-auth/next` in `next-auth` * fix: add `core` to npm package * fix: re-export default method * feat: return `body`+`header` instead of `json`,`text` * feat: pass `NEXTAUTH_URL` as a variable to core * refactor: simplify Next.js wrapper * feat: export `client/_utils` * fix(ts): suppress TS errors
1 parent 58a98b6 commit eb33c9d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1429
-974
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,13 @@ node_modules
2929
/client
3030
/css
3131
/lib
32-
/server
32+
/core
3333
/jwt
3434
/react
3535
/adapters.d.ts
3636
/index.d.ts
3737
/index.js
38+
/next
3839

3940
# Development app
4041
app/src/css

app/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"copy:css": "cpx \"../css/**/*\" src/css --watch",
1111
"watch:css": "cd .. && npm run watch:css",
1212
"start": "next start",
13-
"start:email": "npx fake-smtp-server"
13+
"email": "npx fake-smtp-server",
14+
"start:email": "email"
1415
},
1516
"license": "ISC",
1617
"dependencies": {

app/pages/_app.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import { SessionProvider } from "next-auth/react"
22
import "./styles.css"
33

4-
export default function App({
5-
Component,
6-
pageProps: { session, ...pageProps },
7-
}) {
4+
export default function App({ Component, pageProps }) {
85
return (
9-
<SessionProvider session={session}>
6+
<SessionProvider session={pageProps.session}>
107
<Component {...pageProps} />
118
</SessionProvider>
129
)

app/pages/api/auth/[...nextauth].ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import NextAuth from "next-auth"
1+
import NextAuth, { NextAuthOptions } from "next-auth"
22
import EmailProvider from "next-auth/providers/email"
33
import GitHubProvider from "next-auth/providers/github"
44
import Auth0Provider from "next-auth/providers/auth0"
@@ -37,8 +37,7 @@ import AzureB2C from "next-auth/providers/azure-ad-b2c"
3737
// domain: process.env.FAUNA_DOMAIN,
3838
// })
3939
// const adapter = FaunaAdapter(client)
40-
41-
export default NextAuth({
40+
export const authOptions: NextAuthOptions = {
4241
// adapter,
4342
providers: [
4443
// E-mail
@@ -58,8 +57,8 @@ export default NextAuth({
5857
credentials: {
5958
password: { label: "Password", type: "password" },
6059
},
61-
async authorize(credentials, req) {
62-
if (credentials.password === "password") {
60+
async authorize(credentials) {
61+
if (credentials.password === "pw") {
6362
return {
6463
name: "Fill Murray",
6564
email: "bill@fillmurray.com",
@@ -179,4 +178,6 @@ export default NextAuth({
179178
logo: "https://next-auth.js.org/img/logo/logo-sm.png",
180179
brandColor: "#1786fb",
181180
},
182-
})
181+
}
182+
183+
export default NextAuth(authOptions)

app/pages/protected-ssr.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// This is an example of how to protect content using server rendering
2-
import { getSession } from "next-auth/react"
2+
import { getServerSession } from "next-auth/next"
3+
import { authOptions } from "./api/auth/[...nextauth]"
34
import Layout from "../components/layout"
45
import AccessDenied from "../components/access-denied"
56

@@ -25,7 +26,7 @@ export default function Page({ content, session }) {
2526
}
2627

2728
export async function getServerSideProps(context) {
28-
const session = await getSession(context)
29+
const session = await getServerSession(context, authOptions)
2930
let content = null
3031

3132
if (session) {

config/babel.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ module.exports = (api) => {
3838
],
3939
},
4040
{
41-
test: ["../src/server/pages/*.tsx"],
41+
test: ["../src/core/pages/*.tsx"],
4242
presets: ["preact"],
4343
plugins: [
4444
[

package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@
2424
".": "./index.js",
2525
"./jwt": "./jwt/index.js",
2626
"./react": "./react/index.js",
27+
"./core": "./core/index.js",
28+
"./next": "./next/index.js",
29+
"./client/_utils": "./client/_utils.js",
2730
"./providers/*": "./providers/*.js"
2831
},
2932
"scripts": {
3033
"build": "npm run build:js && npm run build:css",
31-
"clean": "rm -rf client css lib providers server jwt react index.d.ts index.js adapters.d.ts",
34+
"clean": "rm -rf client css lib providers core jwt react next index.d.ts index.js adapters.d.ts",
3235
"build:js": "npm run clean && npm run generate-providers && tsc && babel --config-file ./config/babel.config.js src --out-dir . --extensions \".tsx,.ts,.js,.jsx\"",
3336
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir . && node config/wrap-css.js",
3437
"dev:setup": "npm i && npm run generate-providers && npm run build:css && cd app && npm i",
@@ -47,8 +50,10 @@
4750
"css",
4851
"jwt",
4952
"react",
53+
"next",
54+
"client",
5055
"providers",
51-
"server",
56+
"core",
5257
"index.d.ts",
5358
"index.js",
5459
"adapters.d.ts"
@@ -137,7 +142,7 @@
137142
"types",
138143
".next",
139144
"dist",
140-
"/server",
145+
"/core",
141146
"/react.js"
142147
],
143148
"globals": {
File renamed without changes.
File renamed without changes.

src/core/index.ts

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import logger from "../lib/logger"
2+
import * as routes from "./routes"
3+
import renderPage from "./pages"
4+
import type { NextAuthOptions } from "./types"
5+
import { init } from "./init"
6+
import { Cookie } from "./lib/cookie"
7+
8+
import { NextAuthAction } from "../lib/types"
9+
10+
export interface IncomingRequest {
11+
/** @default "http://localhost:3000" */
12+
host?: string
13+
method: string
14+
cookies?: Record<string, any>
15+
headers?: Record<string, any>
16+
query?: Record<string, any>
17+
body?: Record<string, any>
18+
action: NextAuthAction
19+
providerId?: string
20+
error?: string
21+
}
22+
23+
export interface NextAuthHeader {
24+
key: string
25+
value: string
26+
}
27+
28+
export interface OutgoingResponse<
29+
Body extends string | Record<string, any> | any[] = any
30+
> {
31+
status?: number
32+
headers?: NextAuthHeader[]
33+
body?: Body
34+
redirect?: string
35+
cookies?: Cookie[]
36+
}
37+
38+
interface NextAuthHandlerParams {
39+
req: IncomingRequest
40+
options: NextAuthOptions
41+
}
42+
43+
export async function NextAuthHandler<
44+
Body extends string | Record<string, any> | any[]
45+
>(params: NextAuthHandlerParams): Promise<OutgoingResponse<Body>> {
46+
const { options: userOptions, req } = params
47+
const { action, providerId, error } = req
48+
49+
const { options, cookies } = await init({
50+
userOptions,
51+
action,
52+
providerId,
53+
host: req.host,
54+
callbackUrl: req.body?.callbackUrl ?? req.query?.callbackUrl,
55+
csrfToken: req.body?.csrfToken,
56+
cookies: req.cookies,
57+
isPost: req.method === "POST",
58+
})
59+
60+
const sessionToken =
61+
req.cookies?.[options.cookies.sessionToken.name] ||
62+
req.headers?.Authorization?.replace("Bearer ", "")
63+
64+
const codeVerifier = req.cookies?.[options.cookies.pkceCodeVerifier.name]
65+
66+
if (req.method === "GET") {
67+
const render = renderPage({ options, query: req.query, cookies })
68+
const { pages } = options
69+
switch (action) {
70+
case "providers":
71+
return (await routes.providers(options.providers)) as any
72+
case "session":
73+
return (await routes.session({ options, sessionToken })) as any
74+
case "csrf":
75+
return {
76+
headers: [{ key: "Content-Type", value: "application/json" }],
77+
body: { csrfToken: options.csrfToken } as any,
78+
cookies,
79+
}
80+
case "signin":
81+
if (pages.signIn) {
82+
let signinUrl = `${pages.signIn}${
83+
pages.signIn.includes("?") ? "&" : "?"
84+
}callbackUrl=${options.callbackUrl}`
85+
if (error) signinUrl = `${signinUrl}&error=${error}`
86+
return { redirect: signinUrl, cookies }
87+
}
88+
89+
return render.signin()
90+
case "signout":
91+
if (pages.signOut) return { redirect: pages.signOut, cookies }
92+
93+
return render.signout()
94+
case "callback":
95+
if (options.provider) {
96+
const callback = await routes.callback({
97+
body: req.body,
98+
query: req.query,
99+
method: req.method,
100+
headers: req.headers,
101+
options,
102+
sessionToken,
103+
codeVerifier,
104+
})
105+
if (callback.cookies) cookies.push(...callback.cookies)
106+
return { ...callback, cookies }
107+
}
108+
break
109+
case "verify-request":
110+
if (pages.verifyRequest) {
111+
return { redirect: pages.verifyRequest, cookies }
112+
}
113+
return render.verifyRequest()
114+
case "error":
115+
if (pages.error) {
116+
return {
117+
redirect: `${pages.error}${
118+
pages.error.includes("?") ? "&" : "?"
119+
}error=${error}`,
120+
cookies,
121+
}
122+
}
123+
124+
// These error messages are displayed in line on the sign in page
125+
if (
126+
[
127+
"Signin",
128+
"OAuthSignin",
129+
"OAuthCallback",
130+
"OAuthCreateAccount",
131+
"EmailCreateAccount",
132+
"Callback",
133+
"OAuthAccountNotLinked",
134+
"EmailSignin",
135+
"CredentialsSignin",
136+
"SessionRequired",
137+
].includes(error as string)
138+
) {
139+
return { redirect: `${options.url}/signin?error=${error}`, cookies }
140+
}
141+
142+
return render.error({ error })
143+
default:
144+
}
145+
} else if (req.method === "POST") {
146+
switch (action) {
147+
case "signin":
148+
// Verified CSRF Token required for all sign in routes
149+
if (options.csrfTokenVerified && options.provider) {
150+
const signin = await routes.signin({
151+
query: req.query,
152+
body: req.body,
153+
options,
154+
})
155+
if (signin.cookies) cookies.push(...signin.cookies)
156+
return { ...signin, cookies }
157+
}
158+
159+
return { redirect: `${options.url}/signin?csrf=true`, cookies }
160+
case "signout":
161+
// Verified CSRF Token required for signout
162+
if (options.csrfTokenVerified) {
163+
const signout = await routes.signout({ options, sessionToken })
164+
if (signout.cookies) cookies.push(...signout.cookies)
165+
return { ...signout, cookies }
166+
}
167+
return { redirect: `${options.url}/signout?csrf=true`, cookies }
168+
case "callback":
169+
if (options.provider) {
170+
// Verified CSRF Token required for credentials providers only
171+
if (
172+
options.provider.type === "credentials" &&
173+
!options.csrfTokenVerified
174+
) {
175+
return { redirect: `${options.url}/signin?csrf=true`, cookies }
176+
}
177+
178+
const callback = await routes.callback({
179+
body: req.body,
180+
query: req.query,
181+
method: req.method,
182+
headers: req.headers,
183+
options,
184+
sessionToken,
185+
codeVerifier,
186+
})
187+
if (callback.cookies) cookies.push(...callback.cookies)
188+
return { ...callback, cookies }
189+
}
190+
break
191+
case "_log":
192+
if (userOptions.logger) {
193+
try {
194+
const { code, level, ...metadata } = req.body ?? {}
195+
logger[level](code, metadata)
196+
} catch (error) {
197+
// If logging itself failed...
198+
logger.error("LOGGER_ERROR", error)
199+
}
200+
}
201+
return {}
202+
default:
203+
}
204+
}
205+
206+
return {
207+
status: 400,
208+
body: `Error: Action ${action} with HTTP ${req.method} is not supported by NextAuth.js` as any,
209+
}
210+
}

0 commit comments

Comments
 (0)