// Note about signIn() and signOut() methods: // // On signIn() and signOut() we pass 'json: true' to request a response in JSON // instead of HTTP as redirect URLs on other domains are not returned to // requests made using the fetch API in the browser, and we need to ask the API // to return the response as a JSON object (the end point still defaults to // returning an HTTP response with a redirect for non-JavaScript clients). // // We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks. import * as React from "react" import _logger, { proxyLogger } from "../utils/logger" import parseUrl from "../utils/parse-url" import { Session } from ".." import { BroadcastChannel, CtxOrReq, apiBaseUrl, fetchData, now, AuthClientConfig, } from "../client/_utils" import type { ClientSafeProvider, LiteralUnion, SessionProviderProps, SignInAuthorizationParams, SignInOptions, SignInResponse, SignOutParams, SignOutResponse, UseSessionOptions, } from "./types" import type { BuiltInProviderType, RedirectableProviderType, } from "../providers" export * from "./types" // 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'. const __NEXTAUTH: AuthClientConfig = { 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: () => {}, } const broadcast = BroadcastChannel() const logger = proxyLogger(_logger, __NEXTAUTH.basePath) function useOnline() { const [isOnline, setIsOnline] = React.useState( typeof navigator !== "undefined" ? navigator.onLine : false ) const setOnline = () => setIsOnline(true) const setOffline = () => setIsOnline(false) React.useEffect(() => { window.addEventListener("online", setOnline) window.addEventListener("offline", setOffline) return () => { window.removeEventListener("online", setOnline) window.removeEventListener("offline", setOffline) } }, []) return isOnline } type UpdateSession = (data?: any) => Promise export type SessionContextValue = R extends true ? | { update: UpdateSession; data: Session; status: "authenticated" } | { update: UpdateSession; data: null; status: "loading" } : | { update: UpdateSession; data: Session; status: "authenticated" } | { update: UpdateSession data: null status: "unauthenticated" | "loading" } export const SessionContext = React.createContext?.< SessionContextValue | undefined >(undefined) /** * React Hook that gives you access * to the logged in user's session data. * * [Documentation](https://next-auth.js.org/getting-started/client#usesession) */ export function useSession( options?: UseSessionOptions ): SessionContextValue { if (!SessionContext) { throw new Error("React Context is unavailable in Server Components") } // @ts-expect-error Satisfy TS if branch on line below const value: SessionContextValue = 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 = `/api/auth/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 type GetSessionParams = CtxOrReq & { event?: "storage" | "timer" | "hidden" | string triggerEvent?: boolean broadcast?: boolean } export async function getSession(params?: GetSessionParams) { const session = await fetchData( "session", __NEXTAUTH, logger, params ) if (params?.broadcast ?? true) { broadcast.post({ event: "session", data: { trigger: "getSession" } }) } return session } /** * Returns the current Cross Site Request Forgery Token (CSRF Token) * required to make POST requests (e.g. for signing in and signing out). * You likely only need to use this if you are not using the built-in * `signIn()` and `signOut()` methods. * * [Documentation](https://next-auth.js.org/getting-started/client#getcsrftoken) */ export async function getCsrfToken(params?: CtxOrReq) { const response = await fetchData<{ csrfToken: string }>( "csrf", __NEXTAUTH, logger, params ) return response?.csrfToken } /** * It calls `/api/auth/providers` and returns * a list of the currently configured authentication providers. * It can be useful if you are creating a dynamic custom sign in page. * * [Documentation](https://next-auth.js.org/getting-started/client#getproviders) */ export async function getProviders() { return await fetchData< Record, ClientSafeProvider> >("providers", __NEXTAUTH, logger) } /** * Client-side method to initiate a signin flow * or send the user to the signin page listing all possible providers. * Automatically adds the CSRF token to the request. * * [Documentation](https://next-auth.js.org/getting-started/client#signin) */ export async function signIn< P extends RedirectableProviderType | undefined = undefined >( provider?: LiteralUnion< P extends RedirectableProviderType ? P | BuiltInProviderType : BuiltInProviderType >, options?: SignInOptions, authorizationParams?: SignInAuthorizationParams ): Promise< P extends RedirectableProviderType ? SignInResponse | undefined : undefined > { const { callbackUrl = window.location.href, redirect = true } = options ?? {} const baseUrl = apiBaseUrl(__NEXTAUTH) const providers = await getProviders() if (!providers) { window.location.href = `${baseUrl}/error` return } if (!provider || !(provider in providers)) { window.location.href = `${baseUrl}/signin?${new URLSearchParams({ callbackUrl, })}` return } const isCredentials = providers[provider].type === "credentials" const isEmail = providers[provider].type === "email" const isSupportingReturn = isCredentials || isEmail const signInUrl = `${baseUrl}/${ isCredentials ? "callback" : "signin" }/${provider}` const _signInUrl = `${signInUrl}${authorizationParams ? `?${new URLSearchParams(authorizationParams)}` : ""}` const res = await fetch(_signInUrl, { method: "post", headers: { "Content-Type": "application/x-www-form-urlencoded", }, // @ts-expect-error body: new URLSearchParams({ ...options, csrfToken: await getCsrfToken(), callbackUrl, json: true, }), }) const data = await res.json() // TODO: Do not redirect for Credentials and Email providers by default in next major if (redirect || !isSupportingReturn) { const url = data.url ?? callbackUrl 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") if (res.ok) { await __NEXTAUTH._getSession({ event: "storage" }) } return { error, status: res.status, ok: res.ok, url: error ? null : data.url, } as any } /** * Signs the user out, by removing the session cookie. * Automatically adds the CSRF token to the request. * * [Documentation](https://next-auth.js.org/getting-started/client#signout) */ export async function signOut( options?: SignOutParams ): Promise { const { callbackUrl = window.location.href } = options ?? {} const baseUrl = apiBaseUrl(__NEXTAUTH) const fetchOptions = { method: "post", headers: { "Content-Type": "application/x-www-form-urlencoded", }, // @ts-expect-error body: new URLSearchParams({ csrfToken: await getCsrfToken(), callbackUrl, json: true, }), } const res = await fetch(`${baseUrl}/signout`, fetchOptions) const data = await res.json() broadcast.post({ event: "session", data: { trigger: "signout" } }) if (options?.redirect ?? true) { const url = data.url ?? callbackUrl 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() // @ts-expect-error return } await __NEXTAUTH._getSession({ event: "storage" }) return data } /** * Provider to wrap the app in to make session data available globally. * Can also be used to throttle the number of requests to the endpoint * `/api/auth/session`. * * [Documentation](https://next-auth.js.org/getting-started/client#sessionprovider) */ export function SessionProvider(props: SessionProviderProps) { 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("CLIENT_SESSION_ERROR", error as Error) } finally { setLoading(false) } } __NEXTAUTH._getSession() return () => { __NEXTAUTH._lastSync = 0 __NEXTAUTH._session = undefined __NEXTAUTH._getSession = () => {} } }, []) React.useEffect(() => { // 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. const unsubscribe = broadcast.receive(() => __NEXTAUTH._getSession({ event: "storage" }) ) return () => unsubscribe() }, []) 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: any = React.useMemo( () => ({ data: session, status: loading ? "loading" : session ? "authenticated" : "unauthenticated", async update(data) { if (loading || !session) return setLoading(true) const newSession = await fetchData( "session", __NEXTAUTH, logger, { req: { body: { csrfToken: await getCsrfToken(), data } } } ) setLoading(false) if (newSession) { setSession(newSession) broadcast.post({ event: "session", data: { trigger: "getSession" } }) } return newSession }, }), [session, loading] ) return ( {children} ) }