Socket
Socket
Sign inDemoInstall

@auth/core

Package Overview
Dependencies
Maintainers
2
Versions
96
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@auth/core - npm Package Compare versions

Comparing version 0.35.2 to 0.35.3

2

jwt.d.ts

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc