Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make missing secret an error #3143

Merged
merged 16 commits into from
Nov 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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