Skip to content

Commit

Permalink
feat: make missing secret an error (nextauthjs#3143)
Browse files Browse the repository at this point in the history
BREAKING CHANGE:

It is now required to set a `secret` in production.
  • Loading branch information
mnphpexpert committed Nov 15, 2021
1 parent 2df8a08 commit 1046404
Show file tree
Hide file tree
Showing 15 changed files with 225 additions and 148 deletions.
3 changes: 3 additions & 0 deletions app/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ module.exports = {

return config
},
typescript: {
ignoreBuildErrors: true,
},
experimental: {
externalDir: true,
},
Expand Down
1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"clean": "rm -rf .next",
"dev": "npm-run-all --parallel dev:next watch:css copy:css ",
"dev:next": "next dev",
"build": "next build",
"copy:css": "cpx \"../css/**/*\" src/css --watch",
"watch:css": "cd .. && npm run watch:css",
"start": "next start",
Expand Down
22 changes: 10 additions & 12 deletions app/pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ export const authOptions: NextAuthOptions = {
providers: [
// E-mail
// Start fake e-mail server with `npm run start:email`
EmailProvider({
server: {
host: "127.0.0.1",
auth: null,
secure: false,
port: 1025,
tls: { rejectUnauthorized: false },
},
}),
// EmailProvider({
// server: {
// host: "127.0.0.1",
// auth: null,
// secure: false,
// port: 1025,
// tls: { rejectUnauthorized: false },
// },
// }),
// Credentials
CredentialsProvider({
name: "Credentials",
Expand Down Expand Up @@ -168,9 +168,7 @@ export const authOptions: NextAuthOptions = {
primaryUserFlow: process.env.AZURE_B2C_PRIMARY_USER_FLOW,
}),
],
jwt: {
secret: process.env.SECRET,
},
secret: process.env.SECRET,
debug: true,
theme: {
colorScheme: "auto",
Expand Down
6 changes: 2 additions & 4 deletions app/pages/api/examples/jwt.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
// This is an example of how to read a JSON Web Token from an API route
import jwt from "next-auth/jwt"

const secret = process.env.SECRET
import { getToken } from "next-auth/jwt"

export default async (req, res) => {
const token = await jwt.getToken({ req, secret, encryption: true })
const token = await getToken({ req, secret: process.env.SECRET })
res.send(JSON.stringify(token, null, 2))
}
27 changes: 27 additions & 0 deletions src/core/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import type { Adapter } from "../adapters"
* @source https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
*/
export class UnknownError extends Error {
code: string
constructor(error: Error | string) {
// Support passing error or string
super((error as Error)?.message ?? error)
this.name = "UnknownError"
this.code = (error as any).code
if (error instanceof Error) {
this.stack = error.stack
}
Expand All @@ -36,6 +38,31 @@ export class AccountNotLinkedError extends UnknownError {
name = "AccountNotLinkedError"
}

export class MissingAPIRoute extends UnknownError {
name = "MissingAPIRouteError"
code = "MISSING_NEXTAUTH_API_ROUTE_ERROR"
}

export class MissingSecret extends UnknownError {
name = "MissingSecretError"
code = "NO_SECRET"
}

export class MissingAuthorize extends UnknownError {
name = "MissingAuthorizeError"
code = "CALLBACK_CREDENTIALS_HANDLER_ERROR"
}

export class MissingAdapter extends UnknownError {
name = "MissingAdapterError"
code = "EMAIL_REQUIRES_ADAPTER_ERROR"
}

export class UnsupportedStrategy extends UnknownError {
name = "UnsupportedStrategyError"
code = "CALLBACK_CREDENTIALS_JWT_ERROR"
}

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

export function upperSnake(s: string) {
Expand Down
37 changes: 30 additions & 7 deletions src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import logger from "../lib/logger"
import logger, { setLogger } from "../lib/logger"
import * as routes from "./routes"
import renderPage from "./pages"
import type { NextAuthOptions } from "./types"
import { init } from "./init"
import { Cookie, SessionStore } from "./lib/cookie"
import { assertConfig } from "./lib/assert"
import { SessionStore } from "./lib/cookie"

import { NextAuthAction } from "../lib/types"
import type { NextAuthOptions } from "./types"
import type { NextAuthAction } from "../lib/types"
import type { Cookie } from "./lib/cookie"
import type { ErrorType } from "./pages/error"

export interface IncomingRequest {
/** @default "http://localhost:3000" */
Expand Down Expand Up @@ -35,7 +38,7 @@ export interface OutgoingResponse<
cookies?: Cookie[]
}

interface NextAuthHandlerParams {
export interface NextAuthHandlerParams {
req: IncomingRequest
options: NextAuthOptions
}
Expand All @@ -44,6 +47,26 @@ export async function NextAuthHandler<
Body extends string | Record<string, any> | any[]
>(params: NextAuthHandlerParams): Promise<OutgoingResponse<Body>> {
const { options: userOptions, req } = params

setLogger(userOptions.logger, userOptions.debug)

const assertionResult = assertConfig(params)

if (typeof assertionResult === "string") {
logger.warn(assertionResult)
} else if (assertionResult instanceof Error) {
// Bail out early if there's an error in the user config
const { pages, theme } = userOptions
logger.error(assertionResult.code, assertionResult)
if (pages?.error) {
return {
redirect: `${pages.error}?error=Configuration`,
}
}
const render = renderPage({ theme })
return render.error({ error: "configuration" })
}

const { action, providerId, error } = req

const { options, cookies } = await init({
Expand All @@ -64,7 +87,7 @@ export async function NextAuthHandler<
)

if (req.method === "GET") {
const render = renderPage({ options, query: req.query, cookies })
const render = renderPage({ ...options, query: req.query, cookies })
const { pages } = options
switch (action) {
case "providers":
Expand Down Expand Up @@ -139,7 +162,7 @@ export async function NextAuthHandler<
return { redirect: `${options.url}/signin?error=${error}`, cookies }
}

return render.error({ error })
return render.error({ error: error as ErrorType })
default:
}
} else if (req.method === "POST") {
Expand Down
5 changes: 0 additions & 5 deletions src/core/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,6 @@ export async function init({
options: InternalOptions
cookies: cookie.Cookie[]
}> {
// If debug enabled, set ENV VAR so that logger logs debug messages
if (userOptions.debug) {
;(process.env._NEXTAUTH_DEBUG as any) = true
}

const url = parseUrl(host)

const secret = createSecret({ userOptions, url })
Expand Down
77 changes: 77 additions & 0 deletions src/core/lib/assert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
MissingAdapter,
MissingAPIRoute,
MissingAuthorize,
MissingSecret,
UnsupportedStrategy,
} from "../errors"

import type { NextAuthHandlerParams } from ".."
import type { WarningCode } from "../../lib/logger"

type ConfigError =
| MissingAPIRoute
| MissingSecret
| UnsupportedStrategy
| MissingAuthorize
| MissingAdapter

/**
* Verify that the user configured `next-auth` correctly.
* Good place to mention deprecations as well.
*
* REVIEW: Make some of these and corresponding docs less Next.js specific?
*/
export function assertConfig(
params: NextAuthHandlerParams
): ConfigError | WarningCode | undefined {
const { options, req } = params

if (!req.query?.nextauth) {
return new MissingAPIRoute(
"Cannot find [...nextauth].{js,ts} in `/pages/api/auth`. Make sure the filename is written correctly."
)
}

if (!options.secret) {
if (process.env.NODE_ENV === "production") {
return new MissingSecret("Please define a `secret` in production.")
} else {
return "NO_SECRET"
}
}

if (!req.host) return "NEXTAUTH_URL"

let hasCredentials, hasEmail

options.providers.forEach(({ type }) => {
if (type === "credentials") hasCredentials = true
else if (type === "email") hasEmail = true
})

if (hasCredentials) {
const dbStrategy = options.session?.strategy === "database"
const onlyCredentials = !options.providers.some(
(p) => p.type !== "credentials"
)
if (dbStrategy || onlyCredentials) {
return new UnsupportedStrategy(
"Signin in with credentials only supported if JWT strategy is enabled"
)
}

const credentialsNoAuthorize = options.providers.some(
(p) => p.type === "credentials" && !p.authorize
)
if (credentialsNoAuthorize) {
return new MissingAuthorize(
"Must define an authorize() handler to use credentials authentication provider"
)
}
}

if (hasEmail && !options.adapter) {
return new MissingAdapter("E-mail login requires an adapter.")
}
}
5 changes: 2 additions & 3 deletions src/core/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,8 @@ export function hashToken(token: string, options: InternalOptions<"email">) {
/**
* Secret used salt cookies and tokens (e.g. for CSRF protection).
* If no secret option is specified then it creates one on the fly
* based on options passed here. A options contains unique data, such as
* OAuth provider secrets and database credentials it should be sufficent.
*/
* based on options passed here. If options contains unique data, such as
* OAuth provider secrets and database credentials it should be sufficent. If no secret provided in production, we throw an error. */
export default function createSecret(params: {
userOptions: NextAuthOptions
url: InternalUrl
Expand Down
20 changes: 13 additions & 7 deletions src/core/pages/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Theme } from "../.."
import { InternalUrl } from "../../lib/parse-url"

export interface ErrorProps {
url: InternalUrl
theme: Theme
url?: InternalUrl
theme?: Theme
error?: string
}

Expand All @@ -14,19 +14,25 @@ interface ErrorView {
signin?: JSX.Element
}

export type ErrorType =
| "default"
| "configuration"
| "accessdenied"
| "verification"

/** Renders an error page. */
export default function ErrorPage(props: ErrorProps) {
const { url, error = "default", theme } = props
const signinPageUrl = `${url}/signin`

const errors: Record<string, ErrorView> = {
const errors: Record<ErrorType, ErrorView> = {
default: {
status: 200,
heading: "Error",
message: (
<p>
<a className="site" href={url.origin}>
{url.host}
<a className="site" href={url?.origin}>
{url?.host}
</a>
</p>
),
Expand Down Expand Up @@ -85,12 +91,12 @@ export default function ErrorPage(props: ErrorProps) {
dangerouslySetInnerHTML={{
__html: `
:root {
--brand-color: ${theme.brandColor}
--brand-color: ${theme?.brandColor};
}
`,
}}
/>
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
{theme?.logo && <img src={theme.logo} alt="Logo" className="logo" />}
<div className="card">
<h1>{heading}</h1>
<div className="message">{message}</div>
Expand Down
Loading

0 comments on commit 1046404

Please sign in to comment.