Compare commits

...

6 Commits

Author SHA1 Message Date
Balázs Orbán
17b789822d fix: make oauth_token_secret and oauth_token available (#1322)
* fix: add oauth_token_secret to requests

* chore: remove console.log

* refactor: follow casing from response
2021-04-14 21:26:15 +02:00
Ovidiu Dan
fd12194c0c docs(provider): Explain how to get access to LinkedIn authentication (#1706) 2021-04-12 18:46:20 +02:00
Balázs Orbán
1c662e9ddc fix(page): fall back to default error page (#1700) 2021-04-12 03:56:47 +02:00
Balázs Orbán
968903d227 fix(oauth): support response_mode=form_post (#1669)
* chore: alias dev script to next

* feat(core): fallback to body when reading state

* refactor: set csrfToken on req.options implicitly

Ensures we do this similarly than
in other handlers like pkce, state, extendRes, callbackUrlHandler etc.

* chore: add code comment for debugging
2021-04-12 00:24:05 +02:00
Balázs Orbán
3dedf6c26c fix(provider): proper check of protection property (#1694)
* fix(provider): proper check of protection property

* chore: add comment
2021-04-12 00:15:29 +02:00
Amauri Dias
d1dbfe1023 fix: truly replace .flat() to support Node <11 again (#1691) 2021-04-11 23:20:37 +02:00
13 changed files with 124 additions and 64 deletions

View File

@@ -42,7 +42,7 @@ npm i
> NOTE: You can add any environment variables to .env.local that you would like to use in your dev app.
> You can find the next-auth config under`pages/api/auth/[...nextauth].js`.
1. Start the dev application/server and CSS watching:
1. Start the dev application/server:
```sh
npm run dev
```

View File

@@ -10,7 +10,8 @@
"build": "npm run build:js && npm run build:css",
"build:js": "babel --config-file ./config/babel.config.json src --out-dir dist",
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir dist && node config/wrap-css.js",
"dev": "next | npm run watch:css",
"dev:with-css": "next | npm run watch:css",
"dev": "next",
"watch": "npm run watch:js | npm run watch:css",
"watch:js": "babel --config-file ./config/babel.config.json --watch src --out-dir dist",
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",

View File

@@ -6,6 +6,27 @@ import Providers from 'next-auth/providers'
// const prisma = new PrismaClient()
export default NextAuth({
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
// cookies: {
// csrfToken: {
// name: 'next-auth.csrf-token',
// options: {
// httpOnly: true,
// sameSite: 'none',
// path: '/',
// secure: true
// }
// },
// pkceCodeVerifier: {
// name: 'next-auth.pkce.code_verifier',
// options: {
// httpOnly: true,
// sameSite: 'none',
// path: '/',
// secure: true
// }
// }
// },
providers: [
Providers.Email({
server: process.env.EMAIL_SERVER,
@@ -19,6 +40,11 @@ export default NextAuth({
clientId: process.env.AUTH0_ID,
clientSecret: process.env.AUTH0_SECRET,
domain: process.env.AUTH0_DOMAIN,
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
// protection: ["pkce", "state"],
// authorizationParams: {
// response_mode: 'form_post'
// }
protection: 'pkce'
}),
Providers.Twitter({

View File

@@ -82,6 +82,7 @@ export interface NextAuthInternalOptions extends Pick<NextAuthOptions, NextAuthS
basePath?: string
action?: string
csrfToken?: string
csrfTokenVerified?: boolean
}
export interface NextAuthRequest extends NextApiRequest {

View File

@@ -6,12 +6,12 @@ import * as cookie from './lib/cookie'
import * as defaultEvents from './lib/default-events'
import * as defaultCallbacks from './lib/default-callbacks'
import parseProviders from './lib/providers'
import callbackUrlHandler from './lib/callback-url-handler'
import extendRes from './lib/extend-res'
import * as routes from './routes'
import renderPage from './pages'
import csrfTokenHandler from './lib/csrf-token-handler'
import createSecret from './lib/create-secret'
import callbackUrlHandler from './lib/callback-url-handler'
import extendRes from './lib/extend-res'
import csrfTokenHandler from './lib/csrf-token-handler'
import * as pkce from './lib/oauth/pkce-handler'
import * as state from './lib/oauth/state-handler'
@@ -67,20 +67,18 @@ async function NextAuthHandler (req, res, userOptions) {
const secret = createSecret({ userOptions, basePath, baseUrl })
const { csrfToken, csrfTokenVerified } = csrfTokenHandler(req, res, cookies, secret)
const providers = parseProviders({ providers: userOptions.providers, baseUrl, basePath })
const provider = providers.find(({ id }) => id === providerId)
if (provider &&
provider.type === 'oauth' && provider.version?.startsWith('2') &&
(!provider.protection && provider.state !== false)
) {
provider.protection = 'state' // Default to state, as we did in 3.1 REVIEW: should we use "pkce" or "none" as default?
}
if (typeof provider?.protection === 'string') {
provider.protection = [provider.protection]
// Protection only works on OAuth 2.x providers
if (provider?.type === 'oauth' && provider.version?.startsWith('2')) {
// When provider.state is undefined, we still want this to pass
if (!provider.protection && provider.state !== false) {
// Default to state, as we did in 3.1 REVIEW: should we use "pkce" or "none" as default?
provider.protection = ['state']
} else if (typeof provider.protection === 'string') {
provider.protection = [provider.protection]
}
}
const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle
@@ -107,7 +105,6 @@ async function NextAuthHandler (req, res, userOptions) {
provider,
cookies,
secret,
csrfToken,
providers,
// Session options
session: {
@@ -138,6 +135,7 @@ async function NextAuthHandler (req, res, userOptions) {
logger
}
csrfTokenHandler(req, res)
await callbackUrlHandler(req, res)
const render = renderPage(req, res)
@@ -150,7 +148,7 @@ async function NextAuthHandler (req, res, userOptions) {
case 'session':
return routes.session(req, res)
case 'csrf':
return res.json({ csrfToken })
return res.json({ csrfToken: req.options.csrfToken })
case 'signin':
if (pages.signIn) {
let signinUrl = `${pages.signIn}${pages.signIn.includes('?') ? '&' : '?'}callbackUrl=${req.options.callbackUrl}`
@@ -203,7 +201,7 @@ async function NextAuthHandler (req, res, userOptions) {
switch (action) {
case 'signin':
// Verified CSRF Token required for all sign in routes
if (csrfTokenVerified && provider) {
if (req.options.csrfTokenVerified && provider) {
if (await pkce.handleSignin(req, res)) return
if (await state.handleSignin(req, res)) return
return routes.signin(req, res)
@@ -212,14 +210,14 @@ async function NextAuthHandler (req, res, userOptions) {
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
case 'signout':
// Verified CSRF Token required for signout
if (csrfTokenVerified) {
if (req.options.csrfTokenVerified) {
return routes.signout(req, res)
}
return res.redirect(`${baseUrl}${basePath}/signout?csrf=true`)
case 'callback':
if (provider) {
// Verified CSRF Token required for credentials providers only
if (provider.type === 'credentials' && !csrfTokenVerified) {
if (provider.type === 'credentials' && !req.options.csrfTokenVerified) {
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
}

View File

@@ -14,29 +14,30 @@ import * as cookie from './cookie'
* For more details, see the following OWASP links:
* https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie
* https://owasp.org/www-chapter-london/assets/slides/David_Johansson-Double_Defeat_of_Double-Submit_Cookie.pdf
* @param {import("..").NextAuthRequest} req
* @param {import("..").NextAuthResponse} res
*/
export default function csrfTokenHandler (req, res, cookies, secret) {
const { csrfToken: csrfTokenFromRequest } = req.body
let csrfTokenFromCookie
let csrfTokenVerified = false
if (req.cookies[cookies.csrfToken.name]) {
const [csrfTokenValue, csrfTokenHash] = req.cookies[cookies.csrfToken.name].split('|')
if (csrfTokenHash === createHash('sha256').update(`${csrfTokenValue}${secret}`).digest('hex')) {
export default function csrfTokenHandler (req, res) {
const { cookies, secret } = req.options
if (cookies.csrfToken.name in req.cookies) {
const [csrfToken, csrfTokenHash] = req.cookies[cookies.csrfToken.name].split('|')
const expectedCsrfTokenHash = createHash('sha256').update(`${csrfToken}${secret}`).digest('hex')
if (csrfTokenHash === expectedCsrfTokenHash) {
// If hash matches then we trust the CSRF token value
csrfTokenFromCookie = csrfTokenValue
// If this is a POST request and the CSRF Token in the Post request matches
// the cookie we have already verified is one we have set, then token is verified!
if (req.method === 'POST' && csrfTokenFromCookie === csrfTokenFromRequest) { csrfTokenVerified = true }
// If this is a POST request and the CSRF Token in the POST request matches
// the cookie we have already verified is the one we have set, then the token is verified!
const csrfTokenVerified = req.method === 'POST' && csrfToken === req.body.csrfToken
req.options.csrfToken = csrfToken
req.options.csrfTokenVerified = csrfTokenVerified
return
}
}
if (!csrfTokenFromCookie) {
// If no csrfToken - because it's not been set yet, or because the hash doesn't match
// (e.g. because it's been modifed or because the secret has changed) create a new token.
csrfTokenFromCookie = randomBytes(32).toString('hex')
const newCsrfTokenCookie = `${csrfTokenFromCookie}|${createHash('sha256').update(`${csrfTokenFromCookie}${secret}`).digest('hex')}`
cookie.set(res, cookies.csrfToken.name, newCsrfTokenCookie, cookies.csrfToken.options)
}
return { csrfToken: csrfTokenFromCookie, csrfTokenVerified }
// If no csrfToken from cookie - because it's not been set yet,
// or because the hash doesn't match (e.g. because it's been modifed or because the secret has changed)
// create a new token.
const csrfToken = randomBytes(32).toString('hex')
const csrfTokenHash = createHash('sha256').update(`${csrfToken}${secret}`).digest('hex')
const csrfTokenCookie = `${csrfToken}|${csrfTokenHash}`
cookie.set(res, cookies.csrfToken.name, csrfTokenCookie, cookies.csrfToken.options)
req.options.csrfToken = csrfToken
}

View File

@@ -59,14 +59,16 @@ export default async function oAuthCallback (req) {
try {
// Handle OAuth v1.x
const {
oauth_token: oauthToken, oauth_verifier: oauthVerifier
} = req.query
const tokens = await client.getOAuthAccessToken(oauthToken, null, oauthVerifier)
// eslint-disable-next-line camelcase
const { oauth_token, oauth_verifier } = req.query
// eslint-disable-next-line camelcase
const { token_secret } = await client.getOAuthRequestToken(provider.params)
const tokens = await client.getOAuthAccessToken(oauth_token, token_secret, oauth_verifier)
const profileData = await client.get(
provider.profileUrl,
tokens.accessToken,
tokens.refreshToken
tokens.oauth_token,
tokens.oauth_token_secret
)
return getProfile({ profileData, tokens, provider })
@@ -89,6 +91,10 @@ export default async function oAuthCallback (req) {
* expires_in?: string | Date | null
* refresh_token?: string
* id_token?: string
* token?: string
* token_secret?: string
* tokenSecret?: string
* params?: any
* }
* provider: import("../..").Provider
* user?: object

View File

@@ -54,23 +54,36 @@ export default function oAuthClient (provider) {
const originalGetOAuth1AccessToken = oauth1Client.getOAuthAccessToken.bind(oauth1Client)
oauth1Client.getOAuthAccessToken = (...args) => {
return new Promise((resolve, reject) => {
originalGetOAuth1AccessToken(...args, (error, accessToken, refreshToken, results) => {
// eslint-disable-next-line camelcase
originalGetOAuth1AccessToken(...args, (error, oauth_token, oauth_token_secret, params) => {
if (error) {
return reject(error)
}
resolve({ accessToken, refreshToken, results })
resolve({
// TODO: Remove, this is only kept for backward compativility
// These are not in the OAuth 1.x spec
accessToken: oauth_token,
refreshToken: oauth_token_secret,
results: params,
oauth_token,
oauth_token_secret,
params
})
})
})
}
const originalGetOAuthRequestToken = oauth1Client.getOAuthRequestToken.bind(oauth1Client)
oauth1Client.getOAuthRequestToken = (...args) => {
oauth1Client.getOAuthRequestToken = (params = {}) => {
return new Promise((resolve, reject) => {
originalGetOAuthRequestToken(...args, (error, oauthToken) => {
// eslint-disable-next-line camelcase
originalGetOAuthRequestToken(params, (error, oauth_token, oauth_token_secret, params) => {
if (error) {
return reject(error)
}
resolve(oauthToken)
resolve({ oauth_token, oauth_token_secret, params })
})
})
}

View File

@@ -16,7 +16,8 @@ const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
export async function handleCallback (req, res) {
const { cookies, provider, baseUrl, basePath } = req.options
try {
if (!provider.protection.includes('pkce')) { // Provider does not support PKCE, nothing to do.
// Provider does not support PKCE, nothing to do.
if (!provider.protection?.includes('pkce')) {
return
}
@@ -50,7 +51,7 @@ export async function handleCallback (req, res) {
export async function handleSignin (req, res) {
const { cookies, provider, baseUrl, basePath } = req.options
try {
if (!provider.protection.includes('pkce')) { // Provider does not support PKCE, nothing to do.
if (!provider.protection?.includes('pkce')) { // Provider does not support PKCE, nothing to do.
return
}
// Started login flow, add generated pkce to req.options and (encrypted) code_verifier to a cookie

View File

@@ -12,11 +12,12 @@ import { OAuthCallbackError } from '../../../lib/errors'
export async function handleCallback (req, res) {
const { csrfToken, provider, baseUrl, basePath } = req.options
try {
if (!provider.protection.includes('state')) { // Provider does not support state, nothing to do.
// Provider does not support state, nothing to do.
if (!provider.protection?.includes('state')) {
return
}
const { state } = req.query
const state = req.query.state || req.body.state
const expectedState = createHash('sha256').update(csrfToken).digest('hex')
logger.debug(
@@ -41,7 +42,7 @@ export async function handleCallback (req, res) {
export async function handleSignin (req, res) {
const { provider, baseUrl, basePath, csrfToken } = req.options
try {
if (![provider.protection].flat().includes('state')) { // Provider does not support state, nothing to do.
if (!provider.protection?.includes('state')) { // Provider does not support state, nothing to do.
return
}

View File

@@ -5,13 +5,17 @@ import logger from '../../../lib/logger'
export default async function getAuthorizationUrl (req) {
const { provider } = req.options
delete req.query?.nextauth
const params = {
...provider.authorizationParams,
...req.query
}
const client = oAuthClient(provider)
if (provider.version?.startsWith('2.')) {
delete req.query?.nextauth
// Handle OAuth v2.x
let url = client.getAuthorizeUrl({
...provider.authorizationParams,
...req.query,
...params,
redirect_uri: provider.callbackUrl,
scope: provider.scope
})
@@ -34,8 +38,12 @@ export default async function getAuthorizationUrl (req) {
}
try {
const oAuthToken = await client.getOAuthRequestToken()
const url = `${provider.authorizationUrl}?oauth_token=${oAuthToken}`
const tokens = await client.getOAuthRequestToken(params)
const url = `${provider.authorizationUrl}?${new URLSearchParams({
oauth_token: tokens.oauth_token,
oauth_token_secret: tokens.oauth_token_secret,
...tokens.params
})}`
logger.debug('GET_AUTHORIZATION_URL', url)
return url
} catch (error) {

View File

@@ -52,7 +52,7 @@ export default function error ({ baseUrl, basePath, error = 'default', res }) {
}
}
const { statusCode, heading, message, signin } = errors[error.toLowerCase()]
const { statusCode, heading, message, signin } = errors[error.toLowerCase()] ?? errors.default
res.status(statusCode)

View File

@@ -11,6 +11,10 @@ https://docs.microsoft.com/en-us/linkedin/shared/authentication/authorization-co
https://www.linkedin.com/developers/apps/
From the Auth tab get the client ID and client secret. On the same tab, add redirect URLs such as http://localhost:3000/api/auth/callback/linkedin so LinkedIn can correctly redirect back to your application. Finally, head over to the Products tab and enable the "Sign In with LinkedIn" product. The LinkedIn team will review and approve your request before you can test it out.
![image](https://user-images.githubusercontent.com/330396/114429603-68195600-9b72-11eb-8311-62e58383c42b.png)
## Example
```js