mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7fc562b49 | ||
|
|
f3be5e87f6 | ||
|
|
844c9b147c | ||
|
|
c9e16fb71e | ||
|
|
a7d34f97c8 | ||
|
|
f20d6790c8 | ||
|
|
53baf6d67d | ||
|
|
255c822dfb | ||
|
|
31c03c96d1 | ||
|
|
74df39a678 | ||
|
|
714d80a4f5 | ||
|
|
3d5c669a05 | ||
|
|
29977f108f | ||
|
|
7d2e16a6bb | ||
|
|
af157dac07 | ||
|
|
1bf56a218e | ||
|
|
4824f8c02a | ||
|
|
a4d831d1b9 |
9
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
9
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -12,7 +12,12 @@ body:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
### Important :exclamation:
|
||||
Please help us maintain this project more efficiently! Before creating the issue make sure you shouldn't be creating it in one the below repos instead:
|
||||
Please help us maintain this project more efficiently!
|
||||
Is this your first time contributing? See this video https://www.youtube.com/watch?v=cuoNzXFLitc
|
||||
|
||||
**Providing incorrect/insufficient information or skipping steps to reproduce the bug may result in closing the issue or converting to discussion without further explanation.**
|
||||
|
||||
Before creating the issue make sure you shouldn't be creating it in one the below repos instead:
|
||||
- Docs related: https://github.com/nextauthjs/docs
|
||||
- Adapter related: https://github.com/nextauthjs/adapters
|
||||
|
||||
@@ -50,8 +55,6 @@ body:
|
||||
We encourage you to use one of the templates set up on **CodeSandbox** to reproduce your issue:
|
||||
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
|
||||
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
|
||||
|
||||
🚧 – _If you don't provide any way to reproduce the bug, the issue is at risk of being closed._
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -36,6 +36,8 @@ node_modules
|
||||
/index.d.ts
|
||||
/index.js
|
||||
/next
|
||||
/middleware.d.ts
|
||||
/middleware.js
|
||||
|
||||
# Development app
|
||||
app/src/css
|
||||
|
||||
@@ -7,7 +7,7 @@ NEXTAUTH_URL=http://localhost:3000
|
||||
# https://generate-secret.vercel.app/32 to generate a secret.
|
||||
# Note: Changing a secret may invalidate existing sessions
|
||||
# and/or verification tokens.
|
||||
SECRET=secret
|
||||
NEXTAUTH_SECRET=secret
|
||||
|
||||
AUTH0_ID=
|
||||
AUTH0_SECRET=
|
||||
@@ -33,6 +33,9 @@ TWITTER_SECRET=
|
||||
LINE_ID=
|
||||
LINE_SECRET=
|
||||
|
||||
TRAKT_ID=
|
||||
TRAKT_SECRET=
|
||||
|
||||
# Example configuration for a Gmail account (will need SMTP enabled)
|
||||
EMAIL_SERVER=smtps://user@gmail.com:password@smtp.gmail.com:465
|
||||
EMAIL_FROM=user@gmail.com
|
||||
|
||||
@@ -103,6 +103,11 @@ export default function Header() {
|
||||
<a>Email</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href="/middleware-protected">
|
||||
<a>Middleware protected</a>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@prisma/client": "^3.7.0",
|
||||
"fake-smtp-server": "^0.8.0",
|
||||
"faunadb": "^4.4.1",
|
||||
"next": "^12.0.7",
|
||||
"next": "^12.0.8",
|
||||
"nodemailer": "^6.7.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
|
||||
@@ -28,6 +28,7 @@ import AzureB2C from "next-auth/providers/azure-ad-b2c"
|
||||
import OsuProvider from "next-auth/providers/osu"
|
||||
import AppleProvider from "next-auth/providers/apple"
|
||||
import PatreonProvider from "next-auth/providers/patreon"
|
||||
import TraktProvider from "next-auth/providers/trakt"
|
||||
|
||||
// import { PrismaAdapter } from "@next-auth/prisma-adapter"
|
||||
// import { PrismaClient } from "@prisma/client"
|
||||
@@ -189,9 +190,12 @@ export const authOptions: NextAuthOptions = {
|
||||
PatreonProvider({
|
||||
clientId: process.env.PATREON_ID,
|
||||
clientSecret: process.env.PATREON_SECRET,
|
||||
})
|
||||
}),
|
||||
TraktProvider({
|
||||
clientId: process.env.TRAKT_ID,
|
||||
clientSecret: process.env.TRAKT_SECRET,
|
||||
}),
|
||||
],
|
||||
secret: process.env.SECRET,
|
||||
debug: true,
|
||||
theme: {
|
||||
colorScheme: "auto",
|
||||
|
||||
44
app/pages/middleware-protected/_middleware.js
Normal file
44
app/pages/middleware-protected/_middleware.js
Normal file
@@ -0,0 +1,44 @@
|
||||
export { default } from "next-auth/middleware"
|
||||
|
||||
// Other ways to use this middleware
|
||||
|
||||
// import withAuth from "next-auth/middleware"
|
||||
// import { withAuth } from "next-auth/middleware"
|
||||
|
||||
// export function middleware(req, ev) {
|
||||
// return withAuth(req)
|
||||
// }
|
||||
|
||||
// export function middleware(req, ev) {
|
||||
// return withAuth(req, ev)
|
||||
// }
|
||||
|
||||
// export function middleware(req, ev) {
|
||||
// return withAuth(req, {
|
||||
// callbacks: {
|
||||
// authorized: ({ token }) => !!token,
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
|
||||
// export default withAuth(function middleware(req, ev) {
|
||||
// console.log(req.nextauth.token)
|
||||
// })
|
||||
|
||||
// export default withAuth(
|
||||
// function middleware(req, ev) {
|
||||
// console.log(req, ev)
|
||||
// return undefined // NOTE: `NextMiddleware` should allow returning `void`
|
||||
// },
|
||||
// {
|
||||
// callbacks: {
|
||||
// authorized: ({ token }) => token.name === "Balázs Orbán",
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
|
||||
// export default withAuth({
|
||||
// callbacks: {
|
||||
// authorized: ({ token }) => !!token,
|
||||
// },
|
||||
// })
|
||||
9
app/pages/middleware-protected/index.js
Normal file
9
app/pages/middleware-protected/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import Layout from "components/layout"
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Page protected by Middleware</h1>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
3334
package-lock.json
generated
3334
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@@ -31,12 +31,13 @@
|
||||
"./react": "./react/index.js",
|
||||
"./core": "./core/index.js",
|
||||
"./next": "./next/index.js",
|
||||
"./middleware": "./middleware.js",
|
||||
"./client/_utils": "./client/_utils.js",
|
||||
"./providers/*": "./providers/*.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:js && npm run build:css",
|
||||
"clean": "rm -rf client css lib providers core jwt react next index.d.ts index.js adapters.d.ts",
|
||||
"clean": "rm -rf client css lib providers core jwt react next index.d.ts index.js adapters.d.ts middleware.d.ts middleware.js",
|
||||
"build:js": "npm run clean && npm run 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",
|
||||
"dev:setup": "npm i && npm run generate-providers && npm run build:css && cd app && npm i",
|
||||
@@ -61,7 +62,9 @@
|
||||
"core",
|
||||
"index.d.ts",
|
||||
"index.js",
|
||||
"adapters.d.ts"
|
||||
"adapters.d.ts",
|
||||
"middleware.d.ts",
|
||||
"middleware.js"
|
||||
],
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -77,8 +80,8 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"nodemailer": "^6.6.5",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
"react": "^17.0.2 || ^18.0.0-0",
|
||||
"react-dom": "^17.0.2 || ^18.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"nodemailer": {
|
||||
@@ -120,7 +123,7 @@
|
||||
"jest": "^27.4.3",
|
||||
"jest-watch-typeahead": "^1.0.0",
|
||||
"msw": "^0.36.3",
|
||||
"next": "12.0.7",
|
||||
"next": "12.0.9",
|
||||
"postcss-cli": "^9.0.2",
|
||||
"postcss-nested": "^5.0.6",
|
||||
"prettier": "2.4.1",
|
||||
|
||||
@@ -16,6 +16,8 @@ type ConfigError =
|
||||
| MissingAuthorize
|
||||
| MissingAdapter
|
||||
|
||||
let twitterWarned = false
|
||||
|
||||
/**
|
||||
* Verify that the user configured `next-auth` correctly.
|
||||
* Good place to mention deprecations as well.
|
||||
@@ -45,11 +47,13 @@ export function assertConfig(
|
||||
if (!req.host) return "NEXTAUTH_URL"
|
||||
|
||||
let hasCredentials, hasEmail
|
||||
let hasTwitterProvider
|
||||
|
||||
options.providers.forEach(({ type }) => {
|
||||
if (type === "credentials") hasCredentials = true
|
||||
else if (type === "email") hasEmail = true
|
||||
})
|
||||
for (const provider of options.providers) {
|
||||
if (provider.type === "credentials") hasCredentials = true
|
||||
else if (provider.type === "email") hasEmail = true
|
||||
else if (provider.id === "twitter") hasTwitterProvider = true
|
||||
}
|
||||
|
||||
if (hasCredentials) {
|
||||
const dbStrategy = options.session?.strategy === "database"
|
||||
@@ -75,4 +79,9 @@ export function assertConfig(
|
||||
if (hasEmail && !options.adapter) {
|
||||
return new MissingAdapter("E-mail login requires an adapter.")
|
||||
}
|
||||
|
||||
if (!twitterWarned && hasTwitterProvider) {
|
||||
twitterWarned = true
|
||||
return "TWITTER_OAUTH_2_BETA"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import { Theme } from "../.."
|
||||
import { InternalUrl } from "../../lib/parse-url"
|
||||
|
||||
/**
|
||||
* The following errors are passed as error query parameters to the default or overridden error page.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/pages#error-page) */
|
||||
export type ErrorType =
|
||||
| "default"
|
||||
| "configuration"
|
||||
| "accessdenied"
|
||||
| "verification"
|
||||
|
||||
export interface ErrorProps {
|
||||
url?: InternalUrl
|
||||
theme?: Theme
|
||||
error?: string
|
||||
error?: ErrorType
|
||||
}
|
||||
|
||||
interface ErrorView {
|
||||
@@ -14,12 +24,6 @@ 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
|
||||
@@ -87,15 +91,17 @@ export default function ErrorPage(props: ErrorProps) {
|
||||
status,
|
||||
html: (
|
||||
<div className="error">
|
||||
{ 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>{heading}</h1>
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
import { Theme } from "../.."
|
||||
import { InternalProvider } from "../../lib/types"
|
||||
|
||||
/**
|
||||
* The following errors are passed as error query parameters to the default or overridden sign-in page.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/pages#sign-in-page) */
|
||||
export type SignInErrorTypes =
|
||||
| "Signin"
|
||||
| "OAuthSignin"
|
||||
| "OAuthCallback"
|
||||
| "OAuthCreateAccount"
|
||||
| "EmailCreateAccount"
|
||||
| "Callback"
|
||||
| "OAuthAccountNotLinked"
|
||||
| "EmailSignin"
|
||||
| "CredentialsSignin"
|
||||
| "SessionRequired"
|
||||
| "default"
|
||||
|
||||
export interface SignInServerPageParams {
|
||||
csrfToken: string
|
||||
providers: InternalProvider[]
|
||||
callbackUrl: string
|
||||
email: string
|
||||
error: string
|
||||
error: SignInErrorTypes
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
@@ -39,7 +56,7 @@ export default function SigninPage(props: SignInServerPageParams) {
|
||||
)
|
||||
}
|
||||
|
||||
const errors: Record<string, string> = {
|
||||
const errors: Record<SignInErrorTypes, string> = {
|
||||
Signin: "Try signing in with a different account.",
|
||||
OAuthSignin: "Try signing in with a different account.",
|
||||
OAuthCallback: "Try signing in with a different account.",
|
||||
@@ -59,16 +76,17 @@ export default function SigninPage(props: SignInServerPageParams) {
|
||||
|
||||
return (
|
||||
<div className="signin">
|
||||
|
||||
{ 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">
|
||||
{error && (
|
||||
|
||||
@@ -70,7 +70,7 @@ export default async function signin(params: {
|
||||
return {
|
||||
redirect: `${url}/error?${new URLSearchParams({
|
||||
error: error as string,
|
||||
})}}`,
|
||||
})}`,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,10 @@ export interface NextAuthOptions {
|
||||
providers: Provider[]
|
||||
/**
|
||||
* A random string used to hash tokens, sign cookies and generate cryptographic keys.
|
||||
* If not specified is uses a hash of all configuration options, including Client ID / Secrets for entropy.
|
||||
* The default behavior is volatile, and **it is strongly recommended** you explicitly specify a value
|
||||
* to avoid invalidating end user sessions when configuration changes are deployed.
|
||||
* If not specified, it falls back to `jwt.secret` or `NEXTAUTH_SECRET` from environment vairables.
|
||||
* Otherwise it will use a hash of all configuration options, including Client ID / Secrets for entropy.
|
||||
*
|
||||
* NOTE: The last behavior is extrmely volatile, and will throw an error in production.
|
||||
* * **Default value**: `string` (SHA hash of the "options" object)
|
||||
* * **Required**: No - **but strongly recommended**!
|
||||
*
|
||||
|
||||
@@ -13,11 +13,8 @@ const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60 // 30 days
|
||||
const now = () => (Date.now() / 1000) | 0
|
||||
|
||||
/** Issues a JWT. By default, the JWT is encrypted using "A256GCM". */
|
||||
export async function encode({
|
||||
token = {},
|
||||
secret,
|
||||
maxAge = DEFAULT_MAX_AGE,
|
||||
}: JWTEncodeParams) {
|
||||
export async function encode(params: JWTEncodeParams) {
|
||||
const { token = {}, secret, maxAge = DEFAULT_MAX_AGE } = params
|
||||
const encryptionSecret = await getDerivedEncryptionKey(secret)
|
||||
return await new EncryptJWT(token)
|
||||
.setProtectedHeader({ alg: "dir", enc: "A256GCM" })
|
||||
@@ -28,10 +25,8 @@ export async function encode({
|
||||
}
|
||||
|
||||
/** Decodes a NextAuth.js issued JWT. */
|
||||
export async function decode({
|
||||
token,
|
||||
secret,
|
||||
}: JWTDecodeParams): Promise<JWT | null> {
|
||||
export async function decode(params: JWTDecodeParams): Promise<JWT | null> {
|
||||
const { token, secret } = params
|
||||
if (!token) return null
|
||||
const encryptionSecret = await getDerivedEncryptionKey(secret)
|
||||
const { payload } = await jwtDecrypt(token, encryptionSecret, {
|
||||
@@ -42,7 +37,7 @@ export async function decode({
|
||||
|
||||
export interface GetTokenParams<R extends boolean = false> {
|
||||
/** The request containing the JWT either in the cookies or in the `Authorization` header. */
|
||||
req: NextApiRequest
|
||||
req: NextApiRequest | Pick<NextApiRequest, "cookies" | "headers">
|
||||
/**
|
||||
* Use secure prefix for cookie name, unless URL in `NEXTAUTH_URL` is http://
|
||||
* or not set (e.g. development or test instance) case use unprefixed name
|
||||
@@ -55,7 +50,11 @@ export interface GetTokenParams<R extends boolean = false> {
|
||||
* @default false
|
||||
*/
|
||||
raw?: R
|
||||
secret: string
|
||||
/**
|
||||
* The same `secret` used in the `NextAuth` configuration.
|
||||
* Defaults to the `NEXTAUTH_SECRET` environment variable.
|
||||
*/
|
||||
secret?: string
|
||||
decode?: JWTOptions["decode"]
|
||||
logger?: LoggerInstance | Console
|
||||
}
|
||||
@@ -78,6 +77,7 @@ export async function getToken<R extends boolean = false>(
|
||||
raw,
|
||||
decode: _decode = decode,
|
||||
logger = console,
|
||||
secret = process.env.NEXTAUTH_SECRET,
|
||||
} = params ?? {}
|
||||
|
||||
if (!req) throw new Error("Must pass `req` to JWT getToken()")
|
||||
@@ -103,7 +103,7 @@ export async function getToken<R extends boolean = false>(
|
||||
|
||||
try {
|
||||
// @ts-expect-error
|
||||
return await _decode({ token, ...params })
|
||||
return await _decode({ token, secret })
|
||||
} catch {
|
||||
// @ts-expect-error
|
||||
return null
|
||||
|
||||
@@ -34,7 +34,11 @@ export interface JWTDecodeParams {
|
||||
}
|
||||
|
||||
export interface JWTOptions {
|
||||
/** The secret used to encode/decode the NextAuth.js issued JWT. */
|
||||
/**
|
||||
* The secret used to encode/decode the NextAuth.js issued JWT.
|
||||
* @deprecated Set the `NEXTAUTH_SECRET` environment vairable or
|
||||
* use the top-level `secret` option instead
|
||||
*/
|
||||
secret: string
|
||||
/**
|
||||
* The maximum age of the NextAuth.js issued JWT in seconds.
|
||||
|
||||
@@ -19,7 +19,7 @@ function hasErrorProperty(
|
||||
return !!(x as any)?.error
|
||||
}
|
||||
|
||||
export type WarningCode = "NEXTAUTH_URL" | "NO_SECRET"
|
||||
export type WarningCode = "NEXTAUTH_URL" | "NO_SECRET" | "TWITTER_OAUTH_2_BETA"
|
||||
|
||||
/**
|
||||
* Override any of the methods, and the rest will use the default logger.
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface InternalUrl {
|
||||
toString: () => string
|
||||
}
|
||||
|
||||
/** Returns an `URL` like object to make requests/redirects from server-side */
|
||||
export default function parseUrl(url?: string): InternalUrl {
|
||||
const defaultUrl = new URL("http://localhost:3000/api/auth")
|
||||
|
||||
|
||||
2
src/middleware.ts
Normal file
2
src/middleware.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from "./next/middleware"
|
||||
export * from "./next/middleware"
|
||||
@@ -19,6 +19,10 @@ async function NextAuthNextHandler(
|
||||
options: NextAuthOptions
|
||||
) {
|
||||
const { nextauth, ...query } = req.query
|
||||
|
||||
options.secret =
|
||||
options.secret ?? options.jwt?.secret ?? process.env.NEXTAUTH_SECRET
|
||||
|
||||
const handler = await NextAuthHandler({
|
||||
req: {
|
||||
host: detectHost(req.headers["x-forwarded-host"]),
|
||||
|
||||
143
src/next/middleware.ts
Normal file
143
src/next/middleware.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import type { NextMiddleware, NextFetchEvent } from "next/server"
|
||||
import type { Awaitable, NextAuthOptions } from ".."
|
||||
import type { JWT } from "../jwt"
|
||||
|
||||
import { NextResponse, NextRequest } from "next/server"
|
||||
|
||||
import { getToken } from "../jwt"
|
||||
import parseUrl from "../lib/parse-url"
|
||||
|
||||
type AuthorizedCallback = (params: {
|
||||
token: JWT | null
|
||||
req: NextRequest
|
||||
}) => Awaitable<boolean>
|
||||
|
||||
export interface NextAuthMiddlewareOptions {
|
||||
/**
|
||||
* Where to redirect the user in case of an error if they weren't logged in.
|
||||
* Similar to `pages` in `NextAuth`.
|
||||
*
|
||||
* ---
|
||||
* [Documentation](https://next-auth.js.org/configuration/pages)
|
||||
*/
|
||||
pages?: NextAuthOptions["pages"]
|
||||
callbacks?: {
|
||||
/**
|
||||
* Callback that receives the user's JWT payload
|
||||
* and returns `true` to allow the user to continue.
|
||||
*
|
||||
* This is similar to the `signIn` callback in `NextAuthOptions`.
|
||||
*
|
||||
* If it returns `false`, the user is redirected to the sign-in page instead
|
||||
*
|
||||
* The default is to let the user continue if they have a valid JWT (basic authentication).
|
||||
*
|
||||
* How to restrict a page and all of it's subpages for admins-only:
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* // `pages/admin/_middleware.js`
|
||||
* import { withAuth } from "next-auth/middleware"
|
||||
*
|
||||
* export default withAuth({
|
||||
* callbacks: {
|
||||
* authorized: ({ token }) => token?.user.isAdmin
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* ---
|
||||
* [Documentation](https://next-auth.js.org/getting-started/nextjs/middleware#api) | [`signIn` callback](configuration/callbacks#sign-in-callback)
|
||||
*/
|
||||
authorized?: AuthorizedCallback
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMiddleware(
|
||||
req: NextRequest,
|
||||
options: NextAuthMiddlewareOptions | undefined,
|
||||
onSuccess?: (token: JWT | null) => Promise<any>
|
||||
) {
|
||||
const signInPage = options?.pages?.signIn ?? "/api/auth/signin"
|
||||
const errorPage = options?.pages?.error ?? "/api/auth/error"
|
||||
const basePath = parseUrl(process.env.NEXTAUTH_URL).path
|
||||
// Avoid infinite redirect loop
|
||||
if (
|
||||
req.nextUrl.pathname.startsWith(basePath) ||
|
||||
[signInPage, errorPage].includes(req.nextUrl.pathname)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!process.env.NEXTAUTH_SECRET) {
|
||||
console.error(
|
||||
`[next-auth][error][NO_SECRET]`,
|
||||
`\nhttps://next-auth.js.org/errors#no_secret`
|
||||
)
|
||||
|
||||
const errorUrl = new URL(errorPage, req.nextUrl.origin)
|
||||
errorUrl.searchParams.append("error", "Configuration")
|
||||
|
||||
return NextResponse.redirect(errorUrl)
|
||||
}
|
||||
|
||||
const token = await getToken({ req: req as any })
|
||||
|
||||
const isAuthorized =
|
||||
(await options?.callbacks?.authorized?.({ req, token })) ?? !!token
|
||||
|
||||
// the user is authorized, let the middleware handle the rest
|
||||
if (isAuthorized) return await onSuccess?.(token)
|
||||
|
||||
// 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.url)
|
||||
return NextResponse.redirect(signInUrl)
|
||||
}
|
||||
|
||||
export type WithAuthArgs =
|
||||
| [NextRequest]
|
||||
| [NextRequest, NextFetchEvent]
|
||||
| [NextRequest, NextAuthMiddlewareOptions]
|
||||
| [NextMiddleware]
|
||||
| [NextMiddleware, NextAuthMiddlewareOptions]
|
||||
| [NextAuthMiddlewareOptions]
|
||||
| []
|
||||
|
||||
/**
|
||||
* Middleware that checks if the user is authenticated/authorized.
|
||||
* If if they aren't, they will be redirected to the login page.
|
||||
* Otherwise, continue.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* // `pages/_middleware.js`
|
||||
* export { default } from "next-auth/middleware"
|
||||
* ```
|
||||
*
|
||||
* ---
|
||||
* [Documentation](https://next-auth.js.org/getting-started/middleware)
|
||||
*/
|
||||
export function withAuth(...args: WithAuthArgs) {
|
||||
if (!args.length || args[0] instanceof NextRequest) {
|
||||
// @ts-expect-error
|
||||
return handleMiddleware(...args)
|
||||
}
|
||||
|
||||
if (typeof args[0] === "function") {
|
||||
const middleware = args[0]
|
||||
const options = args[1] as NextAuthMiddlewareOptions | undefined
|
||||
return async (...args: Parameters<NextMiddleware>) =>
|
||||
await handleMiddleware(args[0], options, async (token) => {
|
||||
;(args[0] as any).nextauth = { token }
|
||||
return await middleware(...args)
|
||||
})
|
||||
}
|
||||
|
||||
const options = args[0]
|
||||
return async (...args: Parameters<NextMiddleware>) =>
|
||||
await handleMiddleware(args[0], options)
|
||||
}
|
||||
|
||||
export default withAuth
|
||||
44
src/providers/authentik.ts
Normal file
44
src/providers/authentik.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface AuthentikProfile {
|
||||
iss: string,
|
||||
sub: string,
|
||||
aud: string,
|
||||
exp: number,
|
||||
iat: number,
|
||||
auth_time: number,
|
||||
acr: string,
|
||||
c_hash: string,
|
||||
nonce: string,
|
||||
at_hash: string,
|
||||
email: string,
|
||||
email_verified: boolean,
|
||||
name: string,
|
||||
given_name: string,
|
||||
family_name: string,
|
||||
preferred_username: string,
|
||||
nickname: string,
|
||||
groups: string[]
|
||||
}
|
||||
|
||||
export default function Authentik<
|
||||
P extends Record<string, any> = AuthentikProfile
|
||||
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
|
||||
return {
|
||||
id: "authentik",
|
||||
name: "Authentik",
|
||||
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
|
||||
type: "oauth",
|
||||
authorization: { params: { scope: "openid email profile" } },
|
||||
checks: ["pkce", "state"],
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name ?? profile.preferred_username,
|
||||
email: profile.email,
|
||||
image: profile.picture,
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -41,13 +41,23 @@ export default function AzureAD<P extends Record<string, any> = AzureADProfile>(
|
||||
},
|
||||
}
|
||||
)
|
||||
const pictureBuffer = await profilePicture.arrayBuffer()
|
||||
const pictureBase64 = Buffer.from(pictureBuffer).toString("base64")
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: `data:image/jpeg;base64, ${pictureBase64}`,
|
||||
|
||||
// Confirm that profile photo was returned
|
||||
if (profilePicture.ok) {
|
||||
const pictureBuffer = await profilePicture.arrayBuffer()
|
||||
const pictureBase64 = Buffer.from(pictureBuffer).toString("base64")
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: `data:image/jpeg;base64, ${pictureBase64}`,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
}
|
||||
}
|
||||
},
|
||||
options,
|
||||
|
||||
56
src/providers/trakt.ts
Normal file
56
src/providers/trakt.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface TraktUser {
|
||||
username: string
|
||||
private: boolean
|
||||
name: string
|
||||
vip: boolean
|
||||
vip_ep: boolean
|
||||
ids: { slug: string }
|
||||
joined_at: string
|
||||
location: string | null
|
||||
about: string | null
|
||||
gender: string | null
|
||||
age: number | null
|
||||
images: { avatar: { full: string } }
|
||||
}
|
||||
|
||||
export default function Trakt<
|
||||
P extends Record<string, any> = TraktUser
|
||||
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
|
||||
return {
|
||||
id: "trakt",
|
||||
name: "Trakt",
|
||||
type: "oauth",
|
||||
authorization: {
|
||||
url: "https://trakt.tv/oauth/authorize",
|
||||
params: { scope: "" }, // when default, trakt returns auth error
|
||||
},
|
||||
token: "https://api.trakt.tv/oauth/token",
|
||||
|
||||
userinfo: {
|
||||
async request(context) {
|
||||
const res = await fetch("https://api.trakt.tv/users/me?extended=full", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${context.tokens.access_token}`,
|
||||
"trakt-api-version": "2",
|
||||
"trakt-api-key": context.provider.clientId as string,
|
||||
},
|
||||
})
|
||||
|
||||
if (res.ok) return await res.json()
|
||||
|
||||
throw new Error("Expected 200 OK from the userinfo endpoint")
|
||||
},
|
||||
},
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.ids.slug,
|
||||
name: profile.name,
|
||||
email: null, // trakt does not provide user emails
|
||||
image: profile.images.avatar.full, // trakt does not allow hotlinking
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -166,15 +166,10 @@ export interface TwitterProfile {
|
||||
}
|
||||
}
|
||||
|
||||
let warned = false
|
||||
export default function Twitter<
|
||||
P extends Record<string, any> = TwitterLegacyProfile | TwitterProfile
|
||||
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
|
||||
if (!warned && options.version === "2.0") {
|
||||
warned = true
|
||||
console.warn(
|
||||
"Opted-in to Twitter OAuth 2.0. See the docs https://next-auth.js.org/providers/twitter#oauth-2"
|
||||
)
|
||||
if (options.version === "2.0") {
|
||||
return {
|
||||
id: "twitter",
|
||||
name: "Twitter",
|
||||
|
||||
@@ -379,16 +379,18 @@ export function SessionProvider(props: SessionProviderProps) {
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
const { refetchOnWindowFocus = true } = props
|
||||
// Listen for when the page is visible, if the user switches tabs
|
||||
// and makes our tab visible again, re-fetch the session.
|
||||
// and makes our tab visible again, re-fetch the session, but only if
|
||||
// this feature is not disabled.
|
||||
const visibilityHandler = () => {
|
||||
if (document.visibilityState === "visible")
|
||||
if (refetchOnWindowFocus && document.visibilityState === "visible")
|
||||
__NEXTAUTH._getSession({ event: "visibilitychange" })
|
||||
}
|
||||
document.addEventListener("visibilitychange", visibilityHandler, false)
|
||||
return () =>
|
||||
document.removeEventListener("visibilitychange", visibilityHandler, false)
|
||||
}, [])
|
||||
}, [props.refetchOnWindowFocus])
|
||||
|
||||
React.useEffect(() => {
|
||||
const { refetchInterval } = props
|
||||
|
||||
@@ -70,4 +70,9 @@ export interface SessionProviderProps {
|
||||
* If set to `0` (default), the session is not polled.
|
||||
*/
|
||||
refetchInterval?: number
|
||||
/**
|
||||
* `SessionProvider` automatically refetches the session when the user switches between windows.
|
||||
* This option activates this behaviour if set to `true` (default).
|
||||
*/
|
||||
refetchOnWindowFocus?: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user