Skip to content

Commit

Permalink
feat: prep work for passkeys and deduplicated registration nodes (#179)
Browse files Browse the repository at this point in the history
  • Loading branch information
hperl authored Feb 8, 2024
1 parent 4254a53 commit a24fc14
Show file tree
Hide file tree
Showing 10 changed files with 154 additions and 41 deletions.
1 change: 1 addition & 0 deletions src/react-components/ory/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const hasGroup = (group: string) => (nodes: UiNode[]) =>

export const hasOidc = hasGroup("oidc")
export const hasPassword = hasGroup("password")
export const hasProfile = hasGroup("profile")
export const hasWebauthn = hasGroup("webauthn")
export const hasPasskey = hasGroup("passkey")
export const hasLookupSecret = hasGroup("lookup_secret")
Expand Down
2 changes: 1 addition & 1 deletion src/react-components/ory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export * from "./sections/oidc-section"
export * from "./sections/oidc-settings-section"
export * from "./sections/password-settings-section"
export * from "./sections/passwordless-section"
export * from "./sections/profile-settings-section"
export * from "./sections/profile-section"
export * from "./sections/registration-section"
export * from "./sections/totp-settings-section"
export * from "./sections/webauthn-settings-section"
Expand Down
20 changes: 9 additions & 11 deletions src/react-components/ory/sections/auth-code-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,15 @@ export const AuthCodeSection = ({
}}
/>
<div className={gridStyle({ gap: 32 })}>
<div className={gridStyle({ gap: 16 })}>
{/* default group is used here automatically for login */}
<FilterFlowNodes
filter={{
nodes: nodes,
groups: "code",
withoutDefaultAttributes: true,
excludeAttributes: ["hidden", "button", "submit"], // the form will take care of default (csrf) hidden fields
}}
/>
</div>
{/* default group is used here automatically for login */}
<FilterFlowNodes
filter={{
nodes: nodes,
groups: "code",
withoutDefaultAttributes: true,
excludeAttributes: ["hidden", "button", "submit"], // the form will take care of default (csrf) hidden fields
}}
/>
{/* include hidden here because we want to have resend support */}
{/* exclude default group because we dont want to map csrf twice */}
<FilterFlowNodes
Expand Down
13 changes: 7 additions & 6 deletions src/react-components/ory/sections/passkey-settings-section.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { JSX } from "react"

import { SettingsFlow } from "@ory/client"
import { gridStyle } from "../../../theme"
import { FilterFlowNodes } from "../helpers/filter-flow-nodes"
import { hasPasskey } from "../helpers/utils"
import { gridStyle } from "../../../theme"

export interface PasskeySettingsProps {
flow: SettingsFlow
Expand All @@ -14,18 +14,19 @@ export const PasskeySettingsSection = ({
}: PasskeySettingsProps): JSX.Element | null => {
const filter = {
nodes: flow.ui.nodes,
groups: ["passkey", "webauthn"],
groups: "passkey",
withoutDefaultGroup: true,
}

return hasPasskey(flow.ui.nodes) ? (
<div>
<div className={gridStyle({ gap: 32 })}>
<FilterFlowNodes
filter={{ ...filter, attributes: "submit,button" }}
buttonOverrideProps={{ fullWidth: false }}
filter={{ ...filter, excludeAttributes: "onclick,button" }}
/>

<FilterFlowNodes
filter={{ ...filter, excludeAttributes: "submit,button" }}
filter={{ ...filter, attributes: "onclick,button" }}
buttonOverrideProps={{ fullWidth: false }}
/>
</div>
) : null
Expand Down
54 changes: 41 additions & 13 deletions src/react-components/ory/sections/passwordless-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import { hasPasskey, hasWebauthn } from "../helpers/utils"
export const PasswordlessSection = (
flow: SelfServiceFlow,
): JSX.Element | null => {
return hasWebauthn(flow.ui.nodes) || hasPasskey(flow.ui.nodes) ? (
return hasWebauthn(flow.ui.nodes) ? (
<div className={gridStyle({ gap: 32 })}>
<div className={gridStyle({ gap: 16 })}>
<FilterFlowNodes
filter={{
nodes: flow.ui.nodes,
// we will also map default fields here but not oidc and password fields
groups: ["webauthn", "passkey"],
groups: ["webauthn"],
withoutDefaultAttributes: true,
excludeAttributes: ["hidden", "button", "submit"], // the form will take care of hidden fields
}}
Expand All @@ -24,7 +24,7 @@ export const PasswordlessSection = (
<FilterFlowNodes
filter={{
nodes: flow.ui.nodes,
groups: ["webauthn", "passkey"],
groups: ["webauthn"],
withoutDefaultAttributes: true,
attributes: ["button", "submit"],
}}
Expand All @@ -33,24 +33,52 @@ export const PasswordlessSection = (
) : null
}

export const PasswordlessLoginSection = (
flow: SelfServiceFlow,
): JSX.Element | null => {
if (hasPasskey(flow.ui.nodes)) {
return (
<div className={gridStyle({ gap: 32 })}>
export const PasskeySection = (flow: SelfServiceFlow): JSX.Element | null => {
return hasPasskey(flow.ui.nodes) ? (
<div className={gridStyle({ gap: 32 })}>
<div className={gridStyle({ gap: 16 })}>
<FilterFlowNodes
filter={{
nodes: flow.ui.nodes,
groups: ["webauthn", "passkey"],
// we will also map default fields here but not oidc and password fields
groups: ["passkey"],
withoutDefaultAttributes: true,
attributes: ["button", "submit"],
excludeAttributes: ["hidden", "button", "submit"], // the form will take care of hidden fields
}}
/>
</div>
)
}
<FilterFlowNodes
filter={{
nodes: flow.ui.nodes,
groups: ["passkey"],
withoutDefaultAttributes: true,
attributes: ["button", "submit"],
}}
/>
</div>
) : null
}

export const PasskeyLoginSection = (
flow: SelfServiceFlow,
): JSX.Element | null => {
return hasPasskey(flow.ui.nodes) ? (
<div className={gridStyle({ gap: 32 })}>
<FilterFlowNodes
filter={{
nodes: flow.ui.nodes,
groups: ["passkey"],
withoutDefaultAttributes: true,
attributes: ["button", "submit"],
}}
/>
</div>
) : null
}

export const PasswordlessLoginSection = (
flow: SelfServiceFlow,
): JSX.Element | null => {
if (hasWebauthn(flow.ui.nodes)) {
return (
<div className={gridStyle({ gap: 32 })}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { JSX } from "react"
import { SettingsFlow } from "@ory/client"
import { gridStyle } from "../../../theme"
import { FilterFlowNodes } from "../helpers/filter-flow-nodes"
import { SelfServiceFlow } from "../helpers/types"
import { hasProfile } from "../helpers/utils"

export interface ProfileSettingsProps {
flow: SettingsFlow
Expand All @@ -24,3 +26,27 @@ export const ProfileSettingsSection = ({
</div>
)
}

export const ProfileRegistrationSection = (
flow: SelfServiceFlow,
): JSX.Element | null => {
return hasProfile(flow.ui.nodes) ? (
<div className={gridStyle({ gap: 32 })}>
<FilterFlowNodes
filter={{
nodes: flow.ui.nodes,
groups: ["profile"],
excludeAttributes: "submit,hidden",
}}
/>
<FilterFlowNodes
filter={{
nodes: flow.ui.nodes,
groups: ["profile"],
excludeAttributes: "hidden",
attributes: "submit",
}}
/>
</div>
) : null
}
3 changes: 2 additions & 1 deletion src/react-components/ory/sections/registration-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ export const RegistrationSection = ({
filter={{
nodes: nodes,
groups: ["password"],
excludeAttributes: "submit",
excludeAttributes: "submit,hidden",
}}
/>
</div>
<FilterFlowNodes
filter={{
nodes: nodes,
groups: ["password"],
excludeAttributes: "hidden",
attributes: "submit",
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ export const WebAuthnSettingsSection = ({
return hasWebauthn(flow.ui.nodes) ? (
<div className={gridStyle({ gap: 32 })}>
<FilterFlowNodes
filter={{ ...filter, excludeAttributes: "submit,button" }}
filter={{ ...filter, excludeAttributes: "onclick,button" }}
/>
<FilterFlowNodes
filter={{ ...filter, attributes: "submit,button" }}
filter={{ ...filter, attributes: "onclick,button" }}
buttonOverrideProps={{ fullWidth: false }}
/>
</div>
Expand Down
70 changes: 64 additions & 6 deletions src/react-components/ory/user-auth-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
hasLookupSecret,
hasPasskey,
hasPassword,
hasProfile,
hasTotp,
hasWebauthn,
} from "./helpers/utils"
Expand All @@ -35,10 +36,13 @@ import { LoggedInInfo } from "./sections/logged-info"
import { LoginSection } from "./sections/login-section"
import { OIDCSection } from "./sections/oidc-section"
import {
PasskeyLoginSection,
PasskeySection,
PasswordlessLoginSection,
PasswordlessSection,
} from "./sections/passwordless-section"
import { RegistrationSection } from "./sections/registration-section"
import { ProfileRegistrationSection } from "./sections/profile-section"

export interface LoginSectionAdditionalProps {
forgotPasswordURL?: CustomHref | string
Expand Down Expand Up @@ -198,7 +202,9 @@ export const UserAuthCard = ({
let $flow: JSX.Element | null = null
let $oidc: JSX.Element | null = null
let $code: JSX.Element | null = null
let $passwordless: JSX.Element | null = null
let $passwordlessWebauthn: JSX.Element | null = null
let $passkey: JSX.Element | null = null
let $profile: JSX.Element | null = null
let message: MessageSectionProps | null = null

// the user might need to logout on the second factor page.
Expand All @@ -214,9 +220,17 @@ export const UserAuthCard = ({
// passwordless can be shown if the user is not logged in (e.g. exclude 2FA screen) or if the flow is a registration flow.
// we want the login section to handle passwordless as well when we have a 2FA screen.
const canShowPasswordless = () =>
!!$passwordless &&
!!$passwordlessWebauthn &&
(!isLoggedIn(flow as LoginFlow) || flowType === "registration")

// passkey can be shown if the user is not logged in (e.g. exclude 2FA screen) or if the flow is a registration flow.
// we want the login section to handle passwordless as well when we have a 2FA screen.
const canShowPasskey = () =>
!!$passkey &&
(!isLoggedIn(flow as LoginFlow) || flowType === "registration")

const canShowProfile = () => !!$profile && flowType === "registration"

// the current flow is a two factor flow if the user is logged in and has any of the second factor methods enabled.
const isTwoFactor = () =>
flowType === "login" &&
Expand Down Expand Up @@ -252,7 +266,7 @@ export const UserAuthCard = ({
<FilterFlowNodes
filter={{
nodes: flow.ui.nodes,
groups: ["passkey", "webauthn"],
groups: ["passkey"],
withoutDefaultGroup: true,
}}
/>
Expand All @@ -269,6 +283,17 @@ export const UserAuthCard = ({
/>
</UserAuthForm>
),
hasProfile(flow.ui.nodes) && (
<UserAuthForm flow={flow} data-testid="profile-flow">
<FilterFlowNodes
filter={{
nodes: flow.ui.nodes,
groups: "profile",
withoutDefaultGroup: true,
}}
/>
</UserAuthForm>
),
hasTotp(flow.ui.nodes) && (
<UserAuthForm
flow={flow}
Expand Down Expand Up @@ -328,7 +353,8 @@ export const UserAuthCard = ({

switch (flowType) {
case "login":
$passwordless = PasswordlessLoginSection(flow)
$passwordlessWebauthn = PasswordlessLoginSection(flow)
$passkey = PasskeyLoginSection(flow)
$oidc = OIDCSection(flow)
$code = AuthCodeSection({ nodes: flow.ui.nodes })

Expand Down Expand Up @@ -366,7 +392,9 @@ export const UserAuthCard = ({
}
break
case "registration":
$passwordless = PasswordlessSection(flow)
$passwordlessWebauthn = PasswordlessSection(flow)
$passkey = PasskeySection(flow)
$profile = ProfileRegistrationSection(flow)
$oidc = OIDCSection(flow)
$code = AuthCodeSection({ nodes: flow.ui.nodes })
$flow = RegistrationSection({
Expand Down Expand Up @@ -493,6 +521,25 @@ export const UserAuthCard = ({
</>
)}

{canShowPasskey() && (
<>
<Divider />
<UserAuthForm
flow={flow}
submitOnEnter={true}
onSubmit={onSubmit}
data-testid={"passkey-flow"}
formFilterOverride={{
nodes: flow.ui.nodes,
groups: ["default", "passkey"],
attributes: "hidden",
}}
>
{$passkey}
</UserAuthForm>
</>
)}

{canShowPasswordless() && (
<>
<Divider />
Expand All @@ -503,13 +550,24 @@ export const UserAuthCard = ({
data-testid={"passwordless-flow"}
formFilterOverride={{
nodes: flow.ui.nodes,
groups: ["default", "webauthn"],
attributes: "hidden",
}}
>
{$passwordless}
{$passwordlessWebauthn}
</UserAuthForm>
</>
)}

{$profile && (
<>
<Divider />
<UserAuthForm flow={flow} data-testid={`${flowType}-flow-profile`}>
{$profile}
</UserAuthForm>
</>
)}

{message && MessageSection(message)}
</div>
</Card>
Expand Down
2 changes: 1 addition & 1 deletion src/react-components/ory/user-settings-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
import { LookupSecretSettingsSection } from "./sections/lookup-secret-settings-section"
import { OIDCSettingsSection } from "./sections/oidc-settings-section"
import { PasswordSettingsSection } from "./sections/password-settings-section"
import { ProfileSettingsSection } from "./sections/profile-settings-section"
import { ProfileSettingsSection } from "./sections/profile-section"
import { TOTPSettingsSection } from "./sections/totp-settings-section"
import { WebAuthnSettingsSection } from "./sections/webauthn-settings-section"
import { useIntl } from "react-intl"
Expand Down

0 comments on commit a24fc14

Please sign in to comment.