@auth/core
Version:
Authentication for the Web.
159 lines (158 loc) • 7.27 kB
JavaScript
import * as checks from "./checks.js";
import * as o from "oauth4webapi";
import { OAuthCallbackError, OAuthProfileParseError, } from "../../../../errors.js";
import { isOIDCProvider } from "../../../utils/providers.js";
/**
* Handles the following OAuth steps.
* https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1
* https://www.rfc-editor.org/rfc/rfc6749#section-4.1.3
* https://openid.net/specs/openid-connect-core-1_0.html#UserInfoRequest
*
* @note Although requesting userinfo is not required by the OAuth2.0 spec,
* we fetch it anyway. This is because we always want a user profile.
*/
export async function handleOAuth(query, cookies, options, randomState) {
const { logger, provider } = options;
let as;
const { token, userinfo } = provider;
// Falls back to authjs.dev if the user only passed params
if ((!token?.url || token.url.host === "authjs.dev") &&
(!userinfo?.url || userinfo.url.host === "authjs.dev")) {
// We assume that issuer is always defined as this has been asserted earlier
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const issuer = new URL(provider.issuer);
const discoveryResponse = await o.discoveryRequest(issuer);
const discoveredAs = await o.processDiscoveryResponse(issuer, discoveryResponse);
if (!discoveredAs.token_endpoint)
throw new TypeError("TODO: Authorization server did not provide a token endpoint.");
if (!discoveredAs.userinfo_endpoint)
throw new TypeError("TODO: Authorization server did not provide a userinfo endpoint.");
as = discoveredAs;
}
else {
as = {
issuer: provider.issuer ?? "https://authjs.dev", // TODO: review fallback issuer
token_endpoint: token?.url.toString(),
userinfo_endpoint: userinfo?.url.toString(),
};
}
const client = {
client_id: provider.clientId,
client_secret: provider.clientSecret,
...provider.client,
};
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);
/** https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2.1 */
if (o.isOAuth2Error(codeGrantParams)) {
const cause = { providerId: provider.id, ...codeGrantParams };
logger.debug("OAuthCallbackError", cause);
throw new OAuthCallbackError("OAuth Provider returned an error", cause);
}
const codeVerifier = await checks.pkce.use(cookies, resCookies, options);
let redirect_uri = provider.callbackUrl;
if (!options.isOnRedirectProxy && provider.redirectProxyUrl) {
redirect_uri = provider.redirectProxyUrl;
}
let codeGrantResponse = await o.authorizationCodeGrantRequest(as, client, codeGrantParams, redirect_uri, codeVerifier ?? "auth", // TODO: review fallback code verifier,
{
[o.customFetch]: (...args) => {
if (!provider.checks.includes("pkce") &&
args[1]?.body instanceof URLSearchParams) {
args[1].body.delete("code_verifier");
}
return fetch(...args);
},
clientPrivateKey: provider.token?.clientPrivateKey,
});
if (provider.token?.conform) {
codeGrantResponse =
(await provider.token.conform(codeGrantResponse.clone())) ??
codeGrantResponse;
}
let challenges;
if ((challenges = o.parseWwwAuthenticateChallenges(codeGrantResponse))) {
for (const challenge of challenges) {
console.log("challenge", challenge);
}
throw new Error("TODO: Handle www-authenticate challenges as needed");
}
let profile = {};
let tokens;
if (isOIDCProvider(provider)) {
const nonce = await checks.nonce.use(cookies, resCookies, options);
const processedCodeResponse = await o.processAuthorizationCodeOpenIDResponse(as, client, codeGrantResponse, nonce ?? o.expectNoNonce);
if (o.isOAuth2Error(processedCodeResponse)) {
console.log("error", processedCodeResponse);
throw new Error("TODO: Handle OIDC response body error");
}
const idTokenClaims = o.getValidatedIdTokenClaims(processedCodeResponse);
profile = idTokenClaims;
if (provider.idToken === false) {
const userinfoResponse = await o.userInfoRequest(as, client, processedCodeResponse.access_token);
profile = await o.processUserInfoResponse(as, client, idTokenClaims.sub, userinfoResponse);
}
tokens = processedCodeResponse;
}
else {
const processedCodeResponse = await o.processAuthorizationCodeOAuth2Response(as, client, codeGrantResponse);
tokens = processedCodeResponse;
if (o.isOAuth2Error(processedCodeResponse)) {
console.log("error", processedCodeResponse);
throw new Error("TODO: Handle OAuth 2.0 response body error");
}
if (userinfo?.request) {
const _profile = await userinfo.request({ tokens, provider });
if (_profile instanceof Object)
profile = _profile;
}
else if (userinfo?.url) {
const userinfoResponse = await o.userInfoRequest(as, client, processedCodeResponse.access_token);
profile = await userinfoResponse.json();
}
else {
throw new TypeError("No userinfo endpoint configured");
}
}
if (tokens.expires_in) {
tokens.expires_at =
Math.floor(Date.now() / 1000) + Number(tokens.expires_in);
}
const profileResult = await getUserAndAccount(profile, provider, tokens, logger);
return { ...profileResult, profile, cookies: resCookies };
}
/**
* Returns the user and account that is going to be created in the database.
* @internal
*/
export async function getUserAndAccount(OAuthProfile, provider, tokens, logger) {
try {
const userFromProfile = await provider.profile(OAuthProfile, tokens);
const user = {
...userFromProfile,
id: crypto.randomUUID(),
email: userFromProfile.email?.toLowerCase(),
};
return {
user,
account: {
...tokens,
provider: provider.id,
type: provider.type,
providerAccountId: userFromProfile.id ?? crypto.randomUUID(),
},
};
}
catch (e) {
// If we didn't get a response either there was a problem with the provider
// response *or* the user cancelled the action with the provider.
//
// Unfortunately, we can't tell which - at least not in a way that works for
// all providers, so we return an empty object; the user should then be
// redirected back to the sign up page. We log the error to help developers
// who might be trying to debug this when configuring a new provider.
logger.debug("getProfile error details", OAuthProfile);
logger.error(new OAuthProfileParseError(e, { provider: provider.id }));
}
}