UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

133 lines (131 loc) • 5.49 kB
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