Skip to content

Commit

Permalink
feat: passwordless strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
hperl committed Dec 11, 2023
1 parent 36e85dd commit a138736
Show file tree
Hide file tree
Showing 16 changed files with 168 additions and 54 deletions.
2 changes: 2 additions & 0 deletions src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
"settings.navigation-profile": "Profil",
"settings.navigation-totp": "Zwei-Faktor App",
"settings.navigation-webauthn": "Sicherheitsschlüssel",
"settings.navigation-passkey": "Passkeys",
"settings.subtitle-instructions": "Hier können Sie Einstellungen zu Ihrem Konto vornehmen. Für manche Einstellungen müssen Sie Ihre Identität erneut bestätigen.",
"settings.title": "Kontoeinstellungen",
"settings.title-lookup-secret": "Manage 2FA Backup Recovery Codes",
Expand All @@ -166,6 +167,7 @@
"settings.title-profile": "Profil Einstellungen",
"settings.title-totp": "Zwei-Faktor TOTP App",
"settings.title-webauthn": "Sicherheitsschlüssel verwalten",
"settings.title-passkey": "Passkeys verwalten",
"verification.registration-button": "Registrieren",
"verification.registration-label": "Sie haben noch kein Konto?",
"verification.title": "Verifizieren Sie ihr Konto"
Expand Down
2 changes: 2 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
"settings.navigation-profile": "Profile",
"settings.navigation-totp": "Authenticator App",
"settings.navigation-webauthn": "Hardware Tokens",
"settings.navigation-passkey": "Passkeys",
"settings.subtitle-instructions": "Here you can manage settings related to your account. Keep in mind that certain actions require you to re-authenticate.",
"settings.title": "Account Settings",
"settings.title-lookup-secret": "Manage 2FA Backup Recovery Codes",
Expand All @@ -166,6 +167,7 @@
"settings.title-profile": "Profile Settings",
"settings.title-totp": "Manage 2FA TOTP Authenticator App",
"settings.title-webauthn": "Manage Hardware Tokens",
"settings.title-passkey": "Manage Passkeys",
"verification.registration-button": "Sign up",
"verification.registration-label": "Don't have an account?",
"verification.title": "Verify your account"
Expand Down
2 changes: 2 additions & 0 deletions src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
"settings.navigation-profile": "Perfil",
"settings.navigation-totp": "Aplicación Autenticadora",
"settings.navigation-webauthn": "Tokens de Hardware",
"settings.navigation-passkey": "Passkeys",
"settings.subtitle-instructions": "Aquí puede gestionar las configuraciones relacionadas con su cuenta. Tenga en cuenta que ciertas acciones requieren que vuelva a autenticarse.",
"settings.title": "Configuraciones de la Cuenta",
"settings.title-lookup-secret": "Gestionar Códigos de Recuperación de Respaldo de 2FA",
Expand All @@ -166,6 +167,7 @@
"settings.title-profile": "Configuraciones del Perfil",
"settings.title-totp": "Gestionar Aplicación Autenticadora de 2FA",
"settings.title-webauthn": "Gestionar Tokens de Hardware",
"settings.title-passkey": "Gestionar Passkeys",
"verification.registration-button": "Registrarse",
"verification.registration-label": "¿No tiene una cuenta?",
"verification.title": "Verificar su cuenta"
Expand Down
2 changes: 2 additions & 0 deletions src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
"settings.navigation-profile": "Profil",
"settings.navigation-totp": "Application d'authentification TOTP",
"settings.navigation-webauthn": "Tokens matériels",
"settings.navigation-passkey": "Passkeys",
"settings.subtitle-instructions": "Ici, vous pouvez gérer les paramètres liés à votre compte. Gardez à l'esprit que certaines actions nécessitent une nouvelle authentification.",
"settings.title": "Paramètres du compte",
"settings.title-lookup-secret": "Gérer les codes de récupération 2FA",
Expand All @@ -166,6 +167,7 @@
"settings.title-profile": "Paramètres de profil",
"settings.title-totp": "Gérer l'application d'authentification TOTP 2FA",
"settings.title-webauthn": "Gérer les tokens matériels",
"settings.title-passkey": "Gérer les Passkeys",
"verification.registration-button": "S'inscrire",
"verification.registration-label": "Vous n'avez pas de compte ?",
"verification.title": "Vérifiez votre compte"
Expand Down
2 changes: 2 additions & 0 deletions src/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
"settings.navigation-profile": "Profiel",
"settings.navigation-totp": "Authenticator App",
"settings.navigation-webauthn": "Hardware Tokens",
"settings.navigation-passkey": "Passkeys",
"settings.subtitle-instructions": "Hier kun je instellingen beheren die verband houden met je account. Houd er rekening mee dat bepaalde acties vereisen dat je opnieuw wordt geauthenticeerd.",
"settings.title": "Accountinstellingen",
"settings.title-lookup-secret": "Beheer 2FA Backup Herstelcodes",
Expand All @@ -166,6 +167,7 @@
"settings.title-profile": "Profielinstellingen",
"settings.title-totp": "Beheer 2FA TOTP Authenticator App",
"settings.title-webauthn": "Beheer Hardware Tokens",
"settings.title-passkey": "Beheer Passkeys",
"verification.registration-button": "Registreren",
"verification.registration-label": "Heb je nog geen account?",
"verification.title": "Verifieer je account"
Expand Down
2 changes: 2 additions & 0 deletions src/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
"settings.navigation-profile": "Perfil",
"settings.navigation-totp": "Aplicação Autenticador 2FA",
"settings.navigation-webauthn": "Tokens de Hardware",
"settings.navigation-passkey": "Passkeys",
"settings.subtitle-instructions": "Aqui pode gerar configurações relacionadas com a sua conta. Lembre-se de que certas ações exigem que efetue novamente login.",
"settings.title": "Configurações da Conta",
"settings.title-lookup-secret": "Gerar Códigos de Recuperação de Backup de 2FA",
Expand All @@ -166,6 +167,7 @@
"settings.title-profile": "Configurações de Perfil",
"settings.title-totp": "Gerar Aplicação Autenticador 2FA",
"settings.title-webauthn": "Gerar Tokens de Hardware",
"settings.title-passkey": "Gerar Passkeys",
"verification.registration-button": "Registar",
"verification.registration-label": "Não tem uma conta?",
"verification.title": "Verifique a sua conta"
Expand Down
2 changes: 2 additions & 0 deletions src/locales/se.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
"settings.navigation-profile": "Profil",
"settings.navigation-totp": "Autentiserings App",
"settings.navigation-webauthn": "Hårdvaru Tokens",
"settings.navigation-passkey": "Passkeys",
"settings.subtitle-instructions": "Här kan du hantera inställningar relaterade till ditt konto. Tänk på att vissa åtgärder kräver att du autentiseras på nytt.",
"settings.title": "Kontoinställningar",
"settings.title-lookup-secret": "Hantera 2FA backup-återställningskoder",
Expand All @@ -166,6 +167,7 @@
"settings.title-profile": "Profilinställningar",
"settings.title-totp": "Hantera 2FA TOTP Autentiserings App",
"settings.title-webauthn": "Hantera Hårdvaru Tokens",
"settings.title-passkey": "Hantera Passkeys",
"verification.registration-button": "Skapa konto",
"verification.registration-label": "Har du inget konto?",
"verification.title": "Verifiera ditt konto"
Expand Down
9 changes: 9 additions & 0 deletions src/markup-components/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ import {
UserSettingsScreenProps,
WebAuthnSettingsProps,
WebAuthnSettingsSection as webAuthnSettingsSection,
PasskeySettingsSection as passkeySettingsSection,
PasskeySettingsProps,
} from "../react-components"
import { ComponentWrapper, Context } from "./component-wrapper"

Expand Down Expand Up @@ -191,6 +193,13 @@ export const WebAuthnSettingsSection = (
return ComponentWrapper(webAuthnSettingsSection, props, context)
}

export const PasskeySettingsSection = (
props: PasskeySettingsProps,
context: Context = {},
) => {
return ComponentWrapper(passkeySettingsSection, props, context)
}

export const OIDCSettingsSection = (
props: OIDCSettingsProps,
context: Context = {},
Expand Down
76 changes: 47 additions & 29 deletions src/react-components/ory/helpers/node.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UiNode, UiText } from "@ory/client"
import { UiNode, UiNodeAttributes, UiText } from "@ory/client"
import {
isUiNodeAnchorAttributes,
isUiNodeImageAttributes,
Expand Down Expand Up @@ -106,37 +106,37 @@ export const uiTextToFormattedMessage = (
// context might provide an array of objects instead of a single object
// for example when looking up a recovery code
/*
*
{
"text": {
"id": 1050015,
"text": "3r9noma8, tv14n5tu, ...",
"type": "info",
"context": {
"secrets": [
*
{
"text": {
"id": 1050015,
"text": "3r9noma8, tv14n5tu, ...",
"type": "info",
"context": {
"secret": "3r9noma8"
},
"id": 1050009,
"text": "3r9noma8",
"type": "info"
"secrets": [
{
"context": {
"secret": "3r9noma8"
},
"id": 1050009,
"text": "3r9noma8",
"type": "info"
},
{
"context": {
"secret": "tv14n5tu"
},
"id": 1050009,
"text": "tv14n5tu",
"type": "info"
},
]
}
},
{
"context": {
"secret": "tv14n5tu"
},
"id": 1050009,
"text": "tv14n5tu",
"type": "info"
},
]
}
},
"id": "lookup_secret_codes",
"node_type": "text"
}
*/
"id": "lookup_secret_codes",
"node_type": "text"
}
*/
if (Array.isArray(value)) {
return {
...accumulator,
Expand Down Expand Up @@ -182,6 +182,18 @@ export const uiTextToFormattedMessage = (
)
}

function dataAttributes(attrs: UiNodeAttributes): Record<string, string> {
return Object.entries(attrs).reduce(
(accumulator, [key, value]) => {
if (key.startsWith("data-")) {
accumulator[key] = value
}
return accumulator
},
{} as Record<string, string>,
)
}

export const Node = ({
node,
className,
Expand All @@ -205,6 +217,7 @@ export const Node = ({
header={formatMessage(node.meta.label)}
width={node.attributes.width}
height={node.attributes.height}
{...dataAttributes(node.attributes)}
/>
)
} else if (isUiNodeTextAttributes(node.attributes)) {
Expand Down Expand Up @@ -307,6 +320,7 @@ export const Node = ({
disabled={attrs.disabled}
{...(buttonSocialOverrideProps && buttonSocialOverrideProps)}
{...submit}
{...dataAttributes(attrs)}
/>
) : (
<Button
Expand All @@ -318,6 +332,7 @@ export const Node = ({
disabled={attrs.disabled}
{...(buttonOverrideProps && buttonOverrideProps)}
{...submit}
{...dataAttributes(attrs)}
/>
)
case "datetime-local":
Expand All @@ -335,6 +350,7 @@ export const Node = ({
disabled={attrs.disabled}
defaultChecked={Boolean(attrs.value)}
dataTestid={`node/input/${attrs.name}`}
{...dataAttributes(attrs)}
/>
)
default:
Expand All @@ -356,6 +372,7 @@ export const Node = ({
required={attrs.required}
disabled={attrs.disabled}
pattern={attrs.pattern}
{...dataAttributes(attrs)}
/>
)
}
Expand All @@ -367,6 +384,7 @@ export const Node = ({
data-testid={`node/anchor/${node.attributes.id}`}
className={className}
position="center"
{...dataAttributes(node.attributes)}
>
{formatMessage(node.attributes.title)}
</ButtonLink>
Expand Down
27 changes: 10 additions & 17 deletions src/react-components/ory/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,16 @@

import { UiNode } from "@ory/client"

export const hasOidc = (nodes: UiNode[]) =>
nodes.some(({ group }) => group === "oidc")

export const hasPassword = (nodes: UiNode[]) =>
nodes.some(({ group }) => group === "password")

export const hasWebauthn = (nodes: UiNode[]) =>
nodes.some(({ group }) => group === "webauthn")

export const hasLookupSecret = (nodes: UiNode[]) =>
nodes.some(({ group }) => group === "lookup_secret")

export const hasTotp = (nodes: UiNode[]) =>
nodes.some(({ group }) => group === "totp")

export const hasCode = (nodes: UiNode[]) =>
nodes.some(({ group }) => group === "code")
export const hasGroup = (group: string) => (nodes: UiNode[]) =>
nodes.some(({ type, group: g }) => type === "input" && g === group)

export const hasOidc = hasGroup("oidc")
export const hasPassword = hasGroup("password")
export const hasWebauthn = hasGroup("webauthn")
export const hasPasskey = hasGroup("passkey")
export const hasLookupSecret = hasGroup("lookup_secret")
export const hasTotp = hasGroup("totp")
export const hasCode = hasGroup("code")

export const hasHiddenIdentifier = (nodes: UiNode[]) =>
nodes.some(
Expand Down
1 change: 1 addition & 0 deletions src/react-components/ory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export * from "./sections/profile-settings-section"
export * from "./sections/registration-section"
export * from "./sections/totp-settings-section"
export * from "./sections/webauthn-settings-section"
export * from "./sections/passkey-settings-section"
export * from "./user-auth-card"
export * from "./user-error-card"
export * from "./user-settings-card"
Expand Down
32 changes: 32 additions & 0 deletions src/react-components/ory/sections/passkey-settings-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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"

export interface PasskeySettingsProps {
flow: SettingsFlow
}

export const PasskeySettingsSection = ({
flow,
}: PasskeySettingsProps): JSX.Element | null => {
const filter = {
nodes: flow.ui.nodes,
groups: ["passkey", "webauthn"],
withoutDefaultGroup: true,
}

return hasPasskey(flow.ui.nodes) ? (
<div>
<FilterFlowNodes
filter={{ ...filter, attributes: "submit,button" }}
buttonOverrideProps={{ fullWidth: false }}
/>
<FilterFlowNodes
filter={{ ...filter, excludeAttributes: "submit,button" }}
/>
</div>
) : null
}
25 changes: 21 additions & 4 deletions src/react-components/ory/sections/passwordless-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ import { JSX } from "react"
import { gridStyle } from "../../../theme"
import { FilterFlowNodes } from "../helpers/filter-flow-nodes"
import { SelfServiceFlow } from "../helpers/types"
import { hasWebauthn } from "../helpers/utils"
import { hasPasskey, hasWebauthn } from "../helpers/utils"

export const PasswordlessSection = (
flow: SelfServiceFlow,
): JSX.Element | null => {
return hasWebauthn(flow.ui.nodes) ? (
return hasWebauthn(flow.ui.nodes) || hasPasskey(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"],
groups: ["webauthn", "passkey"],
withoutDefaultAttributes: true,
excludeAttributes: ["hidden", "button", "submit"], // the form will take care of hidden fields
}}
Expand All @@ -24,7 +24,24 @@ export const PasswordlessSection = (
<FilterFlowNodes
filter={{
nodes: flow.ui.nodes,
groups: ["webauthn"],
groups: ["webauthn", "passkey"],
withoutDefaultAttributes: true,
attributes: ["button", "submit"],
}}
/>
</div>
) : null
}

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

0 comments on commit a138736

Please sign in to comment.