@auth/core
Version:
Authentication for the Web.
209 lines (208 loc) • 8.4 kB
JavaScript
import * as jose from "jose";
import * as o from "oauth4webapi";
import { InvalidCheck } from "../../../../errors.js";
import { decode, encode } from "../../../../jwt.js";
/** Returns a signed cookie. */
export async function signCookie(type, value, maxAge, options, data) {
const { cookies, logger } = options;
logger.debug(`CREATE_${type.toUpperCase()}`, { value, maxAge });
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 },
};
}
const PKCE_MAX_AGE = 60 * 15; // 15 minutes in seconds
export const pkce = {
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);
return { cookie, value };
},
/**
* Returns code_verifier if the provider is configured to use PKCE,
* and clears the container cookie afterwards.
* 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;
},
};
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 { }
}
export const state = {
async create(options, data) {
const { provider } = options;
if (!provider.checks.includes("state")) {
if (data) {
throw new InvalidCheck("State data was provided but the provider is not configured to use state");
}
return;
}
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 };
},
/**
* Returns state if the provider is configured to use state,
* and clears the container cookie afterwards.
* 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;
},
};
const NONCE_MAX_AGE = 60 * 15; // 15 minutes in seconds
export const nonce = {
async create(options) {
if (!options.provider.checks.includes("nonce"))
return;
const value = o.generateRandomNonce();
const maxAge = NONCE_MAX_AGE;
const cookie = await signCookie("nonce", value, maxAge, options);
return { cookie, value };
},
/**
* Returns nonce if the provider is configured to use nonce,
* and clears the container cookie afterwards.
* An error is thrown if the nonce is missing or invalid.
* @see https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes
* @see https://danielfett.de/2020/05/16/pkce-vs-nonce-equivalent-or-not/#nonce
*/
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;
},
};
/**
* 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 randomState;
let proxyRedirect;
if (provider.redirectProxyUrl && !query?.state) {
throw new InvalidCheck("Missing state in query, but required for redirect proxy");
}
const state = decodeState(query?.state);
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
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 };
},
/**
* Returns 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,
});
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);
},
};