diff --git a/package-lock.json b/package-lock.json index fd6fe31..475b492 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "@ory/integration-react", "version": "0.0.1", "dependencies": { - "@ory/client": "^0.0.0-next.f88d10559361", + "@ory/client": "1.6.2", "@ory/integrations": "0.2.8", "@ory/themes": "~0.0.101", "classnames": "^2.3.1", @@ -1104,20 +1104,11 @@ } }, "node_modules/@ory/client": { - "version": "0.0.0-next.f88d10559361", - "resolved": "https://registry.npmjs.org/@ory/client/-/client-0.0.0-next.f88d10559361.tgz", - "integrity": "sha512-mItHDBAiefd0wdSxduVDYvnEUiBqJuiDos3P0KVyBlg9d7G++c0Bkz0lUBtqwWP5LWwQoFULAa40QJfgjFQNXw==", - "deprecated": "An incorrect version was published.", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@ory/client/-/client-1.6.2.tgz", + "integrity": "sha512-eeSkFZsrX/hLaariBg2I9PQWueE9IVAV3Tps5UE7CYEvrGziFB1zdv8joQDGMss5O3Yv/CSlSf4rOwTeENDqBg==", "dependencies": { - "axios": "^0.26.1" - } - }, - "node_modules/@ory/client/node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "dependencies": { - "follow-redirects": "^1.14.8" + "axios": "^1.6.1" } }, "node_modules/@ory/integrations": { @@ -1319,6 +1310,14 @@ "axios": "^0.21.4" } }, + "node_modules/@ory/integrations/node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, "node_modules/@ory/integrations/node_modules/next": { "version": "13.2.4", "resolved": "https://registry.npmjs.org/next/-/next-13.2.4.tgz", @@ -2045,13 +2044,33 @@ } }, "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "dependencies": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" } }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -3977,9 +3996,9 @@ "deprecated": "flatten is deprecated in favor of utility frameworks such as lodash." }, "node_modules/follow-redirects": { - "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -9227,21 +9246,11 @@ } }, "@ory/client": { - "version": "0.0.0-next.f88d10559361", - "resolved": "https://registry.npmjs.org/@ory/client/-/client-0.0.0-next.f88d10559361.tgz", - "integrity": "sha512-mItHDBAiefd0wdSxduVDYvnEUiBqJuiDos3P0KVyBlg9d7G++c0Bkz0lUBtqwWP5LWwQoFULAa40QJfgjFQNXw==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@ory/client/-/client-1.6.2.tgz", + "integrity": "sha512-eeSkFZsrX/hLaariBg2I9PQWueE9IVAV3Tps5UE7CYEvrGziFB1zdv8joQDGMss5O3Yv/CSlSf4rOwTeENDqBg==", "requires": { - "axios": "^0.26.1" - }, - "dependencies": { - "axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "requires": { - "follow-redirects": "^1.14.8" - } - } + "axios": "^1.6.1" } }, "@ory/integrations": { @@ -9340,6 +9349,14 @@ "axios": "^0.21.4" } }, + "axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "requires": { + "follow-redirects": "^1.14.0" + } + }, "next": { "version": "13.2.4", "resolved": "https://registry.npmjs.org/next/-/next-13.2.4.tgz", @@ -9859,11 +9876,30 @@ "dev": true }, "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "requires": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + } } }, "axobject-query": { @@ -11291,9 +11327,9 @@ "integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==" }, "follow-redirects": { - "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" }, "forever-agent": { "version": "0.6.1", diff --git a/package.json b/package.json index f3c5970..9ff242c 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "module": "dist/index.mjs", "typings": "dist/index.d.ts", "dependencies": { - "@ory/client": "^0.0.0-next.f88d10559361", + "@ory/client": "1.6.2", "@ory/integrations": "0.2.8", "@ory/themes": "~0.0.101", "classnames": "^2.3.1", diff --git a/pages/api/consent.ts b/pages/api/consent.ts new file mode 100644 index 0000000..5632cf5 --- /dev/null +++ b/pages/api/consent.ts @@ -0,0 +1,80 @@ +import { Configuration, OAuth2Api } from "@ory/client" +import { NextApiRequest, NextApiResponse } from "next" + +const hydra = new OAuth2Api( + new Configuration({ + basePath: process.env.HYDRA_ADMIN_URL, + baseOptions: { + "X-Forwarded-Proto": "https", + withCredentials: true, + }, + }), +) + +// Helper function to extract session data +const extractSession = (identity: any, grantScope: string[]) => { + const session: any = { + access_token: { + roles: identity.metadata_public.roles, + scope: identity.metadata_public.scope, + authorities: identity.metadata_public.authorities, + sources: identity.metadata_public.sources, + user_name: identity.metadata_public.mp_login, + }, + id_token: {}, + } + return session +} + +export default async (req: NextApiRequest, res: NextApiResponse) => { + const { consentChallenge, consentAction, grantScope, remember, identity } = + req.body + + try { + if (req.method === "GET") { + const { consent_challenge } = req.query + const response = await hydra.getOAuth2ConsentRequest({ + consentChallenge: String(consent_challenge), + }) + return res.status(200).json(response.data) + } else { + if (!consentChallenge || !consentAction) { + return res.status(400).json({ error: "Missing required parameters" }) + } + if (consentAction === "accept") { + const { data: body } = await hydra.getOAuth2ConsentRequest({ + consentChallenge, + }) + const session = extractSession(identity, grantScope) + const acceptResponse = await hydra.acceptOAuth2ConsentRequest({ + consentChallenge, + acceptOAuth2ConsentRequest: { + grant_scope: grantScope, + grant_access_token_audience: body.requested_access_token_audience, + session, + remember: Boolean(remember), + remember_for: 3600, + }, + }) + return res + .status(200) + .json({ redirect_to: acceptResponse.data.redirect_to }) + } else { + const rejectResponse = await hydra.rejectOAuth2ConsentRequest({ + consentChallenge, + rejectOAuth2Request: { + error: "access_denied", + error_description: "The resource owner denied the request", + }, + }) + + return res + .status(200) + .json({ redirect_to: rejectResponse.data.redirect_to }) + } + } + } catch (error) { + console.error(error) + return res.status(500).json({ error: "Internal server error" }) + } +} diff --git a/pages/consent.tsx b/pages/consent.tsx new file mode 100644 index 0000000..56fd2b5 --- /dev/null +++ b/pages/consent.tsx @@ -0,0 +1,147 @@ +import { useRouter } from "next/router" +import React, { useEffect, useState } from "react" + +import { MarginCard, CardTitle, TextCenterButton } from "../pkg" +import ory from "../pkg/sdk" + +const Consent = () => { + const router = useRouter() + const [consent, setConsent] = useState(null) + const [identity, setIdentity] = useState(null) + const [csrfToken, setCsrfToken] = useState("") + + useEffect(() => { + const { consent_challenge } = router.query + + ory + .toSession() + .then(({ data }) => { + setIdentity(data.identity) + }) + .catch((e) => console.log(e)) + + if (!consent_challenge) { + // router.push("/404") + return + } + + fetch(`/api/consent?consent_challenge=${consent_challenge}`) + .then((response) => response.json()) + .then((data) => { + if (data.error) { + throw new Error(data.error) + } + setConsent(data) + }) + .catch((err) => { + console.error(err) + }) + }, [router]) + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault() + const form = event.target as HTMLFormElement + const formData = new FormData(form) + + const submitter = (event.nativeEvent as SubmitEvent) + .submitter as HTMLButtonElement + const consentAction = submitter.value + + const consentChallenge = formData.get("consent_challenge") as string + const remember = !!formData.get("remember") + const grantScope = formData.getAll("grant_scope") as string[] + + if (!consentChallenge || !consentAction) { + console.error("consentChallenge or consentAction is missing") + return + } + + fetch("/api/consent", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + consentChallenge, + consentAction, + grantScope, + remember, + identity, // Include any additional identity data if needed + }), + }) + .then((response) => response.json()) + .then((data) => { + if (data.error) { + console.error(data.error) + return + } + router.push(data.redirect_to) + }) + .catch((err) => { + console.error(err) + }) + } + + if (!consent) { + return
Loading...
+ } + + return ( + +
+ Consent Request +
+ + +
+ +

{consent.client.client_name || consent.client.client_id}

+
+
+ + {consent.requested_scope.map((scope: string) => ( +
+ + +
+ ))} +
+

+
+ + +
+

+ + +
+
+
+ ) +} + +export default Consent diff --git a/pages/login.tsx b/pages/login.tsx index efb849b..257828c 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -31,6 +31,7 @@ const Login: NextPage = () => { // AAL = Authorization Assurance Level. This implies that we want to upgrade the AAL, meaning that we want // to perform two-factor authentication/verification. aal, + login_challenge: loginChallenge, } = router.query // This might be confusing, but we want to show the user an option @@ -57,15 +58,25 @@ const Login: NextPage = () => { // Otherwise we initialize it ory .createBrowserLoginFlow({ - refresh: Boolean(refresh), + refresh: loginChallenge? true : Boolean(refresh), aal: aal ? String(aal) : undefined, returnTo: returnTo ? String(returnTo) : undefined, + loginChallenge: loginChallenge ? String(loginChallenge) : undefined, }) .then(({ data }) => { setFlow(data) }) .catch(handleFlowError(router, "login", setFlow)) - }, [flowId, router, router.isReady, aal, refresh, returnTo, flow]) + }, [ + flowId, + router, + router.isReady, + aal, + refresh, + returnTo, + loginChallenge, + flow, + ]) const onSubmit = (values: UpdateLoginFlowBody) => router diff --git a/styles/globals.css b/styles/globals.css index 1938de0..fb23b43 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -22,6 +22,18 @@ button, font-size: 16px; } +.consent-button { + border-radius: 8px !important; + background-color: #706ef4 !important; + color: #fff !important; + width: 30%; + border: none; + padding: 8px; + margin: 8px; + font-size: 16px; + font-weight: 600; +} + .Toastify__close-button { width: 10% !important; border-radius: 30px !important;