Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react): make session requireable in useSession #2236

Merged
merged 26 commits into from
Jul 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1833110
feat(react): make session requireable in useSession
balazsorban44 Jun 23, 2021
9ec38bf
fix(ts): mirror types for new `useSession` API
balazsorban44 Jun 23, 2021
277f8ee
test(react): fix next-auth/react tests
balazsorban44 Jun 23, 2021
2979893
feat(react): expose `loading` on `useSession` for easier migration
balazsorban44 Jun 26, 2021
b4a8b42
Merge branch 'next' into feat/use-session-required
balazsorban44 Jun 26, 2021
5ece06e
test(ts): fix types
balazsorban44 Jun 26, 2021
2a085a2
test(react): fix useSession test
balazsorban44 Jun 26, 2021
0372e29
test(react): test `useSession` without `SessionProvider`
balazsorban44 Jun 26, 2021
59cacee
test(react): make long ExpectType multiline
balazsorban44 Jun 26, 2021
4f6a3da
Merge branch 'next' into feat/use-session-required
balazsorban44 Jun 29, 2021
12d6911
chore(app): update `useSession` calls in dev app
balazsorban44 Jun 29, 2021
43741ba
docs(react): update README.md
balazsorban44 Jun 29, 2021
c0eccec
test(react): correctly name destructured property
balazsorban44 Jun 29, 2021
d24147b
docs(react): update `useSession()` references
balazsorban44 Jun 29, 2021
61f8f0c
docs: tweak landing page
balazsorban44 Jun 29, 2021
4297c42
fix(react): correctly show loading when `required: true`
balazsorban44 Jun 29, 2021
c07fe79
docs(react): mention SessionRequired error
balazsorban44 Jun 29, 2021
630a8dc
docs(react): explain required sessions in `useSession`
balazsorban44 Jun 29, 2021
f530254
docs(react): query -> request
balazsorban44 Jun 29, 2021
8e762b4
Update www/docs/getting-started/client.md
balazsorban44 Jul 1, 2021
55fc8c0
Merge branch 'next' into feat/use-session-required
balazsorban44 Jul 2, 2021
27998ba
Apply suggestions from code review
balazsorban44 Jul 2, 2021
c353daa
feat(client): remove `loading`
ubbe-xyz Jul 4, 2021
6e88675
refactor: pair review suggestions
ubbe-xyz Jul 4, 2021
05f6955
refactor(client): auth fail handler
ubbe-xyz Jul 5, 2021
40f9b25
Merge branch 'next' into feat/use-session-required
balazsorban44 Jul 5, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,15 @@ NextAuth.js can be used with or without a database.

### Secure by default

- Promotes the use of passwordless sign in mechanisms
- Designed to be secure by default and encourage best practice for safeguarding user data
- Uses Cross Site Request Forgery Tokens on POST routes (sign in, sign out)
- Promotes the use of passwordless sign-in mechanisms
- Designed to be secure by default and encourage best practices for safeguarding user data
- Uses Cross-Site Request Forgery (CSRF) Tokens on POST routes (sign in, sign out)
- Default cookie policy aims for the most restrictive policy appropriate for each cookie
- When JSON Web Tokens are enabled, they are signed by default (JWS) with HS512
- Use JWT encryption (JWE) by setting the option `encryption: true` (defaults to A256GCM)
- Auto-generates symmetric signing and encryption keys for developer convenience
- Features tab/window syncing and keepalive messages to support short lived sessions
- Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org/)
- Features tab/window syncing and session polling to support short lived sessions
- Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org)

Advanced options allow you to define your own routines to handle controlling what accounts are allowed to sign in, for encoding and decoding JSON Web Tokens and to set custom cookie security policies and session properties, so you can control who is able to sign in and how often sessions have to be re-validated.

Expand All @@ -90,6 +90,7 @@ The package at `@types/next-auth` is now deprecated.
### Add API Route

```javascript
// pages/api/auth/[...nextauth].js
import NextAuth from "next-auth"
import Providers from "next-auth/providers"

Expand All @@ -113,13 +114,15 @@ export default NextAuth({
})
```

### Add React Component
### Add React Hook

The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.

```javascript
import { useSession, signIn, signOut } from "next-auth/react"

export default function Component() {
const [session, loading] = useSession()
const { data: session } = useSession()
if (session) {
return (
<>
Expand All @@ -137,7 +140,26 @@ export default function Component() {
}
```

## Acknowledgements
### Share/configure session state

Use the `<SessionProvider>` to allows instances of `useSession()` to share the session object across components. It also takes care of keeping the session updated and synced between tabs/windows.

```jsx title="pages/_app.js"
import { SessionProvider } from "next-auth/react"

export default function App({
Component,
pageProps: { session, ...pageProps }
}) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
)
}
```

## Acknowledgments

[NextAuth.js is made possible thanks to all of its contributors.](https://next-auth.js.org/contributors)

Expand Down
4 changes: 2 additions & 2 deletions app/components/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import styles from "./header.module.css"
// component that works on pages which support both client and server side
// rendering, and avoids any flash incorrect content on initial page load.
export default function Header() {
const [session, loading] = useSession()
const { data: session, status } = useSession()

return (
<header>
Expand All @@ -16,7 +16,7 @@ export default function Header() {
<div className={styles.signedInStatus}>
<p
className={`nojs-show ${
!session && loading ? styles.loading : styles.loaded
!session && status === "loading" ? styles.loading : styles.loaded
}`}
>
{!session && (
Expand Down
21 changes: 1 addition & 20 deletions app/pages/_app.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,12 @@
import { SessionProvider } from "next-auth/react"
import "./styles.css"

// Use the <SessionProvider> to improve performance and allow components that call
// `useSession()` anywhere in your application to access the `session` object.
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
return (
<SessionProvider
// SessionProvider options are not required but can be useful in situations where
// you have a short session maxAge time. Shown here with default values.
// Client Max Age controls how often the useSession in the client should
// contact the server to sync the session state. Value in seconds.
// e.g.
// * 0 - Disabled (always use cache value)
// * 60 - Sync session state with server if it's older than 60 seconds
staleTime={0}
// Keep Alive tells windows / tabs that are signed in to keep sending
// a keep alive request (which extends the current session expiry) to
// prevent sessions in open windows from expiring. Value in seconds.
//
// Note: If a session has expired when keep alive is triggered, all open
// windows / tabs will be updated to reflect the user is signed out.
refetchInterval={0}
session={session}
>
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
)
Expand Down
2 changes: 1 addition & 1 deletion app/pages/credentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function Page() {
setResponse(response)
}

const [session] = useSession()
const { data: session } = useSession()

if (session) {
return (
Expand Down
2 changes: 1 addition & 1 deletion app/pages/email.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default function Page() {
setResponse(response)
}

const [session] = useSession()
const { data: session } = useSession()

if (session) {
return (
Expand Down
20 changes: 6 additions & 14 deletions app/pages/protected.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { useState, useEffect } from "react"
import { useSession } from "next-auth/react"
import Layout from "../components/layout"
import AccessDenied from "../components/access-denied"

export default function Page() {
const [session, loading] = useSession()
const { status } = useSession({
required: true,
})
const [content, setContent] = useState()

// Fetch content from protected route
useEffect(() => {
if (status === "loading") return
const fetchData = async () => {
const res = await fetch("/api/examples/protected")
const json = await res.json()
Expand All @@ -17,19 +19,9 @@ export default function Page() {
}
}
fetchData()
}, [session])
}, [status])

// When rendering client side don't display anything until loading is complete
if (typeof window !== "undefined" && loading) return null

// If no session exists, display access denied message
if (!session) {
return (
<Layout>
<AccessDenied />
</Layout>
)
}
if (status === "loading") return <Layout>Loading...</Layout>

// If session exists, display content
return (
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"prepublishOnly": "npm run build",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"version:pr": "node ./config/version-pr"
"version:pr": "node ./config/version-pr",
"website": "cd www && npm run start"
},
"files": [
"dist",
Expand Down
30 changes: 22 additions & 8 deletions src/client/__tests__/client-provider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ afterAll(() => {
server.close()
})

test("it won't allow to fetch the session in isolation without a session context", () => {
function App() {
useSession()
return null
}

jest.spyOn(console, "error")
console.error.mockImplementation(() => {})

expect(() => render(<App />)).toThrow(
"useSession must be wrapped in a SessionProvider"
)
balazsorban44 marked this conversation as resolved.
Show resolved Hide resolved

console.error.mockRestore()
})

test("fetches the session once and re-uses it for different consumers", async () => {
const sessionRouteCall = jest.fn()

Expand Down Expand Up @@ -66,21 +82,19 @@ test("when there's an existing session, it won't initialize as loading", async (

function ProviderFlow({ options = {} }) {
return (
<>
<SessionProvider {...options}>
<SessionConsumer />
<SessionConsumer testId="2" />
</SessionProvider>
</>
<SessionProvider {...options}>
<SessionConsumer />
<SessionConsumer testId="2" />
</SessionProvider>
)
}

function SessionConsumer({ testId = 1 }) {
const [session, loading] = useSession()
const { data: session, status } = useSession()

return (
<div data-testid={`session-consumer-${testId}`}>
{loading ? "loading" : JSON.stringify(session)}
{status === "loading" ? "loading" : JSON.stringify(session)}
</div>
)
}
41 changes: 38 additions & 3 deletions src/client/react.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,33 @@ const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
/** @type {import("types/internals/react").SessionContext} */
const SessionContext = React.createContext()

export function useSession() {
return React.useContext(SessionContext)
export function useSession(options = {}) {
const value = React.useContext(SessionContext)
balazsorban44 marked this conversation as resolved.
Show resolved Hide resolved

if (process.env.NODE_ENV !== "production" && !value) {
throw new Error("useSession must be wrapped in a SessionProvider")
}

const { required, onUnauthenticated } = options

const requiredAndNotLoading = required && value.status === "unauthenticated"
balazsorban44 marked this conversation as resolved.
Show resolved Hide resolved

React.useEffect(() => {
if (requiredAndNotLoading) {
const url = `/api/auth/signin?${new URLSearchParams({
error: "SessionRequired",
callbackUrl: window.location.href,
})}`
if (onUnauthenticated) onUnauthenticated()
else window.location.replace(url)
}
}, [requiredAndNotLoading, onUnauthenticated])

if (requiredAndNotLoading) {
return { data: value.data, status: "loading" }
}

return value
}

export async function getSession(ctx) {
Expand Down Expand Up @@ -269,7 +294,17 @@ export function SessionProvider(props) {
}
}, [props.refetchInterval])

const value = React.useMemo(() => [session, loading], [session, loading])
const value = React.useMemo(
() => ({
data: session,
status: loading
? "loading"
: session
? "authenticated"
: "unauthenticated",
}),
[session, loading]
)

return (
<SessionContext.Provider value={value}>{children}</SessionContext.Provider>
Expand Down
1 change: 1 addition & 0 deletions src/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ async function NextAuthHandler(req, res, userOptions) {
"OAuthAccountNotLinked",
"EmailSignin",
"CredentialsSignin",
"SessionRequired",
].includes(error)
) {
return res.redirect(`${baseUrl}${basePath}/signin?error=${error}`)
Expand Down
1 change: 1 addition & 0 deletions src/server/pages/signin.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default function signin({
EmailSignin: "Check your email inbox.",
CredentialsSignin:
"Sign in failed. Check the details you provided are correct.",
SessionRequired: "Please sign in to access this page.",
default: "Unable to sign in.",
}

Expand Down
10 changes: 9 additions & 1 deletion types/internals/react.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,12 @@ export interface NextAuthConfig {
_getSession: any
}

export type SessionContext = React.Context<Session>
export type SessionContextValue<R extends boolean = false> = R extends true
?
| { data: Session; status: "authenticated" }
| { data: null; status: "loading" }
:
| { data: Session; status: "authenticated" }
| { data: null; status: "unauthenticated" | "loading" }

export type SessionContext = React.Context<SessionContextValue>
18 changes: 10 additions & 8 deletions types/react-client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from "react"
import { IncomingMessage } from "http"
import { Session } from "."
import { ProviderType } from "./providers"
import { SessionContextValue } from "internals/react"

export interface CtxOrReq {
req?: IncomingMessage
Expand All @@ -17,21 +18,22 @@ export type GetSessionOptions = CtxOrReq & {
triggerEvent?: boolean
}

export interface UseSessionOptions<R extends boolean> {
required: R
/** Defaults to `signIn` */
action?(): void
}

/**
* 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(): [Session | null, boolean]
export function useSession<R extends boolean>(
options?: UseSessionOptions<R>
): SessionContextValue<R>

/**
* Can be called client or server side to return a session asynchronously.
* It calls `/api/auth/session` and returns a promise with a session object,
* or null if no session exists.
*
* [Documentation](https://next-auth.js.org/getting-started/client#getsession)
*/
export function getSession(options?: GetSessionOptions): Promise<Session | null>

/*******************
Expand Down
18 changes: 17 additions & 1 deletion types/tests/react.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,25 @@ const clientSession = {
expires: "1234",
}

// $ExpectType [Session | null, boolean]
/**
* $ExpectType
* | { data: Session; status: "authenticated"; }
* | { data: null; status: "unauthenticated" | "loading"; }
* | { //// data: Session; status: "authenticated"; }
* | { data: null; status: "loading"; }
*/
client.useSession()

// $ExpectType { data: Session; status: "authenticated"; } | { data: null; status: "loading"; }
const session = client.useSession({ required: true })
if (session.status === "loading") {
// $ExpectType null
session.data
} else {
// $ExpectType Session
session.data
}
balazsorban44 marked this conversation as resolved.
Show resolved Hide resolved

// $ExpectType Promise<Session | null>
client.getSession({ req: nextReq })

Expand Down
Loading