Skip to content

Commit

Permalink
Sync (#2)
Browse files Browse the repository at this point in the history
* fix(core): properly construct url (nextauthjs#5984)

* chore(release): bump package version(s) [skip ci]

* fix(core): add protocol if missing

* fix(core): throw error if no action can be determined

* test(core): fix test

* chore(release): bump package version(s) [skip ci]

* chore(docs): add new tutorial (nextauthjs#5604)

Co-authored-by: Nico Domino <[email protected]>

* fix(core): handle `Request` -> `Response` regressions  (nextauthjs#5991)

* fix(next): don't override `Content-Type` by `unstable_getServerSession`

* fix(core): handle `,` while setting `set-cookie`

* chore(release): bump package version(s) [skip ci]

* fix(sequelize): increase sequelize `id_token` column length (nextauthjs#5929)

Co-authored-by: Nico Domino <[email protected]>

* fix(core): correct status code when returning redirects (nextauthjs#6004)

* fix(core): correctly set status when returning redirect

* update tests

* forward other headers

* update test

* remove default 200 status

* fix(core): host detection/NEXTAUTH_URL (nextauthjs#6007)

* rename `host` to `origin` internally

* rename `userOptions` to `authOptions` internally

* use object for `headers` internally

* default `method` to GET

* simplify `unstable_getServerSession`

* allow optional headers

* revert middleware

* wip getURL

* revert host detection

* use old `detectHost`

* fix/add some tests wip

* move more to core, refactor getURL

* better type auth actions

* fix custom path support (w/ api/auth)

* add `getURL` tests

* fix email tests

* fix assert tests

* custom base without api/auth, with trailing slash

* remove parseUrl from assert.ts

* return 400 when wrong url

* fix tests

* refactor

* fix protocol in dev

* fix tests

* fix custom url handling

* add todo comments

* chore(release): bump package version(s) [skip ci]

* update lock file

* fix(next): correctly bundle next-auth/middleware
fixes nextauthjs#6025

* fix(core): preserve incoming set cookies (nextauthjs#6029)

* fix(core): preserve `set-cookie` by the user

* add test

* improve req/res mocking

* refactor

* fix comment typo

* chore(release): bump package version(s) [skip ci]

* make logos optional

* sync with `next-auth`

* clean up `next-auth/edge`

* sync

Co-authored-by: Balázs Orbán <[email protected]>
Co-authored-by: Thomas Desmond <[email protected]>
Co-authored-by: Nico Domino <[email protected]>
Co-authored-by: Cyril Perraud <[email protected]>
  • Loading branch information
5 people authored Dec 12, 2022
1 parent ac0dbae commit e5bfbed
Show file tree
Hide file tree
Showing 41 changed files with 933 additions and 394 deletions.
35 changes: 17 additions & 18 deletions apps/dev/pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import Slack from "next-auth-core/providers/slack"
import Spotify from "next-auth-core/providers/spotify"
import Trakt from "next-auth-core/providers/trakt"
import Twitch from "next-auth-core/providers/twitch"
import Twitter, { TwitterLegacy } from "next-auth-core/providers/twitter"
import Twitter from "next-auth-core/providers/twitter"
import Vk from "next-auth-core/providers/vk"
import Wikimedia from "next-auth-core/providers/wikimedia"
import WorkOS from "next-auth-core/providers/workos"
Expand Down Expand Up @@ -113,7 +113,7 @@ export const authOptions: AuthOptions = {
Spotify({ clientId: process.env.SPOTIFY_ID, clientSecret: process.env.SPOTIFY_SECRET }),
Trakt({ clientId: process.env.TRAKT_ID, clientSecret: process.env.TRAKT_SECRET }),
Twitch({ clientId: process.env.TWITCH_ID, clientSecret: process.env.TWITCH_SECRET }),
Twitter({ version: "2.0", clientId: process.env.TWITTER_ID, clientSecret: process.env.TWITTER_SECRET }),
Twitter({ clientId: process.env.TWITTER_ID, clientSecret: process.env.TWITTER_SECRET }),
// TwitterLegacy({ clientId: process.env.TWITTER_LEGACY_ID, clientSecret: process.env.TWITTER_LEGACY_SECRET }),
Vk({ clientId: process.env.VK_ID, clientSecret: process.env.VK_SECRET }),
Wikimedia({ clientId: process.env.WIKIMEDIA_ID, clientSecret: process.env.WIKIMEDIA_SECRET }),
Expand All @@ -132,25 +132,24 @@ if (authOptions.adapter) {

// TODO: move to next-auth/edge
function Auth(...args: any[]) {
if (args.length === 1)
return async (req: Request) => {
args[0].secret ??= process.env.NEXTAUTH_SECRET

// TODO: remove when `next-auth/react` sends `X-Auth-Return-Redirect`
const shouldRedirect = req.method === "POST" && req.headers.get("Content-Type") === "application/json" ? (await req.clone().json()).json : false

// TODO: This can be directly in core
const res = await AuthHandler(req, args[0])
if (req.headers.get("X-Auth-Return-Redirect") || shouldRedirect) {
const url = res.headers.get("Location")
res.headers.delete("Location")
return new Response(JSON.stringify({ url }), res)
}
return res
const envSecret = process.env.NEXTAUTH_SECRET
const envTrustHost = !!(process.env.NEXTAUTH_URL ?? process.env.AUTH_TRUST_HOST ?? process.env.VERCEL ?? process.env.NODE_ENV !== "production")
if (args.length === 1) {
return (req: Request) => {
args[0].secret ??= envSecret
args[0].trustHost ??= envTrustHost
return AuthHandler(req, args[0])
}
}
args[1].secret ??= envSecret
args[1].trustHost ??= envTrustHost
return AuthHandler(args[0], args[1])
}

export default Auth(authOptions)
// export default Auth(authOptions)

export default function handle(request: Request) {
return Auth(request, authOptions)
}

export const config = { runtime: "experimental-edge" }
4 changes: 4 additions & 0 deletions docs/docs/tutorials.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ title: Tutorials and Explainers
- Learn how to use Sign-In With Ethereum to authenticate your users with their existing Ethereum wallets - identifiers they personally control.
- Example application: [spruceid/siwe-next-auth-example](https://github.com/spruceid/siwe-next-auth-example)

#### [Next.js Authentication with Okta and NextAuth.js 4.0](https://thetombomb.com/posts/nextjs-nextauth-okta) <svg xmlns="http://www.w3.org/2000/svg" style={{ marginLeft: '5px', marginBottom:'-6px'}} height="20" width="20" fill="none" viewBox="0 0 24 24" stroke="currentColor"><title>External</title> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> </svg>

- Learn how to perform authentication with an OIDC Application in Okta and NextAuth.js.

## Fullstack

#### [Build a FullStack App with Next.js, NextAuth.js, Supabase & Prisma](https://themodern.dev/courses/build-a-fullstack-app-with-nextjs-supabase-and-prisma-322389284337222224) <svg xmlns="http://www.w3.org/2000/svg" style={{ marginLeft: '5px', marginBottom:'-6px'}} height="20" width="20" fill="none" viewBox="0 0 24 24" stroke="currentColor"><title>External</title> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> </svg>
Expand Down
4 changes: 4 additions & 0 deletions docs/versioned_docs/version-beta/guides/09-resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ If you did not find a guide or tutorial covering your use case, please [open an
- How to restrict access to pages and API routes.
- [Usage with class components](/tutorials/usage-with-class-components)
- How to use `useSession()` hook with class components.
- [Next.js Authentication with Okta and NextAuth.js 4.0](https://thetombomb.com/posts/nextjs-nextauth-okta)
- Learn how to perform authentication with an OIDC Application in Okta and NextAuth.js.



### Advanced

Expand Down
4 changes: 2 additions & 2 deletions packages/adapter-sequelize/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@next-auth/sequelize-adapter",
"version": "1.0.6",
"version": "1.0.7",
"description": "Sequelize adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
Expand Down Expand Up @@ -42,4 +42,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}
2 changes: 1 addition & 1 deletion packages/adapter-sequelize/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const Account = {
expires_at: { type: DataTypes.INTEGER },
token_type: { type: DataTypes.STRING },
scope: { type: DataTypes.STRING },
id_token: { type: DataTypes.STRING },
id_token: { type: DataTypes.TEXT },
session_state: { type: DataTypes.STRING },
userId: { type: DataTypes.UUID },
}
Expand Down
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
}
},
"scripts": {
"build": "tsc && pnpm css",
"build": "pnpm clean && tsc && pnpm css",
"clean": "rm -rf dist",
"css": "node ./scripts/generate-css.js",
"dev": "pnpm css && tsc -w",
"test": "jest"
Expand Down
11 changes: 10 additions & 1 deletion packages/core/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { EventCallbacks, LoggerInstance } from "."
import type { EventCallbacks, LoggerInstance } from "./types"

/**
* Same as the default `Error`, but it is JSON serializable.
Expand Down Expand Up @@ -76,6 +76,15 @@ export class InvalidEndpoints extends UnknownError {
name = "InvalidEndpoints"
code = "INVALID_ENDPOINTS_ERROR"
}
export class UnknownAction extends UnknownError {
name = "UnknownAction"
code = "UNKNOWN_ACTION_ERROR"
}

export class UntrustedHost extends UnknownError {
name = "UntrustedHost"
code = "UNTRUST_HOST_ERROR"
}

type Method = (...args: any[]) => Promise<any>

Expand Down
62 changes: 47 additions & 15 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ import logger, { setLogger } from "./utils/logger"

import type { ErrorType } from "./pages/error"
import type { AuthOptions, RequestInternal, ResponseInternal } from "./types"
import { UntrustedHost } from "./errors"

export * from "./types"

const configErrorMessage =
"There is a problem with the server configuration. Check the server logs for more information."

async function AuthHandlerInternal<
Body extends string | Record<string, any> | any[]
>(params: {
Expand All @@ -19,10 +23,9 @@ async function AuthHandlerInternal<
/** REVIEW: Is this the best way to skip parsing the body in Node.js? */
parsedBody?: any
}): Promise<ResponseInternal<Body>> {
const { options: userOptions, req } = params
setLogger(userOptions.logger, userOptions.debug)
const { options: authOptions, req } = params

const assertionResult = assertConfig({ options: userOptions, req })
const assertionResult = assertConfig({ options: authOptions, req })

if (Array.isArray(assertionResult)) {
assertionResult.forEach(logger.warn)
Expand All @@ -32,18 +35,13 @@ async function AuthHandlerInternal<

const htmlPages = ["signin", "signout", "error", "verify-request"]
if (!htmlPages.includes(req.action) || req.method !== "GET") {
const message = `There is a problem with the server configuration. Check the server logs for more information.`
return {
status: 500,
headers: { "Content-Type": "application/json" },
body: { message } as any,
body: { message: configErrorMessage } as any,
}
}

// We can throw in development to surface the issue in the browser too.
if (process.env.NODE_ENV === "development") throw assertionResult

const { pages, theme } = userOptions
const { pages, theme } = authOptions

const authOnErrorPage =
pages?.error && req.query?.callbackUrl?.startsWith(pages.error)
Expand All @@ -66,13 +64,13 @@ async function AuthHandlerInternal<
}
}

const { action, providerId, error, method = "GET" } = req
const { action, providerId, error, method } = req

const { options, cookies } = await init({
userOptions,
authOptions,
action,
providerId,
host: req.host,
url: req.url,
callbackUrl: req.body?.callbackUrl ?? req.query?.callbackUrl,
csrfToken: req.body?.csrfToken,
cookies: req.cookies,
Expand Down Expand Up @@ -216,7 +214,7 @@ async function AuthHandlerInternal<
}
break
case "_log":
if (userOptions.logger) {
if (authOptions.logger) {
try {
const { code, level, ...metadata } = req.body ?? {}
logger[level](code, metadata)
Expand Down Expand Up @@ -245,7 +243,41 @@ export async function AuthHandler(
request: Request,
options: AuthOptions
): Promise<Response> {
setLogger(options.logger, options.debug)

if (!options.trustHost) {
const error = new UntrustedHost(
`Host must be trusted. URL was: ${request.url}`
)
logger.error(error.code, error)

return new Response(JSON.stringify({ message: configErrorMessage }), {
status: 500,
headers: { "Content-Type": "application/json" },
})
}

const req = await toInternalRequest(request)
if (req instanceof Error) {
logger.error((req as any).code, req)
return new Response(
`Error: This action with HTTP ${request.method} is not supported.`,
{ status: 400 }
)
}
const internalResponse = await AuthHandlerInternal({ req, options })
return toResponse(internalResponse)

const response = await toResponse(internalResponse)

// If the request expects a return URL, send it as JSON
// instead of doing an actual redirect.
const redirect = response.headers.get("Location")
if (request.headers.has("X-Auth-Return-Redirect") && redirect) {
response.headers.delete("Location")
response.headers.set("Content-Type", "application/json")
return new Response(JSON.stringify({ url: redirect }), {
headers: response.headers,
})
}
return response
}
40 changes: 22 additions & 18 deletions packages/core/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import parseUrl from "./utils/parse-url"
import type { AuthOptions, InternalOptions, RequestInternal } from "."

interface InitParams {
host?: string
userOptions: AuthOptions
url: URL
authOptions: AuthOptions
providerId?: string
action: InternalOptions["action"]
/** Callback URL value extracted from the incoming request. */
Expand All @@ -27,10 +27,10 @@ interface InitParams {

/** Initialize all internal options and cookies. */
export async function init({
userOptions,
authOptions,
providerId,
action,
host,
url: reqUrl,
cookies: reqCookies,
callbackUrl: reqCallbackUrl,
csrfToken: reqCsrfToken,
Expand All @@ -39,7 +39,12 @@ export async function init({
options: InternalOptions
cookies: cookie.Cookie[]
}> {
const url = parseUrl(host)
// TODO: move this to web.ts
const parsed = parseUrl(
reqUrl.origin +
reqUrl.pathname.replace(`/${action}`, "").replace(`/${providerId}`, "")
)
const url = new URL(parsed.toString())

/**
* Secret used to salt cookies and tokens (e.g. for CSRF protection).
Expand All @@ -49,15 +54,14 @@ export async function init({
* If no secret provided in production, we throw an error.
*/
const secret =
userOptions.secret ??
authOptions.secret ??
// TODO: Remove this, always ask the user for a secret, even in dev! (Fix assert.ts too)
(await createHash(JSON.stringify({ ...url, ...userOptions })))
(await createHash(JSON.stringify({ ...url, ...authOptions })))

const { providers, provider } = parseProviders({
providers: userOptions.providers,
providers: authOptions.providers,
url,
providerId,
runtime: userOptions.__internal__?.runtime,
})

const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle by default
Expand All @@ -74,7 +78,7 @@ export async function init({
buttonText: "",
},
// Custom options override defaults
...userOptions,
...authOptions,
// These computed settings can have values in userOptions but we override them
// and are request-specific.
url,
Expand All @@ -83,35 +87,35 @@ export async function init({
provider,
cookies: {
...cookie.defaultCookies(
userOptions.useSecureCookies ?? url.base.startsWith("https://")
authOptions.useSecureCookies ?? url.protocol === "https:"
),
// Allow user cookie options to override any cookie settings above
...userOptions.cookies,
...authOptions.cookies,
},
secret,
providers,
// Session options
session: {
// If no adapter specified, force use of JSON Web Tokens (stateless)
strategy: userOptions.adapter ? "database" : "jwt",
strategy: authOptions.adapter ? "database" : "jwt",
maxAge,
updateAge: 24 * 60 * 60,
generateSessionToken: crypto.randomUUID,
...userOptions.session,
...authOptions.session,
},
// JWT options
jwt: {
secret, // Use application secret if no keys specified
maxAge, // same as session maxAge,
encode: jwt.encode,
decode: jwt.decode,
...userOptions.jwt,
...authOptions.jwt,
},
// Event messages
events: eventsErrorHandler(userOptions.events ?? {}, logger),
adapter: adapterErrorHandler(userOptions.adapter, logger),
events: eventsErrorHandler(authOptions.events ?? {}, logger),
adapter: adapterErrorHandler(authOptions.adapter, logger),
// Callback functions
callbacks: { ...defaultCallbacks, ...userOptions.callbacks },
callbacks: { ...defaultCallbacks, ...authOptions.callbacks },
logger,
callbackUrl: url.origin,
}
Expand Down
Loading

0 comments on commit e5bfbed

Please sign in to comment.