diff --git a/examples/react-spa/README.md b/examples/react-spa/README.md index 93ce3727a..20f2c48f3 100644 --- a/examples/react-spa/README.md +++ b/examples/react-spa/README.md @@ -17,7 +17,7 @@ Ory Elements supports integrating with: - Preact - ExpressJs (experimental) -### Get Started +## Get Started ```shell git clone --depth 1 git@github.com:ory/elements.git @@ -38,8 +38,83 @@ ory tunnel http://localhost:3000 --project --dev The tunnel will now _mirror_ the Ory APIs under `http://localhost:4000` which we have explicity told our React app to use through the `VITE_ORY_SDK_URL` export. -Now you can see Ory Elements in action by opening http://localhost:3000 in your -browser! +Now you can see Ory Elements in action by opening in +your browser! + +### Ory OAuth flows + +Ory Network supports OAuth single sign on (SSO) flows. This is applicable to use +cases where you have multiple first-party applications sharing the same user +pool, e.g. sign in on app A and app B with the same credentials. + +Another use case is sharing user data with third party providers. In this case +you are the provider and allow third party apps to sign in your users without +needing access to their credentials. For example "sign in with Google", you are +Google in this scenario. + + + +You can try it out locally using the following steps: + +1. Create an Ory Network project + +```shell +ory create project --name "TestOAuth" +``` + +2. Create an OAuth Client + +```shell +ory create oauth2-client \ + --project ORY_NETWORK_PROJECT_SLUG_OR_ID \ + --name YOUR_CLIENT_NAME \ + --grant-type authorization_code,refresh_token \ + --response-type code,id_token \ + --scope openid,offline \ + --redirect-uri http://127.0.0.1:5555/callback +``` + +3. Run the Ory CLI tunnel + +```shell +ory tunnel http://localhost:3000 --dev --project +``` + +4. Run the React application + +```shell +export VITE_ORY_SDK_URL=http://localhost:4000 +npm run dev +``` + +5. Perform an OAuth flow + +This is using a test OAuth client, this would be another application / service +or Ory Network project. + +```shell +ory perform authorization-code \ + --client-id ORY_CLIENT_ID \ + --client-secret ORY_CLIENT_SECRET \ + --project ORY_PROJECT_ID \ + --port 5555 \ + --scope openid,offline +``` + +**Please take note to initiate the OAuth flow through the Ory tunnel on + instead of your project slug oryapis URL.** + +For example: + +```diff +- https://.projects.oryapis.com/oauth2/auth ++ http://localhost:4000/oauth2/auth +?audience=&client_id=77e447a8-f0b9-42dc-8d75-676a8ebf5e2e&max_age=0&nonce=mwlotcnpyfytjfwmcxsklnnm&prompt=&redirect_uri=http%3A%2F%2F127.0.0.1%3A5555%2Fcallback&response_type=code&scope=openid+offline&state=hcmykhxolygmattztdednkrs +``` + +The reason for this is so that cookies can be set correctly on localhost. When +deploying to production you will use your own domain attached to your Ory +Network project. ### Using and Modifying the Example diff --git a/examples/react-spa/src/Login.tsx b/examples/react-spa/src/Login.tsx index c1be6e474..2dbc98e9e 100644 --- a/examples/react-spa/src/Login.tsx +++ b/examples/react-spa/src/Login.tsx @@ -7,6 +7,9 @@ import { sdk, sdkError } from "./sdk" export const Login = (): JSX.Element => { const [flow, setFlow] = useState(null) const [searchParams, setSearchParams] = useSearchParams() + const aal2 = searchParams.get("aal2") + const loginChallenge = searchParams.get("login_challenge") + const returnTo = searchParams.get("return_to") const navigate = useNavigate() @@ -26,12 +29,16 @@ export const Login = (): JSX.Element => { // Create a new login flow const createFlow = () => { - const aal2 = searchParams.get("aal2") sdk // aal2 is a query parameter that can be used to request Two-Factor authentication // aal1 is the default authentication level (Single-Factor) // we always pass refresh (true) on login so that the session can be refreshed when there is already an active session - .createBrowserLoginFlow({ refresh: true, aal: aal2 ? "aal2" : "aal1" }) + .createBrowserLoginFlow({ + refresh: true, + aal: aal2 ? "aal2" : "aal1", + ...(loginChallenge && { loginChallenge: loginChallenge }), + ...(returnTo && { returnTo: returnTo }), + }) // flow contains the form fields and csrf token .then(({ data: flow }) => { // Update URI query params to include flow id @@ -80,8 +87,34 @@ export const Login = (): JSX.Element => { flow={flow} // the login card should allow the user to go to the registration page and the recovery page additionalProps={{ - forgotPasswordURL: "/recovery", - signupURL: "/registration", + forgotPasswordURL: { + handler: () => { + const search = new URLSearchParams() + flow.return_to && search.set("return_to", flow.return_to) + navigate( + { + pathname: "/recovery", + search: search.toString(), + }, + { replace: true }, + ) + }, + }, + signupURL: { + handler: () => { + const search = new URLSearchParams() + flow.return_to && search.set("return_to", flow.return_to) + flow.oauth2_login_challenge && + search.set("login_challenge", flow.oauth2_login_challenge) + navigate( + { + pathname: "/registration", + search: search.toString(), + }, + { replace: true }, + ) + }, + }, }} // we might need webauthn support which requires additional js includeScripts={true} diff --git a/examples/react-spa/src/Registration.tsx b/examples/react-spa/src/Registration.tsx index 1625dd839..f95b5191a 100644 --- a/examples/react-spa/src/Registration.tsx +++ b/examples/react-spa/src/Registration.tsx @@ -8,6 +8,9 @@ export const Registration = () => { const [flow, setFlow] = useState(null) const [searchParams, setSearchParams] = useSearchParams() + const returnTo = searchParams.get("return_to") + const loginChallenge = searchParams.get("login_challenge") + const navigate = useNavigate() // Get the flow based on the flowId in the URL (.e.g redirect to this page after flow initialized) @@ -28,7 +31,10 @@ export const Registration = () => { const createFlow = () => { sdk // we don't need to specify the return_to here since we are building an SPA. In server-side browser flows we would need to specify the return_to - .createBrowserRegistrationFlow() + .createBrowserRegistrationFlow({ + ...(returnTo && { returnTo: returnTo }), + ...(loginChallenge && { loginChallenge: loginChallenge }), + }) .then(({ data: flow }) => { // Update URI query params to include flow id setSearchParams({ ["flow"]: flow.id }) @@ -77,7 +83,18 @@ export const Registration = () => { flow={flow} // the registration card needs a way to navigate to the login page additionalProps={{ - loginURL: "/login", + loginURL: { + handler: () => { + const search = new URLSearchParams() + flow.return_to && search.set("return_to", flow.return_to) + flow.oauth2_login_challenge && + search.set("login_challenge", flow.oauth2_login_challenge) + navigate( + { pathname: "/login", search: search.toString() }, + { replace: true }, + ) + }, + }, }} // include the necessary scripts for webauthn to work includeScripts={true} diff --git a/src/react-components/button-link.tsx b/src/react-components/button-link.tsx index cdb4c6e43..1fa483b4a 100644 --- a/src/react-components/button-link.tsx +++ b/src/react-components/button-link.tsx @@ -10,14 +10,14 @@ import { } from "../theme/button-link.css" export interface CustomHref { - href: string - handler: (url: string) => void + href?: string + handler: () => void } const isCustomHref = ( href: CustomHref | string | undefined, ): href is CustomHref => { - return href !== undefined && (href as CustomHref).href !== undefined + return href !== undefined && (href as CustomHref).handler !== undefined } export type ButtonLinkProps = { @@ -43,10 +43,10 @@ export const ButtonLink = ({ if (isCustomHref(href)) { linkProps = { ...linkProps, - href: href.href, + href: href.href ?? "", onClick: (e: MouseEvent) => { e.preventDefault() - href.handler(href.href) + href.handler() }, } } else {