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

The linked-accounts screen

The platform ships a member screen for listing, adding and removing sign-in methods, at /my-account/linked-accounts, but not its template. What the template receives, the routes its buttons call, the password form for a member who has none, and what each removal is refused for.

Who this is for. The theme developer offering members more than one way in. Afterwards you can build the screen, and say what a member will see when they try each button.

The screen

/my-account/linked-accounts, route listUserAuthenticators in the MyAccount module, renders o-auth/user-authenticators/index.html, which your site must write: the framework has a copy under users/user-authenticators/ that this route never reads. It receives:

VariableHolds
enabledAuthenticatorsEvery sign-in method enabled for the site, as name and internal class: local, passkey where passkeys are on, the enabled providers, and coredna
userAuthenticatorsThe member's links, each with getAuthenticatorClassName(), getCreated() and getModified()

The screen is optional. Offer it when members can use more than one method, or need to move from a password to a provider or the other way round. Most sites never expose it, and their members then have no way to add a second method themselves.

The buttons

ButtonRequestRoute name
ConnectPUT /oauth/providers/<name>addOAuthProvider in the OAuth module
RemoveDELETE /oauth/providers/<name>deleteOAuthProvider

Both are forms posted with _method and the CSRF token, and both come back to this screen, with a message when something was refused.

Template code templates/o-auth/user-authenticators/index.html, the list
{foreach $enabledAuthenticators as $name => $class}
  {if $name != 'coredna'}
    {$link = $userAuthenticators->filtered('authenticatorClassName', $class)->first()}
    <form method="post" action="{if $link}{$_url->getPath('OAuth', 'deleteOAuthProvider', ['provider' => $name])}{else}{$_url->getPath('OAuth', 'addOAuthProvider', ['provider' => $name])}{/if}">
      <input type="hidden" name="_method" value="{if $link}DELETE{else}PUT{/if}">
      <input type="hidden" name="_csrf" value="{$_csrfToken}">
      {$name|capitalize}
      {if $link}<span>connected {$link->getCreated()|format:'date'}</span>{/if}
      <button>{if $link}Remove{else}Connect{/if}</button>
    </form>
  {/if}
{/foreach}
{% for name, class in enabledAuthenticators %}
  {% if name != 'coredna' %}
    {% set link = userAuthenticators.filtered('authenticatorClassName', class).first() %}
    <form method="post" action="{{ link
        ? _url.getPath('OAuth', 'deleteOAuthProvider', {'provider': name})
        : _url.getPath('OAuth', 'addOAuthProvider', {'provider': name}) }}">
      <input type="hidden" name="_method" value="{{ link ? 'DELETE' : 'PUT' }}">
      <input type="hidden" name="_csrf" value="{{ _csrfToken }}">
      {{ name|capitalize }}
      {% if link %}<span>connected {{ link.getCreated()|date('j F Y') }}</span>{% endif %}
      <button>{{ link ? 'Remove' : 'Connect' }}</button>
    </form>
  {% endif %}
{% endfor %}

Give each method a proper label in place of its name: "Password" for local, "Passkeys" for passkey.

What Connect does

  • A provider starts the sign-in handshake and returns here when it completes (The sign-in handshake). The provider account must report the same email address as the member's username (Connecting a provider while signed in).
  • The password, for a member who has none, renders o-auth/user-authenticators/add.html, which you also write: a form with password and confirmPassword, posted back to the same address as a PUT with the CSRF token, and showing _validation errors. Once saved, the member can sign in with it. For a member who already has a password, password sign-in is switched back on.
  • Passkeys sends the member to /my-account/passkeys, where passkeys are created (Passkeys).

A member whose account is linked to Core dna manages their other sign-in methods on their Core dna account, not here.

What Remove does, and when it is refused

  • A provider: the link is deleted. The account at the provider is untouched.
  • The password: the link and the stored password are both deleted; adding password sign-in later needs a new password.
  • Passkeys: every passkey of the member is removed.

Two removals are refused, with a message on this screen:

RemovingMessage
The member's last way in"At least one category one user authenticator must be enabled"
The Core dna link"You are not permitted to remove Core dna authenticator from your own profile. Please ask a privileged user to perform this task."

The first message is the platform's wording; say it in the member's terms in your template, for example "Add another way to sign in before removing this one." The policy behind both is in Policy questions for the client.

Go deeper

Was this page helpful?

Your answer goes to the docs team.