Compare commits

...

19 Commits

Author SHA1 Message Date
Balázs Orbán
8d73daf343 update lock file 2022-08-01 13:53:04 +02:00
Balázs Orbán
3470b582a1 fix: normalize email 2022-08-01 13:50:21 +02:00
Balázs Orbán
6d127e33fc chore: update lock file 2022-07-25 13:06:18 +02:00
Balázs Orbán
29db75ad28 chore(release): bump version [skip ci] 2022-07-25 12:31:40 +02:00
Balázs Orbán
d348ca1dc1 fix: reduce logger.error context 2022-07-25 11:14:49 +02:00
Balázs Orbán
d53e1ea6c4 chore: gitignore v4 files 2022-07-25 11:13:44 +02:00
Balázs Orbán
e701342b1a update package-lock.json 2022-07-05 13:49:23 +02:00
Balázs Orbán
8a133bf5fd fix: don't render email in email's HTML body 2022-07-05 13:47:28 +02:00
Balázs Orbán
35a3ea6620 fix: handle invalid email 2022-07-01 12:42:20 +02:00
Balázs Orbán
289800fbb4 chore: bump version 2022-06-23 12:05:39 +02:00
Sylvain Bellone
28eccc3e64 fix: ReferenceError: defaultCookies is not defined (#4711) 2022-06-23 10:50:28 +02:00
Balázs Orbán
e16bf939a9 chore: bump version 2022-06-20 10:07:04 +02:00
Balázs Orbán
9b078c92b2 fix: don't show error on relative callbackUrl 2022-06-20 10:05:36 +02:00
Balázs Orbán
87f6f576b1 fix: handle invalid callbackUrl 2022-06-10 15:11:41 +02:00
Balázs Orbán
50584bdc4c chore: bump version 2022-04-26 12:22:45 +02:00
Balázs Orbán
b4429235c0 fix: more strict default callback url handling 2022-04-26 12:22:11 +02:00
Balázs Orbán
e1b297d06d fix: update default callbacks.redirect 2022-04-14 11:32:16 +02:00
Balázs Orbán
ab764e3793 chore: bump release 2022-03-15 22:50:13 +01:00
Balázs Orbán
c8941e4b3e fix: remove action from bad request response 2022-03-15 22:45:10 +01:00
10 changed files with 272 additions and 206 deletions

7
.gitignore vendored
View File

@@ -61,4 +61,9 @@ app/yarn.lock
/prisma/migrations /prisma/migrations
# Tests # Tests
/coverage /coverage
# v4
packages
apps
docs/providers.json

5
package-lock.json generated
View File

@@ -1,11 +1,12 @@
{ {
"name": "next-auth", "name": "next-auth",
"version": "0.0.0-semantically-released", "version": "3.29.10",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"version": "0.0.0-semantically-released", "name": "next-auth",
"version": "3.29.10",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",

View File

@@ -1,6 +1,6 @@
{ {
"name": "next-auth", "name": "next-auth",
"version": "0.0.0-semantically-released", "version": "3.29.10",
"description": "Authentication for Next.js", "description": "Authentication for Next.js",
"homepage": "https://next-auth.js.org", "homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth.git", "repository": "https://github.com/nextauthjs/next-auth.git",

View File

@@ -43,7 +43,7 @@ const sendVerificationRequest = ({
}, },
(error) => { (error) => {
if (error) { if (error) {
logger.error("SEND_VERIFICATION_EMAIL_ERROR", email, error) logger.error("SEND_VERIFICATION_EMAIL_ERROR", error)
return reject(new Error("SEND_VERIFICATION_EMAIL_ERROR", error)) return reject(new Error("SEND_VERIFICATION_EMAIL_ERROR", error))
} }
return resolve() return resolve()
@@ -53,12 +53,11 @@ const sendVerificationRequest = ({
} }
// Email HTML body // Email HTML body
const html = ({ url, site, email }) => { const html = ({ url, site }) => {
// Insert invisible space into domains and email address to prevent both the // Insert invisible space into domains to prevent the
// email address and the domain from being turned into a hyperlink by email // the domain from being turned into a hyperlink by email
// clients like Outlook and Apple mail, as this is confusing because it seems // clients like Outlook and Apple mail, as this is confusing because it seems
// like they are supposed to click on their email address to sign in. // like they are supposed to click it to sign in.
const escapedEmail = `${email.replace(/\./g, "​.")}`
const escapedSite = `${site.replace(/\./g, "​.")}` const escapedSite = `${site.replace(/\./g, "​.")}`
// Some simple styling options // Some simple styling options
@@ -73,17 +72,12 @@ const html = ({ url, site, email }) => {
<body style="background: ${backgroundColor};"> <body style="background: ${backgroundColor};">
<table width="100%" border="0" cellspacing="0" cellpadding="0"> <table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr> <tr>
<td align="center" style="padding: 10px 0px 20px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};"> <td align="center" style="padding: 10px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
<strong>${escapedSite}</strong> Sign in to <strong>${escapedSite}</strong>
</td> </td>
</tr> </tr>
</table> </table>
<table width="100%" border="0" cellspacing="20" cellpadding="0" style="background: ${mainBackgroundColor}; max-width: 600px; margin: auto; border-radius: 10px;"> <table width="100%" border="0" cellspacing="20" cellpadding="0" style="background: ${mainBackgroundColor}; max-width: 600px; margin: auto; border-radius: 10px;">
<tr>
<td align="center" style="padding: 10px 0px 0px 0px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
Sign in as <strong>${escapedEmail}</strong>
</td>
</tr>
<tr> <tr>
<td align="center" style="padding: 20px 0;"> <td align="center" style="padding: 20px 0;">
<table border="0" cellspacing="0" cellpadding="0"> <table border="0" cellspacing="0" cellpadding="0">

View File

@@ -21,6 +21,16 @@ if (!process.env.NEXTAUTH_URL) {
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set") logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
} }
function isValidHttpUrl(url, baseUrl) {
try {
return /^https?:/.test(
new URL(url, url.startsWith("/") ? baseUrl : undefined).protocol
)
} catch {
return false
}
}
/** /**
* @param {import("next").NextApiRequest} req * @param {import("next").NextApiRequest} req
* @param {import("next").NextApiResponse} res * @param {import("next").NextApiResponse} res
@@ -71,6 +81,23 @@ async function NextAuthHandler(req, res, userOptions) {
...userOptions.cookies, ...userOptions.cookies,
} }
const errorPage = userOptions.pages?.error ?? `${baseUrl}${basePath}/error`
const callbackUrlParam = req.query?.callbackUrl
if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, baseUrl)) {
return res.redirect(`${errorPage}?error=Configuration`)
}
const { callbackUrl: defaultCallbackUrl } = cookie.defaultCookies(
userOptions.useSecureCookies ?? baseUrl.startsWith("https://")
)
const callbackUrlCookie =
req.cookies?.[cookies?.callbackUrl?.name ?? defaultCallbackUrl.name]
if (callbackUrlCookie && !isValidHttpUrl(callbackUrlCookie, baseUrl)) {
return res.redirect(`${errorPage}?error=Configuration`)
}
const secret = createSecret({ userOptions, basePath, baseUrl }) const secret = createSecret({ userOptions, basePath, baseUrl })
const providers = parseProviders({ const providers = parseProviders({
@@ -280,7 +307,9 @@ async function NextAuthHandler(req, res, userOptions) {
} }
return res return res
.status(400) .status(400)
.end(`Error: HTTP ${req.method} is not supported for ${req.url}`) .end(
`Error: This action with HTTP ${req.method} is not supported by NextAuth.js`
)
}) })
} }

View File

@@ -8,115 +8,115 @@
* As only partial functionlity is required, only the code we need has been incorporated here * As only partial functionlity is required, only the code we need has been incorporated here
* (with fixes for specific issues) to keep dependancy size down. * (with fixes for specific issues) to keep dependancy size down.
*/ */
export function set (res, name, value, options = {}) { export function set(res, name, value, options = {}) {
const stringValue = const stringValue =
typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value) typeof value === "object" ? "j:" + JSON.stringify(value) : String(value)
if ('maxAge' in options) { if ("maxAge" in options) {
options.expires = new Date(Date.now() + options.maxAge) options.expires = new Date(Date.now() + options.maxAge)
options.maxAge /= 1000 options.maxAge /= 1000
} }
// Preserve any existing cookies that have already been set in the same session // Preserve any existing cookies that have already been set in the same session
let setCookieHeader = res.getHeader('Set-Cookie') || [] let setCookieHeader = res.getHeader("Set-Cookie") || []
// If not an array (i.e. a string with a single cookie) convert it into an array // If not an array (i.e. a string with a single cookie) convert it into an array
if (!Array.isArray(setCookieHeader)) { if (!Array.isArray(setCookieHeader)) {
setCookieHeader = [setCookieHeader] setCookieHeader = [setCookieHeader]
} }
setCookieHeader.push(_serialize(name, String(stringValue), options)) setCookieHeader.push(_serialize(name, String(stringValue), options))
res.setHeader('Set-Cookie', setCookieHeader) res.setHeader("Set-Cookie", setCookieHeader)
} }
function _serialize (name, val, options) { function _serialize(name, val, options) {
const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/ // eslint-disable-line no-control-regex const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/ // eslint-disable-line no-control-regex
const opt = options || {} const opt = options || {}
const enc = opt.encode || encodeURIComponent const enc = opt.encode || encodeURIComponent
if (typeof enc !== 'function') { if (typeof enc !== "function") {
throw new TypeError('option encode is invalid') throw new TypeError("option encode is invalid")
} }
if (!fieldContentRegExp.test(name)) { if (!fieldContentRegExp.test(name)) {
throw new TypeError('argument name is invalid') throw new TypeError("argument name is invalid")
} }
const value = enc(val) const value = enc(val)
if (value && !fieldContentRegExp.test(value)) { if (value && !fieldContentRegExp.test(value)) {
throw new TypeError('argument val is invalid') throw new TypeError("argument val is invalid")
} }
let str = name + '=' + value let str = name + "=" + value
if (opt.maxAge != null) { if (opt.maxAge != null) {
const maxAge = opt.maxAge - 0 const maxAge = opt.maxAge - 0
if (isNaN(maxAge) || !isFinite(maxAge)) { if (isNaN(maxAge) || !isFinite(maxAge)) {
throw new TypeError('option maxAge is invalid') throw new TypeError("option maxAge is invalid")
} }
str += '; Max-Age=' + Math.floor(maxAge) str += "; Max-Age=" + Math.floor(maxAge)
} }
if (opt.domain) { if (opt.domain) {
if (!fieldContentRegExp.test(opt.domain)) { if (!fieldContentRegExp.test(opt.domain)) {
throw new TypeError('option domain is invalid') throw new TypeError("option domain is invalid")
} }
str += '; Domain=' + opt.domain str += "; Domain=" + opt.domain
} }
if (opt.path) { if (opt.path) {
if (!fieldContentRegExp.test(opt.path)) { if (!fieldContentRegExp.test(opt.path)) {
throw new TypeError('option path is invalid') throw new TypeError("option path is invalid")
} }
str += '; Path=' + opt.path str += "; Path=" + opt.path
} else { } else {
str += '; Path=/' str += "; Path=/"
} }
if (opt.expires) { if (opt.expires) {
let expires = opt.expires let expires = opt.expires
if (typeof opt.expires.toUTCString === 'function') { if (typeof opt.expires.toUTCString === "function") {
expires = opt.expires.toUTCString() expires = opt.expires.toUTCString()
} else { } else {
const dateExpires = new Date(opt.expires) const dateExpires = new Date(opt.expires)
expires = dateExpires.toUTCString() expires = dateExpires.toUTCString()
} }
str += '; Expires=' + expires str += "; Expires=" + expires
} }
if (opt.httpOnly) { if (opt.httpOnly) {
str += '; HttpOnly' str += "; HttpOnly"
} }
if (opt.secure) { if (opt.secure) {
str += '; Secure' str += "; Secure"
} }
if (opt.sameSite) { if (opt.sameSite) {
const sameSite = const sameSite =
typeof opt.sameSite === 'string' typeof opt.sameSite === "string"
? opt.sameSite.toLowerCase() ? opt.sameSite.toLowerCase()
: opt.sameSite : opt.sameSite
switch (sameSite) { switch (sameSite) {
case true: case true:
str += '; SameSite=Strict' str += "; SameSite=Strict"
break break
case 'lax': case "lax":
str += '; SameSite=Lax' str += "; SameSite=Lax"
break break
case 'strict': case "strict":
str += '; SameSite=Strict' str += "; SameSite=Strict"
break break
case 'none': case "none":
str += '; SameSite=None' str += "; SameSite=None"
break break
default: default:
throw new TypeError('option sameSite is invalid') throw new TypeError("option sameSite is invalid")
} }
} }
@@ -134,46 +134,47 @@ function _serialize (name, val, options) {
* @TODO Review cookie settings (names, options) * @TODO Review cookie settings (names, options)
* @return {import("types").CookiesOptions} * @return {import("types").CookiesOptions}
*/ */
export function defaultCookies (useSecureCookies) { export function defaultCookies(useSecureCookies) {
const cookiePrefix = useSecureCookies ? '__Secure-' : '' const cookiePrefix = useSecureCookies ? "__Secure-" : ""
return { return {
// default cookie options // default cookie options
sessionToken: { sessionToken: {
name: `${cookiePrefix}next-auth.session-token`, name: `${cookiePrefix}next-auth.session-token`,
options: { options: {
httpOnly: true, httpOnly: true,
sameSite: 'lax', sameSite: "lax",
path: '/', path: "/",
secure: useSecureCookies secure: useSecureCookies,
} },
}, },
callbackUrl: { callbackUrl: {
name: `${cookiePrefix}next-auth.callback-url`, name: `${cookiePrefix}next-auth.callback-url`,
options: { options: {
sameSite: 'lax', httpOnly: true,
path: '/', sameSite: "lax",
secure: useSecureCookies path: "/",
} secure: useSecureCookies,
},
}, },
csrfToken: { csrfToken: {
// Default to __Host- for CSRF token for additional protection if using useSecureCookies // Default to __Host- for CSRF token for additional protection if using useSecureCookies
// NB: The `__Host-` prefix is stricter than the `__Secure-` prefix. // NB: The `__Host-` prefix is stricter than the `__Secure-` prefix.
name: `${useSecureCookies ? '__Host-' : ''}next-auth.csrf-token`, name: `${useSecureCookies ? "__Host-" : ""}next-auth.csrf-token`,
options: { options: {
httpOnly: true, httpOnly: true,
sameSite: 'lax', sameSite: "lax",
path: '/', path: "/",
secure: useSecureCookies secure: useSecureCookies,
} },
}, },
pkceCodeVerifier: { pkceCodeVerifier: {
name: `${cookiePrefix}next-auth.pkce.code_verifier`, name: `${cookiePrefix}next-auth.pkce.code_verifier`,
options: { options: {
httpOnly: true, httpOnly: true,
sameSite: 'lax', sameSite: "lax",
path: '/', path: "/",
secure: useSecureCookies secure: useSecureCookies,
} },
} },
} }
} }

View File

@@ -15,7 +15,7 @@
* @return {Promise<boolean|never>} Return `true` (or a modified JWT) to allow sign in * @return {Promise<boolean|never>} Return `true` (or a modified JWT) to allow sign in
* Return `false` to deny access * Return `false` to deny access
*/ */
export async function signIn () { export async function signIn() {
return true return true
} }
@@ -28,10 +28,9 @@ export async function signIn () {
* @param {string} baseUrl Default base URL of site (can be used as fallback) * @param {string} baseUrl Default base URL of site (can be used as fallback)
* @return {Promise<string>} URL the client will be redirect to * @return {Promise<string>} URL the client will be redirect to
*/ */
export async function redirect (url, baseUrl) { export async function redirect(url, baseUrl) {
if (url.startsWith(baseUrl)) { if (url.startsWith("/")) return `${baseUrl}${url}`
return url else if (new URL(url).origin === baseUrl) return url
}
return baseUrl return baseUrl
} }
@@ -43,7 +42,7 @@ export async function redirect (url, baseUrl) {
* @param {object} token JSON Web Token (if enabled) * @param {object} token JSON Web Token (if enabled)
* @return {Promise<object>} Session that will be returned to the client * @return {Promise<object>} Session that will be returned to the client
*/ */
export async function session (session) { export async function session(session) {
return session return session
} }
@@ -59,6 +58,6 @@ export async function session (session) {
* @param {object} oAuthProfile OAuth profile - only available on sign in * @param {object} oAuthProfile OAuth profile - only available on sign in
* @return {Promise<object>} JSON Web Token that will be saved * @return {Promise<object>} JSON Web Token that will be saved
*/ */
export async function jwt (token) { export async function jwt(token) {
return token return token
} }

View File

@@ -30,6 +30,7 @@ export default async function oAuthCallback(req) {
provider.id, provider.id,
code code
) )
logger.debug("OAUTH_CALLBACK_HANDLER_ERROR", req.body)
throw error throw error
} }
} }
@@ -62,7 +63,7 @@ export default async function oAuthCallback(req) {
return getProfile({ profileData, provider, tokens, user }) return getProfile({ profileData, provider, tokens, user })
} catch (error) { } catch (error) {
logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", error, provider.id, code) logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", error, provider.id)
throw error throw error
} }
} }
@@ -74,7 +75,11 @@ export default async function oAuthCallback(req) {
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
const { token_secret } = await client.getOAuthRequestToken(provider.params) const { token_secret } = await client.getOAuthRequestToken(provider.params)
const tokens = await client.getOAuthAccessToken(oauth_token, token_secret, oauth_verifier) const tokens = await client.getOAuthAccessToken(
oauth_token,
token_secret,
oauth_verifier
)
const profileData = await client.get( const profileData = await client.get(
provider.profileUrl, provider.profileUrl,
tokens.oauth_token, tokens.oauth_token,
@@ -143,11 +148,11 @@ async function getProfile({ profileData, tokens, provider, user }) {
// If we didn't get a response either there was a problem with the provider // If we didn't get a response either there was a problem with the provider
// response *or* the user cancelled the action with the provider. // response *or* the user cancelled the action with the provider.
// //
// Unfortuately, we can't tell which - at least not in a way that works for // Unfortunately, we can't tell which - at least not in a way that works for
// all providers, so we return an empty object; the user should then be // all providers, so we return an empty object; the user should then be
// redirected back to the sign up page. We log the error to help developers // redirected back to the sign up page. We log the error to help developers
// who might be trying to debug this when configuring a new provider. // who might be trying to debug this when configuring a new provider.
logger.error("OAUTH_PARSE_PROFILE_ERROR", exception, profileData) logger.error("OAUTH_PARSE_PROFILE_ERROR", exception)
return { return {
profile: null, profile: null,
account: null, account: null,

View File

@@ -1,7 +1,7 @@
import { OAuth, OAuth2 } from 'oauth' import { OAuth, OAuth2 } from "oauth"
import querystring from 'querystring' import querystring from "querystring"
import logger from '../../../lib/logger' import logger from "../../../lib/logger"
import { sign as jwtSign } from 'jsonwebtoken' import { sign as jwtSign } from "jsonwebtoken"
/** /**
* @TODO Refactor to remove dependancy on 'oauth' package * @TODO Refactor to remove dependancy on 'oauth' package
@@ -9,8 +9,8 @@ import { sign as jwtSign } from 'jsonwebtoken'
* would be easier to maintain if all the code was native to next-auth. * would be easier to maintain if all the code was native to next-auth.
* @param {import("types/providers").OAuthConfig} provider * @param {import("types/providers").OAuthConfig} provider
*/ */
export default function oAuthClient (provider) { export default function oAuthClient(provider) {
if (provider.version?.startsWith('2.')) { if (provider.version?.startsWith("2.")) {
// Handle OAuth v2.x // Handle OAuth v2.x
const authorizationUrl = new URL(provider.authorizationUrl) const authorizationUrl = new URL(provider.authorizationUrl)
const basePath = authorizationUrl.origin const basePath = authorizationUrl.origin
@@ -34,9 +34,9 @@ export default function oAuthClient (provider) {
provider.accessTokenUrl, provider.accessTokenUrl,
provider.clientId, provider.clientId,
provider.clientSecret, provider.clientSecret,
provider.version || '1.0', provider.version || "1.0",
provider.callbackUrl, provider.callbackUrl,
provider.encoding || 'HMAC-SHA1' provider.encoding || "HMAC-SHA1"
) )
// Promisify get() and getOAuth2AccessToken() for OAuth1 // Promisify get() and getOAuth2AccessToken() for OAuth1
@@ -51,40 +51,48 @@ export default function oAuthClient (provider) {
}) })
}) })
} }
const originalGetOAuth1AccessToken = oauth1Client.getOAuthAccessToken.bind(oauth1Client) const originalGetOAuth1AccessToken =
oauth1Client.getOAuthAccessToken.bind(oauth1Client)
oauth1Client.getOAuthAccessToken = (...args) => { oauth1Client.getOAuthAccessToken = (...args) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
originalGetOAuth1AccessToken(...args, (error, oauth_token, oauth_token_secret, params) => { originalGetOAuth1AccessToken(
if (error) { ...args,
return reject(error) (error, oauth_token, oauth_token_secret, params) => {
if (error) {
return reject(error)
}
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,
})
} }
)
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) const originalGetOAuthRequestToken =
oauth1Client.getOAuthRequestToken.bind(oauth1Client)
oauth1Client.getOAuthRequestToken = (params = {}) => { oauth1Client.getOAuthRequestToken = (params = {}) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
originalGetOAuthRequestToken(params, (error, oauth_token, oauth_token_secret, params) => { originalGetOAuthRequestToken(
if (error) { params,
return reject(error) (error, oauth_token, oauth_token_secret, params) => {
if (error) {
return reject(error)
}
resolve({ oauth_token, oauth_token_secret, params })
} }
resolve({ oauth_token, oauth_token_secret, params }) )
})
}) })
} }
return oauth1Client return oauth1Client
@@ -104,103 +112,112 @@ export default function oAuthClient (provider) {
* @param {import("types/providers").OAuthConfig} provider * @param {import("types/providers").OAuthConfig} provider
* @param {string | undefined} codeVerifier * @param {string | undefined} codeVerifier
*/ */
async function getOAuth2AccessToken (code, provider, codeVerifier) { async function getOAuth2AccessToken(code, provider, codeVerifier) {
const url = provider.accessTokenUrl const url = provider.accessTokenUrl
const params = { ...provider.params } const params = { ...provider.params }
const headers = { ...provider.headers } const headers = { ...provider.headers }
const codeParam = (params.grant_type === 'refresh_token') ? 'refresh_token' : 'code' const codeParam =
params.grant_type === "refresh_token" ? "refresh_token" : "code"
if (!params[codeParam]) { params[codeParam] = code } if (!params[codeParam]) {
params[codeParam] = code
}
if (!params.client_id) { params.client_id = provider.clientId } if (!params.client_id) {
params.client_id = provider.clientId
}
// For Apple the client secret must be generated on-the-fly. // For Apple the client secret must be generated on-the-fly.
// Using the properties in clientSecret to create a JWT. // Using the properties in clientSecret to create a JWT.
if (provider.id === 'apple' && typeof provider.clientSecret === 'object') { if (provider.id === "apple" && typeof provider.clientSecret === "object") {
const { keyId, teamId, privateKey } = provider.clientSecret const { keyId, teamId, privateKey } = provider.clientSecret
const clientSecret = jwtSign({ const clientSecret = jwtSign(
iss: teamId, {
iat: Math.floor(Date.now() / 1000), iss: teamId,
exp: Math.floor(Date.now() / 1000) + (86400 * 180), // 6 months iat: Math.floor(Date.now() / 1000),
aud: 'https://appleid.apple.com', exp: Math.floor(Date.now() / 1000) + 86400 * 180, // 6 months
sub: provider.clientId aud: "https://appleid.apple.com",
}, sub: provider.clientId,
// Automatically convert \\n into \n if found in private key. If the key },
// is passed in an environment variable \n can get escaped as \\n // Automatically convert \\n into \n if found in private key. If the key
privateKey.replace(/\\n/g, '\n'), // is passed in an environment variable \n can get escaped as \\n
{ algorithm: 'ES256', keyid: keyId } privateKey.replace(/\\n/g, "\n"),
{ algorithm: "ES256", keyid: keyId }
) )
params.client_secret = clientSecret params.client_secret = clientSecret
} else { } else {
params.client_secret = provider.clientSecret params.client_secret = provider.clientSecret
} }
if (!params.redirect_uri) { params.redirect_uri = provider.callbackUrl } if (!params.redirect_uri) {
params.redirect_uri = provider.callbackUrl
if (!headers['Content-Type']) { headers['Content-Type'] = 'application/x-www-form-urlencoded' }
// Added as a fix to accomodate change in Twitch OAuth API
if (!headers['Client-ID']) { headers['Client-ID'] = provider.clientId }
// Added as a fix for Reddit Authentication
if (provider.id === 'reddit') {
headers.Authorization = 'Basic ' + Buffer.from((provider.clientId + ':' + provider.clientSecret)).toString('base64')
} }
if (provider.id === 'identity-server4' && !headers.Authorization) { if (!headers["Content-Type"]) {
headers["Content-Type"] = "application/x-www-form-urlencoded"
}
// Added as a fix to accomodate change in Twitch OAuth API
if (!headers["Client-ID"]) {
headers["Client-ID"] = provider.clientId
}
// Added as a fix for Reddit Authentication
if (provider.id === "reddit") {
headers.Authorization =
"Basic " +
Buffer.from(provider.clientId + ":" + provider.clientSecret).toString(
"base64"
)
}
if (provider.id === "identity-server4" && !headers.Authorization) {
headers.Authorization = `Bearer ${code}` headers.Authorization = `Bearer ${code}`
} }
if (provider.protection.includes('pkce')) { if (provider.protection.includes("pkce")) {
params.code_verifier = codeVerifier params.code_verifier = codeVerifier
} }
const postData = querystring.stringify(params) const postData = querystring.stringify(params)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this._request( this._request("POST", url, headers, postData, null, (error, data) => {
'POST', if (error) {
url, logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", error)
headers, return reject(error)
postData, }
null,
(error, data, response) => { let raw
if (error) { try {
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, data, response) // As of http://tools.ietf.org/html/draft-ietf-oauth-v2-07
// responses should be in JSON
raw = JSON.parse(data)
} catch {
// However both Facebook + Github currently use rev05 of the spec and neither
// seem to specify a content-type correctly in their response headers. :(
// Clients of these services suffer a minor performance cost.
raw = querystring.parse(data)
}
let accessToken
if (provider.id === "slack") {
const { ok, error } = raw
if (!ok) {
return reject(error) return reject(error)
} }
let raw accessToken = raw.authed_user.access_token
try { } else {
// As of http://tools.ietf.org/html/draft-ietf-oauth-v2-07 accessToken = raw.access_token
// responses should be in JSON
raw = JSON.parse(data)
} catch {
// However both Facebook + Github currently use rev05 of the spec and neither
// seem to specify a content-type correctly in their response headers. :(
// Clients of these services suffer a minor performance cost.
raw = querystring.parse(data)
}
let accessToken
if (provider.id === 'slack') {
const { ok, error } = raw
if (!ok) {
return reject(error)
}
accessToken = raw.authed_user.access_token
} else {
accessToken = raw.access_token
}
resolve({
accessToken,
accessTokenExpires: null,
refreshToken: raw.refresh_token,
idToken: raw.id_token,
...raw
})
} }
)
resolve({
accessToken,
accessTokenExpires: null,
refreshToken: raw.refresh_token,
idToken: raw.id_token,
...raw,
})
})
}) })
} }
@@ -213,60 +230,69 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
* @param {string} accessToken * @param {string} accessToken
* @param {any} results * @param {any} results
*/ */
async function getOAuth2 (provider, accessToken, results) { async function getOAuth2(provider, accessToken, results) {
let url = provider.profileUrl let url = provider.profileUrl
let httpMethod = 'GET' let httpMethod = "GET"
const headers = { ...provider.headers } const headers = { ...provider.headers }
if (this._useAuthorizationHeaderForGET) { if (this._useAuthorizationHeaderForGET) {
headers.Authorization = this.buildAuthHeader(accessToken) headers.Authorization = this.buildAuthHeader(accessToken)
// Mail.ru & vk.com require 'access_token' as URL request parameter // Mail.ru & vk.com require 'access_token' as URL request parameter
if (['mailru', 'vk'].includes(provider.id)) { if (["mailru", "vk"].includes(provider.id)) {
const safeAccessTokenURL = new URL(url) const safeAccessTokenURL = new URL(url)
safeAccessTokenURL.searchParams.append('access_token', accessToken) safeAccessTokenURL.searchParams.append("access_token", accessToken)
url = safeAccessTokenURL.href url = safeAccessTokenURL.href
} }
// This line is required for Twitch // This line is required for Twitch
if (provider.id === 'twitch') { if (provider.id === "twitch") {
headers['Client-ID'] = provider.clientId headers["Client-ID"] = provider.clientId
} }
accessToken = null accessToken = null
} }
if (provider.id === 'bungie') { if (provider.id === "bungie") {
url = prepareProfileUrl({ provider, url, results }) url = prepareProfileUrl({ provider, url, results })
} }
/** Dropbox requires POST instead of GET /** Dropbox requires POST instead of GET
* Read more: https://www.dropbox.com/developers/reference/auth-types#user * Read more: https://www.dropbox.com/developers/reference/auth-types#user
*/ */
if (provider.id === 'dropbox') { if (provider.id === "dropbox") {
httpMethod = 'POST' httpMethod = "POST"
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this._request(httpMethod, url, headers, null, accessToken, (error, profileData) => { this._request(
if (error) { httpMethod,
return reject(error) url,
headers,
null,
accessToken,
(error, profileData) => {
if (error) {
return reject(error)
}
resolve(profileData)
} }
resolve(profileData) )
})
}) })
} }
/** Bungie needs special handling */ /** Bungie needs special handling */
function prepareProfileUrl ({ provider, url, results }) { function prepareProfileUrl({ provider, url, results }) {
if (!results.membership_id) { if (!results.membership_id) {
// internal error // internal error
// @TODO: handle better // @TODO: handle better
throw new Error('Expected membership_id to be passed.') throw new Error("Expected membership_id to be passed.")
} }
if (!provider.headers?.['X-API-Key']) { if (!provider.headers?.["X-API-Key"]) {
throw new Error('The Bungie provider requires the X-API-Key option to be present in "headers".') throw new Error(
'The Bungie provider requires the X-API-Key option to be present in "headers".'
)
} }
return url.replace('{membershipId}', results.membership_id) return url.replace("{membershipId}", results.membership_id)
} }

View File

@@ -8,14 +8,8 @@ import adapterErrorHandler from "../../adapters/error-handler"
* @param {import("types/internals").NextAuthResponse} res * @param {import("types/internals").NextAuthResponse} res
*/ */
export default async function signin(req, res) { export default async function signin(req, res) {
const { const { provider, baseUrl, basePath, adapter, callbacks, logger } =
provider, req.options
baseUrl,
basePath,
adapter,
callbacks,
logger,
} = req.options
if (!provider.type) { if (!provider.type) {
return res.status(500).end(`Error: Type not specified for ${provider.name}`) return res.status(500).end(`Error: Type not specified for ${provider.name}`)
@@ -44,7 +38,19 @@ export default async function signin(req, res) {
// according to RFC 2821, but in practice this causes more problems than // according to RFC 2821, but in practice this causes more problems than
// it solves. We treat email addresses as all lower case. If anyone // it solves. We treat email addresses as all lower case. If anyone
// complains about this we can make strict RFC 2821 compliance an option. // complains about this we can make strict RFC 2821 compliance an option.
const email = req.body.email?.toLowerCase() ?? null let email = req.body.email?.toLowerCase() ?? null
if (!email) {
return res.redirect(`${baseUrl}${basePath}/error?error=EmailSignin`)
}
// Get the first two elements only,
// separated by `@` from user input.
let [local, domain] = email.split("@")
// The part before "@" can contain a ","
// but we remove it on the domain part
domain = domain.split(",")[0]
email = `${local}@${domain}`
// If is an existing user return a user object (otherwise use placeholder) // If is an existing user return a user object (otherwise use placeholder)
const profile = (await getUserByEmail(email)) || { email } const profile = (await getUserByEmail(email)) || { email }