Compare commits

...

27 Commits

Author SHA1 Message Date
Valentin Hervieu
e5535734f8 fix(client): set useSession loading state correctly (#1468)
This is fixing #1467.

The issue was due to doing the `setLoading(false)` in the finally:  as we can do an early return [here](a7e08e2a32/src/client/index.js (L100-L100)), we would still go to the finally and mark the session as being loaded.

I simply removed the `finally` block to only set the `loading` state to false when:
- the data is ready
- an error occures
2021-03-07 19:11:32 +01:00
Taehwan Noh
ba7aed1057 feat(provider): add Kakao provider (#1459) 2021-03-06 21:52:39 +01:00
Balázs Orbán
a7e08e2a32 fix: make sure useSession populates session correctly (#1462) 2021-03-06 20:02:43 +01:00
mcha
0d13040264 docs(client): fix client.md typos (#1453) 2021-03-06 10:50:29 +01:00
Balázs Orbán
582520f8ef chore: fix typo in feature request template 2021-03-06 00:35:09 +01:00
Sam Bauch
95942519a5 feat(provider): add Osso SAML provider (#1448)
Co-authored-by: @sbauch
2021-03-06 00:21:38 +01:00
Balázs Orbán
f3e64f04cc feat(client): introduce NEXTAUTH_URL_INTERNAL (#1449)
Co-authored-by: @gergelyke
2021-03-06 00:10:36 +01:00
Balázs Orbán
ed5cc4aa65 feat(provider): add Instagram provider (#1447)
Co-authored-by: @PolMrt pol@hey.com
2021-03-05 23:39:50 +01:00
Balázs Orbán
0e20b60229 docs(database): mention CockroachDB
Co-authored-by: @jukbot <jukbot@yellotalk.co>
2021-03-05 22:52:48 +01:00
Balázs Orbán
3aee24b5dc refactor: client improvements (#1428)
* docs(client): add TS definitions to client

* docs(client): add documentation links to public methods

* refactor(client): simplify window sync, simplify logic

* refactor(client): extract repeating logic to _fetchData

* refactor(client): remove clientId

* refactor(client): use session in Provider if passed
2021-03-05 22:47:32 +01:00
Baterka
960ca85907 fix: send only the error message in callback redirects (#1424)
Changed `encodeURIComponent(error)` to `encodeURIComponent(error.message)` to remove prefix (such as `Error: ` and possible stack trace).
Seems like better way of doing it and also safer if server throws some error with sensitive data.
2021-03-05 18:34:11 +01:00
Balázs Orbán
f960cc0f6f chore(docs): upgrade docs dependencies 2021-03-03 20:13:46 +01:00
Balázs Orbán
0f64f3eea7 chore: don't mark bugs as stale
Had a good laugh today 😄:

"This is not stale. Bread goes stale. Bugs don't. They don't just magically go away because time has passed" - Unknown
https://twitter.com/ericclemmons/status/1367000259046604803
2021-03-03 19:54:13 +01:00
yannicktian
71c78e8e24 feat(provider): allow disabling redirection on sign in with email (#1416)
* feat: allow to disable client-side redirect for email provider

* docs(client): mention that redirect can also be disabled for email provider

* feat: only display one email input in email page
2021-03-02 22:38:02 +01:00
dependabot[bot]
d86609a2dc chore(deps): bump prismjs from 1.22.0 to 1.23.0 in /www (#1409)
Bumps [prismjs](https://github.com/PrismJS/prism) from 1.22.0 to 1.23.0.
- [Release notes](https://github.com/PrismJS/prism/releases)
- [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PrismJS/prism/compare/v1.22.0...v1.23.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-01 22:52:46 +01:00
Joost van Wollingen
d0c3400d30 docs(page): Remove unnecessary context param (#1406)
...when calling providers in the docs
2021-03-01 21:39:03 +01:00
Praneeth
172e79cb04 fix(page): add character encoding and page titles (#1380)
* added character encoding fix

* changed multi-line to inline and added title param to send fn in src/server/pages/index.js

* modified the return object of renderPage in src/server/pages/index.js
2021-03-01 21:17:51 +01:00
Balázs Orbán
46d5c76605 docs: reword callbacks.md
Explain the `jwt()` callback before the `session()` callback, as it comes first in the flow.
2021-02-28 18:05:10 +01:00
Zach White
438efd8a9b docs: reword pages.md (#1386)
language edits
2021-02-27 23:43:45 +01:00
Balázs Orbán
d8d497cc91 feat(provider): call generateVerificationToken async (#1378) 2021-02-27 23:33:26 +01:00
Pop Stefan
6152c8afbb docs: added refresh token tutorial link in faq page (#1385) 2021-02-27 20:24:09 +01:00
Balázs Orbán
5ae6f6118c docs: add missing comma
Thx @followbl 😺
2021-02-25 23:29:33 +01:00
sid
96ff048b59 fix(provider): use correct file type for Discord profile img (#1365) 2021-02-23 21:39:27 +01:00
Ariel Weingarten
e80f6e936d docs(provider): Update twitch.md (#1353)
State what redirect URL to add to the Twitch console.
2021-02-22 20:04:38 +01:00
Lawrence Chen
6b5a215fb2 docs(tutorials): refresh token rotation (#1310)
* docs(tutorials): refresh token rotation

* use simple initialization

* be optimistic

Co-authored-by: Balázs Orbán <info@balazsorban.com>

* add yarn.lock to .gitignore

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-02-21 22:30:01 +01:00
Balázs Orbán
782482b9f4 feat: make tokens available in profile callback (#1329)
* feat: make access_token available in profile callback

* docs(provider): mention access_token param in profile callback

* feat: send all available tokens to provider.profile
2021-02-20 22:58:48 +01:00
Balázs Orbán
2d364f246a docs: tweak release badges 2021-02-17 19:14:45 +01:00
35 changed files with 913 additions and 266 deletions

View File

@@ -9,7 +9,7 @@ assignees: ''
A clear and concise description of the feature being proposed.
**Purpose of proposed feature**
A clear and concise description description of why this feature is necessary and what problems it solves.
A clear and concise description of why this feature is necessary and what problems it solves.
**Detail about proposed feature**
A detailed description of how the proposal might work (if you have one).

1
.github/stale.yml vendored
View File

@@ -7,6 +7,7 @@ exemptLabels:
- pinned
- security
- priority
- bug
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable

4
.gitignore vendored
View File

@@ -11,6 +11,8 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn.lock
# Dependencies
node_modules
@@ -37,4 +39,4 @@ www/providers.json
/_work
# Prisma migrations
/prisma/migrations
/prisma/migrations

View File

@@ -23,9 +23,9 @@
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth" alt="Github Stars" />
</a>
<a href="https://www.npmjs.com/package/next-auth">
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth" alt="Github Stable Release" />
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?label=latest" alt="Github Stable Release" />
</a>
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?include_prereleases" alt="Github Prelease" />
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?include_prereleases&label=prerelease&sort=semver" alt="Github Prelease" />
</p>
</p>

View File

@@ -100,6 +100,11 @@ export default function Header () {
<a>Credentials</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/email'>
<a>Email</a>
</Link>
</li>
</ul>
</nav>
</header>

View File

@@ -106,13 +106,15 @@
"next-env.d.ts"
],
"globals": [
"localStorage",
"location",
"fetch"
]
},
"funding": [
{
"type" : "github",
"url" : "https://github.com/sponsors/balazsorban44"
"type": "github",
"url": "https://github.com/sponsors/balazsorban44"
}
]
}

66
pages/email.js Normal file
View File

@@ -0,0 +1,66 @@
import * as React from 'react'
import { signIn, signOut, useSession } from 'next-auth/client'
import Layout from 'components/layout'
export default function Page () {
const [response, setResponse] = React.useState(null)
const [email, setEmail] = React.useState('')
const handleChange = (event) => {
setEmail(event.target.value)
}
const handleLogin = (options) => async (event) => {
event.preventDefault()
if (options.redirect) {
return signIn('email', options)
}
const response = await signIn('email', options)
setResponse(response)
}
const handleLogout = (options) => async (event) => {
if (options.redirect) {
return signOut(options)
}
const response = await signOut(options)
setResponse(response)
}
const [session] = useSession()
if (session) {
return (
<Layout>
<h1>Test different flows for Email logout</h1>
<span className='spacing'>Default:</span>
<button onClick={handleLogout({ redirect: true })}>Logout</button><br />
<span className='spacing'>No redirect:</span>
<button onClick={handleLogout({ redirect: false })}>Logout</button><br />
<p>Response:</p>
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
</Layout>
)
}
return (
<Layout>
<h1>Test different flows for Email login</h1>
<label className='spacing'>
Email address:{' '}
<input type='text' id='email' name='email' value={email} onChange={handleChange} />
</label><br />
<form onSubmit={handleLogin({ redirect: true, email })}>
<span className='spacing'>Default:</span>
<button type='submit'>Sign in with Email</button>
</form>
<form onSubmit={handleLogin({ redirect: false, email })}>
<span className='spacing'>No redirect:</span>
<button type='submit'>Sign in with Email</button>
</form>
<p>Response:</p>
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
</Layout>
)
}

103
src/client/index.d.ts vendored Normal file
View File

@@ -0,0 +1,103 @@
import * as React from 'react'
import { GetServerSidePropsContext } from 'next'
interface DefaultSession {
user: {
name: string | null
email: string | null
image: string | null
}
expires: Date | string
}
interface BroadcastMessage {
event?: 'session'
data?: {
trigger?: 'signout' | 'getSession'
}
clientId: string
timestamp: number
}
type GetSession<S extends Record<string, unknown> = DefaultSession> = (options: {
ctx?: GetServerSidePropsContext
req?: GetServerSidePropsContext['req']
event?: 'storage' | 'timer' | 'hidden' | string
triggerEvent?: boolean
}) => Promise<S>
export interface NextAuthConfig {
baseUrl: string
basePath: string
baseUrlServer: string
basePathServer: string
/** 0 means disabled (don't send); 60 means send every 60 seconds */
keepAlive: number
/** 0 means disabled (only use cache); 60 means sync if last checked > 60 seconds ago */
clientMaxAge: number
/** Used for timestamp since last sycned (in seconds) */
_clientLastSync: number
/** Stores timer for poll interval */
_clientSyncTimer: ReturnType<typeof setTimeout>
/** Tracks if event listeners have been added */
_eventListenersAdded: boolean
/** Stores last session response from hook */
_clientSession: DefaultSession | null | undefined
/** Used to store to function export by getSession() hook */
_getSession: any
}
export type GetCsrfToken = (
ctxOrReq: GetServerSidePropsContext & GetServerSidePropsContext['req']
) => Promise<string | null>
export interface SessionOptions {
baseUrl?: string
basePath?: string
clientMaxAge?: number
keepAlive?: number
}
export type Provider<S extends Record<string, unknown> = DefaultSession > = (options: {
children: React.ReactNode
session: S
options: SessionOptions
}) => React.ReactNode
export type SetOptions = (options: SessionOptions) => void
export type SessionContext = React.createContext<[DefaultSession | null, boolean]>
export type UseSession = () => [any, boolean]
export type GetProviders = () => Promise<any[]>
// Sign in types
export interface SignInOptions {
/** Defaults to the current URL. */
callbackUrl?: string
redirect?: boolean
}
export interface SignInResponse {
error: string | null
status: number
ok: boolean
url: string | null
}
export type SignIn<AuthorizationParams = Record<string, string>> = (
provider?: string,
options?: SignInOptions,
authorizationParams?: AuthorizationParams
) => SignInResponse
// Sign out types
interface SignOutResponse<RedirectType extends boolean=true> {
/** Defaults to the current URL. */
callbackUrl?: string
redirect?: RedirectType
}
export type SignOut<RedirectType extends boolean = true> = (params: SignOutResponse<RedirectType>) => RedirectType extends true ? Promise<{url?: string} | undefined> : undefined

View File

@@ -1,5 +1,3 @@
/// Note: fetch() is built in to Next.js 9.4
//
// Note about signIn() and signOut() methods:
//
// On signIn() and signOut() we pass 'json: true' to request a response in JSON
@@ -20,167 +18,81 @@ import parseUrl from '../lib/parse-url'
// 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'.
/** @type {import(".").NextAuthConfig} */
const __NEXTAUTH = {
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
keepAlive: 0, // 0 == disabled (don't send); 60 == send every 60 seconds
clientMaxAge: 0, // 0 == disabled (only use cache); 60 == sync if last checked > 60 seconds ago
baseUrlServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
basePathServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL).basePath,
keepAlive: 0,
clientMaxAge: 0,
// Properties starting with _ are used for tracking internal app state
_clientLastSync: 0, // used for timestamp since last sycned (in seconds)
_clientSyncTimer: null, // stores timer for poll interval
_eventListenersAdded: false, // tracks if event listeners have been added,
_clientSession: undefined, // stores last session response from hook,
// Generate a unique ID to make it possible to identify when a message
// was sent from this tab/window so it can be ignored to avoid event loops.
_clientId: Math.random().toString(36).substring(2) + Date.now().toString(36),
// Used to store to function export by getSession() hook
_clientLastSync: 0,
_clientSyncTimer: null,
_eventListenersAdded: false,
_clientSession: undefined,
_getSession: () => {}
}
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
const broadcast = BroadcastChannel()
// Add event listners on load
if (typeof window !== 'undefined') {
if (__NEXTAUTH._eventListenersAdded === false) {
__NEXTAUTH._eventListenersAdded = true
if (typeof window !== 'undefined' && !__NEXTAUTH._eventListenersAdded) {
__NEXTAUTH._eventListenersAdded = true
// 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.receive(() => __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)
window.addEventListener('storage', async (event) => {
if (event.key === 'nextauth.message') {
const message = JSON.parse(event.newValue)
if (message?.event === 'session' && message.data) {
// Ignore storage events fired from the same window that created them
if (__NEXTAUTH._clientId === message.clientId) {
return
}
// Fetch new session data but pass 'true' to it not to fire an 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.
await __NEXTAUTH._getSession({ event: 'storage' })
}
}
})
// Listen for document visibilitychange events
let hidden, visibilityChange
if (typeof document.hidden !== 'undefined') { // Opera 12.10 and Firefox 18 and later support
hidden = 'hidden'
visibilityChange = 'visibilitychange'
} else if (typeof document.msHidden !== 'undefined') {
hidden = 'msHidden'
visibilityChange = 'msvisibilitychange'
} else if (typeof document.webkitHidden !== 'undefined') {
hidden = 'webkitHidden'
visibilityChange = 'webkitvisibilitychange'
}
const handleVisibilityChange = () => !document[hidden] && __NEXTAUTH._getSession({ event: visibilityChange })
document.addEventListener('visibilitychange', handleVisibilityChange, false)
}
}
// Method to set options. The documented way is to use the provider, but this
// method is being left in as an alternative, that will be helpful if/when we
// expose a vanilla JavaScript version that doesn't depend on React.
const setOptions = ({
baseUrl,
basePath,
clientMaxAge,
keepAlive
} = {}) => {
if (baseUrl) { __NEXTAUTH.baseUrl = baseUrl }
if (basePath) { __NEXTAUTH.basePath = basePath }
if (clientMaxAge) { __NEXTAUTH.clientMaxAge = clientMaxAge }
if (keepAlive) {
__NEXTAUTH.keepAlive = keepAlive
if (typeof window !== 'undefined' && keepAlive > 0) {
// Clear existing timer (if there is one)
if (__NEXTAUTH._clientSyncTimer !== null) { clearTimeout(__NEXTAUTH._clientSyncTimer) }
// Set next timer to trigger in number of seconds
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
// Only invoke keepalive when a session exists
if (__NEXTAUTH._clientSession) {
await __NEXTAUTH._getSession({ event: 'timer' })
}
}, keepAlive * 1000)
}
}
}
// Universal method (client + server)
// If passed 'appContext' via getInitialProps() in _app.js then get the req
// object from ctx and use that for the req value to allow getSession() to
// work seemlessly in getInitialProps() on server side pages *and* in _app.js.
export async function getSession ({ ctx, req = ctx?.req, triggerEvent = true } = {}) {
const baseUrl = _apiBaseUrl()
const fetchOptions = req ? { headers: { cookie: req.headers.cookie } } : {}
const session = await _fetchData(`${baseUrl}/session`, fetchOptions)
if (triggerEvent) {
_sendMessage({ event: 'session', data: { trigger: 'getSession' } })
}
return session
}
// Universal method (client + server)
// If passed 'appContext' via getInitialProps() in _app.js then get the req
// object from ctx and use that for the req value to allow getCsrfToken() to
// work seemlessly in getInitialProps() on server side pages *and* in _app.js.
async function getCsrfToken ({ ctx, req = ctx?.req } = {}) {
const baseUrl = _apiBaseUrl()
const fetchOptions = req ? { headers: { cookie: req.headers.cookie } } : {}
const data = await _fetchData(`${baseUrl}/csrf`, fetchOptions)
return data && data.csrfToken ? data.csrfToken : null
}
// Universal method (client + server); does not require request headers
const getProviders = async () => {
const baseUrl = _apiBaseUrl()
return _fetchData(`${baseUrl}/providers`)
// Listen for document visibility change events and
// if visibility of the document changes, re-fetch the session.
document.addEventListener('visibilitychange', () => {
!document.hidden && __NEXTAUTH._getSession({ event: 'visibilitychange' })
}, false)
}
// Context to store session data globally
const SessionContext = createContext()
// Client side method
export const useSession = (session) => {
// Try to use context if we can
const value = useContext(SessionContext)
// If we have no Provider in the tree, call the actual hook
if (value === undefined) {
return _useSessionHook(session)
}
return value
/**
* React Hook that gives you access
* to the logged in user's session data.
*
* [Documentation](https://next-auth.js.org/getting-started/client#usesession)
* @type {import(".").UseSession}
*/
export function useSession (session) {
const context = useContext(SessionContext)
if (context) return context
return _useSessionHook(session)
}
// Internal hook for getting session from the api.
const _useSessionHook = (session) => {
function _useSessionHook (session) {
const [data, setData] = useState(session)
const [loading, setLoading] = useState(true)
const [loading, setLoading] = useState(!data)
useEffect(() => {
const _getSession = async ({ event = null } = {}) => {
__NEXTAUTH._getSession = async ({ event = null } = {}) => {
try {
const triggredByEvent = (event !== null)
const triggeredByStorageEvent = !!((event && event === 'storage'))
const triggredByEvent = event !== null
const triggeredByStorageEvent = event === 'storage'
const clientMaxAge = __NEXTAUTH.clientMaxAge
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
const currentTime = Math.floor(new Date().getTime() / 1000)
const currentTime = _now()
const clientSession = __NEXTAUTH._clientSession
// Updates triggered by a storage event *always* trigger an update and we
// always update if we don't have any value for the current session state.
if (triggeredByStorageEvent === false && clientSession !== undefined) {
if (!triggeredByStorageEvent && clientSession !== undefined) {
if (clientMaxAge === 0 && triggredByEvent !== true) {
// 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
@@ -204,13 +116,14 @@ const _useSessionHook = (session) => {
// Update clientLastSync before making response to avoid repeated
// invokations that would otherwise be triggered while we are still
// waiting for a response.
__NEXTAUTH._clientLastSync = Math.floor(new Date().getTime() / 1000)
__NEXTAUTH._clientLastSync = _now()
// If this call was invoked via a storage event (i.e. another window) then
// tell getSession not to trigger an event when it calls to avoid an
// infinate loop.
const triggerEvent = (triggeredByStorageEvent === false)
const newClientSessionData = await getSession({ triggerEvent })
const newClientSessionData = await getSession({
triggerEvent: !triggeredByStorageEvent
})
// Save session state internally, just so we can track that we've checked
// if a session exists at least once.
@@ -220,27 +133,64 @@ const _useSessionHook = (session) => {
setLoading(false)
} catch (error) {
logger.error('CLIENT_USE_SESSION_ERROR', error)
setLoading(false)
}
}
__NEXTAUTH._getSession = _getSession
_getSession()
__NEXTAUTH._getSession()
})
return [data, loading]
}
/**
* 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)
* @type {import(".").GetSession}
*/
export async function getSession (ctx) {
const session = await _fetchData('session', ctx)
if (ctx?.triggerEvent ?? 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)
* @type {import(".").GetCsrfToken}
*/
async function getCsrfToken (ctx) {
return (await _fetchData('csrf', ctx))?.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)
* @type {import(".").GetProviders}
*/
export async function getProviders () {
return _fetchData('providers')
}
/**
* 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)
* @see https://next-auth.js.org/getting-started/client#signin
* @param {string} [provider]
* @param {SignInOptions} [options]
* @param {object} [authorizationParams]
* @return {Promise<SignInResponse | undefined>}
* @typedef {{callbackUrl?: string; redirect?: boolean}} SignInOptions
* @typedef {{error: string | null; status: number; ok: boolean}} SignInResponse
* Automatically adds the CSRF token to the request.
*
* [Documentation](https://next-auth.js.org/getting-started/client#signin)
* @type {import(".").SignIn}
*/
export async function signIn (provider, options = {}, authorizationParams = {}) {
const {
@@ -258,6 +208,9 @@ export async function signIn (provider, options = {}, authorizationParams = {})
return
}
const isCredentials = providers[provider].type === 'credentials'
const isEmail = providers[provider].type === 'email'
const canRedirectBeDisabled = isCredentials || isEmail
const signInUrl = isCredentials
? `${baseUrl}/callback/${provider}`
: `${baseUrl}/signin/${provider}`
@@ -279,7 +232,7 @@ export async function signIn (provider, options = {}, authorizationParams = {})
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
const res = await fetch(_signInUrl, fetchOptions)
const data = await res.json()
if (redirect || !isCredentials) {
if (redirect || !canRedirectBeDisabled) {
const url = data.url ?? callbackUrl
window.location = url
// If url contains a hash, the browser does not reload the page. We reload manually
@@ -304,10 +257,10 @@ export async function signIn (provider, options = {}, authorizationParams = {})
/**
* Signs the user out, by removing the session cookie.
* (Automatically adds the CSRF token to the request)
* @param {SignOutOptions} [options]
* @returns {Promise<{url?: string} | undefined>}
* @typedef {{callbackUrl?: string; redirect?: boolean;}} SignOutOptions
* Automatically adds the CSRF token to the request.
*
* [Documentation](https://next-auth.js.org/getting-started/client#signout)
* @type {import(".").SignOut}
*/
export async function signOut (options = {}) {
const {
@@ -328,7 +281,7 @@ export async function signOut (options = {}) {
}
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
const data = await res.json()
_sendMessage({ event: 'session', data: { trigger: 'signout' } })
broadcast.post({ event: 'session', data: { trigger: 'signout' } })
if (redirect) {
const url = data.url ?? callbackUrl
window.location = url
@@ -342,40 +295,118 @@ export async function signOut (options = {}) {
return data
}
// Provider to wrap the app in to make session data available globally
export const Provider = ({ children, session, options }) => {
setOptions(options)
return createElement(SessionContext.Provider, { value: useSession(session) }, children)
}
// Method to set options. The documented way is to use the provider, but this
// method is being left in as an alternative, that will be helpful if/when we
// expose a vanilla JavaScript version that doesn't depend on React.
/** @type {import(".").SetOptions} */
export function setOptions ({ baseUrl, basePath, clientMaxAge, keepAlive } = {}) {
if (baseUrl) __NEXTAUTH.baseUrl = baseUrl
if (basePath) __NEXTAUTH.basePath = basePath
if (clientMaxAge) __NEXTAUTH.clientMaxAge = clientMaxAge
if (keepAlive) {
__NEXTAUTH.keepAlive = keepAlive
if (typeof window === 'undefined') return
const _fetchData = async (url, options = {}) => {
try {
const res = await fetch(url, options)
const data = await res.json()
return Promise.resolve(Object.keys(data).length > 0 ? data : null) // Return null if data empty
} catch (error) {
logger.error('CLIENT_FETCH_ERROR', url, error)
return Promise.resolve(null)
// Clear existing timer (if there is one)
if (__NEXTAUTH._clientSyncTimer !== null) {
clearTimeout(__NEXTAUTH._clientSyncTimer)
}
// Set next timer to trigger in number of seconds
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
// Only invoke keepalive when a session exists
if (!__NEXTAUTH._clientSession) return
await __NEXTAUTH._getSession({ event: 'timer' })
}, keepAlive * 1000)
}
}
const _apiBaseUrl = () => {
/**
* 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#provider)
* @type {import(".").Provider}
*/
export function Provider ({ children, session, options }) {
setOptions(options)
return createElement(
SessionContext.Provider,
{ value: useSession(session) },
children
)
}
/**
* If passed 'appContext' via getInitialProps() in _app.js
* then get the req object from ctx and use that for the
* req value to allow _fetchData to
* work seemlessly in getInitialProps() on server side
* pages *and* in _app.js.
*/
async function _fetchData (path, { ctx, req = ctx?.req } = {}) {
try {
const baseUrl = await _apiBaseUrl()
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
const res = await fetch(`${baseUrl}/${path}`, options)
const data = await res.json()
return Object.keys(data).length > 0 ? data : null // Return null if data empty
} catch (error) {
logger.error('CLIENT_FETCH_ERROR', path, error)
return null
}
}
function _apiBaseUrl () {
if (typeof window === 'undefined') {
// NEXTAUTH_URL should always be set explicitly to support server side calls - log warning if not set
if (!process.env.NEXTAUTH_URL) { logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set') }
if (!process.env.NEXTAUTH_URL) {
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
}
// Return absolute path when called server side
return `${__NEXTAUTH.baseUrl}${__NEXTAUTH.basePath}`
} else {
// Return relative path when called client side
return __NEXTAUTH.basePath
return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}`
}
// Return relative path when called client side
return __NEXTAUTH.basePath
}
const _sendMessage = (message) => {
if (typeof localStorage !== 'undefined') {
const timestamp = Math.floor(new Date().getTime() / 1000)
localStorage.setItem('nextauth.message', JSON.stringify({ ...message, clientId: __NEXTAUTH._clientId, timestamp })) // eslint-disable-line
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
function _now () {
return Math.floor(Date.now() / 1000)
}
/**
* Inspired by [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)
* Only not using it directly, because Safari does not support it.
*
* https://caniuse.com/?search=broadcastchannel
*/
function BroadcastChannel (name = 'nextauth.message') {
return {
/**
* Get notified by other tabs/windows.
* @param {(message: import(".").BroadcastMessage) => void} onReceive
*/
receive (onReceive) {
if (typeof window === 'undefined') return
window.addEventListener('storage', async (event) => {
if (event.key !== name) return
/** @type {import(".").BroadcastMessage} */
const message = JSON.parse(event.newValue)
if (message?.event !== 'session' || !message?.data) return
onReceive(message)
})
},
/** Notify other tabs/windows. */
post (message) {
if (typeof localStorage === 'undefined') return
localStorage.setItem(name,
JSON.stringify({ ...message, timestamp: _now() })
)
}
}
}

5
src/lib/logger.d.ts vendored
View File

@@ -3,3 +3,8 @@ export interface LoggerInstance {
error: (code?: string, ...message: unknown[]) => void
debug: (code?: string, ...message: unknown[]) => void
}
export declare function proxyLogger (logger: LoggerInstance, basePath: string): LoggerInstance
const _logger: LoggerInstance
export default _logger

View File

@@ -14,7 +14,7 @@ export default (options) => {
const defaultAvatarNumber = parseInt(profile.discriminator) % 5
profile.image_url = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNumber}.png`
} else {
const format = profile.premium_type === 1 || profile.premium_type === 2 ? 'gif' : 'png'
const format = profile.avatar.startsWith('a_') ? 'gif' : 'png'
profile.image_url = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`
}
return {

View File

@@ -18,12 +18,15 @@ import GitHub from './github'
import GitLab from './gitlab'
import Google from './google'
import IdentityServer4 from './identity-server4'
import Instagram from './instagram'
import Kakao from './kakao'
import LINE from './line'
import LinkedIn from './linkedin'
import MailRu from './mailru'
import Medium from './medium'
import Netlify from './netlify'
import Okta from './okta'
import Osso from './osso'
import Reddit from './reddit'
import Salesforce from './salesforce'
import Slack from './slack'
@@ -55,12 +58,15 @@ export default {
GitLab,
Google,
IdentityServer4,
Instagram,
Kakao,
LINE,
LinkedIn,
MailRu,
Medium,
Netlify,
Okta,
Osso,
Reddit,
Salesforce,
Slack,

View File

@@ -0,0 +1,50 @@
/**
* @param {import("../server").Provider} options
* @example
*
* ```js
* // pages/api/auth/[...nextauth].js
* import Providers from `next-auth/providers`
* ...
* providers: [
* Providers.Instagram({
* clientId: process.env.INSTAGRAM_CLIENT_ID,
* clientSecret: process.env.INSTAGRAM_CLIENT_SECRET
* })
* ]
* ...
*
* // pages/index
* import { signIn } from "next-auth/client"
* ...
* <button onClick={() => signIn("instagram")}>
* Sign in
* </button>
* ...
* ```
* *Resources:*
* - [NextAuth.js Documentation](https://next-auth.js.org/providers/instagram)
* - [Instagram Documentation](https://developers.facebook.com/docs/instagram-basic-display-api/getting-started)
* - [Configuration](https://developers.facebook.com/apps)
*/
export default function Instagram (options) {
return {
id: 'instagram',
name: 'Instagram',
type: 'oauth',
version: '2.0',
scope: 'user_profile',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://api.instagram.com/oauth/access_token',
authorizationUrl: 'https://api.instagram.com/oauth/authorize?response_type=code',
profileUrl: 'https://graph.instagram.com/me?fields=id,username,account_type,name',
async profile (profile) {
return {
id: profile.id,
name: profile.username,
email: null,
image: null
}
}
}
}

21
src/providers/kakao.js Normal file
View File

@@ -0,0 +1,21 @@
export default (options) => {
return {
id: 'kakao',
name: 'Kakao',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://kauth.kakao.com/oauth/token',
authorizationUrl: 'https://kauth.kakao.com/oauth/authorize?response_type=code',
profileUrl: 'https://kapi.kakao.com/v2/user/me',
profile: (profile) => {
return {
id: profile.id,
name: profile.kakao_account?.profile.nickname,
email: profile.kakao_account?.email,
image: profile.kakao_account?.profile.profile_image_url
}
},
...options
}
}

20
src/providers/osso.js Normal file
View File

@@ -0,0 +1,20 @@
export default (options) => {
return {
id: 'osso',
name: 'SAML SSO',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
accessTokenUrl: `https://${options.domain}/oauth/token`,
authorizationUrl: `https://${options.domain}/oauth/authorize?response_type=code`,
profileUrl: `https://${options.domain}/oauth/me`,
profile: (profile) => {
return {
id: profile.id,
name: profile.name || profile.email,
email: profile.email
}
},
...options
}
}

View File

@@ -108,7 +108,7 @@ async function getProfile ({ profileData, tokens, provider, user }) {
logger.debug('PROFILE_DATA', profileData)
const profile = await provider.profile(profileData)
const profile = await provider.profile(profileData, tokens)
// Return profile, raw profile and auth provider details
return {
profile: {

View File

@@ -10,7 +10,7 @@ export default async function email (email, provider, options) {
const secret = provider.secret || options.secret
// Generate token
const token = provider.generateVerificationToken?.() ?? randomBytes(32).toString('hex')
const token = await provider.generateVerificationToken?.() ?? randomBytes(32).toString('hex')
// Send email with link containing token (the unhashed version)
const url = `${baseUrl}${basePath}/callback/${encodeURIComponent(provider.id)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`

View File

@@ -9,14 +9,34 @@ export default function renderPage (req, res) {
const { baseUrl, basePath, callbackUrl, csrfToken, providers, theme } = req.options
res.setHeader('Content-Type', 'text/html')
function send (html) {
res.send(`<!DOCTYPE html><head><style type="text/css">${css()}</style><meta name="viewport" content="width=device-width, initial-scale=1"></head><body class="__next-auth-theme-${theme}"><div class="page">${html}</div></body></html>`)
function send ({ html, title }) {
res.send(`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>${css()}</style><title>${title}</title></head><body class="__next-auth-theme-${theme}"><div class="page">${html}</div></body></html>`)
}
return {
signin (props) { send(signin({ csrfToken, providers, callbackUrl, ...req.query, ...props })) },
signout (props) { send(signout({ csrfToken, baseUrl, basePath, ...props })) },
verifyRequest (props) { send(verifyRequest({ baseUrl, ...props })) },
error (props) { send(error({ basePath, baseUrl, res, ...props })) }
signin (props) {
send({
html: signin({ csrfToken, providers, callbackUrl, ...req.query, ...props }),
title: 'Sign In'
})
},
signout (props) {
send({
html: signout({ csrfToken, baseUrl, basePath, ...props }),
title: 'Sign Out'
})
},
verifyRequest (props) {
send({
html: verifyRequest({ baseUrl, ...props }),
title: 'Verify Request'
})
},
error (props) {
send({
html: error({ basePath, baseUrl, res, ...props }),
title: 'Error'
})
}
}
}

View File

@@ -72,7 +72,7 @@ export default async function callback (req, res) {
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
}
// TODO: Remove in a future major release
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
@@ -168,7 +168,7 @@ export default async function callback (req, res) {
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
}
// TODO: Remove in a future major release
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
@@ -239,7 +239,7 @@ export default async function callback (req, res) {
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
}
return res.redirect(error)
}
@@ -254,7 +254,7 @@ export default async function callback (req, res) {
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
}
return res.redirect(error)
}

View File

@@ -112,52 +112,6 @@ callbacks: {
The redirect callback may be invoked more than once in the same flow.
:::
## Session callback
The session callback is called whenever a session is checked.
e.g. `getSession()`, `useSession()`, `/api/auth/session`
* When using database sessions, the User object is passed as an argument.
* When using JSON Web Tokens for sessions, the JWT payload is provided instead.
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
/**
* @param {object} session Session object
* @param {object} token User object (if using database sessions)
* JSON Web Token (if not using database sessions)
* @return {object} Session that will be returned to the client
*/
async session(session, token) {
if(token?.accessToken) {
// Add property to session, like an access_token from a provider
session.accessToken = token.accessToken
}
return session
}
}
...
```
:::tip
When using JSON Web Tokens the `jwt()` callback is invoked before the `session()` callback, so anything you add to the
JSON Web Token will be immediately available in the session callback, like for example an `access_token` from a provider.
:::
:::tip
To better represent its value, when using a JWT session, the second parameter should be called `token` (This is the same thing you return from the `jwt` callback). If you use a database, call it `user`.
:::
:::warning
The session object is not persisted server side, even when using database sessions - only data such as the session token, the user, and the expiry time is stored in the session table.
If you need to persist session data server side, you can use the `accessToken` returned for the session as a key - and connect to the database in the `session()` callback to access it. Session `accessToken` values do not rotate and are valid as long as the session is valid.
If using JSON Web Tokens instead of database sessions, you should use the User ID or a unique key stored in the token (you will need to generate a key for this yourself on sign in, as access tokens for sessions are not generated when using JSON Web Tokens).
:::
## JWT callback
This JSON Web Token callback is called whenever a JSON Web Token is created (i.e. at sign
@@ -206,3 +160,47 @@ NextAuth.js does not limit how much data you can store in a JSON Web Token, howe
If you need to persist a large amount of data, you will need to persist it elsewhere (e.g. in a database). You can store a key that can be used to look up that data in the `session()` callback.
:::
## Session callback
The session callback is called whenever a session is checked. By default, only a subset of the token is returned for increased security. If you want to make something available you added to the token through the `jwt()` callback, you have to explicitely forward it here to make it available to the client.
e.g. `getSession()`, `useSession()`, `/api/auth/session`
* When using database sessions, the User object is passed as an argument.
* When using JSON Web Tokens for sessions, the JWT payload is provided instead.
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
/**
* @param {object} session Session object
* @param {object} token User object (if using database sessions)
* JSON Web Token (if not using database sessions)
* @return {object} Session that will be returned to the client
*/
async session(session, token) {
// Add property to session, like an access_token from a provider.
session.accessToken = token.accessToken
return session
}
}
...
```
:::tip
When using JSON Web Tokens the `jwt()` callback is invoked before the `session()` callback, so anything you add to the
JSON Web Token will be immediately available in the session callback, like for example an `access_token` from a provider.
:::
:::tip
To better represent its value, when using a JWT session, the second parameter should be called `token` (This is the same thing you return from the `jwt()` callback). If you use a database, call it `user`.
:::
:::warning
The session object is not persisted server side, even when using database sessions - only data such as the session token, the user, and the expiry time is stored in the session table.
If you need to persist session data server side, you can use the `accessToken` returned for the session as a key - and connect to the database in the `session()` callback to access it. Session `accessToken` values do not rotate and are valid as long as the session is valid.
If using JSON Web Tokens instead of database sessions, you should use the User ID or a unique key stored in the token (you will need to generate a key for this yourself on sign in, as access tokens for sessions are not generated when using JSON Web Tokens).
:::

View File

@@ -136,17 +136,44 @@ Install module:
database: 'mariadb://username:password@127.0.0.1:3306/database_name'
```
### Postgres
### Postgres / CockroachDB
Install module:
`npm i pg`
#### Example
PostgresDB
```js
database: 'postgres://username:password@127.0.0.1:5432/database_name'
```
CockroachDB
```js
database: 'postgres://username:password@127.0.0.1:26257/database_name'
```
If the node is using Self-signed cert
```js
database: {
type: "cockroachdb",
host: process.env.DATABASE_HOST,
port: 26257,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
ssl: {
rejectUnauthorized: false,
ca: fs.readFileSync('/path/to/server-certificates/root.crt').toString()
},
},
```
Read more: [https://node-postgres.com/features/ssl](https://node-postgres.com/features/ssl)
---
### Microsoft SQL Server
Install module:
@@ -166,7 +193,7 @@ Install module:
#### Example
```js
database: 'mongodb://username:password@127.0.0.1:27017/database_name'
database: 'mongodb://username:password@127.0.0.1:3306/database_name'
```
### SQLite
@@ -182,9 +209,6 @@ Install module:
database: 'sqlite://localhost/:memory:'
```
---
## Other databases
See the [documentation for adapters](/schemas/adapters) for more information on advanced configuration, including how to use NextAuth.js with other databases using a [custom adapter](/tutorials/creating-a-database-adapter).

View File

@@ -21,6 +21,14 @@ _e.g. `NEXTAUTH_URL=https://example.com/custom-route/api/auth`_
To set environment variables on Vercel, you can use the [dashboard](https://vercel.com/dashboard) or the `now env` command.
:::
### NEXTAUTH_URL_INTERNAL
If provided, server-side calls will use this instead of `NEXTAUTH_URL`. Useful in environments when the server doesn't have access to the canonical URL of your site. Defaults to `NEXTAUTH_URL`.
```
NEXTAUTH_URL_INTERNAL=http://10.240.8.16
```
---
## Options
@@ -328,7 +336,7 @@ export default NextAuth({
},
warn(code, ...message) {
log.warn(code, message)
}
},
debug(code, ...message) {
log.debug(code, message)
}

View File

@@ -7,7 +7,7 @@ NextAuth.js automatically creates simple, unbranded authentication pages for han
The options displayed on the sign up page are automatically generated based on the providers specified in the options passed to NextAuth.js.
To add a custom login page, for example. You can use the `pages` option:
To add a custom login page, you can use the `pages` option:
```javascript title="pages/api/auth/[...nextauth].js"
...
@@ -42,9 +42,9 @@ export default function SignIn({ providers }) {
)
}
SignIn.getInitialProps = async (context) => {
SignIn.getInitialProps = async () => {
return {
providers: await providers(context)
providers: await providers()
}
}
```
@@ -121,4 +121,4 @@ signIn('credentials', { username: 'jsmith', password: '1234' })
:::tip
Remember to put any custom pages in a folder outside **/pages/api** which is reserved for API code. As per the examples above, a location convention suggestion is `pages/auth/...`.
:::
:::

View File

@@ -78,7 +78,10 @@ As an example of what this looks like, this is the the provider object returned
requestTokenUrl: "https://accounts.google.com/o/oauth2/auth",
authorizationUrl: "https://accounts.google.com/o/oauth2/auth?response_type=code",
profileUrl: "https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
async profile(profile) {
async profile(profile, tokens) {
// You can use the tokens, in case you want to fetch more profile information
// For example several OAuth provider does not return e-mail by default.
// Depending on your provider, will have tokens like `access_token`, `id_token` and or `refresh_token`
return {
id: profile.id,
name: profile.name,

View File

@@ -116,7 +116,7 @@ NextAuth.js records Refresh Tokens and Access Tokens on sign in (if supplied by
You can then look them up from the database or persist them to the JSON Web Token.
Note: NextAuth.js does not currently handle Access Token rotation for OAuth providers for you, if this is something you need, currently you will need to write the logic to handle that yourself.
Note: NextAuth.js does not currently handle Access Token rotation for OAuth providers for you, however you can check out [this tutorial](/tutorials/refresh-token-rotation) if you want to implement it.
### When I sign in with another account with the same email address, why are accounts not linked automatically?

View File

@@ -212,7 +212,18 @@ The URL must be considered valid by the [redirect callback handler](/configurati
#### Using the redirect: false option
When you use the `credentials` provider, you might not want the user to redirect to an error page if an error occurs, so you can handle any errors (like wrong credentials given by the user) on the same page. For that, you can pass `redirect: false` in the second parameter object. `signIn` then will return a Promise, that resolves to the following:
:::note
The redirect option is only available for `credentials` and `email` providers.
:::
In some cases, you might want to deal with the sign in response on the same page and disable the default redirection. For example, if an error occurs (like wrong credentials given by the user), you might want to handle the error on the same page. For that, you can pass `redirect: false` in the second parameter object.
e.g.
- `signIn('credentials', { redirect: false, password: 'password' })`
- `signIn('email', { redirect: false, email: 'bill@fillmurray.com' })`
`signIn` will then return a Promise, that resolves to the following:
```ts
{
@@ -345,7 +356,7 @@ export default function App ({ Component, pageProps }) {
:::note
**These options have no effect on clients that are not signed in.**
Every tab/window maintains it's own copy of the local session state; the session it is not stored in shared storage like localStorage or sessionStorage. Any update in one tab/window triggers a message to other tabs/windows to update their own session state.
Every tab/window maintains its own copy of the local session state; the session is not stored in shared storage like localStorage or sessionStorage. Any update in one tab/window triggers a message to other tabs/windows to update their own session state.
Using low values for `clientMaxAge` or `keepAlive` will increase network traffic and load on authenticated clients and may impact hosting costs and performance.
:::

View File

@@ -184,3 +184,17 @@ const text = ({ url, site }) => `Sign in to ${site}\n${url}\n\n`
:::tip
If you want to generate great looking email client compatible HTML with React, check out https://mjml.io
:::
## Customising the Verification Token
By default, we are generating a random verification token. You can define a `generateVerificationToken` method in your provider options if you want to override it:
```js title="pages/api/auth/[...nextauth].js"
providers: [
Providers.Email({
async generateVerificationToken() {
return "ABC123"
}
})
],

View File

@@ -0,0 +1,42 @@
---
id: instagram
title: Instagram
---
## Documentation
https://developers.facebook.com/docs/instagram-basic-display-api/getting-started
## Configuration
https://developers.facebook.com/apps/
## Example
```jsx
// pages/api/auth/[...nextauth].js
import Providers from `next-auth/providers`
...
providers: [
Providers.Instagram({
clientId: process.env.INSTAGRAM_CLIENT_ID,
clientSecret: process.env.INSTAGRAM_CLIENT_SECRET
})
]
...
// pages/index.jsx
import { signIn } from "next-auth/client"
...
<button onClick={() => signIn("instagram")}>
Sign in
</button>
...
```
:::warning
Email address is not returned by the Instagram API.
:::
:::tip
Instagram display app required callback URL to be configured in your Facebook app and Facebook required you to use **https** even for localhost! In order to do that, you either need to [add an SSL to your localhost](https://www.freecodecamp.org/news/how-to-get-https-working-on-your-local-development-environment-in-5-minutes-7af615770eec/) or use a proxy such as [ngrock](https://ngrok.com/docs).
:::

View File

@@ -0,0 +1,32 @@
---
id: kakao
title: Kakao
---
## Documentation
https://developers.kakao.com/product/kakaoLogin
## Configuration
https://developers.kakao.com/docs/latest/en/kakaologin/common
## Example
```js
import Providers from `next-auth/providers`
...
providers: [
Providers.Kakao({
clientId: process.env.KAKAO_CLIENT_ID,
clientSecret: process.env.KAKAO_CLIENT_SECRET
})
]
...
```
## Instructions
### Configuration
Create a provider and a Kakao application at `https://developers.kakao.com/console/app`. In the settings of the app under Kakao Login, activate web app, change consent items and configure callback URL.

View File

@@ -0,0 +1,39 @@
---
id: osso
title: Osso
---
## Documentation
Osso is an open source service that handles SAML authentication against Identity Providers, normalizes profiles, and makes those profiles available to you in an OAuth 2.0 code grant flow.
If you don't yet have an Osso instance, you can use [Osso's Demo App](https://demo.ossoapp.com) for your testing purposes. For documentation on deploying an Osso instance, see https://ossoapp.com/docs/deploy/overview/
## Configuration
You can configure your OAuth Clients on your Osso Admin UI, i.e. https://demo.ossoapp.com/admin/config - you'll need to get a Client ID and Secret and allow-list your redirect URIs.
[SAML SSO differs a bit from OAuth](https://ossoapp.com/blog/saml-vs-oauth) - for every tenant who wants to sign in to your application using SAML, you and your customer need to perform a multi-step configuration in Osso's Admin UI and the admin dashboard of the tenant's Identity Provider. Osso provides documentation for providers like Okta and OneLogin, cloud-based IDPs who also offer a developer account that's useful for testing. Osso also provides a [Mock IDP](https://idp.ossoapp.com) that you can use for testing without needing to sign up for an Identity Provider service.
See Osso's complete configuration and testing documentation at https://ossoapp.com/docs/configure/overview
## Example
A full example application is available at https://github.com/enterprise-oss/osso-next-auth-example and https://nextjs-demo.ossoapp.com
```js
import Providers from `next-auth/providers`
...
providers: [
Providers.Osso({
clientId: process.env.OSSO_CLIENT_ID,
clientSecret: process.env.OSSO_CLIENT_SECRET,
domain: process.env.OSSO_DOMAIN
})
}
...
```
:::note
`domain` should be the fully qualified domain  e.g. `demo.ossoapp.com`
:::

View File

@@ -11,6 +11,8 @@ https://dev.twitch.tv/docs/authentication
https://dev.twitch.tv/console/apps
Add the following redirect URL into the console `http://<your-next-app-url>/api/auth/callback/twitch`
## Example
```js
@@ -23,4 +25,4 @@ providers: [
})
]
...
```
```

View File

@@ -9,6 +9,10 @@ _These tutorials are contributed by the community and hosted on this site._
_New submissions and edits are welcome!_
### [Refresh Token Rotation](tutorials/refresh-token-rotation)
How to implement refresh token rotation.
### [Securing pages and API routes](tutorials/securing-pages-and-api-routes)
How to restrict access to pages and API routes.
@@ -67,7 +71,6 @@ This example shows how to implement a fullstack app in TypeScript with Next.js u
This `dev.to` tutorial walks one through adding NextAuth.js to an existing project. Including setting up the OAuth client id and secret, adding the API routes for authentication, protecting pages and api routes behind that authentication, etc.
### [Adding Sign in With Apple Next JS](https://thesiddd.com/blog/apple-auth)
This tutorial walks step by step on how to get Sign In with Apple working (both locally and on a deployed website) using NextAuth.js.

View File

@@ -0,0 +1,139 @@
---
id: refresh-token-rotation
title: Refresh Token Rotation
---
While NextAuth.js doesn't automatically handle access token rotation for OAuth providers yet, this functionality can be implemented using [callbacks](https://next-auth.js.org/configuration/callbacks).
## Source Code
_A working example can be accessed [here](https://github.com/lawrencecchen/next-auth-refresh-tokens)._
## Implementation
### Server Side
Using a [JWT callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) and a [session callback](https://next-auth.js.org/configuration/callbacks#session-callback), we can persist OAuth tokens and refresh them when they expire.
Below is a sample implementation using Google's Identity Provider. Please note that the OAuth 2.0 request in the `refreshAccessToken()` function will vary between different providers, but the core logic should remain similar.
```js title="pages/auth/[...nextauth.js]"
import NextAuth from "next-auth";
import Providers from "next-auth/providers";
const GOOGLE_AUTHORIZATION_URL =
"https://accounts.google.com/o/oauth2/v2/auth?" +
new URLSearchParams({
prompt: "consent",
access_type: "offline",
response_type: "code",
});
/**
* Takes a token, and returns a new token with updated
* `accessToken` and `accessTokenExpires`. If an error occurs,
* returns the old token and an error property
*/
async function refreshAccessToken(token) {
try {
const url =
"https://oauth2.googleapis.com/token?" +
new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
grant_type: "refresh_token",
refresh_token: token.refreshToken,
});
const response = await fetch(url, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
method: "POST",
});
const refreshedTokens = await response.json();
if (!response.ok) {
throw refreshedTokens;
}
return {
...token,
accessToken: refreshedTokens.access_token,
accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, // Fall back to old refresh token
};
} catch (error) {
console.log(error);
return {
...token,
error: "RefreshAccessTokenError",
};
}
}
export default NextAuth({
providers: [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorizationUrl: GOOGLE_AUTHORIZATION_URL,
}),
],
callbacks: {
async jwt(token, user, account) {
// Initial sign in
if (account && user) {
return {
accessToken: account.accessToken,
accessTokenExpires: Date.now() + account.expires_in * 1000,
refreshToken: account.refresh_token,
user,
};
}
// Return previous token if the access token has not expired yet
if (Date.now() < token.accessTokenExpires) {
return token;
}
// Access token has expired, try to update it
return refreshAccessToken(token);
},
async session(session, token) {
if (token) {
session.user = token.user;
session.accessToken = token.accessToken;
session.error = token.error;
}
return session;
},
},
});
```
### Client Side
The `RefreshAccessTokenError` error that is caught in the `refreshAccessToken()` method is passed all the way to the client. This means that you can direct the user to the sign in flow if we cannot refresh their token.
We can handle this functionality as a side effect:
```js title="pages/auth/[...nextauth.js]"
import { signIn, useSession } from "next-auth/client";
import { useEffect } from "react";
const HomePage() {
const [session] = useSession();
useEffect(() => {
if (session?.error === "RefreshAccessTokenError") {
signIn(); // Force sign in to hopefully resolve error
}
}, [session]);
return (...)
}
```

6
www/package-lock.json generated
View File

@@ -10900,9 +10900,9 @@
"integrity": "sha512-MgMhSdHuHymNRqD6KM3eGS0PNqgK9q4QF5P0yoQQvpB6jNjeSAi3jcSAz0Sua/t9fa4xDOMar9HJbLa08gl9ug=="
},
"prismjs": {
"version": "1.22.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.22.0.tgz",
"integrity": "sha512-lLJ/Wt9yy0AiSYBf212kK3mM5L8ycwlyTlSxHBAneXLR0nzFMlZ5y7riFPF3E33zXOF2IH95xdY5jIyZbM9z/w==",
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.23.0.tgz",
"integrity": "sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA==",
"requires": {
"clipboard": "^2.0.0"
}

View File

@@ -11,16 +11,16 @@
"generate-providers": "node ./scripts/generate-providers.js"
},
"dependencies": {
"@docusaurus/core": "^2.0.0-alpha.66",
"@docusaurus/preset-classic": "^2.0.0-alpha.66",
"@docusaurus/core": "^2.0.0-alpha.70",
"@docusaurus/preset-classic": "^2.0.0-alpha.70",
"classnames": "^2.2.6",
"docusaurus-lunr-search": "^2.1.7",
"docusaurus-lunr-search": "^2.1.10",
"jose": "^2.0.2",
"lodash.times": "^4.3.2",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-marquee-slider": "^1.1.2",
"styled-components": "^5.2.0"
"styled-components": "^5.2.1"
},
"browserslist": {
"production": [
@@ -35,6 +35,6 @@
]
},
"devDependencies": {
"standard": "^15.0.0"
"standard": "^16.0.3"
}
}