Skip to content

Commit

Permalink
fix: CustomHref and update react-spa example (#154)
Browse files Browse the repository at this point in the history
* feat: react-spa use login_challenge

* chore: cleanup
  • Loading branch information
Benehiko authored Oct 5, 2023
1 parent 723aebf commit 880456c
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 19 deletions.
81 changes: 78 additions & 3 deletions examples/react-spa/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Ory Elements supports integrating with:
- Preact
- ExpressJs (experimental)

### Get Started
## Get Started

```shell
git clone --depth 1 [email protected]:ory/elements.git
Expand All @@ -38,8 +38,83 @@ ory tunnel http://localhost:3000 --project <project-slug> --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 <http://localhost:3000> 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.

<https://www.ory.sh/docs/hydra/guides/custom-ui-oauth2>

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 <slug>
```

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
<http://localhost:4000> instead of your project slug oryapis URL.**

For example:

```diff
- https://<project-slug>.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

Expand Down
73 changes: 66 additions & 7 deletions examples/react-spa/src/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,42 @@ import { useCallback, useEffect, useState } from "react"
import { useNavigate, useSearchParams } from "react-router-dom"
import { sdk, sdkError } from "./sdk"

/**
* Login is a React component that renders the login form using Ory Elements.
* It is used to handle the login flow for a variety of authentication methods
* and authentication levels (e.g. Single-Factor and Two-Factor)
*
* The Login component also handles the OAuth2 login flow (as an OAuth2 provider)
* For more information regarding OAuth2 login, please see the following documentation:
* https://www.ory.sh/docs/oauth2-oidc/custom-login-consent/flow
*
*/
export const Login = (): JSX.Element => {
const [flow, setFlow] = useState<LoginFlow | null>(null)
const [searchParams, setSearchParams] = useSearchParams()

// The aal is set as a query parameter by your Ory project
// aal1 is the default authentication level (Single-Factor)
// aal2 is a query parameter that can be used to request Two-Factor authentication
// https://www.ory.sh/docs/kratos/mfa/overview
const aal2 = searchParams.get("aal2")

// The login_challenge is a query parameter set by the Ory OAuth2 login flow
// Switching between pages should keep the login_challenge in the URL so that the
// OAuth flow can be completed upon completion of another flow (e.g. Registration).
// https://www.ory.sh/docs/oauth2-oidc/custom-login-consent/flow
const loginChallenge = searchParams.get("login_challenge")

// The return_to is a query parameter is set by you when you would like to redirect
// the user back to a specific URL after login is successful
// In most cases it is not necessary to set a return_to if the UI business logic is
// handled by the SPA.
//
// In OAuth flows this value might be ignored in favor of keeping the OAuth flow
// intact between multiple flows (e.g. Login -> Recovery -> Settings -> OAuth2 Consent)
// https://www.ory.sh/docs/oauth2-oidc/identity-provider-integration-settings
const returnTo = searchParams.get("return_to")

const navigate = useNavigate()

// Get the flow based on the flowId in the URL (.e.g redirect to this page after flow initialized)
Expand All @@ -26,12 +58,13 @@ 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
Expand Down Expand Up @@ -80,8 +113,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}
Expand Down
13 changes: 12 additions & 1 deletion examples/react-spa/src/Recovery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,18 @@ export const Recovery = () => {
// the flow is always required since it contains the UI form elements, UI error messages and csrf token
flow={flow}
// the recovery form should allow users to navigate to the login page
additionalProps={{ loginURL: "/login" }}
additionalProps={{
loginURL: {
handler: () => {
navigate(
{
pathname: "/login",
},
{ replace: true },
)
},
},
}}
// submit the form data to Ory
onSubmit={({ body }) => submitFlow(body as UpdateRecoveryFlowBody)}
/>
Expand Down
40 changes: 38 additions & 2 deletions examples/react-spa/src/Registration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,32 @@ import { useCallback, useEffect, useState } from "react"
import { useNavigate, useSearchParams } from "react-router-dom"
import { sdk, sdkError } from "./sdk"

/** Registration is a React component that renders the Registration form using Ory Elements.
* It is used to handle the registration flow for a variety of authentication methods.
*
* The Registration component also handles the OAuth2 registration flow (as an OAuth2 provider)
* For more information regarding OAuth2 registration, please see the following documentation:
* https://www.ory.sh/docs/oauth2-oidc/custom-login-consent/flow
*
*/
export const Registration = () => {
const [flow, setFlow] = useState<RegistrationFlow | null>(null)
const [searchParams, setSearchParams] = useSearchParams()

// The return_to is a query parameter is set by you when you would like to redirect
// the user back to a specific URL after registration is successful
// In most cases it is not necessary to set a return_to if the UI business logic is
// handled by the SPA.
// In OAuth flows this value might be ignored in favor of keeping the OAuth flow
// intact between multiple flows (e.g. Login -> Recovery -> Settings -> OAuth2 Consent)
// https://www.ory.sh/docs/oauth2-oidc/identity-provider-integration-settings
const returnTo = searchParams.get("return_to")

// The login_challenge is a query parameter set by the Ory OAuth2 registration flow
// Switching between pages should keep the login_challenge in the URL so that the
// OAuth flow can be completed upon completion of another flow (e.g. Login).
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)
Expand All @@ -28,7 +50,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 })
Expand Down Expand Up @@ -77,7 +102,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}
Expand Down
6 changes: 5 additions & 1 deletion examples/react-spa/src/Verification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ export const Verification = (): JSX.Element => {
flow={flow}
// we want users to be able to go back to the login page from the verification page
additionalProps={{
signupURL: "/registration",
signupURL: {
handler: () => {
navigate({ pathname: "/registration" }, { replace: true })
},
},
}}
// submit the verification form data to Ory
onSubmit={({ body }) => submitFlow(body as UpdateVerificationFlowBody)}
Expand Down
10 changes: 5 additions & 5 deletions src/react-components/button-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 {
Expand Down

0 comments on commit 880456c

Please sign in to comment.