mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
20 Commits
@next-auth
...
feat/oauth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f48e0f1e2d | ||
|
|
2eef2f85d5 | ||
|
|
bf31512071 | ||
|
|
7cf49566a6 | ||
|
|
2469e44572 | ||
|
|
408b6b175f | ||
|
|
92dfc3c8b0 | ||
|
|
8c5d9faad6 | ||
|
|
49a8d51f79 | ||
|
|
c0d251731d | ||
|
|
acc9966285 | ||
|
|
6dd44000d7 | ||
|
|
2267292f5f | ||
|
|
883b36e2b2 | ||
|
|
029edb93ee | ||
|
|
ebcc3280df | ||
|
|
70a16e82c3 | ||
|
|
1365211a4e | ||
|
|
6df5773220 | ||
|
|
e3e3cf12d1 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -103,7 +103,7 @@ jobs:
|
||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc
|
||||
pnpm publish --no-git-checks --access public --tag experimental
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN_PKG }}
|
||||
- name: Comment version on PR
|
||||
uses: NejcZdovc/comment-pr@v1
|
||||
with:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -30,7 +30,7 @@ packages/next-auth/providers
|
||||
packages/next-auth/src/providers/oauth-types.ts
|
||||
packages/next-auth/client
|
||||
packages/next-auth/css
|
||||
packages/next-auth/lib
|
||||
packages/next-auth/utils
|
||||
packages/next-auth/core
|
||||
packages/next-auth/jwt
|
||||
packages/next-auth/react
|
||||
|
||||
@@ -65,7 +65,7 @@ async function SKNextAuthHandler(
|
||||
query: Object.fromEntries(url.searchParams),
|
||||
headers: request.headers,
|
||||
method: request.method,
|
||||
cookies: cookie.parse(request.headers.get("cookie")),
|
||||
cookies: cookie.parse(request.headers.get("cookie") ?? ""),
|
||||
action: nextauth[0] as NextAuthAction,
|
||||
providerId: nextauth[1],
|
||||
error: nextauth[1],
|
||||
@@ -91,7 +91,7 @@ export async function getServerSession(
|
||||
host: import.meta.env.VITE_NEXTAUTH_URL,
|
||||
action: "session",
|
||||
method: "GET",
|
||||
cookies: cookie.parse(request.headers.get("cookie")),
|
||||
cookies: cookie.parse(request.headers.get("cookie") ?? ""),
|
||||
headers: request.headers,
|
||||
},
|
||||
options,
|
||||
|
||||
@@ -15,7 +15,7 @@ https://dashboard.workos.com
|
||||
|
||||
The **WorkOS Provider** comes with a set of default options:
|
||||
|
||||
- [WorkOS Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/workos.js)
|
||||
- [WorkOS Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/workos.ts)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@next-auth/sequelize-adapter",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.4",
|
||||
"description": "Sequelize adapter for next-auth.",
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/adapters",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
transform: {
|
||||
".(ts|tsx)$": "ts-jest",
|
||||
".(js|jsx)$": "babel-jest", // jest's default
|
||||
".(ts|tsx)$": "@swc/jest",
|
||||
".(js|jsx)$": "@swc/jest", // jest's default
|
||||
},
|
||||
transformIgnorePatterns: ["[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$"],
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.24.0",
|
||||
"@typescript-eslint/parser": "^4.24.0",
|
||||
"babel-jest": "^27.4.2",
|
||||
"eslint": "^7.27.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-standard-with-typescript": "^20.0.0",
|
||||
|
||||
@@ -25,7 +25,7 @@ module.exports = (api) => {
|
||||
ignore: [
|
||||
"../src/**/__tests__/**",
|
||||
"../src/adapters.ts",
|
||||
"../src/lib/types.ts",
|
||||
"../src/core/types.ts",
|
||||
"../src/providers/oauth-types.ts",
|
||||
],
|
||||
comments: false,
|
||||
@@ -33,7 +33,7 @@ module.exports = (api) => {
|
||||
{
|
||||
test: [
|
||||
"../src/react/index.tsx",
|
||||
"../src/lib/logger.ts",
|
||||
"../src/utils/logger.ts",
|
||||
"../src/core/errors.ts",
|
||||
"../src/client/**",
|
||||
],
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
/** @type {import('@jest/types').Config.InitialOptions} */
|
||||
module.exports = {
|
||||
transform: {
|
||||
"\\.(js|jsx|ts|tsx)$": [
|
||||
"babel-jest",
|
||||
{ configFile: "./config/babel.config.js" },
|
||||
],
|
||||
"\\.(js|jsx|ts|tsx)$": ["@swc/jest", require("./swc.config")],
|
||||
},
|
||||
rootDir: "../src",
|
||||
setupFilesAfterEnv: ["../config/jest-setup.js"],
|
||||
13
packages/next-auth/config/jest.core.config.js
Normal file
13
packages/next-auth/config/jest.core.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/** @type {import('@jest/types').Config.InitialOptions} */
|
||||
module.exports = {
|
||||
transform: {
|
||||
"\\.(js|jsx|ts|tsx)$": ["@swc/jest", require("./swc.config")],
|
||||
},
|
||||
rootDir: "..",
|
||||
testMatch: ["**/*.test.ts"],
|
||||
setupFilesAfterEnv: ["./config/jest-setup.js"],
|
||||
watchPlugins: [
|
||||
"jest-watch-typeahead/filename",
|
||||
"jest-watch-typeahead/testname",
|
||||
],
|
||||
}
|
||||
17
packages/next-auth/config/swc.config.js
Normal file
17
packages/next-auth/config/swc.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: "typescript",
|
||||
tsx: true,
|
||||
},
|
||||
transform: {
|
||||
react: {
|
||||
runtime: "automatic",
|
||||
pragma: "React.createElement",
|
||||
pragmaFrag: "React.Fragment",
|
||||
throwIfNamespace: true,
|
||||
useBuiltins: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-auth",
|
||||
"version": "4.3.4",
|
||||
"version": "4.5.0",
|
||||
"description": "Authentication for Next.js",
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/next-auth.git",
|
||||
@@ -37,11 +37,13 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm clean && pnpm build:js && pnpm build:css",
|
||||
"clean": "rm -rf client css lib providers core jwt react next index.d.ts index.js adapters.d.ts middleware.d.ts middleware.js",
|
||||
"clean": "rm -rf client css utils providers core jwt react next index.d.ts index.js adapters.d.ts middleware.d.ts middleware.js",
|
||||
"build:js": "pnpm clean && pnpm generate-providers && tsc && babel --config-file ./config/babel.config.js src --out-dir . --extensions \".tsx,.ts,.js,.jsx\"",
|
||||
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir . && node config/wrap-css.js",
|
||||
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir .",
|
||||
"test": "jest --config ./config/jest.config.js",
|
||||
"test:client": "jest --config ./config/jest.client.config.js",
|
||||
"test:core": "jest --config ./config/jest.core.config.js",
|
||||
"test": "pnpm test:core && pnpm test:client",
|
||||
"prepublishOnly": "pnpm build",
|
||||
"generate-providers": "node ./config/generate-providers.js",
|
||||
"setup": "pnpm generate-providers",
|
||||
@@ -66,10 +68,10 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.16.3",
|
||||
"@panva/hkdf": "^1.0.1",
|
||||
"@panva/oauth4webapi": "^0.0.10",
|
||||
"cookie": "^0.4.1",
|
||||
"jose": "^4.3.7",
|
||||
"oauth": "^0.9.15",
|
||||
"openid-client": "^5.1.0",
|
||||
"preact": "^10.6.3",
|
||||
"preact-render-to-string": "^5.1.19",
|
||||
"uuid": "^8.3.2"
|
||||
@@ -85,38 +87,40 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.16.0",
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/plugin-proposal-optional-catch-binding": "^7.16.0",
|
||||
"@babel/plugin-transform-runtime": "^7.16.4",
|
||||
"@babel/preset-env": "^7.16.4",
|
||||
"@babel/preset-react": "^7.16.0",
|
||||
"@babel/preset-typescript": "^7.16.0",
|
||||
"@babel/cli": "^7.17.10",
|
||||
"@babel/core": "^7.18.2",
|
||||
"@babel/plugin-proposal-optional-catch-binding": "^7.16.7",
|
||||
"@babel/plugin-transform-runtime": "^7.18.2",
|
||||
"@babel/preset-env": "^7.18.2",
|
||||
"@babel/preset-react": "^7.17.12",
|
||||
"@babel/preset-typescript": "^7.17.12",
|
||||
"@next-auth/tsconfig": "workspace:^0.0.0",
|
||||
"@testing-library/dom": "^8.11.3",
|
||||
"@testing-library/jest-dom": "^5.16.1",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/react-hooks": "^7.0.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/node": "^16.11.12",
|
||||
"@swc/core": "^1.2.198",
|
||||
"@swc/jest": "^0.2.21",
|
||||
"@testing-library/dom": "^8.13.0",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@testing-library/react-hooks": "^8.0.0",
|
||||
"@testing-library/user-event": "^14.2.0",
|
||||
"@types/node": "^17.0.42",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
"@types/oauth": "^0.9.1",
|
||||
"@types/react": "^17.0.37",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"babel-jest": "^27.4.2",
|
||||
"@types/react": "^18.0.2",
|
||||
"@types/react-dom": "^18.0.5",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"babel-plugin-jsx-pragmatic": "^1.0.2",
|
||||
"babel-preset-preact": "^2.0.0",
|
||||
"cssnano": "^5.0.12",
|
||||
"jest": "^27.4.3",
|
||||
"jest-watch-typeahead": "^1.0.0",
|
||||
"msw": "^0.36.3",
|
||||
"next": "12.1.0",
|
||||
"postcss": "^8.4.12",
|
||||
"postcss-cli": "^9.0.2",
|
||||
"cssnano": "^5.1.11",
|
||||
"jest": "^28.1.1",
|
||||
"jest-environment-jsdom": "^28.1.1",
|
||||
"jest-watch-typeahead": "^1.1.0",
|
||||
"msw": "^0.42.1",
|
||||
"next": "^12.1.6",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss-cli": "^9.1.0",
|
||||
"postcss-nested": "^5.0.6",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -2,11 +2,11 @@ import { useState } from "react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { server, mockCSRFToken } from "./helpers/mocks"
|
||||
import logger from "../../lib/logger"
|
||||
import logger from "../../utils/logger"
|
||||
import { getCsrfToken } from "../../react"
|
||||
import { rest } from "msw"
|
||||
|
||||
jest.mock("../../lib/logger", () => ({
|
||||
jest.mock("../../utils/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
|
||||
@@ -3,10 +3,10 @@ import userEvent from "@testing-library/user-event"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { server, mockProviders } from "./helpers/mocks"
|
||||
import { getProviders } from "../../react"
|
||||
import logger from "../../lib/logger"
|
||||
import logger from "../../utils/logger"
|
||||
import { rest } from "msw"
|
||||
|
||||
jest.mock("../../lib/logger", () => ({
|
||||
jest.mock("../../utils/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { rest } from "msw"
|
||||
import { server, mockSession } from "./helpers/mocks"
|
||||
import logger from "../../lib/logger"
|
||||
import logger from "../../utils/logger"
|
||||
import { useState, useEffect } from "react"
|
||||
import { getSession } from "../../react"
|
||||
import { getBroadcastEvents } from "./helpers/utils"
|
||||
|
||||
jest.mock("../../lib/logger", () => ({
|
||||
jest.mock("../../utils/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import logger from "../../lib/logger"
|
||||
import logger from "../../utils/logger"
|
||||
import {
|
||||
server,
|
||||
mockCredentialsResponse,
|
||||
@@ -13,7 +13,7 @@ import { rest } from "msw"
|
||||
|
||||
const { location } = window
|
||||
|
||||
jest.mock("../../lib/logger", () => ({
|
||||
jest.mock("../../utils/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import logger, { setLogger } from "../lib/logger"
|
||||
import logger, { setLogger } from "../utils/logger"
|
||||
import { detectHost } from "../utils/detect-host"
|
||||
import * as routes from "./routes"
|
||||
import renderPage from "./pages"
|
||||
import { init } from "./init"
|
||||
import { assertConfig } from "./lib/assert"
|
||||
import { SessionStore } from "./lib/cookie"
|
||||
|
||||
import type { NextAuthOptions } from "./types"
|
||||
import type { NextAuthAction } from "../lib/types"
|
||||
import type { NextAuthAction, NextAuthOptions } from "./types"
|
||||
import type { Cookie } from "./lib/cookie"
|
||||
import type { ErrorType } from "./pages/error"
|
||||
|
||||
export interface IncomingRequest {
|
||||
export interface RequestInternal {
|
||||
/** @default "http://localhost:3000" */
|
||||
host?: string
|
||||
method?: string
|
||||
@@ -39,18 +39,55 @@ export interface OutgoingResponse<
|
||||
}
|
||||
|
||||
export interface NextAuthHandlerParams {
|
||||
req: IncomingRequest
|
||||
req: Request | RequestInternal
|
||||
options: NextAuthOptions
|
||||
}
|
||||
|
||||
async function getBody(req: Request): Promise<Record<string, any> | undefined> {
|
||||
try {
|
||||
return await req.json()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// TODO:
|
||||
async function toInternalRequest(
|
||||
req: RequestInternal | Request
|
||||
): Promise<RequestInternal> {
|
||||
if (req instanceof Request) {
|
||||
const url = new URL(req.url)
|
||||
// TODO: handle custom paths?
|
||||
const nextauth = url.pathname.split("/").slice(3)
|
||||
const headers = Object.fromEntries(req.headers.entries())
|
||||
const query: Record<string, any> = Object.fromEntries(
|
||||
url.searchParams.entries()
|
||||
)
|
||||
query.nextauth = nextauth
|
||||
|
||||
return {
|
||||
action: nextauth[0] as NextAuthAction,
|
||||
method: req.method,
|
||||
headers,
|
||||
body: await getBody(req),
|
||||
cookies: {},
|
||||
providerId: nextauth[1],
|
||||
error: url.searchParams.get("error") ?? nextauth[1],
|
||||
host: detectHost(headers["x-forwarded-host"] ?? headers.host),
|
||||
query,
|
||||
}
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
export async function NextAuthHandler<
|
||||
Body extends string | Record<string, any> | any[]
|
||||
>(params: NextAuthHandlerParams): Promise<OutgoingResponse<Body>> {
|
||||
const { options: userOptions, req } = params
|
||||
const { options: userOptions, req: incomingRequest } = params
|
||||
|
||||
const req = await toInternalRequest(incomingRequest)
|
||||
|
||||
setLogger(userOptions.logger, userOptions.debug)
|
||||
|
||||
const assertionResult = assertConfig(params)
|
||||
const assertionResult = assertConfig({ options: userOptions, req })
|
||||
|
||||
if (typeof assertionResult === "string") {
|
||||
logger.warn(assertionResult)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { NextAuthOptions } from ".."
|
||||
import logger from "../lib/logger"
|
||||
import parseUrl from "../lib/parse-url"
|
||||
import { InternalOptions } from "../lib/types"
|
||||
import logger from "../utils/logger"
|
||||
import parseUrl from "../utils/parse-url"
|
||||
import { adapterErrorHandler, eventsErrorHandler } from "./errors"
|
||||
import parseProviders from "./lib/providers"
|
||||
import createSecret from "./lib/utils"
|
||||
@@ -10,7 +9,9 @@ import * as jwt from "../jwt"
|
||||
import { defaultCallbacks } from "./lib/default-callbacks"
|
||||
import { createCSRFToken } from "./lib/csrf-token"
|
||||
import { createCallbackUrl } from "./lib/callback-url"
|
||||
import { IncomingRequest } from "."
|
||||
import { RequestInternal } from "."
|
||||
|
||||
import type { InternalOptions } from "./types"
|
||||
|
||||
interface InitParams {
|
||||
host?: string
|
||||
@@ -23,7 +24,7 @@ interface InitParams {
|
||||
csrfToken?: string
|
||||
/** Is the incoming request a POST request? */
|
||||
isPost: boolean
|
||||
cookies: IncomingRequest["cookies"]
|
||||
cookies: RequestInternal["cookies"]
|
||||
}
|
||||
|
||||
/** Initialize all internal options and cookies. */
|
||||
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
UnsupportedStrategy,
|
||||
InvalidCallbackUrl,
|
||||
} from "../errors"
|
||||
import parseUrl from "../../lib/parse-url"
|
||||
import parseUrl from "../../utils/parse-url"
|
||||
import { defaultCookies } from "./cookie"
|
||||
|
||||
import type { NextAuthHandlerParams } from ".."
|
||||
import type { WarningCode } from "../../lib/logger"
|
||||
import type { NextAuthHandlerParams, RequestInternal } from ".."
|
||||
import type { WarningCode } from "../../utils/logger"
|
||||
|
||||
type ConfigError =
|
||||
| MissingAPIRoute
|
||||
@@ -21,9 +21,11 @@ type ConfigError =
|
||||
|
||||
let twitterWarned = false
|
||||
|
||||
function isValidHttpUrl(url: string) {
|
||||
function isValidHttpUrl(url: string, baseUrl: string) {
|
||||
try {
|
||||
return /^https?:/.test(new URL(url).protocol)
|
||||
return /^https?:/.test(
|
||||
new URL(url, url.startsWith("/") ? baseUrl : undefined).protocol
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
@@ -36,7 +38,9 @@ function isValidHttpUrl(url: string) {
|
||||
* REVIEW: Make some of these and corresponding docs less Next.js specific?
|
||||
*/
|
||||
export function assertConfig(
|
||||
params: NextAuthHandlerParams
|
||||
params: NextAuthHandlerParams & {
|
||||
req: RequestInternal
|
||||
}
|
||||
): ConfigError | WarningCode | undefined {
|
||||
const { options, req } = params
|
||||
|
||||
@@ -57,23 +61,24 @@ export function assertConfig(
|
||||
|
||||
const callbackUrlParam = req.query?.callbackUrl as string | undefined
|
||||
|
||||
if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam)) {
|
||||
const url = parseUrl(req.host)
|
||||
|
||||
if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, url.base)) {
|
||||
return new InvalidCallbackUrl(
|
||||
`Invalid callback URL. Received: ${callbackUrlParam}`
|
||||
)
|
||||
}
|
||||
|
||||
// This is below the callbackUrlParam check because it would obscure the error
|
||||
if (!req.host) return "NEXTAUTH_URL"
|
||||
|
||||
const url = parseUrl(req.host)
|
||||
|
||||
const { callbackUrl: defaultCallbackUrl } = defaultCookies(
|
||||
options.useSecureCookies ?? url.base.startsWith("https://")
|
||||
)
|
||||
const callbackUrlCookie =
|
||||
req.cookies?.[options.cookies?.callbackUrl?.name ?? defaultCallbackUrl.name]
|
||||
|
||||
if (callbackUrlCookie && !isValidHttpUrl(callbackUrlCookie)) {
|
||||
if (callbackUrlCookie && !isValidHttpUrl(callbackUrlCookie, url.base)) {
|
||||
return new InvalidCallbackUrl(
|
||||
`Invalid callback URL. Received: ${callbackUrlCookie}`
|
||||
)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { randomBytes, randomUUID } from "crypto"
|
||||
import { AccountNotLinkedError } from "../errors"
|
||||
import { fromDate } from "./utils"
|
||||
import { randomBytes, randomUUID } from "crypto"
|
||||
import { InternalOptions } from "../../lib/types"
|
||||
import { AdapterSession, AdapterUser } from "../../adapters"
|
||||
import { JWT } from "../../jwt"
|
||||
import { Account, User } from "../.."
|
||||
import { SessionToken } from "./cookie"
|
||||
|
||||
import type { InternalOptions } from "../types"
|
||||
import type { AdapterSession, AdapterUser } from "../../adapters"
|
||||
import type { JWT } from "../../jwt"
|
||||
import type { Account, User } from "../.."
|
||||
import type { SessionToken } from "./cookie"
|
||||
|
||||
/**
|
||||
* This function handles the complex flow of signing users in, and either creating,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { InternalOptions } from "../../lib/types"
|
||||
import type { InternalOptions } from "../types"
|
||||
|
||||
interface CreateCallbackUrlParams {
|
||||
options: InternalOptions
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createHash, randomBytes } from "crypto"
|
||||
import { InternalOptions } from "../../lib/types"
|
||||
|
||||
import type { InternalOptions } from "../types"
|
||||
|
||||
interface CreateCSRFTokenParams {
|
||||
options: InternalOptions
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomBytes } from "crypto"
|
||||
import { InternalOptions } from "../../../lib/types"
|
||||
import { hashToken } from "../utils"
|
||||
import type { InternalOptions } from "../../types"
|
||||
|
||||
/**
|
||||
* Starts an e-mail login flow, by generating a token,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { discoveryRequest, processDiscoveryResponse } from "@panva/oauth4webapi"
|
||||
|
||||
import type { AuthorizationServer } from "@panva/oauth4webapi"
|
||||
import type { InternalProvider } from "src/lib/types"
|
||||
|
||||
export default async function getAuthorizationServer(
|
||||
provider: InternalProvider<"oauth">
|
||||
): Promise<AuthorizationServer> {
|
||||
if (provider.idToken) {
|
||||
const issuer = new URL(provider.issuer as string)
|
||||
return await discoveryRequest(issuer).then(
|
||||
async (response) => await processDiscoveryResponse(issuer, response)
|
||||
)
|
||||
} else {
|
||||
return {
|
||||
issuer: provider.issuer as string,
|
||||
authorization_endpoint:
|
||||
typeof provider.authorization === "string"
|
||||
? provider.authorization
|
||||
: provider.authorization?.url,
|
||||
token_endpoint:
|
||||
typeof provider.token === "string"
|
||||
? provider.token
|
||||
: provider.token?.url,
|
||||
userinfo_endpoint:
|
||||
typeof provider.userinfo === "string"
|
||||
? provider.userinfo
|
||||
: provider.userinfo?.url,
|
||||
jwks_uri: provider.jwks_uri,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { openidClient } from "./client"
|
||||
import { oAuth1Client } from "./client-legacy"
|
||||
import { createState } from "./state-handler"
|
||||
import { createPKCE } from "./pkce-handler"
|
||||
import getAuthorizationServer from "./authorization-server"
|
||||
|
||||
import type { AuthorizationParameters } from "openid-client"
|
||||
import type { InternalOptions } from "../../../lib/types"
|
||||
import type { IncomingRequest } from "../.."
|
||||
import type { InternalOptions } from "../../types"
|
||||
import type { RequestInternal } from "../.."
|
||||
import type { Cookie } from "../cookie"
|
||||
|
||||
/**
|
||||
@@ -16,7 +16,7 @@ import type { Cookie } from "../cookie"
|
||||
*/
|
||||
export default async function getAuthorizationUrl(params: {
|
||||
options: InternalOptions<"oauth">
|
||||
query: IncomingRequest["query"]
|
||||
query: RequestInternal["query"]
|
||||
}) {
|
||||
const { options, query } = params
|
||||
const { logger, provider } = options
|
||||
@@ -50,28 +50,53 @@ export default async function getAuthorizationUrl(params: {
|
||||
return { redirect: url }
|
||||
}
|
||||
|
||||
const client = await openidClient(options)
|
||||
const authorizationServer = await getAuthorizationServer(provider)
|
||||
|
||||
if (!authorizationServer.authorization_endpoint) throw new Error()
|
||||
const authorizationUrl = new URL(authorizationServer.authorization_endpoint)
|
||||
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
authorizationUrl.searchParams.set(key, value as string)
|
||||
}
|
||||
|
||||
authorizationUrl.searchParams.set("client_id", provider.clientId as string)
|
||||
authorizationUrl.searchParams.set("redirect_uri", provider.callbackUrl)
|
||||
|
||||
if (typeof provider.authorization !== "string" && provider.authorization) {
|
||||
const { params: authorizationEndpointParams } = provider.authorization
|
||||
|
||||
if (typeof authorizationEndpointParams?.response_type === "string") {
|
||||
authorizationUrl.searchParams.set(
|
||||
"response_type",
|
||||
authorizationEndpointParams.response_type
|
||||
)
|
||||
}
|
||||
if (typeof authorizationEndpointParams?.scope === "string") {
|
||||
authorizationUrl.searchParams.set(
|
||||
"scope",
|
||||
authorizationEndpointParams.scope
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const authorizationParams: AuthorizationParameters = params
|
||||
const cookies: Cookie[] = []
|
||||
|
||||
const pkce = await createPKCE(options, authorizationServer)
|
||||
authorizationUrl.searchParams.set("code_challenge", pkce.code_challenge)
|
||||
authorizationUrl.searchParams.set(
|
||||
"code_challenge_method",
|
||||
pkce.code_challenge_method
|
||||
)
|
||||
cookies.push(pkce.cookie)
|
||||
|
||||
const state = await createState(options)
|
||||
if (state) {
|
||||
authorizationParams.state = state.value
|
||||
if (!pkce.isSupported && state) {
|
||||
authorizationUrl.searchParams.set("state", state.value)
|
||||
cookies.push(state.cookie)
|
||||
}
|
||||
|
||||
const pkce = await createPKCE(options)
|
||||
if (pkce) {
|
||||
authorizationParams.code_challenge = pkce.code_challenge
|
||||
authorizationParams.code_challenge_method = pkce.code_challenge_method
|
||||
cookies.push(pkce.cookie)
|
||||
}
|
||||
|
||||
const url = client.authorizationUrl(authorizationParams)
|
||||
|
||||
logger.debug("GET_AUTHORIZATION_URL", { url, cookies })
|
||||
return { redirect: url, cookies }
|
||||
logger.debug("GET_AUTHORIZATION_URL", { authorizationUrl, cookies })
|
||||
return { redirect: authorizationUrl.href, cookies }
|
||||
} catch (error) {
|
||||
logger.error("GET_AUTHORIZATION_URL_ERROR", error as Error)
|
||||
throw error
|
||||
|
||||
@@ -1,25 +1,40 @@
|
||||
import { TokenSet } from "openid-client"
|
||||
import { openidClient } from "./client"
|
||||
import { oAuth1Client } from "./client-legacy"
|
||||
import { useState } from "./state-handler"
|
||||
import { usePKCECodeVerifier } from "./pkce-handler"
|
||||
import { OAuthCallbackError } from "../../errors"
|
||||
import {
|
||||
authorizationCodeGrantRequest,
|
||||
expectNoState,
|
||||
getValidatedIdTokenClaims,
|
||||
isOAuth2Error,
|
||||
processAuthorizationCodeOAuth2Response,
|
||||
processAuthorizationCodeOpenIDResponse,
|
||||
processUserInfoResponse,
|
||||
userInfoRequest,
|
||||
validateAuthResponse,
|
||||
} from "@panva/oauth4webapi"
|
||||
import getAuthorizationServer from "./authorization-server"
|
||||
|
||||
import type { CallbackParamsType } from "openid-client"
|
||||
import type { Account, LoggerInstance, Profile } from "../../.."
|
||||
import type { OAuthChecks, OAuthConfig } from "../../../providers"
|
||||
import type { InternalOptions } from "../../../lib/types"
|
||||
import type { IncomingRequest, OutgoingResponse } from "../.."
|
||||
import type { InternalOptions } from "../../types"
|
||||
import type { RequestInternal, OutgoingResponse } from "../.."
|
||||
import type { Cookie } from "../cookie"
|
||||
import type {
|
||||
OAuth2Error,
|
||||
OAuth2TokenEndpointResponse,
|
||||
OpenIDTokenEndpointResponse,
|
||||
} from "@panva/oauth4webapi"
|
||||
|
||||
export default async function oAuthCallback(params: {
|
||||
options: InternalOptions<"oauth">
|
||||
query: IncomingRequest["query"]
|
||||
body: IncomingRequest["body"]
|
||||
method: Required<IncomingRequest>["method"]
|
||||
cookies: IncomingRequest["cookies"]
|
||||
query: RequestInternal["query"]
|
||||
body: RequestInternal["body"]
|
||||
method: Required<RequestInternal>["method"]
|
||||
cookies: RequestInternal["cookies"]
|
||||
}): Promise<GetProfileResult & { cookies?: OutgoingResponse["cookies"] }> {
|
||||
const { options, query, body, method, cookies } = params
|
||||
const { options, query, body, cookies } = params
|
||||
const { logger, provider } = options
|
||||
|
||||
const errorMessage = body?.error ?? query?.error
|
||||
@@ -65,81 +80,111 @@ export default async function oAuthCallback(params: {
|
||||
}
|
||||
|
||||
try {
|
||||
const client = await openidClient(options)
|
||||
const client = openidClient(provider)
|
||||
const authorizationServer = await getAuthorizationServer(provider)
|
||||
|
||||
let tokens: TokenSet
|
||||
let tokens:
|
||||
| OpenIDTokenEndpointResponse
|
||||
| OAuth2TokenEndpointResponse
|
||||
| OAuth2Error
|
||||
|
||||
const checks: OAuthChecks = {}
|
||||
let expectedState: string | typeof expectNoState = expectNoState
|
||||
const resCookies: Cookie[] = []
|
||||
const authParams = new URLSearchParams(query)
|
||||
|
||||
const codeVerifier = cookies?.[options.cookies.pkceCodeVerifier.name]
|
||||
const pkce = await usePKCECodeVerifier(
|
||||
codeVerifier,
|
||||
options,
|
||||
authorizationServer
|
||||
)
|
||||
resCookies.push(pkce.cookie)
|
||||
|
||||
const state = await useState(cookies?.[options.cookies.state.name], options)
|
||||
|
||||
if (state) {
|
||||
checks.state = state.value
|
||||
if (!pkce.isSupported && state) {
|
||||
resCookies.push(state.cookie)
|
||||
expectedState = state.value
|
||||
authParams.append("state", state.value)
|
||||
}
|
||||
|
||||
const codeVerifier = cookies?.[options.cookies.pkceCodeVerifier.name]
|
||||
const pkce = await usePKCECodeVerifier(codeVerifier, options)
|
||||
if (pkce) {
|
||||
checks.code_verifier = pkce.codeVerifier
|
||||
resCookies.push(pkce.cookie)
|
||||
const callbackParameters = validateAuthResponse(
|
||||
authorizationServer,
|
||||
client,
|
||||
authParams,
|
||||
expectedState
|
||||
)
|
||||
if (isOAuth2Error(callbackParameters)) {
|
||||
throw new OAuthCallbackError(callbackParameters.error)
|
||||
}
|
||||
|
||||
const params: CallbackParamsType = {
|
||||
...client.callbackParams({
|
||||
url: `http://n?${new URLSearchParams(query)}`,
|
||||
// TODO: Ask to allow object to be passed upstream:
|
||||
// https://github.com/panva/node-openid-client/blob/3ae206dfc78c02134aa87a07f693052c637cab84/types/index.d.ts#L439
|
||||
// @ts-expect-error
|
||||
body,
|
||||
method,
|
||||
}),
|
||||
// @ts-expect-error
|
||||
...provider.token?.params,
|
||||
}
|
||||
const response = await authorizationCodeGrantRequest(
|
||||
authorizationServer,
|
||||
client,
|
||||
callbackParameters,
|
||||
provider.callbackUrl,
|
||||
pkce.codeVerifier
|
||||
)
|
||||
|
||||
// @ts-expect-error
|
||||
if (provider.token?.request) {
|
||||
// @ts-expect-error
|
||||
if (typeof provider.token !== "string" && provider.token?.request) {
|
||||
const params = {
|
||||
...callbackParameters,
|
||||
...provider.token?.params,
|
||||
}
|
||||
const checks = new URLSearchParams()
|
||||
if (state) checks.append("state", state.value)
|
||||
checks.append("code_verifier", pkce.codeVerifier)
|
||||
const response = await provider.token.request({
|
||||
provider,
|
||||
params,
|
||||
checks,
|
||||
client,
|
||||
})
|
||||
tokens = new TokenSet(response.tokens)
|
||||
tokens = response.tokens
|
||||
} else if (provider.idToken) {
|
||||
tokens = await client.callback(provider.callbackUrl, params, checks)
|
||||
tokens = await processAuthorizationCodeOpenIDResponse(
|
||||
authorizationServer,
|
||||
client,
|
||||
response
|
||||
)
|
||||
} else {
|
||||
tokens = await client.oauthCallback(provider.callbackUrl, params, checks)
|
||||
tokens = await processAuthorizationCodeOAuth2Response(
|
||||
authorizationServer,
|
||||
client,
|
||||
response
|
||||
)
|
||||
}
|
||||
|
||||
// REVIEW: How can scope be returned as an array?
|
||||
if (Array.isArray(tokens.scope)) {
|
||||
tokens.scope = tokens.scope.join(" ")
|
||||
if (isOAuth2Error(tokens)) {
|
||||
throw new OAuthCallbackError(tokens.error)
|
||||
}
|
||||
|
||||
let profile: Profile
|
||||
// @ts-expect-error
|
||||
if (provider.userinfo?.request) {
|
||||
// @ts-expect-error
|
||||
let profile: Profile | Response
|
||||
if (typeof provider.userinfo !== "string" && provider.userinfo?.request) {
|
||||
profile = await provider.userinfo.request({
|
||||
provider,
|
||||
tokens,
|
||||
client,
|
||||
})
|
||||
} else if (provider.idToken) {
|
||||
profile = tokens.claims()
|
||||
const idToken = getValidatedIdTokenClaims(tokens)
|
||||
|
||||
profile = await processUserInfoResponse(
|
||||
authorizationServer,
|
||||
client,
|
||||
idToken?.sub as string,
|
||||
response
|
||||
)
|
||||
} else {
|
||||
profile = await client.userinfo(tokens, {
|
||||
// @ts-expect-error
|
||||
params: provider.userinfo?.params,
|
||||
})
|
||||
profile = await userInfoRequest(
|
||||
authorizationServer,
|
||||
client,
|
||||
tokens.access_token
|
||||
)
|
||||
}
|
||||
|
||||
const profileResult = await getProfile({
|
||||
profile,
|
||||
profile: profile as Profile,
|
||||
provider,
|
||||
tokens,
|
||||
logger,
|
||||
@@ -156,7 +201,7 @@ export default async function oAuthCallback(params: {
|
||||
|
||||
export interface GetProfileParams {
|
||||
profile: Profile
|
||||
tokens: TokenSet
|
||||
tokens: OpenIDTokenEndpointResponse | OAuth2TokenEndpointResponse
|
||||
provider: OAuthConfig<any>
|
||||
logger: LoggerInstance
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// We have the intentions to provide only minor fixes for this in the future.
|
||||
|
||||
import { OAuth } from "oauth"
|
||||
import { InternalOptions } from "src/lib/types"
|
||||
import type { InternalOptions } from "../../types"
|
||||
|
||||
/**
|
||||
* Client supporting OAuth 1.x
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
import { Issuer, Client, custom } from "openid-client"
|
||||
import { InternalOptions } from "src/lib/types"
|
||||
import { InternalProvider } from "src/lib/types"
|
||||
import type { Client as WebApiClient } from "@panva/oauth4webapi"
|
||||
import { Issuer, custom } from "openid-client"
|
||||
import type { Client } from "openid-client"
|
||||
import type { InternalOptions } from "../../types"
|
||||
|
||||
/**
|
||||
* NOTE: We can add auto discovery of the provider's endpoint
|
||||
* that requires only one endpoint to be specified by the user.
|
||||
* Check out `Issuer.discover`
|
||||
*
|
||||
* Client supporting OAuth 2.x and OIDC
|
||||
*/
|
||||
export function webApiClient(provider: InternalProvider<"oauth">): WebApiClient {
|
||||
return {
|
||||
client_id: provider.clientId as string,
|
||||
client_secret: provider.clientSecret as string,
|
||||
token_endpoint_auth_method: "client_secret_basic",
|
||||
...provider.client,
|
||||
}
|
||||
}
|
||||
export async function openidClient(
|
||||
options: InternalOptions<"oauth">
|
||||
): Promise<Client> {
|
||||
const provider = options.provider
|
||||
|
||||
|
||||
if (provider.httpOptions) custom.setHttpOptionsDefaults(provider.httpOptions)
|
||||
|
||||
|
||||
let issuer: Issuer
|
||||
if (provider.wellKnown) {
|
||||
issuer = await Issuer.discover(provider.wellKnown)
|
||||
@@ -30,21 +37,4 @@ export async function openidClient(
|
||||
userinfo_endpoint: provider.userinfo?.url ?? provider.userinfo,
|
||||
})
|
||||
}
|
||||
|
||||
const client = new issuer.Client(
|
||||
{
|
||||
client_id: provider.clientId as string,
|
||||
client_secret: provider.clientSecret as string,
|
||||
redirect_uris: [provider.callbackUrl],
|
||||
...provider.client,
|
||||
},
|
||||
provider.jwks
|
||||
)
|
||||
|
||||
// allow a 10 second skew
|
||||
// See https://github.com/nextauthjs/next-auth/issues/3032
|
||||
// and https://github.com/nextauthjs/next-auth/issues/3067
|
||||
client[custom.clock_tolerance] = 10
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
9
packages/next-auth/src/core/lib/oauth/helper.ts
Normal file
9
packages/next-auth/src/core/lib/oauth/helper.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { generateRandomCodeVerifier } from "@panva/oauth4webapi"
|
||||
|
||||
/**
|
||||
* Generate random `state` value encoded as base64url. This method returns oauth4webapi's `generateRandomCodeVerifier` for convenience.
|
||||
* @see {@link https://github.com/panva/oauth4webapi/blob/main/docs/functions/generateRandomCodeVerifier.md generateRandomCodeVerifier.}
|
||||
*/
|
||||
export function generateRandomState() {
|
||||
return generateRandomCodeVerifier()
|
||||
}
|
||||
@@ -1,30 +1,43 @@
|
||||
import * as jwt from "../../../jwt"
|
||||
import {
|
||||
generateRandomCodeVerifier,
|
||||
calculatePKCECodeChallenge,
|
||||
} from "@panva/oauth4webapi"
|
||||
import { generators } from "openid-client"
|
||||
import type { InternalOptions } from "src/lib/types"
|
||||
import type { InternalOptions } from "../../types"
|
||||
import type { Cookie } from "../cookie"
|
||||
import type { AuthorizationServer } from "@panva/oauth4webapi"
|
||||
|
||||
const PKCE_CODE_CHALLENGE_METHOD = "S256"
|
||||
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
||||
|
||||
/**
|
||||
* Check if PKCE is supported by the authorization server.
|
||||
* @see {@link https://datatracker.ietf.org/doc/html/rfc8414#section-2 code_challenge_methods_supported}
|
||||
* @param as The authorization server. @see {@link https://github.com/panva/oauth4webapi/blob/main/docs/interfaces/AuthorizationServer.md AuthorizationServer}
|
||||
*/
|
||||
function isPKCESupported(as: AuthorizationServer) {
|
||||
return !!as.code_challenge_methods_supported?.includes(
|
||||
PKCE_CODE_CHALLENGE_METHOD
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `code_challenge` and `code_challenge_method`
|
||||
* and saves them in a cookie.
|
||||
*/
|
||||
export async function createPKCE(options: InternalOptions<"oauth">): Promise<
|
||||
| undefined
|
||||
| {
|
||||
code_challenge: string
|
||||
code_challenge_method: "S256"
|
||||
cookie: Cookie
|
||||
}
|
||||
> {
|
||||
const { cookies, logger, provider } = options
|
||||
if (!provider.checks?.includes("pkce")) {
|
||||
// Provider does not support PKCE, return nothing.
|
||||
return
|
||||
}
|
||||
const code_verifier = generators.codeVerifier()
|
||||
const code_challenge = generators.codeChallenge(code_verifier)
|
||||
export async function createPKCE(
|
||||
options: InternalOptions<"oauth">,
|
||||
as: AuthorizationServer
|
||||
): Promise<{
|
||||
code_challenge: string
|
||||
code_challenge_method: "S256"
|
||||
cookie: Cookie
|
||||
isSupported: boolean
|
||||
}> {
|
||||
const { cookies, logger } = options
|
||||
const code_verifier = generateRandomCodeVerifier()
|
||||
const code_challenge = await calculatePKCECodeChallenge(code_verifier)
|
||||
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + PKCE_MAX_AGE * 1000)
|
||||
@@ -51,34 +64,37 @@ export async function createPKCE(options: InternalOptions<"oauth">): Promise<
|
||||
value: encryptedCodeVerifier,
|
||||
options: { ...cookies.pkceCodeVerifier.options, expires },
|
||||
},
|
||||
isSupported: isPKCESupported(as),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns code_verifier if provider uses PKCE,
|
||||
* Returns `code_verifier`,
|
||||
* and clears the container cookie afterwards.
|
||||
*/
|
||||
export async function usePKCECodeVerifier(
|
||||
codeVerifier: string | undefined,
|
||||
options: InternalOptions<"oauth">
|
||||
): Promise<{ codeVerifier: string; cookie: Cookie } | undefined> {
|
||||
const { cookies, provider } = options
|
||||
options: InternalOptions<"oauth">,
|
||||
as: AuthorizationServer
|
||||
): Promise<{ codeVerifier: string; cookie: Cookie; isSupported: boolean }> {
|
||||
if (codeVerifier === undefined) throw new Error("Invalid code verifier")
|
||||
|
||||
if (!provider?.checks?.includes("pkce") || !codeVerifier) {
|
||||
return
|
||||
}
|
||||
const { cookies } = options
|
||||
|
||||
const pkce = (await jwt.decode({
|
||||
const pkce = await jwt.decode({
|
||||
...options.jwt,
|
||||
token: codeVerifier,
|
||||
})) as any
|
||||
})
|
||||
|
||||
if (pkce === null) throw new Error("Invalid code verifier")
|
||||
|
||||
return {
|
||||
codeVerifier: pkce?.code_verifier ?? undefined,
|
||||
codeVerifier: pkce.code_verifier as string,
|
||||
cookie: {
|
||||
name: cookies.pkceCodeVerifier.name,
|
||||
value: "",
|
||||
options: { ...cookies.pkceCodeVerifier.options, maxAge: 0 },
|
||||
},
|
||||
isSupported: isPKCESupported(as),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { generators } from "openid-client"
|
||||
|
||||
import type { InternalOptions } from "src/lib/types"
|
||||
import type { InternalOptions } from "../../types"
|
||||
import type { Cookie } from "../cookie"
|
||||
import { generateRandomState } from "./helper"
|
||||
|
||||
const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
||||
|
||||
@@ -16,7 +17,7 @@ export async function createState(
|
||||
return
|
||||
}
|
||||
|
||||
const state = generators.state()
|
||||
const state = generateRandomState()
|
||||
|
||||
const encodedState = await jwt.encode({
|
||||
...jwt,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { merge } from "../../lib/merge"
|
||||
import { merge } from "../../utils/merge"
|
||||
|
||||
import type { InternalProvider } from "../../lib/types"
|
||||
import type { InternalProvider } from "../types"
|
||||
import type { Provider } from "../../providers"
|
||||
import type { InternalUrl } from "../../lib/parse-url"
|
||||
import type { InternalUrl } from "../../utils/parse-url"
|
||||
|
||||
/**
|
||||
* Adds `signinUrl` and `callbackUrl` to each provider
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createHash } from "crypto"
|
||||
import { NextAuthOptions } from "../.."
|
||||
import { InternalOptions } from "../../lib/types"
|
||||
import { InternalUrl } from "../../lib/parse-url"
|
||||
|
||||
import type { NextAuthOptions } from "../.."
|
||||
import type { InternalOptions } from "../types"
|
||||
import type { InternalUrl } from "../../utils/parse-url"
|
||||
|
||||
/**
|
||||
* Takes a number in seconds and returns the date in the future.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Theme } from "../.."
|
||||
import { InternalUrl } from "../../lib/parse-url"
|
||||
import { InternalUrl } from "../../utils/parse-url"
|
||||
|
||||
/**
|
||||
* The following errors are passed as error query parameters to the default or overridden error page.
|
||||
|
||||
@@ -5,13 +5,13 @@ import VerifyRequestPage from "./verify-request"
|
||||
import ErrorPage from "./error"
|
||||
import css from "../../css"
|
||||
|
||||
import type { InternalOptions } from "../../lib/types"
|
||||
import type { IncomingRequest, OutgoingResponse } from ".."
|
||||
import type { InternalOptions } from "../types"
|
||||
import type { RequestInternal, OutgoingResponse } from ".."
|
||||
import type { Cookie } from "../lib/cookie"
|
||||
import type { ErrorType } from "./error"
|
||||
|
||||
type RenderPageParams = {
|
||||
query?: IncomingRequest["query"]
|
||||
query?: RequestInternal["query"]
|
||||
cookies?: Cookie[]
|
||||
} & Partial<
|
||||
Pick<
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Theme } from "../.."
|
||||
import { InternalProvider } from "../../lib/types"
|
||||
import type { InternalProvider, Theme } from "../types"
|
||||
|
||||
/**
|
||||
* The following errors are passed as error query parameters to the default or overridden sign-in page.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Theme } from "../.."
|
||||
import { InternalUrl } from "../../lib/parse-url"
|
||||
import { InternalUrl } from "../../utils/parse-url"
|
||||
|
||||
export interface SignoutProps {
|
||||
url: InternalUrl
|
||||
@@ -12,15 +12,17 @@ export default function SignoutPage(props: SignoutProps) {
|
||||
|
||||
return (
|
||||
<div className="signout">
|
||||
{ theme.brandColor && <style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
{theme.brandColor && (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
:root {
|
||||
--brand-color: ${theme.brandColor}
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/> }
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
|
||||
<div className="card">
|
||||
<h1>Signout</h1>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Theme } from "../.."
|
||||
import { InternalUrl } from "../../lib/parse-url"
|
||||
import { InternalUrl } from "../../utils/parse-url"
|
||||
|
||||
interface VerifyRequestPageProps {
|
||||
url: InternalUrl
|
||||
@@ -11,15 +11,17 @@ export default function VerifyRequestPage(props: VerifyRequestPageProps) {
|
||||
|
||||
return (
|
||||
<div className="verify-request">
|
||||
{ theme.brandColor && <style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
{theme.brandColor && (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
:root {
|
||||
--brand-color: ${theme.brandColor}
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/> }
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
|
||||
<div className="card">
|
||||
<h1>Check your email</h1>
|
||||
|
||||
@@ -2,19 +2,19 @@ import oAuthCallback from "../lib/oauth/callback"
|
||||
import callbackHandler from "../lib/callback-handler"
|
||||
import { hashToken } from "../lib/utils"
|
||||
|
||||
import type { InternalOptions } from "../../lib/types"
|
||||
import type { IncomingRequest, OutgoingResponse } from ".."
|
||||
import type { InternalOptions } from "../types"
|
||||
import type { RequestInternal, OutgoingResponse } from ".."
|
||||
import type { Cookie, SessionStore } from "../lib/cookie"
|
||||
import type { User } from "../.."
|
||||
|
||||
/** Handle callbacks from login services */
|
||||
export default async function callback(params: {
|
||||
options: InternalOptions<"oauth" | "credentials" | "email">
|
||||
query: IncomingRequest["query"]
|
||||
method: Required<IncomingRequest>["method"]
|
||||
body: IncomingRequest["body"]
|
||||
headers: IncomingRequest["headers"]
|
||||
cookies: IncomingRequest["cookies"]
|
||||
query: RequestInternal["query"]
|
||||
method: Required<RequestInternal>["method"]
|
||||
body: RequestInternal["body"]
|
||||
headers: RequestInternal["headers"]
|
||||
cookies: RequestInternal["cookies"]
|
||||
sessionStore: SessionStore
|
||||
}): Promise<OutgoingResponse> {
|
||||
const { options, query, body, method, headers, sessionStore } = params
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OutgoingResponse } from ".."
|
||||
import { InternalProvider } from "../../lib/types"
|
||||
import type { OutgoingResponse } from ".."
|
||||
import type { InternalProvider } from "../types"
|
||||
|
||||
export interface PublicProvider {
|
||||
id: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { fromDate } from "../lib/utils"
|
||||
|
||||
import type { Adapter } from "../../adapters"
|
||||
import type { InternalOptions } from "../../lib/types"
|
||||
import type { InternalOptions } from "../types"
|
||||
import type { OutgoingResponse } from ".."
|
||||
import type { Session } from "../.."
|
||||
import type { SessionStore } from "../lib/cookie"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import getAuthorizationUrl from "../lib/oauth/authorization-url"
|
||||
import emailSignin from "../lib/email/signin"
|
||||
import { IncomingRequest, OutgoingResponse } from ".."
|
||||
import { InternalOptions } from "../../lib/types"
|
||||
import { Account, User } from "../.."
|
||||
import type { RequestInternal, OutgoingResponse } from ".."
|
||||
import type { InternalOptions } from "../types"
|
||||
import type { Account, User } from "../.."
|
||||
|
||||
/** Handle requests to /api/auth/signin */
|
||||
export default async function signin(params: {
|
||||
options: InternalOptions<"oauth" | "email">
|
||||
query: IncomingRequest["query"]
|
||||
body: IncomingRequest["body"]
|
||||
query: RequestInternal["query"]
|
||||
body: RequestInternal["body"]
|
||||
}): Promise<OutgoingResponse> {
|
||||
const { options, query, body } = params
|
||||
const { url, adapter, callbacks, logger, provider } = options
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Adapter } from "../../adapters"
|
||||
import type { InternalOptions } from "../../lib/types"
|
||||
import type { InternalOptions } from "../types"
|
||||
import type { OutgoingResponse } from ".."
|
||||
import type { SessionStore } from "../lib/cookie"
|
||||
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import type { Adapter } from "../adapters"
|
||||
import type { Provider, CredentialInput, ProviderType } from "../providers"
|
||||
import type {
|
||||
Provider,
|
||||
CredentialInput,
|
||||
ProviderType,
|
||||
OAuthConfig,
|
||||
EmailConfig,
|
||||
CredentialsConfig,
|
||||
} from "../providers"
|
||||
import type { TokenSetParameters } from "openid-client"
|
||||
import type { JWT, JWTOptions } from "../jwt"
|
||||
import type { LoggerInstance } from "../lib/logger"
|
||||
import type { LoggerInstance } from "../utils/logger"
|
||||
import type { CookieSerializeOptions } from "cookie"
|
||||
import type { TokenEndpointResponse } from "@panva/oauth4webapi"
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
import { InternalUrl } from "../utils/parse-url"
|
||||
|
||||
export type Awaitable<T> = T | PromiseLike<T>
|
||||
|
||||
@@ -213,7 +225,7 @@ export interface Theme {
|
||||
* Some of them are available with different casing,
|
||||
* but they refer to the same value.
|
||||
*/
|
||||
export type TokenSet = TokenSetParameters
|
||||
export type TokenSet = TokenEndpointResponse
|
||||
|
||||
/**
|
||||
* Usually contains information about the provider being used
|
||||
@@ -471,3 +483,71 @@ export interface DefaultUser {
|
||||
* [`profile` OAuth provider callback](https://next-auth.js.org/configuration/providers#using-a-custom-provider)
|
||||
*/
|
||||
export interface User extends Record<string, unknown>, DefaultUser {}
|
||||
|
||||
// Below are types that are only supposed be used by next-auth internally
|
||||
|
||||
/** @internal */
|
||||
export type InternalProvider<T extends ProviderType = any> = (T extends "oauth"
|
||||
? OAuthConfig<any>
|
||||
: T extends "email"
|
||||
? EmailConfig
|
||||
: T extends "credentials"
|
||||
? CredentialsConfig
|
||||
: never) & {
|
||||
signinUrl: string
|
||||
callbackUrl: string
|
||||
}
|
||||
|
||||
export type NextAuthAction =
|
||||
| "providers"
|
||||
| "session"
|
||||
| "csrf"
|
||||
| "signin"
|
||||
| "signout"
|
||||
| "callback"
|
||||
| "verify-request"
|
||||
| "error"
|
||||
| "_log"
|
||||
|
||||
/** @internal */
|
||||
export interface InternalOptions<T extends ProviderType = any> {
|
||||
providers: InternalProvider[]
|
||||
/**
|
||||
* Parsed from `NEXTAUTH_URL` or `x-forwarded-host` on Vercel.
|
||||
* @default "http://localhost:3000/api/auth"
|
||||
*/
|
||||
url: InternalUrl
|
||||
action: NextAuthAction
|
||||
provider: T extends string
|
||||
? InternalProvider<T>
|
||||
: InternalProvider<T> | undefined
|
||||
csrfToken?: string
|
||||
csrfTokenVerified?: boolean
|
||||
secret: string
|
||||
theme: Theme
|
||||
debug: boolean
|
||||
logger: LoggerInstance
|
||||
session: Required<SessionOptions>
|
||||
pages: Partial<PagesOptions>
|
||||
jwt: JWTOptions
|
||||
events: Partial<EventCallbacks>
|
||||
adapter?: Adapter
|
||||
callbacks: CallbacksOptions
|
||||
cookies: CookiesOptions
|
||||
callbackUrl: string
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface NextAuthRequest extends NextApiRequest {
|
||||
options: InternalOptions
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type NextAuthResponse<T = any> = NextApiResponse<T>
|
||||
|
||||
/** @internal */
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||
export type NextAuthApiHandler<Result = void, Response = any> = (
|
||||
req: NextAuthRequest,
|
||||
res: NextAuthResponse<Response>
|
||||
) => Awaitable<Result>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * from "./core/types"
|
||||
|
||||
export type { IncomingRequest, OutgoingResponse } from "./core"
|
||||
export type { RequestInternal, OutgoingResponse } from "./core"
|
||||
|
||||
export * from "./next"
|
||||
export { default } from "./next"
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
import type {
|
||||
CallbacksOptions,
|
||||
CookiesOptions,
|
||||
EventCallbacks,
|
||||
LoggerInstance,
|
||||
PagesOptions,
|
||||
SessionOptions,
|
||||
Theme,
|
||||
Awaitable,
|
||||
} from ".."
|
||||
|
||||
import type {
|
||||
OAuthConfig,
|
||||
EmailConfig,
|
||||
CredentialsConfig,
|
||||
ProviderType,
|
||||
} from "../providers"
|
||||
import type { JWTOptions } from "../jwt"
|
||||
import type { Adapter } from "../adapters"
|
||||
import { InternalUrl } from "./parse-url"
|
||||
|
||||
// Below are types that are only supposed be used by next-auth internally
|
||||
|
||||
/** @internal */
|
||||
export type InternalProvider<T extends ProviderType = any> = (T extends "oauth"
|
||||
? OAuthConfig<any>
|
||||
: T extends "email"
|
||||
? EmailConfig
|
||||
: T extends "credentials"
|
||||
? CredentialsConfig
|
||||
: never) & {
|
||||
signinUrl: string
|
||||
callbackUrl: string
|
||||
}
|
||||
|
||||
export type NextAuthAction =
|
||||
| "providers"
|
||||
| "session"
|
||||
| "csrf"
|
||||
| "signin"
|
||||
| "signout"
|
||||
| "callback"
|
||||
| "verify-request"
|
||||
| "error"
|
||||
| "_log"
|
||||
|
||||
/** @internal */
|
||||
export interface InternalOptions<T extends ProviderType = any> {
|
||||
providers: InternalProvider[]
|
||||
/**
|
||||
* Parsed from `NEXTAUTH_URL` or `x-forwarded-host` on Vercel.
|
||||
* @default "http://localhost:3000/api/auth"
|
||||
*/
|
||||
url: InternalUrl
|
||||
action: NextAuthAction
|
||||
provider: T extends string
|
||||
? InternalProvider<T>
|
||||
: InternalProvider<T> | undefined
|
||||
csrfToken?: string
|
||||
csrfTokenVerified?: boolean
|
||||
secret: string
|
||||
theme: Theme
|
||||
debug: boolean
|
||||
logger: LoggerInstance
|
||||
session: Required<SessionOptions>
|
||||
pages: Partial<PagesOptions>
|
||||
jwt: JWTOptions
|
||||
events: Partial<EventCallbacks>
|
||||
adapter?: Adapter
|
||||
callbacks: CallbacksOptions
|
||||
cookies: CookiesOptions
|
||||
callbackUrl: string
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface NextAuthRequest extends NextApiRequest {
|
||||
options: InternalOptions
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type NextAuthResponse<T = any> = NextApiResponse<T>
|
||||
|
||||
/** @internal */
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||
export type NextAuthApiHandler<Result = void, Response = any> = (
|
||||
req: NextAuthRequest,
|
||||
res: NextAuthResponse<Response>
|
||||
) => Awaitable<Result>
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextAuthHandler } from "../core"
|
||||
import { setCookie, detectHost } from "./utils"
|
||||
import { detectHost } from "../utils/detect-host"
|
||||
import { setCookie } from "./utils"
|
||||
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
@@ -11,7 +12,7 @@ import type {
|
||||
NextAuthAction,
|
||||
NextAuthRequest,
|
||||
NextAuthResponse,
|
||||
} from "../lib/types"
|
||||
} from "../core/types"
|
||||
|
||||
async function NextAuthNextHandler(
|
||||
req: NextApiRequest,
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { JWT, JWTOptions } from "../jwt"
|
||||
import { NextResponse, NextRequest } from "next/server"
|
||||
|
||||
import { getToken } from "../jwt"
|
||||
import parseUrl from "../lib/parse-url"
|
||||
import parseUrl from "../utils/parse-url"
|
||||
|
||||
type AuthorizedCallback = (params: {
|
||||
token: JWT | null
|
||||
@@ -25,9 +25,9 @@ export interface NextAuthMiddlewareOptions {
|
||||
/**
|
||||
* You can override the default cookie names and options for any of the cookies
|
||||
* by this middleware. Similar to `cookies` in `NextAuth`.
|
||||
*
|
||||
*
|
||||
* Useful if the token is stored in not a default cookie.
|
||||
*
|
||||
*
|
||||
* ---
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#cookies)
|
||||
*
|
||||
@@ -36,11 +36,16 @@ export interface NextAuthMiddlewareOptions {
|
||||
* You should **try to avoid using advanced options** unless you are very comfortable using them.
|
||||
*
|
||||
*/
|
||||
cookies?: Partial<Record<keyof Pick<keyof NextAuthOptions["cookies"], "sessionToken">, Omit<CookieOption, "options">>>
|
||||
cookies?: Partial<
|
||||
Record<
|
||||
keyof Pick<keyof NextAuthOptions["cookies"], "sessionToken">,
|
||||
Omit<CookieOption, "options">
|
||||
>
|
||||
>
|
||||
|
||||
/**
|
||||
* If a custom jwt `decode` method is set in `[...nextauth].ts`, the same method should be set here also.
|
||||
*
|
||||
*
|
||||
* ---
|
||||
* [Documentation](https://next-auth.js.org/configuration/nextjs#custom-jwt-decode-method)
|
||||
*/
|
||||
@@ -109,7 +114,7 @@ async function handleMiddleware(
|
||||
const token = await getToken({
|
||||
req,
|
||||
decode: options?.jwt?.decode,
|
||||
cookieName: options?.cookies?.sessionToken?.name
|
||||
cookieName: options?.cookies?.sessionToken?.name,
|
||||
})
|
||||
|
||||
const isAuthorized =
|
||||
@@ -120,7 +125,10 @@ async function handleMiddleware(
|
||||
|
||||
// the user is not logged in, redirect to the sign-in page
|
||||
const signInUrl = new URL(signInPage, req.nextUrl.origin)
|
||||
signInUrl.searchParams.append("callbackUrl", `${req.nextUrl.pathname}${req.nextUrl.search}`)
|
||||
signInUrl.searchParams.append(
|
||||
"callbackUrl",
|
||||
`${req.nextUrl.pathname}${req.nextUrl.search}`
|
||||
)
|
||||
return NextResponse.redirect(signInUrl)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,3 @@ export function setCookie(res, cookie: Cookie) {
|
||||
setCookieHeader.push(cookieHeader)
|
||||
res.setHeader("Set-Cookie", setCookieHeader)
|
||||
}
|
||||
|
||||
/** Extract the host from the environment */
|
||||
export function detectHost(forwardedHost: any) {
|
||||
// If we detect a Vercel environment, we can trust the host
|
||||
if (process.env.VERCEL) return forwardedHost
|
||||
// If `NEXTAUTH_URL` is `undefined` we fall back to "http://localhost:3000"
|
||||
return process.env.NEXTAUTH_URL
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IncomingRequest } from "../core"
|
||||
import type { RequestInternal } from "../core"
|
||||
import type { CommonProviderOptions } from "."
|
||||
import type { User, Awaitable } from ".."
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface CredentialsConfig<
|
||||
credentials: C
|
||||
authorize: (
|
||||
credentials: Record<keyof C, string> | undefined,
|
||||
req: Pick<IncomingRequest, "body" | "query" | "headers" | "method">
|
||||
req: Pick<RequestInternal, "body" | "query" | "headers" | "method">
|
||||
) => Awaitable<(Omit<User, "id"> | { id?: string }) | null>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AuthorizationServer, userInfoRequest } from "@panva/oauth4webapi"
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
interface FacebookPictureData {
|
||||
@@ -26,11 +27,17 @@ export default function Facebook<P extends FacebookProfile>(
|
||||
// https://developers.facebook.com/docs/graph-api/reference/user/#fields
|
||||
params: { fields: "id,name,email,picture" },
|
||||
async request({ tokens, client, provider }) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return await client.userinfo(tokens.access_token!, {
|
||||
// @ts-expect-error
|
||||
params: provider.userinfo?.params,
|
||||
// @ts-expect-error
|
||||
const userinfo_endpoint = new URL(provider.userinfo?.url)
|
||||
// @ts-expect-error
|
||||
Object.entries(provider.userinfo?.params).forEach(([key, value]) => {
|
||||
userinfo_endpoint.searchParams.append(key, value as string)
|
||||
})
|
||||
const as: AuthorizationServer = {
|
||||
issuer: options.issuer as string,
|
||||
userinfo_endpoint: userinfo_endpoint.href,
|
||||
}
|
||||
return await userInfoRequest(as, client, tokens.access_token)
|
||||
},
|
||||
},
|
||||
profile(profile: P) {
|
||||
|
||||
@@ -35,6 +35,9 @@ export default function Instagram(options) {
|
||||
token: "https://api.instagram.com/oauth/access_token",
|
||||
userinfo:
|
||||
"https://graph.instagram.com/me?fields=id,username,account_type,name",
|
||||
client: {
|
||||
token_endpoint_auth_method: 'client_secret_post',
|
||||
},
|
||||
async profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
|
||||
@@ -1,33 +1,20 @@
|
||||
import type { CommonProviderOptions } from "../providers"
|
||||
import type { Profile, TokenSet, User, Awaitable } from ".."
|
||||
|
||||
import type {
|
||||
AuthorizationParameters,
|
||||
CallbackParamsType,
|
||||
Issuer,
|
||||
ClientMetadata,
|
||||
IssuerMetadata,
|
||||
OAuthCallbackChecks,
|
||||
OpenIDCallbackChecks,
|
||||
HttpOptions,
|
||||
} from "openid-client"
|
||||
import type { JWK } from "jose"
|
||||
|
||||
type Client = InstanceType<Issuer["Client"]>
|
||||
import type { AuthorizationServer, Client } from "@panva/oauth4webapi"
|
||||
|
||||
export type { OAuthProviderType } from "./oauth-types"
|
||||
|
||||
type ChecksType = "pkce" | "state" | "none"
|
||||
|
||||
export type OAuthChecks = OpenIDCallbackChecks | OAuthCallbackChecks
|
||||
|
||||
type PartialIssuer = Partial<Pick<IssuerMetadata, "jwks_endpoint" | "issuer">>
|
||||
type PartialIssuer = Partial<Pick<AuthorizationServer, "jwks_uri" | "issuer">>
|
||||
|
||||
type UrlParams = Record<string, unknown>
|
||||
|
||||
type EndpointRequest<C, R, P> = (
|
||||
context: C & {
|
||||
/** `openid-client` Client */
|
||||
/** `oauth4webapi` Client */
|
||||
client: Client
|
||||
/** Provider is passed for convenience, ans also contains the `callbackUrl`. */
|
||||
provider: OAuthConfig<P> & {
|
||||
@@ -61,8 +48,7 @@ export type EndpointHandler<
|
||||
R = any
|
||||
> = AdvancedEndpointHandler<P, C, R>
|
||||
|
||||
export type AuthorizationEndpointHandler =
|
||||
EndpointHandler<AuthorizationParameters>
|
||||
export type AuthorizationEndpointHandler = EndpointHandler<UrlParams>
|
||||
|
||||
export type TokenEndpointHandler = EndpointHandler<
|
||||
UrlParams,
|
||||
@@ -71,12 +57,12 @@ export type TokenEndpointHandler = EndpointHandler<
|
||||
* Parameters extracted from the request to the `/api/auth/callback/:providerId` endpoint.
|
||||
* Contains params like `state`.
|
||||
*/
|
||||
params: CallbackParamsType
|
||||
params: URLSearchParams
|
||||
/**
|
||||
* When using this custom flow, make sure to do all the necessary security checks.
|
||||
* Thist object contains parameters you have to match against the request to make sure it is valid.
|
||||
* This object contains parameters you have to match against the request to make sure it is valid.
|
||||
*/
|
||||
checks: OAuthChecks
|
||||
checks: URLSearchParams
|
||||
},
|
||||
{
|
||||
tokens: TokenSet
|
||||
@@ -86,7 +72,7 @@ export type TokenEndpointHandler = EndpointHandler<
|
||||
export type UserinfoEndpointHandler = EndpointHandler<
|
||||
UrlParams,
|
||||
{ tokens: TokenSet },
|
||||
Profile
|
||||
Profile | Response
|
||||
>
|
||||
|
||||
export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
|
||||
@@ -112,7 +98,7 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
|
||||
version?: string
|
||||
profile?: (profile: P, tokens: TokenSet) => Awaitable<User & { id: string }>
|
||||
checks?: ChecksType | ChecksType[]
|
||||
client?: Partial<ClientMetadata>
|
||||
client?: Partial<Client>
|
||||
jwks?: { keys: JWK[] }
|
||||
clientId?: string
|
||||
clientSecret?: string
|
||||
@@ -128,11 +114,6 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
|
||||
idToken?: boolean
|
||||
// TODO: only allow for BattleNet
|
||||
region?: string
|
||||
// TODO: only allow for some
|
||||
issuer?: string
|
||||
/** Read more at: https://github.com/panva/node-openid-client/tree/main/docs#customizing-http-requests */
|
||||
httpOptions?: HttpOptions
|
||||
|
||||
/**
|
||||
* The options provided by the user.
|
||||
* We will perform a deep-merge of these values
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
import {
|
||||
authorizationCodeGrantRequest,
|
||||
AuthorizationServer,
|
||||
isOAuth2Error,
|
||||
processAuthorizationCodeOAuth2Response,
|
||||
} from "@panva/oauth4webapi"
|
||||
|
||||
export interface TwitterLegacyProfile {
|
||||
id: number
|
||||
@@ -183,13 +189,33 @@ export default function Twitter<
|
||||
url: "https://api.twitter.com/2/oauth2/token",
|
||||
// TODO: Remove this
|
||||
async request({ client, params, checks, provider }) {
|
||||
const response = await client.oauthCallback(
|
||||
provider.callbackUrl,
|
||||
const as: AuthorizationServer = {
|
||||
issuer: options.issuer as string,
|
||||
// @ts-expect-error
|
||||
token_endpoint: provider.token?.url,
|
||||
}
|
||||
const additionalParameters = new URLSearchParams()
|
||||
additionalParameters.append("client_id", options.clientId)
|
||||
const response = await authorizationCodeGrantRequest(
|
||||
as,
|
||||
client,
|
||||
params,
|
||||
checks,
|
||||
{ exchangeBody: { client_id: options.clientId } }
|
||||
provider.callbackUrl,
|
||||
checks.get("code_verifier") as string,
|
||||
{
|
||||
additionalParameters,
|
||||
}
|
||||
)
|
||||
return { tokens: response }
|
||||
const tokens = await processAuthorizationCodeOAuth2Response(
|
||||
as,
|
||||
client,
|
||||
response
|
||||
)
|
||||
|
||||
if (isOAuth2Error(tokens)) {
|
||||
throw new Error()
|
||||
}
|
||||
return { tokens }
|
||||
},
|
||||
},
|
||||
userinfo: {
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
// We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks.
|
||||
|
||||
import * as React from "react"
|
||||
import _logger, { proxyLogger } from "../lib/logger"
|
||||
import parseUrl from "../lib/parse-url"
|
||||
import _logger, { proxyLogger } from "../utils/logger"
|
||||
import parseUrl from "../utils/parse-url"
|
||||
import { Session } from ".."
|
||||
import {
|
||||
BroadcastChannel,
|
||||
|
||||
7
packages/next-auth/src/utils/detect-host.ts
Normal file
7
packages/next-auth/src/utils/detect-host.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/** Extract the host from the environment */
|
||||
export function detectHost(forwardedHost: any) {
|
||||
// If we detect a Vercel environment, we can trust the host
|
||||
if (process.env.VERCEL) return forwardedHost
|
||||
// If `NEXTAUTH_URL` is `undefined` we fall back to "http://localhost:3000"
|
||||
return process.env.NEXTAUTH_URL
|
||||
}
|
||||
27
packages/next-auth/tests/assert.test.ts
Normal file
27
packages/next-auth/tests/assert.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { handler } from "./lib"
|
||||
|
||||
it("Show error page if secret is not defined", async () => {
|
||||
const { res, log } = await handler(
|
||||
{ providers: [], secret: undefined },
|
||||
{ prod: true }
|
||||
)
|
||||
|
||||
expect(res.status).toBe(500)
|
||||
expect(res.html).toMatch(/there is a problem with the server configuration./i)
|
||||
expect(res.html).toMatch(/check the server logs for more information./i)
|
||||
|
||||
expect(log.error).toBeCalledWith("NO_SECRET", expect.anything())
|
||||
})
|
||||
|
||||
it("Allow relative `callbackUrl`", async () => {
|
||||
const { res, log } = await handler(
|
||||
{ providers: [] },
|
||||
{ prod: true, params: { callbackUrl: "/callback" } }
|
||||
)
|
||||
|
||||
expect(res.status).not.toBe(500)
|
||||
expect(log.error).not.toBeCalledWith(
|
||||
"INVALID_CALLBACK_URL_ERROR",
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
49
packages/next-auth/tests/lib.ts
Normal file
49
packages/next-auth/tests/lib.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { LoggerInstance, NextAuthOptions } from "../src"
|
||||
import { NextAuthHandler } from "../src/core"
|
||||
|
||||
export async function handler(
|
||||
options: NextAuthOptions,
|
||||
{
|
||||
prod,
|
||||
path,
|
||||
params,
|
||||
}: {
|
||||
prod?: boolean
|
||||
path?: string
|
||||
params?: URLSearchParams | Record<string, string>
|
||||
}
|
||||
) {
|
||||
// @ts-ignore
|
||||
if (prod) process.env.NODE_ENV = "production"
|
||||
|
||||
const mockLogger: LoggerInstance = {
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
}
|
||||
const url = new URL(
|
||||
`http://localhost/api/auth/${path ?? "signin"}?${new URLSearchParams(
|
||||
params ?? {}
|
||||
)}`
|
||||
)
|
||||
const req = new Request(url, {
|
||||
headers: {
|
||||
host: "",
|
||||
},
|
||||
})
|
||||
const response = await NextAuthHandler({
|
||||
req,
|
||||
options: { secret: "secret", ...options, logger: mockLogger },
|
||||
})
|
||||
// @ts-ignore
|
||||
if (prod) process.env.NODE_ENV = "test"
|
||||
|
||||
return {
|
||||
res: {
|
||||
...response,
|
||||
html:
|
||||
response.headers?.[0].value === "text/html" ? response.body : undefined,
|
||||
},
|
||||
log: mockLogger,
|
||||
}
|
||||
}
|
||||
@@ -16,5 +16,5 @@
|
||||
"baseUrl": ".",
|
||||
"outDir": "."
|
||||
},
|
||||
"exclude": ["./*.js", "./*.d.ts", "config", "**/__tests__"]
|
||||
"exclude": ["./*.js", "./*.d.ts", "config", "**/__tests__", "tests"]
|
||||
}
|
||||
|
||||
30517
pnpm-lock.yaml
generated
30517
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user