Guide 07 · Developer docs

Members, accounts and gated content

Building a member area on Core dna: who a member is in the data, the sign-in flows and their templates, single sign-on and passkeys, the my-account routing convention, members-only content, sessions and the page cache, and the security checks to run before launch.

L2
Unit 03 · Authenticators, SSO and linked accounts

Passkeys

Members can sign in with a passkey instead of a password: a credential kept by their device or password manager. The settings, the script that runs the browser side, enrolling at registration or from the account, signing in, the management screen, and the browser and domain constraints to plan for.

Who this is for. The theme developer adding passkeys to a member site. Afterwards you can switch them on, add the markup to the sign-in, registration and account pages, and tell the client what members need.

The settings

In the OAuth module's settings, under Passkeys:

SettingDefaultEffect
EnableOffPasskeys on for the site
Forbid Backend LoginOffOn, passkeys cannot be used to sign in to the admin
Relying party IDEmpty: the hostname of each requestThe domain passkeys belong to. Set a parent domain, such as example.com, only to share passkeys between its subdomains; it must be one of the site's hostnames or a parent of one. Changing it stops existing passkeys working
Relying party nameEmpty: the site's nameThe name the browser or password manager shows when a passkey is created
Require user verificationOnThe device must verify the person (fingerprint, face, PIN or unlock) whenever a passkey is created or used

With passkeys on, the account templates receive passkeyEnabled, and the site answers /.well-known/passkey-endpoints, which tells devices where members manage their passkeys.

The script

The browser's side is done by a script the platform serves, /assets/passkeys.js. Include it on each page that uses passkeys, with the page's script nonce, and it wires up elements by their CSS classes and data- attributes; it also exposes window.CorednaPasskeys for code of your own. On pages for signed-in members it needs the CSRF token, which it finds in a data-csrf-token attribute on an enclosing element, among other places.

Enrolling a passkey

A passkey is added in one of two places, and both tie it to an account the person has proved is theirs:

  • From the account, signed in, on /my-account/passkeys (below).
  • At registration, instead of a password: the registration form posts passkey=1, the account is created, and the member is sent to the platform's /oauth/passkey/add page to create the passkey, within a short window and in the same browser. They are signed in once it is created. Show the option only when passkeyEnabled is true (The form).

A member whose account is linked to Core dna manages passkeys on their Core dna account, not on the site.

Signing in with a passkey

On the sign-in page, two pieces of markup do it: the username field offers the member's passkeys as they type, where the browser supports it, and a button signs in with a passkey on demand. The button stays hidden until the script finds the browser can use passkeys.

Template code In templates/users/login.html
{if $passkeyEnabled}
  <form method="post" action="{$_url->getPath('login')}" class="coredna-passkey-conditional"
        data-options-url="{$_url->getFrontendPath('OAuth', 'passkeyLoginOptions')}"
        data-username-input="#username" data-error-target="#passkey-error">
    <input id="username" name="username" autocomplete="username webauthn" required>
    <input type="password" name="password" autocomplete="current-password" required>
    <button>Sign in</button>
    <button type="button" class="coredna-passkey-ui coredna-passkey-login" hidden
            data-options-url="{$_url->getFrontendPath('OAuth', 'passkeyLoginOptions')}"
            data-username-input="#username" data-error-target="#passkey-error">Sign in with a passkey</button>
  </form>
  <div id="passkey-error" hidden><span data-passkey-message></span></div>
  <script src="/assets/passkeys.js" nonce="{$_inlineScriptNonce}" defer></script>
{/if}
{% if passkeyEnabled %}
  <form method="post" action="{{ _url.getPath('login') }}" class="coredna-passkey-conditional"
        data-options-url="{{ _url.getFrontendPath('OAuth', 'passkeyLoginOptions') }}"
        data-username-input="#username" data-error-target="#passkey-error">
    <input id="username" name="username" autocomplete="username webauthn" required>
    <input type="password" name="password" autocomplete="current-password" required>
    <button>Sign in</button>
    <button type="button" class="coredna-passkey-ui coredna-passkey-login" hidden
            data-options-url="{{ _url.getFrontendPath('OAuth', 'passkeyLoginOptions') }}"
            data-username-input="#username" data-error-target="#passkey-error">Sign in with a passkey</button>
  </form>
  <div id="passkey-error" hidden><span data-passkey-message></span></div>
  <script src="/assets/passkeys.js" nonce="{{ _inlineScriptNonce }}" defer></script>
{% endif %}

The username field must carry autocomplete="username webauthn" for the passkey suggestions to appear; a shared input partial that sets autocomplete="off" stops them. On success the script follows the platform's redirect, so the member lands where any sign-in lands. Errors appear in the element named by data-error-target, in plain words such as "Passkey request was cancelled or timed out. Please try again."

The passkeys screen

/my-account/passkeys, route passkeys in the MyAccount module, lists the member's passkeys and lets them add, rename and remove them. It renders users/passkeys/index.html. The framework ships one in Smarty that works as it is; override it to restyle, and on a Twig site write your own. It receives:

VariableHolds
passkeysThe member's passkeys, each with getName(), getAuthenticatorName() (the password manager or device that made it, such as "iCloud Keychain", or nothing when unknown), getCreated(), getLastUsed() and isBackedUp()
passkeyEnabledWhether passkeys are on for the site
passkeyAllowedWhether this member may use them; false for accounts linked to Core dna
hasEmailWhether the member has an email address, for recovering access if every passkey is lost

The buttons are markup for the script: an "Add a passkey" button with the class coredna-passkey-register and the options and add routes in data- attributes, and rename and remove buttons with the class coredna-passkey-action. Copy the framework's file as the starting point rather than writing the attributes from memory. Removing the member's last passkey takes passkey sign-in off their account, and is refused like any other last way in.

Constraints to plan for

  • Passkeys belong to a domain. A passkey created on a development or staging hostname does not work on the production domain, and members cannot carry passkeys between unrelated domains. Test enrolment and sign-in on the production hostname before launch, and do not change the relying party ID once members have passkeys.
  • Browser and device support. Current browsers on current operating systems support passkeys; the suggestions in the username field need a recent browser, and the script falls back to the button where they are missing. Keep another way in, a password or a provider, for members on older devices.
  • Recovery. A member who loses every device holding their passkey needs another way in. With an email address on the account, the forgotten-password flow is that way, so warn members without one, as the framework's screen does.
  • Administrators can see and remove a user's passkeys on the user's record in the admin, but cannot create one for them: creating a passkey needs the member's own device.
Go deeper

Was this page helpful?

Your answer goes to the docs team.