Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: CustomHref and update react-spa example #154

Merged
merged 2 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Comment on lines +44 to +117
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kmherrmann This might be what you wanted a couple months ago 😅

The reason why you got cookie problems when testing out the OAuth flows were that the link generated by the SDK was always on the project slug URL and never localhost. This means you set OAuth cookies on the oryapis.com domain instead of localhost.


### Using and Modifying the Example

Expand Down
41 changes: 37 additions & 4 deletions examples/react-spa/src/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { sdk, sdkError } from "./sdk"
export const Login = (): JSX.Element => {
const [flow, setFlow] = useState<LoginFlow | null>(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()

Expand All @@ -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
Expand Down Expand Up @@ -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}
Expand Down
21 changes: 19 additions & 2 deletions examples/react-spa/src/Registration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ export const Registration = () => {
const [flow, setFlow] = useState<RegistrationFlow | null>(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)
Expand All @@ -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 })
Expand Down Expand Up @@ -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}
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
Loading