Skip to content

Commit

Permalink
feat: add basePath option (#9686)
Browse files Browse the repository at this point in the history
* fix: parse action and provider id

* delete unused file

* use basePath in parseProviders

* use basePath for error redirects

* throw error with invalid signin url (GET with providerId)

* fix test

* fix tests
  • Loading branch information
balazsorban44 committed Jan 19, 2024
1 parent 60cb83e commit 963086b
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 71 deletions.
12 changes: 8 additions & 4 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export async function Auth(
): Promise<Response | ResponseInternal> {
setLogger(config.logger, config.debug)

const internalRequest = await toInternalRequest(request)
const internalRequest = await toInternalRequest(request, config)

if (internalRequest instanceof Error) {
logger.error(internalRequest)
Expand Down Expand Up @@ -170,8 +170,7 @@ export async function Auth(
const page = (isAuthError && error.kind) || "error"
const params = new URLSearchParams({ error: type })
const path =
config.pages?.[page] ??
`${internalRequest.url.pathname}/${page.toLowerCase()}`
config.pages?.[page] ?? `${config.basePath}/${page.toLowerCase()}`

const url = `${internalRequest.url.origin}${path}?${params}`

Expand Down Expand Up @@ -412,11 +411,16 @@ export interface AuthConfig {
redirectProxyUrl?: string

/**
*
* Use this option to enable experimental features.
* When enabled, it will print a warning message to the console.
* @note Experimental features are not guaranteed to be stable and may change or be removed without notice. Please use with caution.
* @default {}
*/
experimental?: Record<string, boolean>
/**
* The base path of the Auth.js API endpoints.
*
* @default "/auth"
*/
basePath?: string
}
2 changes: 1 addition & 1 deletion packages/core/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export async function AuthInternal(
case "session":
return await actions.session(options, sessionStore, cookies)
case "signin":
return render.signin(error)
return render.signin(providerId, error)
case "signout":
return render.signout()
case "verify-request":
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/lib/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { createCSRFToken } from "./actions/callback/oauth/csrf-token.js"
import { AdapterError, EventError } from "../errors.js"
import parseProviders from "./utils/providers.js"
import { logger, type LoggerInstance } from "./utils/logger.js"
import parseUrl from "./utils/parse-url.js"
import { merge } from "./utils/merge.js"

import type {
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/lib/pages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import SigninPage from "./signin.js"
import SignoutPage from "./signout.js"
import css from "./styles.js"
import VerifyRequestPage from "./verify-request.js"
import { UnknownAction } from "../../errors.js"

import type {
ErrorPageParam,
InternalOptions,
RequestInternal,
ResponseInternal,
Expand Down Expand Up @@ -72,7 +72,8 @@ export default function renderPage(params: RenderPageParams) {
),
}
},
signin(error: any) {
signin(providerId?: string, error?: any) {
if (providerId) throw new UnknownAction("Unsupported action")
if (pages?.signIn) {
let signinUrl = `${pages.signIn}${
pages.signIn.includes("?") ? "&" : "?"
Expand Down
36 changes: 0 additions & 36 deletions packages/core/src/lib/utils/parse-url.ts

This file was deleted.

3 changes: 2 additions & 1 deletion packages/core/src/lib/utils/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export default function parseProviders(params: {
providers: InternalProvider[]
provider?: InternalProvider
} {
const { url, providerId, options } = params
const { providerId, options } = params
const url = new URL(options.basePath ?? "/auth", params.url.origin)

const providers = params.providers.map((p) => {
const provider = typeof p === "function" ? p() : p
Expand Down
75 changes: 49 additions & 26 deletions packages/core/src/lib/utils/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { UnknownAction } from "../../errors.js"

import type {
AuthAction,
AuthConfig,
RequestInternal,
ResponseInternal,
} from "../../types.js"
Expand Down Expand Up @@ -31,35 +32,22 @@ const actions: AuthAction[] = [
]

export async function toInternalRequest(
req: Request
req: Request,
config: AuthConfig
): Promise<RequestInternal | Error> {
try {
let originalUrl = new URL(req.url.replace(/\/$/, ""))
let url = new URL(originalUrl)
const pathname = url.pathname.replace(/\/$/, "")
if (req.method !== "GET" && req.method !== "POST")
throw new UnknownAction("Only GET and POST requests are supported.")

const action = actions.find((a) => pathname.includes(a))
if (!action) {
throw new UnknownAction(`Cannot detect action in pathname (${pathname}).`)
}
// Defaults are usually set in the `init` function, but this is needed below
config.basePath ??= "/auth"

// Remove anything after the basepath
const re = new RegExp(`/${action}.*`)
url = new URL(url.href.replace(re, ""))
const url = new URL(req.url)

if (req.method !== "GET" && req.method !== "POST") {
throw new UnknownAction("Only GET and POST requests are supported.")
}

const providerIdOrAction = pathname.split("/").pop()
let providerId
if (
providerIdOrAction &&
!action.includes(providerIdOrAction) &&
["signin", "callback"].includes(action)
) {
providerId = providerIdOrAction
}
const { action, providerId } = parseActionAndProviderId(
url.pathname,
config.basePath
)

return {
url,
Expand All @@ -69,8 +57,8 @@ export async function toInternalRequest(
headers: Object.fromEntries(req.headers),
body: req.body ? await getBody(req) : undefined,
cookies: parseCookie(req.headers.get("cookie") ?? "") ?? {},
error: originalUrl.searchParams.get("error") ?? undefined,
query: Object.fromEntries(originalUrl.searchParams),
error: url.searchParams.get("error") ?? undefined,
query: Object.fromEntries(url.searchParams),
}
} catch (e) {
return e as Error
Expand Down Expand Up @@ -130,3 +118,38 @@ export function randomString(size: number) {
const bytes = crypto.getRandomValues(new Uint8Array(size))
return Array.from(bytes).reduce(r, "")
}

function isAction(action: string): action is AuthAction {
return actions.includes(action as AuthAction)
}

/** @internal Parse the action and provider id from a URL pathname. */
export function parseActionAndProviderId(
pathname: string,
base: string
): {
action: AuthAction
providerId?: string
} {
const a = pathname.split(base)

if (a.length !== 2 || a[0] !== "")
throw new UnknownAction(`Cannot parse action at ${pathname}`)

const [_, actionAndProviderId] = a

const b = actionAndProviderId.replace(/^\//, "").split("/")

if (b.length !== 1 && b.length !== 2)
throw new UnknownAction(`Cannot parse action at ${pathname}`)

const [action, providerId] = b

if (!isAction(action))
throw new UnknownAction(`Cannot parse action at ${pathname}`)

if (providerId && !["signin", "callback"].includes(action))
throw new UnknownAction(`Cannot parse action at ${pathname}`)

return { action, providerId }
}
1 change: 1 addition & 0 deletions packages/core/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const testConfig: AuthConfig = {
providers: [GitHub],
trustHost: true,
secret: AUTH_SECRET,
basePath: "/api/auth",
}

let authConfig: AuthConfig
Expand Down
60 changes: 60 additions & 0 deletions packages/core/test/url-parsing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest"

import { parseActionAndProviderId } from "../src/lib/utils/web"
import { UnknownAction } from "../src/errors"

describe("parse the action and provider id", () => {
it.each([
{
path: "/auuth/signin",
error: "Cannot parse action at /auuth/signin",
basePath: "/auth",
},
{
path: "/api/auth/signin",
error: "Cannot parse action at /api/auth/signin",
basePath: "/auth",
},
{
path: "/auth/auth/signin/github",
error: "Cannot parse action at /auth/auth/signin/github",
basePath: "/auth",
},
{
path: "/api/auth/signinn",
error: "Cannot parse action at /api/auth/signinn",
basePath: "/api/auth",
},
{
path: "/api/auth/signinn/github",
error: "Cannot parse action at /api/auth/signinn/github",
basePath: "/api/auth",
},
{
path: "/api/auth/signin/github",
action: "signin",
providerId: "github",
basePath: "/api/auth",
},
{
path: "/api/auth/signin/github/github",
error: "Cannot parse action at /api/auth/signin/github/github",
basePath: "/api/auth",
},
{
path: "/api/auth/signin/api/auth/signin/github",
error: "Cannot parse action at /api/auth/signin/api/auth/signin/github",
basePath: "/api/auth",
},
])("$path", ({ path, error, basePath, action, providerId }) => {
if (action || providerId) {
const parsed = parseActionAndProviderId(path, basePath)
expect(parsed.action).toBe(action)
expect(parsed.providerId).toBe(providerId)
} else {
expect(() => parseActionAndProviderId(path, basePath)).toThrow(
new UnknownAction(error)
)
}
})
})

1 comment on commit 963086b

@vercel
Copy link

@vercel vercel bot commented on 963086b Jan 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.