Table of Contents

Authentication

Manage user authentication in frontend (username/password, OTP, MFA, external providers/SSO)

The Users - Authentication app renders a frontend authentication UI (login form) and handles sign-in attempts (including MFA/OTP flows and external providers (SSO)). Your job as an implementer is mostly to:

  1. Render the right inputs for the currently active login mode
  2. Post the expected field names back to the platform
  3. React to Model.Result to show errors/next-step UI

The app/system supports the following login methods:

Method Description Notes
Username/password Classic login Use Username/Password when you want a familiar login and don’t require step-up security.
Multifactor Authentication (MFA) Username + password + verification step by link or code Use MFA when you want password + second factor (best for employees, admins, high-risk users).
OTP (One-time Password) Username + emailed password Use OTP / Magic link when you want "passwordless" (best for B2B buyers who hate passwords, or when you want to reduce credential stuffing risk).
Magic link Username + emailed link (no password) Use OTP / Magic link when you want "passwordless" (best for B2B buyers who hate passwords, or when you want to reduce credential stuffing risk).
External Authentication Login with e.g. Azure AD, Google, etc. Use External authentication when identity is owned elsewhere (Azure AD, Google Workspace, etc.) or when you want SSO.

The login method used depends on the login type selected for the solution - or directly on the user, via the login type set on the user's own settings. A login type set on the user overrides the solution-wide default for that user, for both frontend and backend login. This means that it's possible to have several "login tracks" active on a solution at the same time.

App settings

Users login

From the paragraph app you have the following settings as an editor, regardless of the solution's login type:

  • Login template: choose a Razor template from /Templates/Users/UserAuthentication/Login
  • One-time code authentication: sender name, sender email, email subject, and the one-time code email template, used for the MFA and OTP login types
  • Magic-link authentication: sender name, sender email, email subject, and the magic-link email template, used for the magic link login type
  • Redirect after authentication
    • Redirect to specific page: choose a fallback page to send users to after a successful login
    • Redirect back to referrer: if enabled, a successful login redirects to the page the user came from (when available)
  • Pages
    • Page to set password
    • Page to create user

The one-time code and magic-link settings are always available, even when the solution's login type is Normal - this lets you configure them ahead of time, or for individual users who have MFA, OTP, or magic link set directly on their account while the rest of the solution stays on Normal.

Note
  • If both Redirect back to referrer is enabled and a valid referrer exists, it wins; otherwise the module falls back to Redirect to specific page.
  • The old ForgotPasswordLink editor value is obsolete. Use the Page to set password setting instead.

Login templates

The login middleware looks for specific form keys to decide what to do - these are submitted to the system using .cshtml login templates located under /Templates/Users/UserAuthentication/Login.

That path is the classic template location. On a Swift solution, place login templates under the active design's folder instead, e.g. /Templates/Designs/Swift-v2/Users/UserAuthentication/Login. Swift does not ship a ready-made login template for MFA/OTP/magic link yet - use the examples below as a starting point.

In these templates you should inherit ViewModelTemplate<UserAuthenticationViewModel> and import the Dynamicweb.Users.Frontend.UserAuthentication namespace.

Common fields used for all types of templates:

  • redirect (hidden): return URL after success (use Model.RedirectAfterLogin)
  • Autologin (checkbox):"keep me signed in" (persistent cookie)
  • username
  • password (only for password-based flows)
  • shopid (optional; only relevant if you use shop separation)

A series of special fields are then used to trigger specific login flows. Regardless of the method used, your template should treat Model.Result as the canonical "what just happened?"-flag.

Notable return values (not exhaustive):

  • Success
  • IncorrectLogin
  • PasswordExpired
  • ExceededFailedLogOnLimit, LoginLocked
  • External: ExternalProviderAuthenticationFailed, ExternalProviderEmailNotProvided, …
  • MFA: MfaVerificationRequired, MfaVerificationFailed

A key nuance: "MfaVerificationRequired" is not an error. It means: Step 1 succeeded enough that we’re now waiting for verification. Below you can find minimal examples for all types of login methods with explanations.

Username/password

A minimal form for a simple username/password login form could look like this:

<form method="post">
  <input type="hidden" name="redirect" value="@Model.RedirectAfterLogin" />
  <input name="username" required />
  <input name="password" type="password" required />
  <button type="submit">Sign in</button>
</form>

Multifactor authentication (MFA)

MFA is a two-step UX:

Step 1: username + password (same as classic login)

If credentials are valid, the backend responds with MfaVerificationRequired and (on the first required attempt) sends the verification email.

Step 2: code entry form

<form method="post">
  <input type="hidden" name="redirect" value="@Model.RedirectAfterLogin" />
  <input type="hidden" name="dologin" value="1" />
  <input name="code" autocomplete="one-time-code" required />
  <button type="submit">Verify</button>
</form>

That maps to MFA verification on the server.

One-time password (OTP)

OTP is also two steps, but step 1 is username-only:

Step 1: "send code"

<form method="post">
  <input type="hidden" name="redirect" value="@Model.RedirectAfterLogin" />
  <input name="username" required />
  <input type="hidden" name="dologin" value="1" />
  <button type="submit">Send me a code</button>
</form>

The dologin key is the "yes I really mean it" flag that makes username-only posts legal. Then show the same code entry form as MFA step 2 (dologin + code).

Same step 1 as OTP (username + dologin), but the email contains a link instead of a code. Clicking it hits the site with:

  • DwExtranetMagicLink=<encrypted token>

…which the middleware verifies and signs the user in.

No extra Razor needed beyond your usual "result banner" (users might land back on the login page briefly depending on redirect).

External authentication

Render buttons like:

@foreach (var p in Model.ExternalLogins) {
  <form method="post">
    <input type="hidden" name="redirect" value="@Model.RedirectAfterLogin" />
    <button type="submit" name="DwExternalLoginProvider" value="@p.Id">
      Continue with @p.Name
    </button>
  </form>
}

That starts the external authentication "challenge" flow.

When the provider redirects back, the middleware finalizes sign-in on DwExtranetExternalLogin=true.

Adaptive template

Instead of implementing separate templates for each login method, you can create a single template which branches on Model.LoginType and Model.Result.

Below you will find a full example which supports all methods - it assumes you use Bootstrap-ish CSS, adjust as necessary.

Using this template:

  • Posting username/password triggers normal auth (or MFA step 1)
  • Posting username + dologin triggers passwordless step 1 (OTP/Magic link)
  • When the backend replies with MfaVerificationRequired, the module can send the email (code or magic link) and your template switches to the verification form
  • Posting dologin + code verifies
  • Posting DwExternalLoginProvider starts external login
@inherits ViewModelTemplate<Dynamicweb.Users.Frontend.UserAuthentication.UserAuthenticationViewModel>
@using Dynamicweb.Users.Frontend.UserAuthentication
@using Dynamicweb.Rendering
@using Dynamicweb

@{
    // Handy flags
    bool requiresVerification =
        Model.Result == UserAuthenticationResultType.MfaVerificationRequired
        || Model.Result == UserAuthenticationResultType.MfaVerificationFailed;

    // "Passwordless" login types (server will email code or magic link after the first POST)
    bool isPasswordless =
        Model.LoginType.ToString().Equals("TOTP", StringComparison.OrdinalIgnoreCase)
        || Model.LoginType.ToString().Equals("MagicLinks", StringComparison.OrdinalIgnoreCase);

    bool isMfa =
        Model.LoginType.ToString().Equals("MFA", StringComparison.OrdinalIgnoreCase);

    string? redirect = Model.RedirectAfterLogin;
}

<div class="login">

  @* Result banner *@
  @if (Model.Result != UserAuthenticationResultType.None
      && Model.Result != UserAuthenticationResultType.MfaVerificationRequired) {

      var isSuccess =
          Model.Result == UserAuthenticationResultType.Success
          || Model.Result == UserAuthenticationResultType.PasswordChangedSuccess;

      <div class="alert alert-@(isSuccess ? "success" : "danger")" role="alert">
          @Translate(Model.Result.ToString())
      </div>
  }

  @* Step 2: verification (code entry) *@
  @if (requiresVerification) {

      <h2>@Translate("Verify sign-in")</h2>

      @if (Model.MfaCodeExpiration.HasValue) {
          <p class="text-muted">
              @Translate("Enter the code we sent you. It expires at:")
              <strong>@Model.MfaCodeExpiration.Value.ToString("HH:mm:ss")</strong>
          </p>
      }

      <form method="post">
          <input type="hidden" name="redirect" value="@redirect" />
          <input type="hidden" name="dologin" value="1" />

          <label for="code">@Translate("Code")</label>
          <input id="code" name="code" autocomplete="one-time-code" required />

          <div class="mt-3">
              <button type="submit" class="btn btn-primary">
                  @Translate("Verify")
              </button>
          </div>
      </form>

      @* Optional: a "start over" link could just reload the page *@
  }
  else {

      @* Step 1: primary login *@
      <h2>@Translate("Sign in")</h2>

      <form method="post">
          <input type="hidden" name="redirect" value="@redirect" />

          <label for="username">@Translate("Email")</label>
          <input id="username" name="username" type="email" autocomplete="username" required />

          @* Password is required for classic login + MFA (first step) *@
          @if (!isPasswordless) {
              <label for="password">@Translate("Password")</label>
              <input id="password" name="password" type="password" autocomplete="current-password" required />
          }
          else {
              @* This is the critical "passwordless trigger" *@
              <input type="hidden" name="dologin" value="1" />
              <p class="text-muted">
                  @Translate("We’ll email you a code or a sign-in link.")
              </p>
          }

          <div class="mt-2">
              <input type="checkbox" value="True" name="Autologin" id="remember-me" />
              <label for="remember-me">@Translate("Keep me signed in")</label>
          </div>

          <div class="mt-3">
              <button type="submit" class="btn btn-primary">
                  @(isPasswordless ? Translate("Send me a code") : Translate("Sign in"))
              </button>
          </div>

          <div class="mt-3">
              @if (!string.IsNullOrEmpty(Model.CreatePasswordLink)) {
                  <a href="@Model.CreatePasswordLink">@Translate("Forgot / set password")</a>
              }
              @if (!string.IsNullOrEmpty(Model.CreateUserLink)) {
                  <span> · </span>
                  <a href="@Model.CreateUserLink">@Translate("Create account")</a>
              }
          </div>
      </form>

      @* External providers *@
      @if (Model.ExternalLogins?.Any() == true) {
          <hr />
          <div class="external-logins">
              <div class="text-muted">@Translate("Or continue with")</div>

              @foreach (var p in Model.ExternalLogins) {
                  <form method="post" class="mt-2">
                      <input type="hidden" name="redirect" value="@redirect" />
                      <button type="submit" name="DwExternalLoginProvider" value="@p.Id" class="btn btn-outline-primary">
                          @if (!string.IsNullOrEmpty(p.Icon)) {
                              <img src="@p.Icon" alt="" style="height:18px; width:auto;" />
                          }
                          <span>@p.Name</span>
                      </button>
                  </form>
              }
          </div>
      }
  }

</div>

Email templates

When verification via email is required - for MFA, OTP, and magic link - the module can be set up to send out emails: a one-time code email for MFA/OTP, or a magic link email. Which one is sent depends on the active login type for the user or solution.

Templates for these emails should be placed under /Templates/Users/UserAuthentication/Email.

On a Swift solution, place these under the active design's folder instead, e.g. /Templates/Designs/Swift-v2/Users/UserAuthentication/Email. Swift does not ship a ready-made template for these yet.

OTP email template example

@inherits ViewModelTemplate<Dynamicweb.Users.Frontend.UserAuthentication.Email.OneTimeCodeEmailViewModel>
@using Dynamicweb.Users.Frontend.UserAuthentication.Email

<h1>Your one-time code</h1>
<p><strong>@Model.Code</strong></p>
<p>This code is valid for @Model.CodeLifetime seconds.</p>

<p style="color:#666">
  Requested from @Model.IP using @Model.Browser on @Model.OperationSystem.
</p>
@inherits ViewModelTemplate<Dynamicweb.Users.Frontend.UserAuthentication.Email.LinkEmailViewModel>
@using Dynamicweb.Users.Frontend.UserAuthentication.Email

<h1>Your sign-in link</h1>
<p><a href="@Model.Link">Click here to sign in</a></p>

<p style="color:#666">
  This link expires at @Model.Expiration.
</p>

Implementation tips

A few practical details to keep in mind when building your login templates.

Updating an existing login template

If you have an existing template like the current one (username/password + external providers), you mainly need to add:

  1. A branch that detects Model.Result == MfaVerificationRequired (and failed) and renders the code-entry form
  2. A passwordless "send code" mode (username + hidden dologin) when Model.LoginType is passwordless
  3. Add handling for MfaVerificationFailed in your result messages

Your current template is a good starting point; it already renders results and external providers, but it always requires a password and never shows the verification step.

Code lifetime + max attempts

Code lifetime is controlled by /Globalsettings/Modules/Extranet/LogOn/CodeLifetimeInSeconds (minimum 10s, default 120s) or via the Frontend login settings.

Verification attempts are capped (max failed code input attempts = 3). Codes and magic links are single-use: a successful verification removes it from the cache so it cannot be replayed.

So in your UI:

  • Show an "expires at" hint using Model.MfaCodeExpiration when present
  • When MfaVerificationFailed, show a clear message and let users restart the flow

Redirect handling

You don’t need to reverse-engineer redirect rules. Just post redirect=@Model.RedirectAfterLogin back as a hidden field.

The platform will:

  • Prefer the redirect value if valid and safe
  • Otherwise try user/group start page
  • Otherwise referrer
  • Otherwise /

Accessibility basics

Keep the form usable for assistive technology:

  • Use real <label for=""> bindings
  • Use autocomplete="username" / "current-password" / "one-time-code" where relevant
  • Put the error message near the submit button (people scan there)
To top