mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d73daf343 | ||
|
|
3470b582a1 | ||
|
|
6d127e33fc | ||
|
|
29db75ad28 | ||
|
|
d348ca1dc1 | ||
|
|
d53e1ea6c4 | ||
|
|
e701342b1a | ||
|
|
8a133bf5fd | ||
|
|
35a3ea6620 | ||
|
|
289800fbb4 | ||
|
|
28eccc3e64 | ||
|
|
e16bf939a9 | ||
|
|
9b078c92b2 | ||
|
|
87f6f576b1 | ||
|
|
50584bdc4c | ||
|
|
b4429235c0 | ||
|
|
e1b297d06d | ||
|
|
ab764e3793 | ||
|
|
c8941e4b3e |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -61,4 +61,9 @@ app/yarn.lock
|
||||
/prisma/migrations
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/coverage
|
||||
|
||||
# v4
|
||||
packages
|
||||
apps
|
||||
docs/providers.json
|
||||
5
package-lock.json
generated
5
package-lock.json
generated
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "next-auth",
|
||||
"version": "0.0.0-semantically-released",
|
||||
"version": "3.29.10",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "0.0.0-semantically-released",
|
||||
"name": "next-auth",
|
||||
"version": "3.29.10",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-auth",
|
||||
"version": "0.0.0-semantically-released",
|
||||
"version": "3.29.10",
|
||||
"description": "Authentication for Next.js",
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/next-auth.git",
|
||||
|
||||
@@ -43,7 +43,7 @@ const sendVerificationRequest = ({
|
||||
},
|
||||
(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 resolve()
|
||||
@@ -53,12 +53,11 @@ const sendVerificationRequest = ({
|
||||
}
|
||||
|
||||
// Email HTML body
|
||||
const html = ({ url, site, email }) => {
|
||||
// Insert invisible space into domains and email address to prevent both the
|
||||
// email address and the domain from being turned into a hyperlink by email
|
||||
const html = ({ url, site }) => {
|
||||
// Insert invisible space into domains to prevent the
|
||||
// the domain from being turned into a hyperlink by email
|
||||
// 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.
|
||||
const escapedEmail = `${email.replace(/\./g, "​.")}`
|
||||
// like they are supposed to click it to sign in.
|
||||
const escapedSite = `${site.replace(/\./g, "​.")}`
|
||||
|
||||
// Some simple styling options
|
||||
@@ -73,17 +72,12 @@ const html = ({ url, site, email }) => {
|
||||
<body style="background: ${backgroundColor};">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 10px 0px 20px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
||||
<strong>${escapedSite}</strong>
|
||||
<td align="center" style="padding: 10px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
||||
Sign in to <strong>${escapedSite}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<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>
|
||||
<td align="center" style="padding: 20px 0;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
|
||||
@@ -21,6 +21,16 @@ if (!process.env.NEXTAUTH_URL) {
|
||||
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").NextApiResponse} res
|
||||
@@ -71,6 +81,23 @@ async function NextAuthHandler(req, res, userOptions) {
|
||||
...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 providers = parseProviders({
|
||||
@@ -280,7 +307,9 @@ async function NextAuthHandler(req, res, userOptions) {
|
||||
}
|
||||
return res
|
||||
.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`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,115 +8,115 @@
|
||||
* 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.
|
||||
*/
|
||||
export function set (res, name, value, options = {}) {
|
||||
export function set(res, name, value, options = {}) {
|
||||
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.maxAge /= 1000
|
||||
}
|
||||
|
||||
// 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 (!Array.isArray(setCookieHeader)) {
|
||||
setCookieHeader = [setCookieHeader]
|
||||
}
|
||||
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 opt = options || {}
|
||||
const enc = opt.encode || encodeURIComponent
|
||||
|
||||
if (typeof enc !== 'function') {
|
||||
throw new TypeError('option encode is invalid')
|
||||
if (typeof enc !== "function") {
|
||||
throw new TypeError("option encode is invalid")
|
||||
}
|
||||
|
||||
if (!fieldContentRegExp.test(name)) {
|
||||
throw new TypeError('argument name is invalid')
|
||||
throw new TypeError("argument name is invalid")
|
||||
}
|
||||
|
||||
const value = enc(val)
|
||||
|
||||
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) {
|
||||
const maxAge = opt.maxAge - 0
|
||||
|
||||
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 (!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 (!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 {
|
||||
str += '; Path=/'
|
||||
str += "; Path=/"
|
||||
}
|
||||
|
||||
if (opt.expires) {
|
||||
let expires = opt.expires
|
||||
if (typeof opt.expires.toUTCString === 'function') {
|
||||
if (typeof opt.expires.toUTCString === "function") {
|
||||
expires = opt.expires.toUTCString()
|
||||
} else {
|
||||
const dateExpires = new Date(opt.expires)
|
||||
expires = dateExpires.toUTCString()
|
||||
}
|
||||
str += '; Expires=' + expires
|
||||
str += "; Expires=" + expires
|
||||
}
|
||||
|
||||
if (opt.httpOnly) {
|
||||
str += '; HttpOnly'
|
||||
str += "; HttpOnly"
|
||||
}
|
||||
|
||||
if (opt.secure) {
|
||||
str += '; Secure'
|
||||
str += "; Secure"
|
||||
}
|
||||
|
||||
if (opt.sameSite) {
|
||||
const sameSite =
|
||||
typeof opt.sameSite === 'string'
|
||||
typeof opt.sameSite === "string"
|
||||
? opt.sameSite.toLowerCase()
|
||||
: opt.sameSite
|
||||
|
||||
switch (sameSite) {
|
||||
case true:
|
||||
str += '; SameSite=Strict'
|
||||
str += "; SameSite=Strict"
|
||||
break
|
||||
case 'lax':
|
||||
str += '; SameSite=Lax'
|
||||
case "lax":
|
||||
str += "; SameSite=Lax"
|
||||
break
|
||||
case 'strict':
|
||||
str += '; SameSite=Strict'
|
||||
case "strict":
|
||||
str += "; SameSite=Strict"
|
||||
break
|
||||
case 'none':
|
||||
str += '; SameSite=None'
|
||||
case "none":
|
||||
str += "; SameSite=None"
|
||||
break
|
||||
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)
|
||||
* @return {import("types").CookiesOptions}
|
||||
*/
|
||||
export function defaultCookies (useSecureCookies) {
|
||||
const cookiePrefix = useSecureCookies ? '__Secure-' : ''
|
||||
export function defaultCookies(useSecureCookies) {
|
||||
const cookiePrefix = useSecureCookies ? "__Secure-" : ""
|
||||
return {
|
||||
// default cookie options
|
||||
sessionToken: {
|
||||
name: `${cookiePrefix}next-auth.session-token`,
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
secure: useSecureCookies
|
||||
}
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
secure: useSecureCookies,
|
||||
},
|
||||
},
|
||||
callbackUrl: {
|
||||
name: `${cookiePrefix}next-auth.callback-url`,
|
||||
options: {
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
secure: useSecureCookies
|
||||
}
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
secure: useSecureCookies,
|
||||
},
|
||||
},
|
||||
csrfToken: {
|
||||
// Default to __Host- for CSRF token for additional protection if using useSecureCookies
|
||||
// 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: {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
secure: useSecureCookies
|
||||
}
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
secure: useSecureCookies,
|
||||
},
|
||||
},
|
||||
pkceCodeVerifier: {
|
||||
name: `${cookiePrefix}next-auth.pkce.code_verifier`,
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
secure: useSecureCookies
|
||||
}
|
||||
}
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
secure: useSecureCookies,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* @return {Promise<boolean|never>} Return `true` (or a modified JWT) to allow sign in
|
||||
* Return `false` to deny access
|
||||
*/
|
||||
export async function signIn () {
|
||||
export async function signIn() {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -28,10 +28,9 @@ export async function signIn () {
|
||||
* @param {string} baseUrl Default base URL of site (can be used as fallback)
|
||||
* @return {Promise<string>} URL the client will be redirect to
|
||||
*/
|
||||
export async function redirect (url, baseUrl) {
|
||||
if (url.startsWith(baseUrl)) {
|
||||
return url
|
||||
}
|
||||
export async function redirect(url, baseUrl) {
|
||||
if (url.startsWith("/")) return `${baseUrl}${url}`
|
||||
else if (new URL(url).origin === baseUrl) return url
|
||||
return baseUrl
|
||||
}
|
||||
|
||||
@@ -43,7 +42,7 @@ export async function redirect (url, baseUrl) {
|
||||
* @param {object} token JSON Web Token (if enabled)
|
||||
* @return {Promise<object>} Session that will be returned to the client
|
||||
*/
|
||||
export async function session (session) {
|
||||
export async function session(session) {
|
||||
return session
|
||||
}
|
||||
|
||||
@@ -59,6 +58,6 @@ export async function session (session) {
|
||||
* @param {object} oAuthProfile OAuth profile - only available on sign in
|
||||
* @return {Promise<object>} JSON Web Token that will be saved
|
||||
*/
|
||||
export async function jwt (token) {
|
||||
export async function jwt(token) {
|
||||
return token
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export default async function oAuthCallback(req) {
|
||||
provider.id,
|
||||
code
|
||||
)
|
||||
logger.debug("OAUTH_CALLBACK_HANDLER_ERROR", req.body)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -62,7 +63,7 @@ export default async function oAuthCallback(req) {
|
||||
|
||||
return getProfile({ profileData, provider, tokens, user })
|
||||
} 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
|
||||
}
|
||||
}
|
||||
@@ -74,7 +75,11 @@ export default async function oAuthCallback(req) {
|
||||
|
||||
// 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 tokens = await client.getOAuthAccessToken(
|
||||
oauth_token,
|
||||
token_secret,
|
||||
oauth_verifier
|
||||
)
|
||||
const profileData = await client.get(
|
||||
provider.profileUrl,
|
||||
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
|
||||
// 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
|
||||
// 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.
|
||||
logger.error("OAUTH_PARSE_PROFILE_ERROR", exception, profileData)
|
||||
logger.error("OAUTH_PARSE_PROFILE_ERROR", exception)
|
||||
return {
|
||||
profile: null,
|
||||
account: null,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { OAuth, OAuth2 } from 'oauth'
|
||||
import querystring from 'querystring'
|
||||
import logger from '../../../lib/logger'
|
||||
import { sign as jwtSign } from 'jsonwebtoken'
|
||||
import { OAuth, OAuth2 } from "oauth"
|
||||
import querystring from "querystring"
|
||||
import logger from "../../../lib/logger"
|
||||
import { sign as jwtSign } from "jsonwebtoken"
|
||||
|
||||
/**
|
||||
* @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.
|
||||
* @param {import("types/providers").OAuthConfig} provider
|
||||
*/
|
||||
export default function oAuthClient (provider) {
|
||||
if (provider.version?.startsWith('2.')) {
|
||||
export default function oAuthClient(provider) {
|
||||
if (provider.version?.startsWith("2.")) {
|
||||
// Handle OAuth v2.x
|
||||
const authorizationUrl = new URL(provider.authorizationUrl)
|
||||
const basePath = authorizationUrl.origin
|
||||
@@ -34,9 +34,9 @@ export default function oAuthClient (provider) {
|
||||
provider.accessTokenUrl,
|
||||
provider.clientId,
|
||||
provider.clientSecret,
|
||||
provider.version || '1.0',
|
||||
provider.version || "1.0",
|
||||
provider.callbackUrl,
|
||||
provider.encoding || 'HMAC-SHA1'
|
||||
provider.encoding || "HMAC-SHA1"
|
||||
)
|
||||
|
||||
// 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) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line camelcase
|
||||
originalGetOAuth1AccessToken(...args, (error, oauth_token, oauth_token_secret, params) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
originalGetOAuth1AccessToken(
|
||||
...args,
|
||||
(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 = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line camelcase
|
||||
originalGetOAuthRequestToken(params, (error, oauth_token, oauth_token_secret, params) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
originalGetOAuthRequestToken(
|
||||
params,
|
||||
(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
|
||||
@@ -104,103 +112,112 @@ export default function oAuthClient (provider) {
|
||||
* @param {import("types/providers").OAuthConfig} provider
|
||||
* @param {string | undefined} codeVerifier
|
||||
*/
|
||||
async function getOAuth2AccessToken (code, provider, codeVerifier) {
|
||||
async function getOAuth2AccessToken(code, provider, codeVerifier) {
|
||||
const url = provider.accessTokenUrl
|
||||
const params = { ...provider.params }
|
||||
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.
|
||||
// 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 clientSecret = jwtSign({
|
||||
iss: teamId,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + (86400 * 180), // 6 months
|
||||
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
|
||||
privateKey.replace(/\\n/g, '\n'),
|
||||
{ algorithm: 'ES256', keyid: keyId }
|
||||
const clientSecret = jwtSign(
|
||||
{
|
||||
iss: teamId,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + 86400 * 180, // 6 months
|
||||
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
|
||||
privateKey.replace(/\\n/g, "\n"),
|
||||
{ algorithm: "ES256", keyid: keyId }
|
||||
)
|
||||
params.client_secret = clientSecret
|
||||
} else {
|
||||
params.client_secret = provider.clientSecret
|
||||
}
|
||||
|
||||
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 (!params.redirect_uri) {
|
||||
params.redirect_uri = provider.callbackUrl
|
||||
}
|
||||
|
||||
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}`
|
||||
}
|
||||
|
||||
if (provider.protection.includes('pkce')) {
|
||||
if (provider.protection.includes("pkce")) {
|
||||
params.code_verifier = codeVerifier
|
||||
}
|
||||
|
||||
const postData = querystring.stringify(params)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this._request(
|
||||
'POST',
|
||||
url,
|
||||
headers,
|
||||
postData,
|
||||
null,
|
||||
(error, data, response) => {
|
||||
if (error) {
|
||||
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, data, response)
|
||||
this._request("POST", url, headers, postData, null, (error, data) => {
|
||||
if (error) {
|
||||
logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", error)
|
||||
return reject(error)
|
||||
}
|
||||
|
||||
let raw
|
||||
try {
|
||||
// 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)
|
||||
}
|
||||
|
||||
let raw
|
||||
try {
|
||||
// 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)
|
||||
}
|
||||
|
||||
accessToken = raw.authed_user.access_token
|
||||
} else {
|
||||
accessToken = raw.access_token
|
||||
}
|
||||
|
||||
resolve({
|
||||
accessToken,
|
||||
accessTokenExpires: null,
|
||||
refreshToken: raw.refresh_token,
|
||||
idToken: raw.id_token,
|
||||
...raw
|
||||
})
|
||||
accessToken = raw.authed_user.access_token
|
||||
} else {
|
||||
accessToken = raw.access_token
|
||||
}
|
||||
)
|
||||
|
||||
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 {any} results
|
||||
*/
|
||||
async function getOAuth2 (provider, accessToken, results) {
|
||||
async function getOAuth2(provider, accessToken, results) {
|
||||
let url = provider.profileUrl
|
||||
let httpMethod = 'GET'
|
||||
let httpMethod = "GET"
|
||||
const headers = { ...provider.headers }
|
||||
|
||||
if (this._useAuthorizationHeaderForGET) {
|
||||
headers.Authorization = this.buildAuthHeader(accessToken)
|
||||
|
||||
// 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)
|
||||
safeAccessTokenURL.searchParams.append('access_token', accessToken)
|
||||
safeAccessTokenURL.searchParams.append("access_token", accessToken)
|
||||
url = safeAccessTokenURL.href
|
||||
}
|
||||
|
||||
// This line is required for Twitch
|
||||
if (provider.id === 'twitch') {
|
||||
headers['Client-ID'] = provider.clientId
|
||||
if (provider.id === "twitch") {
|
||||
headers["Client-ID"] = provider.clientId
|
||||
}
|
||||
accessToken = null
|
||||
}
|
||||
|
||||
if (provider.id === 'bungie') {
|
||||
if (provider.id === "bungie") {
|
||||
url = prepareProfileUrl({ provider, url, results })
|
||||
}
|
||||
|
||||
/** Dropbox requires POST instead of GET
|
||||
* Read more: https://www.dropbox.com/developers/reference/auth-types#user
|
||||
*/
|
||||
if (provider.id === 'dropbox') {
|
||||
httpMethod = 'POST'
|
||||
if (provider.id === "dropbox") {
|
||||
httpMethod = "POST"
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this._request(httpMethod, url, headers, null, accessToken, (error, profileData) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
this._request(
|
||||
httpMethod,
|
||||
url,
|
||||
headers,
|
||||
null,
|
||||
accessToken,
|
||||
(error, profileData) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
resolve(profileData)
|
||||
}
|
||||
resolve(profileData)
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/** Bungie needs special handling */
|
||||
function prepareProfileUrl ({ provider, url, results }) {
|
||||
function prepareProfileUrl({ provider, url, results }) {
|
||||
if (!results.membership_id) {
|
||||
// internal error
|
||||
// @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']) {
|
||||
throw new Error('The Bungie provider requires the X-API-Key option to be present in "headers".')
|
||||
if (!provider.headers?.["X-API-Key"]) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -8,14 +8,8 @@ import adapterErrorHandler from "../../adapters/error-handler"
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export default async function signin(req, res) {
|
||||
const {
|
||||
provider,
|
||||
baseUrl,
|
||||
basePath,
|
||||
adapter,
|
||||
callbacks,
|
||||
logger,
|
||||
} = req.options
|
||||
const { provider, baseUrl, basePath, adapter, callbacks, logger } =
|
||||
req.options
|
||||
|
||||
if (!provider.type) {
|
||||
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
|
||||
// it solves. We treat email addresses as all lower case. If anyone
|
||||
// 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)
|
||||
const profile = (await getUserByEmail(email)) || { email }
|
||||
|
||||
Reference in New Issue
Block a user