/** * * NextAuth.js is the official integration of Auth.js for Next.js applications. It supports both * [Client Components](https://nextjs.org/docs/app/building-your-application/rendering/client-components) and the * [Pages Router](https://nextjs.org/docs/pages). It includes methods for signing in, signing out, hooks, and a React * Context provider to wrap your application and make session data available anywhere. * * For use in [Server Actions](https://nextjs.org/docs/app/api-reference/functions/server-actions), check out [these methods](https://authjs.dev/guides/upgrade-to-v5#methods) * * @module react */ "use client"; import { jsx as _jsx } from "react/jsx-runtime"; import * as React from "react"; import { apiBaseUrl, ClientSessionError, fetchData, now, parseUrl, useOnline, } from "./lib/client.js"; // This behaviour mirrors the default behaviour for getting the site name that // happens server side in server/index.js // 1. An empty value is legitimate when the code is being invoked client side as // relative URLs are valid in that context and so defaults to empty. // 2. When invoked server side the value is picked up from an environment // variable and defaults to 'http://localhost:3000'. export const __NEXTAUTH = { baseUrl: parseUrl(process.env.NEXTAUTH_URL ?? process.env.VERCEL_URL).origin, basePath: parseUrl(process.env.NEXTAUTH_URL).path, baseUrlServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL ?? process.env.NEXTAUTH_URL ?? process.env.VERCEL_URL).origin, basePathServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL ?? process.env.NEXTAUTH_URL).path, _lastSync: 0, _session: undefined, _getSession: () => { }, }; // https://github.com/nextauthjs/next-auth/pull/10762 let broadcastChannel = null; function getNewBroadcastChannel() { if (typeof BroadcastChannel === "undefined") { return { postMessage: () => { }, addEventListener: () => { }, removeEventListener: () => { }, name: "next-auth", onmessage: null, onmessageerror: null, close: () => { }, dispatchEvent: () => false, }; } return new BroadcastChannel("next-auth"); } function broadcast() { if (broadcastChannel === null) { broadcastChannel = getNewBroadcastChannel(); } return broadcastChannel; } // TODO: const logger = { debug: console.debug, error: console.error, warn: console.warn, }; export const SessionContext = React.createContext?.(undefined); /** * React Hook that gives you access to the logged in user's session data and lets you modify it. * * :::info * `useSession` is for client-side use only and when using [Next.js App Router (`app/`)](https://nextjs.org/blog/next-13-4#nextjs-app-router) you should prefer the `auth()` export. * ::: */ export function useSession(options) { if (!SessionContext) { throw new Error("React Context is unavailable in Server Components"); } // @ts-expect-error Satisfy TS if branch on line below const value = React.useContext(SessionContext); if (!value && process.env.NODE_ENV !== "production") { throw new Error("[next-auth]: `useSession` must be wrapped in a "); } const { required, onUnauthenticated } = options ?? {}; const requiredAndNotLoading = required && value.status === "unauthenticated"; React.useEffect(() => { if (requiredAndNotLoading) { const url = `${__NEXTAUTH.basePath}/signin?${new URLSearchParams({ error: "SessionRequired", callbackUrl: window.location.href, })}`; if (onUnauthenticated) onUnauthenticated(); else window.location.href = url; } }, [requiredAndNotLoading, onUnauthenticated]); if (requiredAndNotLoading) { return { data: value.data, update: value.update, status: "loading", }; } return value; } export async function getSession(params) { const session = await fetchData("session", __NEXTAUTH, logger, params); if (params?.broadcast ?? true) { // https://github.com/nextauthjs/next-auth/pull/11470 getNewBroadcastChannel().postMessage({ event: "session", data: { trigger: "getSession" }, }); } return session; } /** * Returns the current Cross-Site Request Forgery Token (CSRF Token) * required to make requests that changes state. (e.g. signing in or out, or updating the session). * * [CSRF Prevention: Double Submit Cookie](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie) */ export async function getCsrfToken() { const response = await fetchData("csrf", __NEXTAUTH, logger); return response?.csrfToken ?? ""; } export async function getProviders() { return fetchData("providers", __NEXTAUTH, logger); } export async function signIn(provider, options, authorizationParams) { const { callbackUrl, ...rest } = options ?? {}; const { redirect = true, redirectTo = callbackUrl ?? window.location.href, ...signInParams } = rest; const baseUrl = apiBaseUrl(__NEXTAUTH); const providers = await getProviders(); if (!providers) { const url = `${baseUrl}/error`; window.location.href = url; return; // TODO: Return error if `redirect: false` } if (!provider || !providers[provider]) { const url = `${baseUrl}/signin?${new URLSearchParams({ callbackUrl: redirectTo, })}`; window.location.href = url; return; // TODO: Return error if `redirect: false` } const providerType = providers[provider].type; if (providerType === "webauthn") { // TODO: Add docs link with explanation throw new TypeError([ `Provider id "${provider}" refers to a WebAuthn provider.`, 'Please use `import { signIn } from "next-auth/webauthn"` instead.', ].join("\n")); } const signInUrl = `${baseUrl}/${providerType === "credentials" ? "callback" : "signin"}/${provider}`; const csrfToken = await getCsrfToken(); const res = await fetch(`${signInUrl}?${new URLSearchParams(authorizationParams)}`, { method: "post", headers: { "Content-Type": "application/x-www-form-urlencoded", "X-Auth-Return-Redirect": "1", }, body: new URLSearchParams({ ...signInParams, csrfToken, callbackUrl: redirectTo, }), }); const data = await res.json(); if (redirect) { const url = data.url ?? redirectTo; window.location.href = url; // If url contains a hash, the browser does not reload the page. We reload manually if (url.includes("#")) window.location.reload(); return; } const error = new URL(data.url).searchParams.get("error") ?? undefined; const code = new URL(data.url).searchParams.get("code") ?? undefined; if (res.ok) { await __NEXTAUTH._getSession({ event: "storage" }); } return { error, code, status: res.status, ok: res.ok, url: error ? null : data.url, }; } export async function signOut(options) { const { redirect = true, redirectTo = options?.callbackUrl ?? window.location.href, } = options ?? {}; const baseUrl = apiBaseUrl(__NEXTAUTH); const csrfToken = await getCsrfToken(); const res = await fetch(`${baseUrl}/signout`, { method: "post", headers: { "Content-Type": "application/x-www-form-urlencoded", "X-Auth-Return-Redirect": "1", }, body: new URLSearchParams({ csrfToken, callbackUrl: redirectTo }), }); const data = await res.json(); broadcast().postMessage({ event: "session", data: { trigger: "signout" } }); if (redirect) { const url = data.url ?? redirectTo; window.location.href = url; // If url contains a hash, the browser does not reload the page. We reload manually if (url.includes("#")) window.location.reload(); return; } await __NEXTAUTH._getSession({ event: "storage" }); return data; } /** * [React Context](https://react.dev/learn/passing-data-deeply-with-context) provider to wrap the app (`pages/`) to make session data available anywhere. * * When used, the session state is automatically synchronized across all open tabs/windows and they are all updated whenever they gain or lose focus * or the state changes (e.g. a user signs in or out) when {@link SessionProviderProps.refetchOnWindowFocus} is `true`. * * :::info * `SessionProvider` is for client-side use only and when using [Next.js App Router (`app/`)](https://nextjs.org/blog/next-13-4#nextjs-app-router) you should prefer the `auth()` export. * ::: */ export function SessionProvider(props) { if (!SessionContext) { throw new Error("React Context is unavailable in Server Components"); } const { children, basePath, refetchInterval, refetchWhenOffline } = props; if (basePath) __NEXTAUTH.basePath = basePath; /** * If session was `null`, there was an attempt to fetch it, * but it failed, but we still treat it as a valid initial value. */ const hasInitialSession = props.session !== undefined; /** If session was passed, initialize as already synced */ __NEXTAUTH._lastSync = hasInitialSession ? now() : 0; const [session, setSession] = React.useState(() => { if (hasInitialSession) __NEXTAUTH._session = props.session; return props.session; }); /** If session was passed, initialize as not loading */ const [loading, setLoading] = React.useState(!hasInitialSession); React.useEffect(() => { __NEXTAUTH._getSession = async ({ event } = {}) => { try { const storageEvent = event === "storage"; // We should always update if we don't have a client session yet // or if there are events from other tabs/windows if (storageEvent || __NEXTAUTH._session === undefined) { __NEXTAUTH._lastSync = now(); __NEXTAUTH._session = await getSession({ broadcast: !storageEvent, }); setSession(__NEXTAUTH._session); return; } if ( // If there is no time defined for when a session should be considered // stale, then it's okay to use the value we have until an event is // triggered which updates it !event || // If the client doesn't have a session then we don't need to call // the server to check if it does (if they have signed in via another // tab or window that will come through as a "stroage" event // event anyway) __NEXTAUTH._session === null || // Bail out early if the client session is not stale yet now() < __NEXTAUTH._lastSync) { return; } // An event or session staleness occurred, update the client session. __NEXTAUTH._lastSync = now(); __NEXTAUTH._session = await getSession(); setSession(__NEXTAUTH._session); } catch (error) { logger.error(new ClientSessionError(error.message, error)); } finally { setLoading(false); } }; __NEXTAUTH._getSession(); return () => { __NEXTAUTH._lastSync = 0; __NEXTAUTH._session = undefined; __NEXTAUTH._getSession = () => { }; }; }, []); React.useEffect(() => { const handle = () => __NEXTAUTH._getSession({ event: "storage" }); // Listen for storage events and update session if event fired from // another window (but suppress firing another event to avoid a loop) // Fetch new session data but tell it to not to fire another event to // avoid an infinite loop. // Note: We could pass session data through and do something like // `setData(message.data)` but that can cause problems depending // on how the session object is being used in the client; it is // more robust to have each window/tab fetch it's own copy of the // session object rather than share it across instances. broadcast().addEventListener("message", handle); return () => broadcast().removeEventListener("message", handle); }, []); 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, but only if // this feature is not disabled. const visibilityHandler = () => { if (refetchOnWindowFocus && document.visibilityState === "visible") __NEXTAUTH._getSession({ event: "visibilitychange" }); }; document.addEventListener("visibilitychange", visibilityHandler, false); return () => document.removeEventListener("visibilitychange", visibilityHandler, false); }, [props.refetchOnWindowFocus]); const isOnline = useOnline(); // TODO: Flip this behavior in next major version const shouldRefetch = refetchWhenOffline !== false || isOnline; React.useEffect(() => { if (refetchInterval && shouldRefetch) { const refetchIntervalTimer = setInterval(() => { if (__NEXTAUTH._session) { __NEXTAUTH._getSession({ event: "poll" }); } }, refetchInterval * 1000); return () => clearInterval(refetchIntervalTimer); } }, [refetchInterval, shouldRefetch]); const value = React.useMemo(() => ({ data: session, status: loading ? "loading" : session ? "authenticated" : "unauthenticated", async update(data) { if (loading) return; setLoading(true); const newSession = await fetchData("session", __NEXTAUTH, logger, typeof data === "undefined" ? undefined : { body: { csrfToken: await getCsrfToken(), data } }); setLoading(false); if (newSession) { setSession(newSession); broadcast().postMessage({ event: "session", data: { trigger: "getSession" }, }); } return newSession; }, }), [session, loading]); return ( // @ts-expect-error _jsx(SessionContext.Provider, { value: value, children: children })); }