From 0028ed98d31bef8240877df758f45817cb506399 Mon Sep 17 00:00:00 2001 From: andhreljaKern Date: Mon, 23 Feb 2026 22:47:34 +0100 Subject: [PATCH 1/8] perf: hydra consent --- Dockerfile | 4 +- pages/api/accept/login.ts | 16 ++ pages/consent.tsx | 254 ++++++++++++++++++++++++++++++++ pages/login.tsx | 51 ++++++- pages/registration.tsx | 18 ++- pkg/sdk/hydra.ts | 13 ++ pkg/sdk/index.ts | 7 +- submodules/javascript-functions | 2 +- submodules/react-components | 2 +- 9 files changed, 351 insertions(+), 16 deletions(-) create mode 100644 pages/api/accept/login.ts create mode 100644 pages/consent.tsx create mode 100644 pkg/sdk/hydra.ts diff --git a/Dockerfile b/Dockerfile index 7df8d3c..5370d18 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-alpine as build +FROM node:18-alpine AS build WORKDIR /app COPY package*.json /app/ @@ -6,4 +6,4 @@ RUN npm install COPY . /app RUN npm run build -ENTRYPOINT /usr/local/bin/npm run start \ No newline at end of file +ENTRYPOINT ["/usr/local/bin/npm", "run", "start"] \ No newline at end of file diff --git a/pages/api/accept/login.ts b/pages/api/accept/login.ts new file mode 100644 index 0000000..f5aff2b --- /dev/null +++ b/pages/api/accept/login.ts @@ -0,0 +1,16 @@ +// pages/api/login/accept.ts — server-side, calls Hydra admin +import hydra from '@/pkg/sdk/hydra' +import type { NextApiRequest, NextApiResponse } from 'next' + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { challenge, subject } = req.body + const { data } = await hydra.acceptOAuth2LoginRequest({ + loginChallenge: challenge, + acceptOAuth2LoginRequest: { + subject, + remember: true, + remember_for: 3600, + }, + }) + res.json({ redirect_to: data.redirect_to }) +} \ No newline at end of file diff --git a/pages/consent.tsx b/pages/consent.tsx new file mode 100644 index 0000000..c7f4956 --- /dev/null +++ b/pages/consent.tsx @@ -0,0 +1,254 @@ +import { AcceptOAuth2ConsentRequestSession, OAuth2ConsentRequest } from "@ory/client" +import { AxiosError } from "axios" +import type { NextPage } from "next" +import Head from "next/head" +import { useRouter } from "next/router" +import { FormEvent, useCallback, useEffect, useMemo, useState } from "react" + +import { KernLogo } from "@/pkg/ui/Icons" +import hydra from "@/pkg/sdk/hydra" +import ory from "@/pkg/sdk" + +const REMEMBER_FOR_SECONDS = 3600 + +const Consent: NextPage = () => { + const router = useRouter() + const { consent_challenge: consentChallenge } = router.query + + const [consentRequest, setConsentRequest] = useState() + const [selectedScopes, setSelectedScopes] = useState([]) + const [remember, setRemember] = useState(true) + const [isSubmitting, setIsSubmitting] = useState(false) + const [errorMessage, setErrorMessage] = useState("") + + const challenge = useMemo( + () => (consentChallenge ? String(consentChallenge) : ""), + [consentChallenge], + ) + + const buildSession = useCallback(async (grantScope: string[]) => { + const session: AcceptOAuth2ConsentRequestSession = { + access_token: {}, + id_token: {}, + } + + try { + const { data } = await ory.toSession() + const identity = data.identity + + if (!identity) { + return session + } + + if (grantScope.includes("email")) { + const addresses = identity.verifiable_addresses || [] + const address = addresses.find((item) => item.via === "email") + if (address) { + session.id_token.email = address.value + session.id_token.email_verified = address.verified + } + } + + if (grantScope.includes("profile")) { + if (identity.traits?.username) { + session.id_token.preferred_username = identity.traits.username as string + } + if (identity.traits?.website) { + session.id_token.website = identity.traits.website as string + } + if (typeof identity.traits?.name === "object") { + const name = identity.traits.name as { first?: string; last?: string } + if (name.first) { + session.id_token.given_name = name.first + } + if (name.last) { + session.id_token.family_name = name.last + } + } else if (typeof identity.traits?.name === "string") { + session.id_token.name = identity.traits.name + } + if (identity.updated_at) { + session.id_token.updated_at = parseInt( + (Date.parse(identity.updated_at) / 1000).toFixed(0), + 10, + ) + } + } + } catch (error) { + // Continue consent even if we can't enrich token claims. + } + + return session + }, []) + + const acceptConsent = useCallback( + async (request: OAuth2ConsentRequest, grantScope: string[]) => { + const { data } = await hydra.acceptOAuth2ConsentRequest({ + consentChallenge: challenge, + acceptOAuth2ConsentRequest: { + grant_scope: grantScope, + grant_access_token_audience: + request.requested_access_token_audience || [], + remember, + remember_for: REMEMBER_FOR_SECONDS, + session: await buildSession(grantScope), + }, + }) + window.location.href = String(data.redirect_to) + }, + [buildSession, challenge, remember], + ) + + useEffect(() => { + if (!router.isReady || !challenge) { + return + } + + hydra + .getOAuth2ConsentRequest({ consentChallenge: challenge }) + .then(async ({ data }) => { + const requestedScope = data.requested_scope || [] + + if (data.skip) { + await acceptConsent(data, requestedScope) + return + } + + setConsentRequest(data) + setSelectedScopes(requestedScope) + }) + .catch((err: AxiosError) => { + setErrorMessage( + (err.response?.data as any)?.error_description || + "Unable to load the consent request.", + ) + }) + }, [acceptConsent, challenge, router.isReady]) + + const handleScopeChange = useCallback((scope: string, checked: boolean) => { + setSelectedScopes((currentScopes) => + checked + ? [...currentScopes, scope] + : currentScopes.filter((currentScope) => currentScope !== scope), + ) + }, []) + + const handleSubmit = useCallback( + async (event: FormEvent) => { + event.preventDefault() + if (!consentRequest || !challenge || isSubmitting) { + return + } + + const formData = new FormData(event.currentTarget) + const action = String(formData.get("consent_action")) + setIsSubmitting(true) + setErrorMessage("") + + try { + if (action === "accept") { + await acceptConsent(consentRequest, selectedScopes) + return + } + + const { data } = await hydra.rejectOAuth2ConsentRequest({ + consentChallenge: challenge, + rejectOAuth2Request: { + error: "access_denied", + error_description: "The resource owner denied the request", + }, + }) + window.location.href = String(data.redirect_to) + } catch (err) { + const error = err as AxiosError + setErrorMessage( + (error.response?.data as any)?.error_description || + "Unable to submit the consent response.", + ) + } finally { + setIsSubmitting(false) + } + }, + [acceptConsent, challenge, consentRequest, isSubmitting, selectedScopes], + ) + + const clientName = + consentRequest?.client?.client_name || + consentRequest?.client?.client_id || + "Unknown Client" + const requestedScopes = consentRequest?.requested_scope || [] + + return ( + <> + + Consent + + +
+ + +
+
+ + ) +} + +export default Consent diff --git a/pages/login.tsx b/pages/login.tsx index e38623f..8b432af 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -26,6 +26,7 @@ const Login: NextPage = () => { const { return_to: returnTo, flow: flowId, + login_challenge: loginChallenge, // Refresh means we want to refresh the session. This is needed, for example, when we want to update the password // of a user. refresh, @@ -39,6 +40,36 @@ const Login: NextPage = () => { if (!router.isReady || initialFlow) { return } + // If there is a challenge, check for existing session first + if (loginChallenge) { + ory.toSession() + .then(({ data }) => { + // Active session exists — let the server accept the challenge + // without forcing re-auth + return fetch(`/api/login/accept`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + challenge: String(loginChallenge), + subject: data.identity.id, + }), + }) + }) + .then(res => res.json()) + .then(({ redirect_to }) => { window.location.href = redirect_to }) + .catch(() => { + // No session — proceed with normal flow creation + ory.createBrowserLoginFlow({ + refresh: Boolean(refresh), + aal: aal ? String(aal) : undefined, + returnTo: returnTo ? String(returnTo) : undefined, + loginChallenge: String(loginChallenge), + }) + .then(({ data }) => setInitialFlow(data)) + .catch(handleFlowError(router, 'login', setInitialFlow)) + }) + return + } // If ?flow=.. was in the URL, we fetch it if (flowId) { ory @@ -55,12 +86,13 @@ const Login: NextPage = () => { refresh: Boolean(refresh), aal: aal ? String(aal) : undefined, returnTo: returnTo ? String(returnTo) : undefined, + loginChallenge: loginChallenge ? String(loginChallenge) : undefined, }) .then(({ data }) => { setInitialFlow(data) }) .catch(handleFlowError(router, "login", setInitialFlow)) - }, [flowId, router, router.isReady, aal, refresh, returnTo, initialFlow]) + }, [flowId, router, router.isReady, aal, refresh, returnTo, loginChallenge, initialFlow]) useEffect(() => { if (!initialFlow) return; @@ -121,6 +153,19 @@ const Login: NextPage = () => { return Promise.reject(err) }) + + const backToLoginQuery = new URLSearchParams({ + ...(returnTo ? { return_to: String(returnTo) } : {}), + ...(loginChallenge ? { login_challenge: String(loginChallenge) } : {}), + }).toString() + const backToLoginHref = `/auth/login${backToLoginQuery ? `?${backToLoginQuery}` : ""}` + + const registrationQuery = new URLSearchParams({ + ...(returnTo ? { return_to: String(returnTo) } : {}), + ...(loginChallenge ? { login_challenge: String(loginChallenge) } : {}), + }).toString() + const registrationHref = `/auth/registration${registrationQuery ? `?${registrationQuery}` : ""}` + return ( <> @@ -132,7 +177,7 @@ const Login: NextPage = () => {

Sign in to your account

Or - Register account - + Register account - no credit card required!

@@ -161,7 +206,7 @@ const Login: NextPage = () => { <> {displayMailForm ? Forgot your password? : null} - : Go back to login + : Go back to login }
diff --git a/pages/registration.tsx b/pages/registration.tsx index 1df3d75..e280593 100644 --- a/pages/registration.tsx +++ b/pages/registration.tsx @@ -22,7 +22,11 @@ const Registration: NextPage = () => { // Get ?flow=... from the URL - const { flow: flowId, return_to: returnTo } = router.query; + const { + flow: flowId, + return_to: returnTo, + login_challenge: loginChallenge, + } = router.query; // In this effect we either initiate a new registration flow, or we fetch an existing registration flow. useEffect(() => { @@ -47,12 +51,13 @@ const Registration: NextPage = () => { ory .createBrowserRegistrationFlow({ returnTo: returnTo ? String(returnTo) : undefined, + loginChallenge: loginChallenge ? String(loginChallenge) : undefined, }) .then(({ data }) => { setInitialFlow(data) }) .catch(handleFlowError(router, "registration", setInitialFlow)) - }, [flowId, router, router.isReady, returnTo, initialFlow]) + }, [flowId, router, router.isReady, returnTo, loginChallenge, initialFlow]) useEffect(() => { if (!initialFlow) return; @@ -97,6 +102,13 @@ const Registration: NextPage = () => { }) } + const backToLoginQuery = new URLSearchParams({ + ...(returnTo ? { return_to: String(returnTo) } : {}), + ...(loginChallenge ? { login_challenge: String(loginChallenge) } : {}), + }).toString() + const backToLoginHref = `/auth/login${backToLoginQuery ? `?${backToLoginQuery}` : ""}` + + return ( <> @@ -117,7 +129,7 @@ const Registration: NextPage = () => {
- Go back to login + Go back to login
diff --git a/pkg/sdk/hydra.ts b/pkg/sdk/hydra.ts new file mode 100644 index 0000000..0e56654 --- /dev/null +++ b/pkg/sdk/hydra.ts @@ -0,0 +1,13 @@ +import { Configuration, OAuth2Api } from '@ory/client' + +const hydra = new OAuth2Api( + new Configuration({ + basePath: '/.ory/hydra/admin/', + baseOptions: { + // mode: 'no-cors', + withCredentials: true, + }, + }), +) + +export default hydra diff --git a/pkg/sdk/index.ts b/pkg/sdk/index.ts index 0139ead..b9ac0b7 100644 --- a/pkg/sdk/index.ts +++ b/pkg/sdk/index.ts @@ -5,12 +5,7 @@ const ory = new FrontendApi( new Configuration({ basePath: '/.ory/kratos/public/', baseOptions: { - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }, - mode: 'no-cors', + // mode: 'no-cors', withCredentials: true, } }) diff --git a/submodules/javascript-functions b/submodules/javascript-functions index 3fcec61..4850e39 160000 --- a/submodules/javascript-functions +++ b/submodules/javascript-functions @@ -1 +1 @@ -Subproject commit 3fcec610b5212e0a26942b289699259e47475088 +Subproject commit 4850e39785215aa8cabcb1cd7df0cd47681be0d5 diff --git a/submodules/react-components b/submodules/react-components index 38ed96a..99e54d6 160000 --- a/submodules/react-components +++ b/submodules/react-components @@ -1 +1 @@ -Subproject commit 38ed96a946fb3cadaa4ac35eaf8260d028fe48b4 +Subproject commit 99e54d67a477fa59655f93a21b8568659903e6c0 From 69b855d20939fe6715761b61e7d725b31b2974fb Mon Sep 17 00:00:00 2001 From: GitHub Actions Runner Date: Tue, 24 Feb 2026 18:07:00 +0000 Subject: [PATCH 2/8] perf: consent page --- pages/api/consent/accept.ts | 23 ++ pages/api/consent/get.ts | 17 ++ pages/api/consent/reject.ts | 20 ++ .../api/{accept/login.ts => login/accept.ts} | 8 +- pages/consent.tsx | 228 ++++++++---------- pages/login.tsx | 7 +- pkg/sdk/hydra.ts | 12 +- 7 files changed, 176 insertions(+), 139 deletions(-) create mode 100644 pages/api/consent/accept.ts create mode 100644 pages/api/consent/get.ts create mode 100644 pages/api/consent/reject.ts rename pages/api/{accept/login.ts => login/accept.ts} (62%) diff --git a/pages/api/consent/accept.ts b/pages/api/consent/accept.ts new file mode 100644 index 0000000..d5be1e4 --- /dev/null +++ b/pages/api/consent/accept.ts @@ -0,0 +1,23 @@ +// pages/api/consent/accept.ts +import hydra from '@/pkg/sdk/hydra' +import type { NextApiRequest, NextApiResponse } from 'next' + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'POST') return res.status(405).end() + const { challenge, grant_scope, grant_access_token_audience, remember, remember_for, session } = req.body + hydra.acceptOAuth2ConsentRequest({ + consentChallenge: challenge, + acceptOAuth2ConsentRequest: { + grant_scope, + grant_access_token_audience, + remember, + remember_for, + session, + }, + }).then(({ data }) => { + return res.json({ redirect_to: data.redirect_to }) + }).catch((err: any) => { + console.error(err?.response?.data ?? err.message) + return res.status(500).json({ error: 'failed to accept consent request' }) + }) +} \ No newline at end of file diff --git a/pages/api/consent/get.ts b/pages/api/consent/get.ts new file mode 100644 index 0000000..16fdbf8 --- /dev/null +++ b/pages/api/consent/get.ts @@ -0,0 +1,17 @@ +// pages/api/consent/get.ts +import hydra from '@/pkg/sdk/hydra' +import type { NextApiRequest, NextApiResponse } from 'next' + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'GET') return res.status(405).end() + const { challenge } = req.query + if (!challenge) return res.status(400).json({ error: 'missing challenge' }) + hydra.getOAuth2ConsentRequest({ consentChallenge: String(challenge) }) + .then(({ data }) => { + return res.json(data) + }) + .catch((err: any) => { + console.error(err?.response?.data ?? err.message) + return res.status(500).json({ error: 'failed to get consent request' }) + }) +} \ No newline at end of file diff --git a/pages/api/consent/reject.ts b/pages/api/consent/reject.ts new file mode 100644 index 0000000..fb87145 --- /dev/null +++ b/pages/api/consent/reject.ts @@ -0,0 +1,20 @@ +// pages/api/consent/reject.ts +import hydra from '@/pkg/sdk/hydra' +import type { NextApiRequest, NextApiResponse } from 'next' + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'POST') return res.status(405).end() + const { challenge } = req.body + hydra.rejectOAuth2ConsentRequest({ + consentChallenge: challenge, + rejectOAuth2Request: { + error: 'access_denied', + error_description: 'The resource owner denied the request', + }, + }).then(({ data }) => { + return res.json({ redirect_to: data.redirect_to }) + }).catch((err: any) => { + console.error(err?.response?.data ?? err.message) + return res.status(500).json({ error: 'failed to reject consent request' }) + }) +} \ No newline at end of file diff --git a/pages/api/accept/login.ts b/pages/api/login/accept.ts similarity index 62% rename from pages/api/accept/login.ts rename to pages/api/login/accept.ts index f5aff2b..e547404 100644 --- a/pages/api/accept/login.ts +++ b/pages/api/login/accept.ts @@ -4,13 +4,17 @@ import type { NextApiRequest, NextApiResponse } from 'next' export default async function handler(req: NextApiRequest, res: NextApiResponse) { const { challenge, subject } = req.body - const { data } = await hydra.acceptOAuth2LoginRequest({ + hydra.acceptOAuth2LoginRequest({ loginChallenge: challenge, acceptOAuth2LoginRequest: { subject, remember: true, remember_for: 3600, }, + }).then(({ data }) => { + return res.json({ redirect_to: data.redirect_to }) + }).catch((err) => { + console.error(err) + return res.status(500).json({ error: 'An error occurred while accepting the login challenge' }) }) - res.json({ redirect_to: data.redirect_to }) } \ No newline at end of file diff --git a/pages/consent.tsx b/pages/consent.tsx index c7f4956..6cce58a 100644 --- a/pages/consent.tsx +++ b/pages/consent.tsx @@ -1,12 +1,9 @@ -import { AcceptOAuth2ConsentRequestSession, OAuth2ConsentRequest } from "@ory/client" -import { AxiosError } from "axios" +import { OAuth2ConsentRequest, AcceptOAuth2ConsentRequestSession } from "@ory/client" import type { NextPage } from "next" import Head from "next/head" import { useRouter } from "next/router" import { FormEvent, useCallback, useEffect, useMemo, useState } from "react" - import { KernLogo } from "@/pkg/ui/Icons" -import hydra from "@/pkg/sdk/hydra" import ory from "@/pkg/sdk" const REMEMBER_FOR_SECONDS = 3600 @@ -26,157 +23,136 @@ const Consent: NextPage = () => { [consentChallenge], ) - const buildSession = useCallback(async (grantScope: string[]) => { + const buildSession = useCallback(async (grantScope: string[]): Promise => { const session: AcceptOAuth2ConsentRequestSession = { access_token: {}, id_token: {}, } - try { const { data } = await ory.toSession() const identity = data.identity - - if (!identity) { - return session - } + if (!identity) return session if (grantScope.includes("email")) { - const addresses = identity.verifiable_addresses || [] - const address = addresses.find((item) => item.via === "email") + const address = identity.verifiable_addresses?.find(a => a.via === "email") if (address) { - session.id_token.email = address.value - session.id_token.email_verified = address.verified + session.id_token!.email = address.value + session.id_token!.email_verified = address.verified } } if (grantScope.includes("profile")) { - if (identity.traits?.username) { - session.id_token.preferred_username = identity.traits.username as string - } - if (identity.traits?.website) { - session.id_token.website = identity.traits.website as string - } + if (identity.traits?.username) session.id_token!.preferred_username = identity.traits.username + if (identity.traits?.website) session.id_token!.website = identity.traits.website if (typeof identity.traits?.name === "object") { const name = identity.traits.name as { first?: string; last?: string } - if (name.first) { - session.id_token.given_name = name.first - } - if (name.last) { - session.id_token.family_name = name.last - } + if (name.first) session.id_token!.given_name = name.first + if (name.last) session.id_token!.family_name = name.last } else if (typeof identity.traits?.name === "string") { - session.id_token.name = identity.traits.name + session.id_token!.name = identity.traits.name } if (identity.updated_at) { - session.id_token.updated_at = parseInt( - (Date.parse(identity.updated_at) / 1000).toFixed(0), - 10, - ) + session.id_token!.updated_at = parseInt((Date.parse(identity.updated_at) / 1000).toFixed(0), 10) } } - } catch (error) { - // Continue consent even if we can't enrich token claims. + } catch { + // continue without enrichment } - return session }, []) - const acceptConsent = useCallback( - async (request: OAuth2ConsentRequest, grantScope: string[]) => { - const { data } = await hydra.acceptOAuth2ConsentRequest({ - consentChallenge: challenge, - acceptOAuth2ConsentRequest: { - grant_scope: grantScope, - grant_access_token_audience: - request.requested_access_token_audience || [], - remember, - remember_for: REMEMBER_FOR_SECONDS, - session: await buildSession(grantScope), - }, - }) - window.location.href = String(data.redirect_to) - }, - [buildSession, challenge, remember], - ) - useEffect(() => { - if (!router.isReady || !challenge) { - return - } + if (!router.isReady || !challenge) return - hydra - .getOAuth2ConsentRequest({ consentChallenge: challenge }) - .then(async ({ data }) => { + fetch(`/refinery-entry/api/consent/get?challenge=${challenge}`) + .then(res => { + if (!res.ok) throw new Error("Failed to fetch consent request") + return res.json() + }) + .then(async (data: OAuth2ConsentRequest) => { const requestedScope = data.requested_scope || [] + // skip=true means user already consented — accept immediately if (data.skip) { - await acceptConsent(data, requestedScope) - return + const session = await buildSession(requestedScope) + return fetch('/refinery-entry/api/consent/accept', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + challenge, + grant_scope: requestedScope, + grant_access_token_audience: data.requested_access_token_audience || [], + remember, + remember_for: REMEMBER_FOR_SECONDS, + session, + }), + }) + .then(res => res.json()) + .then(({ redirect_to }) => { window.location.href = redirect_to }) } setConsentRequest(data) setSelectedScopes(requestedScope) }) - .catch((err: AxiosError) => { - setErrorMessage( - (err.response?.data as any)?.error_description || - "Unable to load the consent request.", - ) - }) - }, [acceptConsent, challenge, router.isReady]) + .catch(err => setErrorMessage(err.message ?? "Unable to load the consent request.")) + }, [router.isReady, challenge, buildSession, remember]) const handleScopeChange = useCallback((scope: string, checked: boolean) => { - setSelectedScopes((currentScopes) => - checked - ? [...currentScopes, scope] - : currentScopes.filter((currentScope) => currentScope !== scope), + setSelectedScopes(prev => + checked ? [...prev, scope] : prev.filter(s => s !== scope) ) }, []) - const handleSubmit = useCallback( - async (event: FormEvent) => { - event.preventDefault() - if (!consentRequest || !challenge || isSubmitting) { - return - } - - const formData = new FormData(event.currentTarget) - const action = String(formData.get("consent_action")) - setIsSubmitting(true) - setErrorMessage("") - - try { - if (action === "accept") { - await acceptConsent(consentRequest, selectedScopes) - return - } + const handleAccept = useCallback(async () => { + if (!consentRequest || !challenge || isSubmitting) return + setIsSubmitting(true) + setErrorMessage("") + try { + const session = await buildSession(selectedScopes) + const res = await fetch('/refinery-entry/api/consent/accept', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + challenge, + grant_scope: selectedScopes, + grant_access_token_audience: consentRequest.requested_access_token_audience || [], + remember, + remember_for: REMEMBER_FOR_SECONDS, + session, + }), + }) + const { redirect_to } = await res.json() + window.location.href = redirect_to + } catch (err: any) { + setErrorMessage(err.message ?? "Unable to accept consent.") + } finally { + setIsSubmitting(false) + } + }, [buildSession, challenge, consentRequest, isSubmitting, remember, selectedScopes]) - const { data } = await hydra.rejectOAuth2ConsentRequest({ - consentChallenge: challenge, - rejectOAuth2Request: { - error: "access_denied", - error_description: "The resource owner denied the request", - }, - }) - window.location.href = String(data.redirect_to) - } catch (err) { - const error = err as AxiosError - setErrorMessage( - (error.response?.data as any)?.error_description || - "Unable to submit the consent response.", - ) - } finally { - setIsSubmitting(false) - } - }, - [acceptConsent, challenge, consentRequest, isSubmitting, selectedScopes], - ) + const handleReject = useCallback(async () => { + if (!challenge || isSubmitting) return + setIsSubmitting(true) + setErrorMessage("") + try { + const res = await fetch('/refinery-entry/api/consent/reject', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ challenge }), + }) + const { redirect_to } = await res.json() + window.location.href = redirect_to + } catch (err: any) { + setErrorMessage(err.message ?? "Unable to reject consent.") + } finally { + setIsSubmitting(false) + } + }, [challenge, isSubmitting]) const clientName = consentRequest?.client?.client_name || consentRequest?.client?.client_id || "Unknown Client" - const requestedScopes = consentRequest?.requested_scope || [] return ( <> @@ -188,26 +164,22 @@ const Consent: NextPage = () => {