@auth/core
Advanced tools
Comparing version 0.35.2 to 0.35.3
@@ -42,3 +42,3 @@ /** | ||
export declare function encode<Payload = JWT>(params: JWTEncodeParams<Payload>): Promise<string>; | ||
/** Decodes a Auth.js issued JWT. */ | ||
/** Decodes an Auth.js issued JWT. */ | ||
export declare function decode<Payload = JWT>(params: JWTDecodeParams): Promise<Payload | null>; | ||
@@ -45,0 +45,0 @@ type GetTokenParamsBase = { |
@@ -61,3 +61,3 @@ /** | ||
} | ||
/** Decodes a Auth.js issued JWT. */ | ||
/** Decodes an Auth.js issued JWT. */ | ||
export async function decode(params) { | ||
@@ -64,0 +64,0 @@ const { token, secret, salt } = params; |
@@ -5,3 +5,3 @@ // TODO: Make this file smaller | ||
import { handleOAuth } from "./oauth/callback.js"; | ||
import { handleState } from "./oauth/checks.js"; | ||
import { state } from "./oauth/checks.js"; | ||
import { createHash } from "../../utils/web.js"; | ||
@@ -19,12 +19,22 @@ import { assertInternalOptionsWebAuthn, verifyAuthenticate, verifyRegister, } from "../../utils/webauthn-utils.js"; | ||
// Use body if the response mode is set to form_post. For all other cases, use query | ||
const payload = provider.authorization?.url.searchParams.get("response_mode") === | ||
const params = provider.authorization?.url.searchParams.get("response_mode") === | ||
"form_post" | ||
? body | ||
: query; | ||
const { proxyRedirect, randomState } = handleState(payload, provider, options.isOnRedirectProxy); | ||
if (proxyRedirect) { | ||
logger.debug("proxy redirect", { proxyRedirect, randomState }); | ||
return { redirect: proxyRedirect }; | ||
// If we have a state and we are on a redirect proxy, we try to parse it | ||
// and see if it contains a valid origin to redirect to. If it does, we | ||
// redirect the user to that origin with the original state. | ||
if (options.isOnRedirectProxy && params?.state) { | ||
// NOTE: We rely on the state being encrypted using a shared secret | ||
// between the proxy and the original server. | ||
const parsedState = await state.decode(params.state, options); | ||
const shouldRedirect = parsedState?.origin && | ||
new URL(parsedState.origin).origin !== options.url.origin; | ||
if (shouldRedirect) { | ||
const proxyRedirect = `${parsedState.origin}?${new URLSearchParams(params)}`; | ||
logger.debug("Proxy redirecting to", proxyRedirect); | ||
return { redirect: proxyRedirect, cookies }; | ||
} | ||
} | ||
const authorizationResult = await handleOAuth(payload, request.cookies, options, randomState); | ||
const authorizationResult = await handleOAuth(params, request.cookies, options); | ||
if (authorizationResult.cookies.length) { | ||
@@ -31,0 +41,0 @@ cookies.push(...authorizationResult.cookies); |
@@ -13,3 +13,3 @@ import * as o from "oauth4webapi"; | ||
*/ | ||
export declare function handleOAuth(query: RequestInternal["query"], cookies: RequestInternal["cookies"], options: InternalOptions<"oauth" | "oidc">, randomState?: string): Promise<{ | ||
export declare function handleOAuth(params: RequestInternal["query"], cookies: RequestInternal["cookies"], options: InternalOptions<"oauth" | "oidc">): Promise<{ | ||
profile: Profile; | ||
@@ -16,0 +16,0 @@ cookies: Cookie[]; |
@@ -14,3 +14,3 @@ import * as checks from "./checks.js"; | ||
*/ | ||
export async function handleOAuth(query, cookies, options, randomState) { | ||
export async function handleOAuth(params, cookies, options) { | ||
const { logger, provider } = options; | ||
@@ -45,4 +45,4 @@ let as; | ||
const resCookies = []; | ||
const state = await checks.state.use(cookies, resCookies, options, randomState); | ||
const codeGrantParams = o.validateAuthResponse(as, client, new URLSearchParams(query), provider.checks.includes("state") ? state : o.skipStateCheck); | ||
const state = await checks.state.use(cookies, resCookies, options); | ||
const codeGrantParams = o.validateAuthResponse(as, client, new URLSearchParams(params), provider.checks.includes("state") ? state : o.skipStateCheck); | ||
/** https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2.1 */ | ||
@@ -49,0 +49,0 @@ if (o.isOAuth2Error(codeGrantParams)) { |
@@ -1,8 +0,10 @@ | ||
import type { CookiesOptions, InternalOptions, RequestInternal, User } from "../../../../types.js"; | ||
import type { InternalOptions, RequestInternal, User } from "../../../../types.js"; | ||
import type { Cookie } from "../../../utils/cookie.js"; | ||
import type { OAuthConfigInternal } from "../../../../providers/oauth.js"; | ||
import type { WebAuthnProviderType } from "../../../../providers/webauthn.js"; | ||
/** Returns a signed cookie. */ | ||
export declare function signCookie(type: keyof CookiesOptions, value: string, maxAge: number, options: InternalOptions<"oauth" | "oidc" | WebAuthnProviderType>, data?: any): Promise<Cookie>; | ||
/** | ||
* @see https://www.rfc-editor.org/rfc/rfc7636 | ||
* @see https://danielfett.de/2020/05/16/pkce-vs-nonce-equivalent-or-not/#pkce | ||
*/ | ||
export declare const pkce: { | ||
/** Creates a PKCE code challenge and verifier pair. The verifier in stored in the cookie. */ | ||
create(options: InternalOptions<"oauth">): Promise<{ | ||
@@ -16,15 +18,16 @@ cookie: Cookie; | ||
* An error is thrown if the code_verifier is missing or invalid. | ||
* @see https://www.rfc-editor.org/rfc/rfc7636 | ||
* @see https://danielfett.de/2020/05/16/pkce-vs-nonce-equivalent-or-not/#pkce | ||
*/ | ||
use(cookies: RequestInternal["cookies"], resCookies: Cookie[], options: InternalOptions<"oauth">): Promise<string | undefined>; | ||
use: (cookies: RequestInternal["cookies"], resCookies: Cookie[], options: InternalOptions<"oidc">) => Promise<string | undefined>; | ||
}; | ||
export declare function decodeState(value: string): { | ||
/** If defined, a redirect proxy is being used to support multiple OAuth apps with a single callback URL */ | ||
interface EncodedState { | ||
origin?: string; | ||
/** Random value for CSRF protection */ | ||
random: string; | ||
} | undefined; | ||
} | ||
/** | ||
* @see https://www.rfc-editor.org/rfc/rfc6749#section-10.12 | ||
* @see https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1 | ||
*/ | ||
export declare const state: { | ||
create(options: InternalOptions<"oauth">, data?: object): Promise<{ | ||
/** Creates a state cookie with an optionally encoded body. */ | ||
create(options: InternalOptions<"oauth">, origin?: string): Promise<{ | ||
cookie: Cookie; | ||
@@ -37,6 +40,6 @@ value: string; | ||
* An error is thrown if the state is missing or invalid. | ||
* @see https://www.rfc-editor.org/rfc/rfc6749#section-10.12 | ||
* @see https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1 | ||
*/ | ||
use(cookies: RequestInternal["cookies"], resCookies: Cookie[], options: InternalOptions<"oauth">, paramRandom?: string): Promise<string | undefined>; | ||
use: (cookies: RequestInternal["cookies"], resCookies: Cookie[], options: InternalOptions<"oidc">) => Promise<string | undefined>; | ||
/** Decodes the state. If it could not be decoded, it throws an error. */ | ||
decode(state: string, options: InternalOptions): Promise<EncodedState>; | ||
}; | ||
@@ -55,19 +58,8 @@ export declare const nonce: { | ||
*/ | ||
use(cookies: RequestInternal["cookies"], resCookies: Cookie[], options: InternalOptions<"oidc">): Promise<string | undefined>; | ||
use: (cookies: RequestInternal["cookies"], resCookies: Cookie[], options: InternalOptions<"oidc">) => Promise<string | undefined>; | ||
}; | ||
/** | ||
* When the authorization flow contains a state, we check if it's a redirect proxy | ||
* and if so, we return the random state and the original redirect URL. | ||
*/ | ||
export declare function handleState(query: RequestInternal["query"], provider: OAuthConfigInternal<any>, isOnRedirectProxy: InternalOptions["isOnRedirectProxy"]): { | ||
randomState: string | undefined; | ||
proxyRedirect?: undefined; | ||
} | { | ||
randomState: string | undefined; | ||
proxyRedirect: string | undefined; | ||
}; | ||
type WebAuthnChallengeCookie = { | ||
interface WebAuthnChallengePayload { | ||
challenge: string; | ||
registerData?: User; | ||
}; | ||
} | ||
export declare const webauthnChallenge: { | ||
@@ -77,8 +69,6 @@ create(options: InternalOptions<WebAuthnProviderType>, challenge: string, registerData?: User): Promise<{ | ||
}>; | ||
/** | ||
* Returns challenge if present, | ||
*/ | ||
use(options: InternalOptions<WebAuthnProviderType>, cookies: RequestInternal["cookies"], resCookies: Cookie[]): Promise<WebAuthnChallengeCookie>; | ||
/** Returns WebAuthn challenge if present. */ | ||
use(options: InternalOptions<WebAuthnProviderType>, cookies: RequestInternal["cookies"], resCookies: Cookie[]): Promise<WebAuthnChallengePayload>; | ||
}; | ||
export {}; | ||
//# sourceMappingURL=checks.d.ts.map |
@@ -1,28 +0,80 @@ | ||
import * as jose from "jose"; | ||
import * as o from "oauth4webapi"; | ||
import { InvalidCheck } from "../../../../errors.js"; | ||
// NOTE: We use the default JWT methods here because they encrypt/decrypt the payload, not just sign it. | ||
import { decode, encode } from "../../../../jwt.js"; | ||
/** Returns a signed cookie. */ | ||
export async function signCookie(type, value, maxAge, options, data) { | ||
const COOKIE_TTL = 60 * 15; // 15 minutes | ||
/** Returns a cookie with a JWT encrypted payload. */ | ||
async function sealCookie(name, payload, options) { | ||
const { cookies, logger } = options; | ||
logger.debug(`CREATE_${type.toUpperCase()}`, { value, maxAge }); | ||
const cookie = cookies[name]; | ||
const expires = new Date(); | ||
expires.setTime(expires.getTime() + maxAge * 1000); | ||
const token = { value }; | ||
if (type === "state" && data) | ||
token.data = data; | ||
const name = cookies[type].name; | ||
return { | ||
name, | ||
value: await encode({ ...options.jwt, maxAge, token, salt: name }), | ||
options: { ...cookies[type].options, expires }, | ||
expires.setTime(expires.getTime() + COOKIE_TTL * 1000); | ||
logger.debug(`CREATE_${name.toUpperCase()}`, { | ||
name: cookie.name, | ||
payload, | ||
COOKIE_TTL, | ||
expires, | ||
}); | ||
const encoded = await encode({ | ||
...options.jwt, | ||
maxAge: COOKIE_TTL, | ||
token: { value: payload }, | ||
salt: cookie.name, | ||
}); | ||
const cookieOptions = { ...cookie.options, expires }; | ||
return { name: cookie.name, value: encoded, options: cookieOptions }; | ||
} | ||
async function parseCookie(name, value, options) { | ||
try { | ||
const { logger, cookies, jwt } = options; | ||
logger.debug(`PARSE_${name.toUpperCase()}`, { cookie: value }); | ||
if (!value) | ||
throw new InvalidCheck(`${name} cookie was missing`); | ||
const parsed = await decode({ | ||
...jwt, | ||
token: value, | ||
salt: cookies[name].name, | ||
}); | ||
if (parsed?.value) | ||
return parsed.value; | ||
throw new Error("Invalid cookie"); | ||
} | ||
catch (error) { | ||
throw new InvalidCheck(`${name} value could not be parsed`, { | ||
cause: error, | ||
}); | ||
} | ||
} | ||
function clearCookie(name, options, resCookies) { | ||
const { logger, cookies } = options; | ||
const cookie = cookies[name]; | ||
logger.debug(`CLEAR_${name.toUpperCase()}`, { cookie }); | ||
resCookies.push({ | ||
name: cookie.name, | ||
value: "", | ||
options: { ...cookies[name].options, maxAge: 0 }, | ||
}); | ||
} | ||
function useCookie(check, name) { | ||
return async function (cookies, resCookies, options) { | ||
const { provider, logger } = options; | ||
if (!provider?.checks?.includes(check)) | ||
return; | ||
const cookieValue = cookies?.[options.cookies[name].name]; | ||
logger.debug(`USE_${name.toUpperCase()}`, { value: cookieValue }); | ||
const parsed = await parseCookie(name, cookieValue, options); | ||
clearCookie(name, options, resCookies); | ||
return parsed; | ||
}; | ||
} | ||
const PKCE_MAX_AGE = 60 * 15; // 15 minutes in seconds | ||
/** | ||
* @see https://www.rfc-editor.org/rfc/rfc7636 | ||
* @see https://danielfett.de/2020/05/16/pkce-vs-nonce-equivalent-or-not/#pkce | ||
*/ | ||
export const pkce = { | ||
/** Creates a PKCE code challenge and verifier pair. The verifier in stored in the cookie. */ | ||
async create(options) { | ||
const code_verifier = o.generateRandomCodeVerifier(); | ||
const value = await o.calculatePKCECodeChallenge(code_verifier); | ||
const maxAge = PKCE_MAX_AGE; | ||
const cookie = await signCookie("pkceCodeVerifier", code_verifier, maxAge, options); | ||
const cookie = await sealCookie("pkceCodeVerifier", code_verifier, options); | ||
return { cookie, value }; | ||
@@ -34,41 +86,17 @@ }, | ||
* An error is thrown if the code_verifier is missing or invalid. | ||
* @see https://www.rfc-editor.org/rfc/rfc7636 | ||
* @see https://danielfett.de/2020/05/16/pkce-vs-nonce-equivalent-or-not/#pkce | ||
*/ | ||
async use(cookies, resCookies, options) { | ||
const { provider } = options; | ||
if (!provider?.checks?.includes("pkce")) | ||
return; | ||
const codeVerifier = cookies?.[options.cookies.pkceCodeVerifier.name]; | ||
if (!codeVerifier) | ||
throw new InvalidCheck("PKCE code_verifier cookie was missing"); | ||
const value = await decode({ | ||
...options.jwt, | ||
token: codeVerifier, | ||
salt: options.cookies.pkceCodeVerifier.name, | ||
}); | ||
if (!value?.value) | ||
throw new InvalidCheck("PKCE code_verifier value could not be parsed"); | ||
// Clear the pkce code verifier cookie after use | ||
resCookies.push({ | ||
name: options.cookies.pkceCodeVerifier.name, | ||
value: "", | ||
options: { ...options.cookies.pkceCodeVerifier.options, maxAge: 0 }, | ||
}); | ||
return value.value; | ||
}, | ||
use: useCookie("pkce", "pkceCodeVerifier"), | ||
}; | ||
const STATE_MAX_AGE = 60 * 15; // 15 minutes in seconds | ||
export function decodeState(value) { | ||
try { | ||
const decoder = new TextDecoder(); | ||
return JSON.parse(decoder.decode(jose.base64url.decode(value))); | ||
} | ||
catch { } | ||
} | ||
const encodedStateSalt = "encodedState"; | ||
/** | ||
* @see https://www.rfc-editor.org/rfc/rfc6749#section-10.12 | ||
* @see https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1 | ||
*/ | ||
export const state = { | ||
async create(options, data) { | ||
/** Creates a state cookie with an optionally encoded body. */ | ||
async create(options, origin) { | ||
const { provider } = options; | ||
if (!provider.checks.includes("state")) { | ||
if (data) { | ||
if (origin) { | ||
throw new InvalidCheck("State data was provided but the provider is not configured to use state"); | ||
@@ -78,6 +106,15 @@ } | ||
} | ||
const encodedState = jose.base64url.encode(JSON.stringify({ ...data, random: o.generateRandomState() })); | ||
const maxAge = STATE_MAX_AGE; | ||
const cookie = await signCookie("state", encodedState, maxAge, options, data); | ||
return { cookie, value: encodedState }; | ||
// IDEA: Allow the user to pass data to be stored in the state | ||
const payload = { | ||
origin, | ||
random: o.generateRandomState(), | ||
}; | ||
const value = await encode({ | ||
secret: options.jwt.secret, | ||
token: payload, | ||
salt: encodedStateSalt, | ||
maxAge: STATE_MAX_AGE, | ||
}); | ||
const cookie = await sealCookie("state", value, options); | ||
return { cookie, value }; | ||
}, | ||
@@ -88,35 +125,22 @@ /** | ||
* An error is thrown if the state is missing or invalid. | ||
* @see https://www.rfc-editor.org/rfc/rfc6749#section-10.12 | ||
* @see https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1 | ||
*/ | ||
async use(cookies, resCookies, options, paramRandom) { | ||
const { provider } = options; | ||
if (!provider.checks.includes("state")) | ||
return; | ||
const state = cookies?.[options.cookies.state.name]; | ||
if (!state) | ||
throw new InvalidCheck("State cookie was missing"); | ||
// IDEA: Let the user do something with the returned state | ||
const encodedState = await decode({ | ||
...options.jwt, | ||
token: state, | ||
salt: options.cookies.state.name, | ||
}); | ||
if (!encodedState?.value) | ||
throw new InvalidCheck("State (cookie) value could not be parsed"); | ||
const decodedState = decodeState(encodedState.value); | ||
if (!decodedState) | ||
throw new InvalidCheck("State (encoded) value could not be parsed"); | ||
if (decodedState.random !== paramRandom) | ||
throw new InvalidCheck(`Random state values did not match. Expected: ${decodedState.random}. Got: ${paramRandom}`); | ||
// Clear the state cookie after use | ||
resCookies.push({ | ||
name: options.cookies.state.name, | ||
value: "", | ||
options: { ...options.cookies.state.options, maxAge: 0 }, | ||
}); | ||
return encodedState.value; | ||
use: useCookie("state", "state"), | ||
/** Decodes the state. If it could not be decoded, it throws an error. */ | ||
async decode(state, options) { | ||
try { | ||
options.logger.debug("DECODE_STATE", { state }); | ||
const payload = await decode({ | ||
secret: options.jwt.secret, | ||
token: state, | ||
salt: encodedStateSalt, | ||
}); | ||
if (payload) | ||
return payload; | ||
throw new Error("Invalid state"); | ||
} | ||
catch (error) { | ||
throw new InvalidCheck("State could not be decoded", { cause: error }); | ||
} | ||
}, | ||
}; | ||
const NONCE_MAX_AGE = 60 * 15; // 15 minutes in seconds | ||
export const nonce = { | ||
@@ -127,4 +151,3 @@ async create(options) { | ||
const value = o.generateRandomNonce(); | ||
const maxAge = NONCE_MAX_AGE; | ||
const cookie = await signCookie("nonce", value, maxAge, options); | ||
const cookie = await sealCookie("nonce", value, options); | ||
return { cookie, value }; | ||
@@ -139,74 +162,32 @@ }, | ||
*/ | ||
async use(cookies, resCookies, options) { | ||
const { provider } = options; | ||
if (!provider?.checks?.includes("nonce")) | ||
return; | ||
const nonce = cookies?.[options.cookies.nonce.name]; | ||
if (!nonce) | ||
throw new InvalidCheck("Nonce cookie was missing"); | ||
const value = await decode({ | ||
...options.jwt, | ||
token: nonce, | ||
salt: options.cookies.nonce.name, | ||
}); | ||
if (!value?.value) | ||
throw new InvalidCheck("Nonce value could not be parsed"); | ||
// Clear the nonce cookie after use | ||
resCookies.push({ | ||
name: options.cookies.nonce.name, | ||
value: "", | ||
options: { ...options.cookies.nonce.options, maxAge: 0 }, | ||
}); | ||
return value.value; | ||
}, | ||
use: useCookie("nonce", "nonce"), | ||
}; | ||
/** | ||
* When the authorization flow contains a state, we check if it's a redirect proxy | ||
* and if so, we return the random state and the original redirect URL. | ||
*/ | ||
export function handleState(query, provider, isOnRedirectProxy) { | ||
let proxyRedirect; | ||
if (provider.redirectProxyUrl && !query?.state) { | ||
throw new InvalidCheck("Missing state in query, but required for redirect proxy"); | ||
} | ||
const state = decodeState(query?.state); | ||
const randomState = state?.random; | ||
if (isOnRedirectProxy) { | ||
if (!state?.origin) | ||
return { randomState }; | ||
proxyRedirect = `${state.origin}?${new URLSearchParams(query)}`; | ||
} | ||
return { randomState, proxyRedirect }; | ||
} | ||
const WEBAUTHN_CHALLENGE_MAX_AGE = 60 * 15; // 15 minutes in seconds | ||
const webauthnChallengeSalt = "encodedWebauthnChallenge"; | ||
export const webauthnChallenge = { | ||
async create(options, challenge, registerData) { | ||
const maxAge = WEBAUTHN_CHALLENGE_MAX_AGE; | ||
const data = { challenge, registerData }; | ||
const cookie = await signCookie("webauthnChallenge", JSON.stringify(data), maxAge, options); | ||
return { cookie }; | ||
return { | ||
cookie: await sealCookie("webauthnChallenge", await encode({ | ||
secret: options.jwt.secret, | ||
token: { challenge, registerData }, | ||
salt: webauthnChallengeSalt, | ||
maxAge: WEBAUTHN_CHALLENGE_MAX_AGE, | ||
}), options), | ||
}; | ||
}, | ||
/** | ||
* Returns challenge if present, | ||
*/ | ||
/** Returns WebAuthn challenge if present. */ | ||
async use(options, cookies, resCookies) { | ||
const challenge = cookies?.[options.cookies.webauthnChallenge.name]; | ||
if (!challenge) | ||
throw new InvalidCheck("Challenge cookie missing"); | ||
const value = await decode({ | ||
...options.jwt, | ||
token: challenge, | ||
salt: options.cookies.webauthnChallenge.name, | ||
const cookieValue = cookies?.[options.cookies.webauthnChallenge.name]; | ||
const parsed = await parseCookie("webauthnChallenge", cookieValue, options); | ||
const payload = await decode({ | ||
secret: options.jwt.secret, | ||
token: parsed, | ||
salt: webauthnChallengeSalt, | ||
}); | ||
if (!value?.value) | ||
throw new InvalidCheck("Challenge value could not be parsed"); | ||
// Clear the pkce code verifier cookie after use | ||
const cookie = { | ||
name: options.cookies.webauthnChallenge.name, | ||
value: "", | ||
options: { ...options.cookies.webauthnChallenge.options, maxAge: 0 }, | ||
}; | ||
resCookies.push(cookie); | ||
return JSON.parse(value.value); | ||
// Clear the WebAuthn challenge cookie after use | ||
clearCookie("webauthnChallenge", options, resCookies); | ||
if (!payload) | ||
throw new InvalidCheck("WebAuthn challenge was missing"); | ||
return payload; | ||
}, | ||
}; |
@@ -29,3 +29,3 @@ import * as checks from "../callback/oauth/checks.js"; | ||
redirect_uri = provider.redirectProxyUrl; | ||
data = { origin: provider.callbackUrl }; | ||
data = provider.callbackUrl; | ||
logger.debug("using redirect proxy", { redirect_uri, data }); | ||
@@ -32,0 +32,0 @@ } |
{ | ||
"name": "@auth/core", | ||
"version": "0.35.2", | ||
"version": "0.35.3", | ||
"description": "Authentication for the Web.", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
/** | ||
* <div style={{backgroundColor: "#000", display: "flex", justifyContent: "space-between", color: "#fff", padding: 16}}> | ||
* <div style={{backgroundColor: "#00a1e0", display: "flex", justifyContent: "space-between", color: "#fff", padding: 16}}> | ||
* <span>Built-in <b>Salesforce</b> integration.</span> | ||
@@ -11,3 +11,3 @@ * <a href="https://www.salesforce.com/ap/?ir=1"> | ||
*/ | ||
import type { OAuthConfig, OAuthUserConfig } from "./index.js"; | ||
import type { OIDCConfig, OIDCUserConfig } from "./index.js"; | ||
export interface SalesforceProfile extends Record<string, any> { | ||
@@ -20,4 +20,2 @@ sub: string; | ||
/** | ||
* Add Salesforce login to your page. | ||
* | ||
* ### Setup | ||
@@ -31,5 +29,5 @@ * | ||
* #### Configuration | ||
*```ts | ||
* ```ts | ||
* import { Auth } from "@auth/core" | ||
* import salesforce from "@auth/core/providers/salesforce" | ||
* import Salesforce from "@auth/core/providers/salesforce" | ||
* | ||
@@ -39,5 +37,5 @@ * const request = new Request(origin) | ||
* providers: [ | ||
* salesforce({ | ||
* clientId: salesforce_CLIENT_ID, | ||
* clientSecret: salesforce_CLIENT_SECRET, | ||
* Salesforce({ | ||
* clientId: AUTH_SALESFORCE_ID, | ||
* clientSecret: AUTH_SALESFORCE_SECRET, | ||
* }), | ||
@@ -50,18 +48,10 @@ * ], | ||
* | ||
* - [Salesforce OAuth documentation](https://help.salesforce.com/articleView?id=remoteaccess_authenticate.htm&type=5) | ||
* - [Auth0 docs](https://auth0.com/docs/authenticate) | ||
* | ||
* ### Notes | ||
* | ||
* By default, Auth.js assumes that the salesforce provider is | ||
* based on the [OAuth 2](https://www.rfc-editor.org/rfc/rfc6749.html) specification. | ||
* The Salesforce provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/salesforce.ts). To override the defaults for your use case, check out [customizing a built-in OAuth provider](https://authjs.dev/guides/configuring-oauth-providers). | ||
* | ||
* :::tip | ||
* ## Help | ||
* | ||
* The Salesforce provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/salesforce.ts). | ||
* To override the defaults for your use case, check out [customizing a built-in OAuth provider](https://authjs.dev/guides/configuring-oauth-providers). | ||
* | ||
* ::: | ||
* | ||
* :::info **Disclaimer** | ||
* | ||
* If you think you found a bug in the default configuration, you can [open an issue](https://authjs.dev/new/provider-issue). | ||
@@ -72,6 +62,4 @@ * | ||
* we might not pursue a resolution. You can ask for more help in [Discussions](https://authjs.dev/new/github-discussions). | ||
* | ||
* ::: | ||
*/ | ||
export default function Salesforce<P extends SalesforceProfile>(options: OAuthUserConfig<P>): OAuthConfig<P>; | ||
export default function Salesforce(options: OIDCUserConfig<SalesforceProfile>): OIDCConfig<SalesforceProfile>; | ||
//# sourceMappingURL=salesforce.d.ts.map |
/** | ||
* Add Salesforce login to your page. | ||
* | ||
* ### Setup | ||
@@ -12,5 +10,5 @@ * | ||
* #### Configuration | ||
*```ts | ||
* ```ts | ||
* import { Auth } from "@auth/core" | ||
* import salesforce from "@auth/core/providers/salesforce" | ||
* import Salesforce from "@auth/core/providers/salesforce" | ||
* | ||
@@ -20,5 +18,5 @@ * const request = new Request(origin) | ||
* providers: [ | ||
* salesforce({ | ||
* clientId: salesforce_CLIENT_ID, | ||
* clientSecret: salesforce_CLIENT_SECRET, | ||
* Salesforce({ | ||
* clientId: AUTH_SALESFORCE_ID, | ||
* clientSecret: AUTH_SALESFORCE_SECRET, | ||
* }), | ||
@@ -31,18 +29,10 @@ * ], | ||
* | ||
* - [Salesforce OAuth documentation](https://help.salesforce.com/articleView?id=remoteaccess_authenticate.htm&type=5) | ||
* - [Auth0 docs](https://auth0.com/docs/authenticate) | ||
* | ||
* ### Notes | ||
* | ||
* By default, Auth.js assumes that the salesforce provider is | ||
* based on the [OAuth 2](https://www.rfc-editor.org/rfc/rfc6749.html) specification. | ||
* The Salesforce provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/salesforce.ts). To override the defaults for your use case, check out [customizing a built-in OAuth provider](https://authjs.dev/guides/configuring-oauth-providers). | ||
* | ||
* :::tip | ||
* ## Help | ||
* | ||
* The Salesforce provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/salesforce.ts). | ||
* To override the defaults for your use case, check out [customizing a built-in OAuth provider](https://authjs.dev/guides/configuring-oauth-providers). | ||
* | ||
* ::: | ||
* | ||
* :::info **Disclaimer** | ||
* | ||
* If you think you found a bug in the default configuration, you can [open an issue](https://authjs.dev/new/provider-issue). | ||
@@ -53,24 +43,14 @@ * | ||
* we might not pursue a resolution. You can ask for more help in [Discussions](https://authjs.dev/new/github-discussions). | ||
* | ||
* ::: | ||
*/ | ||
export default function Salesforce(options) { | ||
const { issuer = "https://login.salesforce.com" } = options; | ||
return { | ||
id: "salesforce", | ||
name: "Salesforce", | ||
type: "oauth", | ||
authorization: `${issuer}/services/oauth2/authorize?display=page`, | ||
token: `${issuer}/services/oauth2/token`, | ||
userinfo: `${issuer}/services/oauth2/userinfo`, | ||
profile(profile) { | ||
return { | ||
id: profile.user_id, | ||
name: null, | ||
email: null, | ||
image: profile.picture, | ||
}; | ||
}, | ||
type: "oidc", | ||
issuer: "https://login.salesforce.com", | ||
idToken: false, | ||
checks: ["pkce", "state", "nonce"], | ||
style: { bg: "#00a1e0" }, | ||
options, | ||
}; | ||
} |
@@ -74,3 +74,3 @@ /** | ||
/** Decodes a Auth.js issued JWT. */ | ||
/** Decodes an Auth.js issued JWT. */ | ||
export async function decode<Payload = JWT>( | ||
@@ -77,0 +77,0 @@ params: JWTDecodeParams |
@@ -13,3 +13,3 @@ // TODO: Make this file smaller | ||
import { handleOAuth } from "./oauth/callback.js" | ||
import { handleState } from "./oauth/checks.js" | ||
import { state } from "./oauth/checks.js" | ||
import { createHash } from "../../utils/web.js" | ||
@@ -61,3 +61,3 @@ | ||
// Use body if the response mode is set to form_post. For all other cases, use query | ||
const payload = | ||
const params = | ||
provider.authorization?.url.searchParams.get("response_mode") === | ||
@@ -68,18 +68,23 @@ "form_post" | ||
const { proxyRedirect, randomState } = handleState( | ||
payload, | ||
provider, | ||
options.isOnRedirectProxy | ||
) | ||
if (proxyRedirect) { | ||
logger.debug("proxy redirect", { proxyRedirect, randomState }) | ||
return { redirect: proxyRedirect } | ||
// If we have a state and we are on a redirect proxy, we try to parse it | ||
// and see if it contains a valid origin to redirect to. If it does, we | ||
// redirect the user to that origin with the original state. | ||
if (options.isOnRedirectProxy && params?.state) { | ||
// NOTE: We rely on the state being encrypted using a shared secret | ||
// between the proxy and the original server. | ||
const parsedState = await state.decode(params.state, options) | ||
const shouldRedirect = | ||
parsedState?.origin && | ||
new URL(parsedState.origin).origin !== options.url.origin | ||
if (shouldRedirect) { | ||
const proxyRedirect = `${parsedState.origin}?${new URLSearchParams(params)}` | ||
logger.debug("Proxy redirecting to", proxyRedirect) | ||
return { redirect: proxyRedirect, cookies } | ||
} | ||
} | ||
const authorizationResult = await handleOAuth( | ||
payload, | ||
params, | ||
request.cookies, | ||
options, | ||
randomState | ||
options | ||
) | ||
@@ -86,0 +91,0 @@ |
@@ -31,6 +31,5 @@ import * as checks from "./checks.js" | ||
export async function handleOAuth( | ||
query: RequestInternal["query"], | ||
params: RequestInternal["query"], | ||
cookies: RequestInternal["cookies"], | ||
options: InternalOptions<"oauth" | "oidc">, | ||
randomState?: string | ||
options: InternalOptions<"oauth" | "oidc"> | ||
) { | ||
@@ -82,8 +81,3 @@ const { logger, provider } = options | ||
const state = await checks.state.use( | ||
cookies, | ||
resCookies, | ||
options, | ||
randomState | ||
) | ||
const state = await checks.state.use(cookies, resCookies, options) | ||
@@ -93,3 +87,3 @@ const codeGrantParams = o.validateAuthResponse( | ||
client, | ||
new URLSearchParams(query), | ||
new URLSearchParams(params), | ||
provider.checks.includes("state") ? state : o.skipStateCheck | ||
@@ -96,0 +90,0 @@ ) |
@@ -1,4 +0,5 @@ | ||
import * as jose from "jose" | ||
import * as o from "oauth4webapi" | ||
import { InvalidCheck } from "../../../../errors.js" | ||
// NOTE: We use the default JWT methods here because they encrypt/decrypt the payload, not just sign it. | ||
import { decode, encode } from "../../../../jwt.js" | ||
@@ -13,45 +14,106 @@ | ||
import type { Cookie } from "../../../utils/cookie.js" | ||
import type { OAuthConfigInternal } from "../../../../providers/oauth.js" | ||
import type { WebAuthnProviderType } from "../../../../providers/webauthn.js" | ||
interface CheckPayload { | ||
interface CookiePayload { | ||
value: string | ||
} | ||
/** Returns a signed cookie. */ | ||
export async function signCookie( | ||
type: keyof CookiesOptions, | ||
value: string, | ||
maxAge: number, | ||
options: InternalOptions<"oauth" | "oidc" | WebAuthnProviderType>, | ||
data?: any | ||
const COOKIE_TTL = 60 * 15 // 15 minutes | ||
/** Returns a cookie with a JWT encrypted payload. */ | ||
async function sealCookie( | ||
name: keyof CookiesOptions, | ||
payload: string, | ||
options: InternalOptions<"oauth" | "oidc" | WebAuthnProviderType> | ||
): Promise<Cookie> { | ||
const { cookies, logger } = options | ||
const cookie = cookies[name] | ||
const expires = new Date() | ||
expires.setTime(expires.getTime() + COOKIE_TTL * 1000) | ||
logger.debug(`CREATE_${type.toUpperCase()}`, { value, maxAge }) | ||
logger.debug(`CREATE_${name.toUpperCase()}`, { | ||
name: cookie.name, | ||
payload, | ||
COOKIE_TTL, | ||
expires, | ||
}) | ||
const expires = new Date() | ||
expires.setTime(expires.getTime() + maxAge * 1000) | ||
const token: any = { value } | ||
if (type === "state" && data) token.data = data | ||
const name = cookies[type].name | ||
return { | ||
name, | ||
value: await encode({ ...options.jwt, maxAge, token, salt: name }), | ||
options: { ...cookies[type].options, expires }, | ||
const encoded = await encode({ | ||
...options.jwt, | ||
maxAge: COOKIE_TTL, | ||
token: { value: payload } satisfies CookiePayload, | ||
salt: cookie.name, | ||
}) | ||
const cookieOptions = { ...cookie.options, expires } | ||
return { name: cookie.name, value: encoded, options: cookieOptions } | ||
} | ||
async function parseCookie( | ||
name: keyof CookiesOptions, | ||
value: string | undefined, | ||
options: InternalOptions | ||
): Promise<string> { | ||
try { | ||
const { logger, cookies, jwt } = options | ||
logger.debug(`PARSE_${name.toUpperCase()}`, { cookie: value }) | ||
if (!value) throw new InvalidCheck(`${name} cookie was missing`) | ||
const parsed = await decode<CookiePayload>({ | ||
...jwt, | ||
token: value, | ||
salt: cookies[name].name, | ||
}) | ||
if (parsed?.value) return parsed.value | ||
throw new Error("Invalid cookie") | ||
} catch (error) { | ||
throw new InvalidCheck(`${name} value could not be parsed`, { | ||
cause: error, | ||
}) | ||
} | ||
} | ||
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds | ||
function clearCookie( | ||
name: keyof CookiesOptions, | ||
options: InternalOptions, | ||
resCookies: Cookie[] | ||
) { | ||
const { logger, cookies } = options | ||
const cookie = cookies[name] | ||
logger.debug(`CLEAR_${name.toUpperCase()}`, { cookie }) | ||
resCookies.push({ | ||
name: cookie.name, | ||
value: "", | ||
options: { ...cookies[name].options, maxAge: 0 }, | ||
}) | ||
} | ||
function useCookie( | ||
check: "state" | "pkce" | "nonce", | ||
name: keyof CookiesOptions | ||
) { | ||
return async function ( | ||
cookies: RequestInternal["cookies"], | ||
resCookies: Cookie[], | ||
options: InternalOptions<"oidc"> | ||
) { | ||
const { provider, logger } = options | ||
if (!provider?.checks?.includes(check)) return | ||
const cookieValue = cookies?.[options.cookies[name].name] | ||
logger.debug(`USE_${name.toUpperCase()}`, { value: cookieValue }) | ||
const parsed = await parseCookie(name, cookieValue, options) | ||
clearCookie(name, options, resCookies) | ||
return parsed | ||
} | ||
} | ||
/** | ||
* @see https://www.rfc-editor.org/rfc/rfc7636 | ||
* @see https://danielfett.de/2020/05/16/pkce-vs-nonce-equivalent-or-not/#pkce | ||
*/ | ||
export const pkce = { | ||
/** Creates a PKCE code challenge and verifier pair. The verifier in stored in the cookie. */ | ||
async create(options: InternalOptions<"oauth">) { | ||
const code_verifier = o.generateRandomCodeVerifier() | ||
const value = await o.calculatePKCECodeChallenge(code_verifier) | ||
const maxAge = PKCE_MAX_AGE | ||
const cookie = await signCookie( | ||
"pkceCodeVerifier", | ||
code_verifier, | ||
maxAge, | ||
options | ||
) | ||
const cookie = await sealCookie("pkceCodeVerifier", code_verifier, options) | ||
return { cookie, value } | ||
@@ -63,59 +125,24 @@ }, | ||
* An error is thrown if the code_verifier is missing or invalid. | ||
* @see https://www.rfc-editor.org/rfc/rfc7636 | ||
* @see https://danielfett.de/2020/05/16/pkce-vs-nonce-equivalent-or-not/#pkce | ||
*/ | ||
async use( | ||
cookies: RequestInternal["cookies"], | ||
resCookies: Cookie[], | ||
options: InternalOptions<"oauth"> | ||
): Promise<string | undefined> { | ||
const { provider } = options | ||
use: useCookie("pkce", "pkceCodeVerifier"), | ||
} | ||
if (!provider?.checks?.includes("pkce")) return | ||
const codeVerifier = cookies?.[options.cookies.pkceCodeVerifier.name] | ||
if (!codeVerifier) | ||
throw new InvalidCheck("PKCE code_verifier cookie was missing") | ||
const value = await decode<CheckPayload>({ | ||
...options.jwt, | ||
token: codeVerifier, | ||
salt: options.cookies.pkceCodeVerifier.name, | ||
}) | ||
if (!value?.value) | ||
throw new InvalidCheck("PKCE code_verifier value could not be parsed") | ||
// Clear the pkce code verifier cookie after use | ||
resCookies.push({ | ||
name: options.cookies.pkceCodeVerifier.name, | ||
value: "", | ||
options: { ...options.cookies.pkceCodeVerifier.options, maxAge: 0 }, | ||
}) | ||
return value.value | ||
}, | ||
interface EncodedState { | ||
origin?: string | ||
random: string | ||
} | ||
const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds | ||
export function decodeState(value: string): | ||
| { | ||
/** If defined, a redirect proxy is being used to support multiple OAuth apps with a single callback URL */ | ||
origin?: string | ||
/** Random value for CSRF protection */ | ||
random: string | ||
} | ||
| undefined { | ||
try { | ||
const decoder = new TextDecoder() | ||
return JSON.parse(decoder.decode(jose.base64url.decode(value))) | ||
} catch {} | ||
} | ||
const encodedStateSalt = "encodedState" | ||
/** | ||
* @see https://www.rfc-editor.org/rfc/rfc6749#section-10.12 | ||
* @see https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1 | ||
*/ | ||
export const state = { | ||
async create(options: InternalOptions<"oauth">, data?: object) { | ||
/** Creates a state cookie with an optionally encoded body. */ | ||
async create(options: InternalOptions<"oauth">, origin?: string) { | ||
const { provider } = options | ||
if (!provider.checks.includes("state")) { | ||
if (data) { | ||
if (origin) { | ||
throw new InvalidCheck( | ||
@@ -128,15 +155,16 @@ "State data was provided but the provider is not configured to use state" | ||
const encodedState = jose.base64url.encode( | ||
JSON.stringify({ ...data, random: o.generateRandomState() }) | ||
) | ||
// IDEA: Allow the user to pass data to be stored in the state | ||
const payload = { | ||
origin, | ||
random: o.generateRandomState(), | ||
} satisfies EncodedState | ||
const value = await encode({ | ||
secret: options.jwt.secret, | ||
token: payload, | ||
salt: encodedStateSalt, | ||
maxAge: STATE_MAX_AGE, | ||
}) | ||
const cookie = await sealCookie("state", value, options) | ||
const maxAge = STATE_MAX_AGE | ||
const cookie = await signCookie( | ||
"state", | ||
encodedState, | ||
maxAge, | ||
options, | ||
data | ||
) | ||
return { cookie, value: encodedState } | ||
return { cookie, value } | ||
}, | ||
@@ -147,50 +175,21 @@ /** | ||
* An error is thrown if the state is missing or invalid. | ||
* @see https://www.rfc-editor.org/rfc/rfc6749#section-10.12 | ||
* @see https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1 | ||
*/ | ||
async use( | ||
cookies: RequestInternal["cookies"], | ||
resCookies: Cookie[], | ||
options: InternalOptions<"oauth">, | ||
paramRandom?: string | ||
): Promise<string | undefined> { | ||
const { provider } = options | ||
if (!provider.checks.includes("state")) return | ||
const state = cookies?.[options.cookies.state.name] | ||
if (!state) throw new InvalidCheck("State cookie was missing") | ||
// IDEA: Let the user do something with the returned state | ||
const encodedState = await decode<CheckPayload>({ | ||
...options.jwt, | ||
token: state, | ||
salt: options.cookies.state.name, | ||
}) | ||
if (!encodedState?.value) | ||
throw new InvalidCheck("State (cookie) value could not be parsed") | ||
const decodedState = decodeState(encodedState.value) | ||
if (!decodedState) | ||
throw new InvalidCheck("State (encoded) value could not be parsed") | ||
if (decodedState.random !== paramRandom) | ||
throw new InvalidCheck( | ||
`Random state values did not match. Expected: ${decodedState.random}. Got: ${paramRandom}` | ||
) | ||
// Clear the state cookie after use | ||
resCookies.push({ | ||
name: options.cookies.state.name, | ||
value: "", | ||
options: { ...options.cookies.state.options, maxAge: 0 }, | ||
}) | ||
return encodedState.value | ||
use: useCookie("state", "state"), | ||
/** Decodes the state. If it could not be decoded, it throws an error. */ | ||
async decode(state: string, options: InternalOptions) { | ||
try { | ||
options.logger.debug("DECODE_STATE", { state }) | ||
const payload = await decode<EncodedState>({ | ||
secret: options.jwt.secret, | ||
token: state, | ||
salt: encodedStateSalt, | ||
}) | ||
if (payload) return payload | ||
throw new Error("Invalid state") | ||
} catch (error) { | ||
throw new InvalidCheck("State could not be decoded", { cause: error }) | ||
} | ||
}, | ||
} | ||
const NONCE_MAX_AGE = 60 * 15 // 15 minutes in seconds | ||
export const nonce = { | ||
@@ -200,4 +199,3 @@ async create(options: InternalOptions<"oidc">) { | ||
const value = o.generateRandomNonce() | ||
const maxAge = NONCE_MAX_AGE | ||
const cookie = await signCookie("nonce", value, maxAge, options) | ||
const cookie = await sealCookie("nonce", value, options) | ||
return { cookie, value } | ||
@@ -212,63 +210,13 @@ }, | ||
*/ | ||
async use( | ||
cookies: RequestInternal["cookies"], | ||
resCookies: Cookie[], | ||
options: InternalOptions<"oidc"> | ||
): Promise<string | undefined> { | ||
const { provider } = options | ||
if (!provider?.checks?.includes("nonce")) return | ||
const nonce = cookies?.[options.cookies.nonce.name] | ||
if (!nonce) throw new InvalidCheck("Nonce cookie was missing") | ||
const value = await decode<CheckPayload>({ | ||
...options.jwt, | ||
token: nonce, | ||
salt: options.cookies.nonce.name, | ||
}) | ||
if (!value?.value) throw new InvalidCheck("Nonce value could not be parsed") | ||
// Clear the nonce cookie after use | ||
resCookies.push({ | ||
name: options.cookies.nonce.name, | ||
value: "", | ||
options: { ...options.cookies.nonce.options, maxAge: 0 }, | ||
}) | ||
return value.value | ||
}, | ||
use: useCookie("nonce", "nonce"), | ||
} | ||
/** | ||
* When the authorization flow contains a state, we check if it's a redirect proxy | ||
* and if so, we return the random state and the original redirect URL. | ||
*/ | ||
export function handleState( | ||
query: RequestInternal["query"], | ||
provider: OAuthConfigInternal<any>, | ||
isOnRedirectProxy: InternalOptions["isOnRedirectProxy"] | ||
) { | ||
let proxyRedirect: string | undefined | ||
const WEBAUTHN_CHALLENGE_MAX_AGE = 60 * 15 // 15 minutes in seconds | ||
if (provider.redirectProxyUrl && !query?.state) { | ||
throw new InvalidCheck( | ||
"Missing state in query, but required for redirect proxy" | ||
) | ||
} | ||
const state = decodeState(query?.state) | ||
const randomState = state?.random | ||
if (isOnRedirectProxy) { | ||
if (!state?.origin) return { randomState } | ||
proxyRedirect = `${state.origin}?${new URLSearchParams(query)}` | ||
} | ||
return { randomState, proxyRedirect } | ||
interface WebAuthnChallengePayload { | ||
challenge: string | ||
registerData?: User | ||
} | ||
const WEBAUTHN_CHALLENGE_MAX_AGE = 60 * 15 // 15 minutes in seconds | ||
type WebAuthnChallengeCookie = { challenge: string; registerData?: User } | ||
const webauthnChallengeSalt = "encodedWebauthnChallenge" | ||
export const webauthnChallenge = { | ||
@@ -280,16 +228,16 @@ async create( | ||
) { | ||
const maxAge = WEBAUTHN_CHALLENGE_MAX_AGE | ||
const data: WebAuthnChallengeCookie = { challenge, registerData } | ||
const cookie = await signCookie( | ||
"webauthnChallenge", | ||
JSON.stringify(data), | ||
maxAge, | ||
options | ||
) | ||
return { cookie } | ||
return { | ||
cookie: await sealCookie( | ||
"webauthnChallenge", | ||
await encode({ | ||
secret: options.jwt.secret, | ||
token: { challenge, registerData } satisfies WebAuthnChallengePayload, | ||
salt: webauthnChallengeSalt, | ||
maxAge: WEBAUTHN_CHALLENGE_MAX_AGE, | ||
}), | ||
options | ||
), | ||
} | ||
}, | ||
/** | ||
* Returns challenge if present, | ||
*/ | ||
/** Returns WebAuthn challenge if present. */ | ||
async use( | ||
@@ -299,26 +247,20 @@ options: InternalOptions<WebAuthnProviderType>, | ||
resCookies: Cookie[] | ||
): Promise<WebAuthnChallengeCookie> { | ||
const challenge = cookies?.[options.cookies.webauthnChallenge.name] | ||
): Promise<WebAuthnChallengePayload> { | ||
const cookieValue = cookies?.[options.cookies.webauthnChallenge.name] | ||
if (!challenge) throw new InvalidCheck("Challenge cookie missing") | ||
const parsed = await parseCookie("webauthnChallenge", cookieValue, options) | ||
const value = await decode<CheckPayload>({ | ||
...options.jwt, | ||
token: challenge, | ||
salt: options.cookies.webauthnChallenge.name, | ||
const payload = await decode<WebAuthnChallengePayload>({ | ||
secret: options.jwt.secret, | ||
token: parsed, | ||
salt: webauthnChallengeSalt, | ||
}) | ||
if (!value?.value) | ||
throw new InvalidCheck("Challenge value could not be parsed") | ||
// Clear the WebAuthn challenge cookie after use | ||
clearCookie("webauthnChallenge", options, resCookies) | ||
// Clear the pkce code verifier cookie after use | ||
const cookie = { | ||
name: options.cookies.webauthnChallenge.name, | ||
value: "", | ||
options: { ...options.cookies.webauthnChallenge.options, maxAge: 0 }, | ||
} | ||
resCookies.push(cookie) | ||
if (!payload) throw new InvalidCheck("WebAuthn challenge was missing") | ||
return JSON.parse(value.value) as WebAuthnChallengeCookie | ||
return payload | ||
}, | ||
} |
@@ -42,6 +42,6 @@ import * as checks from "../callback/oauth/checks.js" | ||
let redirect_uri: string = provider.callbackUrl | ||
let data: object | undefined | ||
let data: string | undefined | ||
if (!options.isOnRedirectProxy && provider.redirectProxyUrl) { | ||
redirect_uri = provider.redirectProxyUrl | ||
data = { origin: provider.callbackUrl } | ||
data = provider.callbackUrl | ||
logger.debug("using redirect proxy", { redirect_uri, data }) | ||
@@ -77,3 +77,3 @@ } | ||
// a random `nonce` must be used for CSRF protection. | ||
if (provider.type === "oidc") provider.checks = ["nonce"] as any | ||
if (provider.type === "oidc") provider.checks = ["nonce"] | ||
} else { | ||
@@ -80,0 +80,0 @@ const { value, cookie } = await checks.pkce.create(options) |
/** | ||
* <div style={{backgroundColor: "#000", display: "flex", justifyContent: "space-between", color: "#fff", padding: 16}}> | ||
* <div style={{backgroundColor: "#00a1e0", display: "flex", justifyContent: "space-between", color: "#fff", padding: 16}}> | ||
* <span>Built-in <b>Salesforce</b> integration.</span> | ||
@@ -11,3 +11,3 @@ * <a href="https://www.salesforce.com/ap/?ir=1"> | ||
*/ | ||
import type { OAuthConfig, OAuthUserConfig } from "./index.js" | ||
import type { OIDCConfig, OIDCUserConfig } from "./index.js" | ||
@@ -22,4 +22,2 @@ export interface SalesforceProfile extends Record<string, any> { | ||
/** | ||
* Add Salesforce login to your page. | ||
* | ||
* ### Setup | ||
@@ -33,5 +31,5 @@ * | ||
* #### Configuration | ||
*```ts | ||
* ```ts | ||
* import { Auth } from "@auth/core" | ||
* import salesforce from "@auth/core/providers/salesforce" | ||
* import Salesforce from "@auth/core/providers/salesforce" | ||
* | ||
@@ -41,5 +39,5 @@ * const request = new Request(origin) | ||
* providers: [ | ||
* salesforce({ | ||
* clientId: salesforce_CLIENT_ID, | ||
* clientSecret: salesforce_CLIENT_SECRET, | ||
* Salesforce({ | ||
* clientId: AUTH_SALESFORCE_ID, | ||
* clientSecret: AUTH_SALESFORCE_SECRET, | ||
* }), | ||
@@ -52,18 +50,10 @@ * ], | ||
* | ||
* - [Salesforce OAuth documentation](https://help.salesforce.com/articleView?id=remoteaccess_authenticate.htm&type=5) | ||
* - [Auth0 docs](https://auth0.com/docs/authenticate) | ||
* | ||
* ### Notes | ||
* | ||
* By default, Auth.js assumes that the salesforce provider is | ||
* based on the [OAuth 2](https://www.rfc-editor.org/rfc/rfc6749.html) specification. | ||
* The Salesforce provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/salesforce.ts). To override the defaults for your use case, check out [customizing a built-in OAuth provider](https://authjs.dev/guides/configuring-oauth-providers). | ||
* | ||
* :::tip | ||
* ## Help | ||
* | ||
* The Salesforce provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/salesforce.ts). | ||
* To override the defaults for your use case, check out [customizing a built-in OAuth provider](https://authjs.dev/guides/configuring-oauth-providers). | ||
* | ||
* ::: | ||
* | ||
* :::info **Disclaimer** | ||
* | ||
* If you think you found a bug in the default configuration, you can [open an issue](https://authjs.dev/new/provider-issue). | ||
@@ -74,26 +64,16 @@ * | ||
* we might not pursue a resolution. You can ask for more help in [Discussions](https://authjs.dev/new/github-discussions). | ||
* | ||
* ::: | ||
*/ | ||
export default function Salesforce<P extends SalesforceProfile>( | ||
options: OAuthUserConfig<P> | ||
): OAuthConfig<P> { | ||
const { issuer = "https://login.salesforce.com" } = options | ||
export default function Salesforce( | ||
options: OIDCUserConfig<SalesforceProfile> | ||
): OIDCConfig<SalesforceProfile> { | ||
return { | ||
id: "salesforce", | ||
name: "Salesforce", | ||
type: "oauth", | ||
authorization: `${issuer}/services/oauth2/authorize?display=page`, | ||
token: `${issuer}/services/oauth2/token`, | ||
userinfo: `${issuer}/services/oauth2/userinfo`, | ||
profile(profile) { | ||
return { | ||
id: profile.user_id, | ||
name: null, | ||
email: null, | ||
image: profile.picture, | ||
} | ||
}, | ||
type: "oidc", | ||
issuer: "https://login.salesforce.com", | ||
idToken: false, | ||
checks: ["pkce", "state", "nonce"], | ||
style: { bg: "#00a1e0" }, | ||
options, | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
2
39
1712091
44121