better-auth
Version:
The most comprehensive authentication framework for TypeScript.
133 lines (131 loc) • 5.49 kB
JavaScript
import { generateRandomString } from "../../crypto/random.mjs";
import "../../crypto/index.mjs";
import { getSessionFromCtx } from "../../api/routes/session.mjs";
import "../../api/index.mjs";
import { APIError } from "better-call";
//#region src/plugins/mcp/authorize.ts
function redirectErrorURL(url, error, description) {
return `${url.includes("?") ? "&" : "?"}error=${error}&error_description=${description}`;
}
async function authorizeMCPOAuth(ctx, options) {
ctx.setHeader("Access-Control-Allow-Origin", "*");
ctx.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
ctx.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
ctx.setHeader("Access-Control-Max-Age", "86400");
const opts = {
codeExpiresIn: 600,
defaultScope: "openid",
...options,
scopes: [
"openid",
"profile",
"email",
"offline_access",
...options?.scopes || []
]
};
if (!ctx.request) throw new APIError("UNAUTHORIZED", {
error_description: "request not found",
error: "invalid_request"
});
const session = await getSessionFromCtx(ctx);
if (!session) {
/**
* If the user is not logged in, we need to redirect them to the
* login page.
*/
await ctx.setSignedCookie("oidc_login_prompt", JSON.stringify(ctx.query), ctx.context.secret, {
maxAge: 600,
path: "/",
sameSite: "lax"
});
const queryFromURL = ctx.request.url?.split("?")[1];
throw ctx.redirect(`${options.loginPage}?${queryFromURL}`);
}
const query = ctx.query;
if (!query.client_id) throw ctx.redirect(`${ctx.context.baseURL}/error?error=invalid_client`);
if (!query.response_type) throw ctx.redirect(redirectErrorURL(`${ctx.context.baseURL}/error`, "invalid_request", "response_type is required"));
const client = await ctx.context.adapter.findOne({
model: "oauthApplication",
where: [{
field: "clientId",
value: ctx.query.client_id
}]
}).then((res) => {
if (!res) return null;
return {
...res,
redirectUrls: res.redirectUrls.split(","),
metadata: res.metadata ? JSON.parse(res.metadata) : {}
};
});
if (!client) throw ctx.redirect(`${ctx.context.baseURL}/error?error=invalid_client`);
const redirectURI = client.redirectUrls.find((url) => url === ctx.query.redirect_uri);
if (!redirectURI || !query.redirect_uri)
/**
* show UI error here warning the user that the redirect URI is invalid
*/
throw new APIError("BAD_REQUEST", { message: "Invalid redirect URI" });
if (client.disabled) throw ctx.redirect(`${ctx.context.baseURL}/error?error=client_disabled`);
if (query.response_type !== "code") throw ctx.redirect(`${ctx.context.baseURL}/error?error=unsupported_response_type`);
const requestScope = query.scope?.split(" ").filter((s) => s) || opts.defaultScope.split(" ");
const invalidScopes = requestScope.filter((scope) => {
return !opts.scopes.includes(scope);
});
if (invalidScopes.length) throw ctx.redirect(redirectErrorURL(query.redirect_uri, "invalid_scope", `The following scopes are invalid: ${invalidScopes.join(", ")}`));
if ((!query.code_challenge || !query.code_challenge_method) && options.requirePKCE) throw ctx.redirect(redirectErrorURL(query.redirect_uri, "invalid_request", "pkce is required"));
if (!query.code_challenge_method) query.code_challenge_method = "plain";
if (!["s256", options.allowPlainCodeChallengeMethod ? "plain" : "s256"].includes(query.code_challenge_method?.toLowerCase() || "")) throw ctx.redirect(redirectErrorURL(query.redirect_uri, "invalid_request", "invalid code_challenge method"));
const code = generateRandomString(32, "a-z", "A-Z", "0-9");
const codeExpiresInMs = opts.codeExpiresIn * 1e3;
const expiresAt = new Date(Date.now() + codeExpiresInMs);
try {
/**
* Save the code in the database
*/
await ctx.context.internalAdapter.createVerificationValue({
value: JSON.stringify({
clientId: client.clientId,
redirectURI: query.redirect_uri,
scope: requestScope,
userId: session.user.id,
authTime: new Date(session.session.createdAt).getTime(),
requireConsent: query.prompt === "consent",
state: query.prompt === "consent" ? query.state : null,
codeChallenge: query.code_challenge,
codeChallengeMethod: query.code_challenge_method,
nonce: query.nonce
}),
identifier: code,
expiresAt
});
} catch {
throw ctx.redirect(redirectErrorURL(query.redirect_uri, "server_error", "An error occurred while processing the request"));
}
if (query.prompt !== "consent") {
const redirectURIWithCode$1 = new URL(redirectURI);
redirectURIWithCode$1.searchParams.set("code", code);
if (ctx.query.state) redirectURIWithCode$1.searchParams.set("state", ctx.query.state);
throw ctx.redirect(redirectURIWithCode$1.toString());
}
if (options?.consentPage) {
await ctx.setSignedCookie("oidc_consent_prompt", code, ctx.context.secret, {
maxAge: 600,
path: "/",
sameSite: "lax"
});
const urlParams = new URLSearchParams();
urlParams.set("consent_code", code);
urlParams.set("client_id", client.clientId);
urlParams.set("scope", requestScope.join(" "));
const consentURI = `${options.consentPage}?${urlParams.toString()}`;
throw ctx.redirect(consentURI);
}
const redirectURIWithCode = new URL(redirectURI);
redirectURIWithCode.searchParams.set("code", code);
if (ctx.query.state) redirectURIWithCode.searchParams.set("state", ctx.query.state);
throw ctx.redirect(redirectURIWithCode.toString());
}
//#endregion
export { authorizeMCPOAuth };
//# sourceMappingURL=authorize.mjs.map