Developer forum

Forum » Development » Implement a DW10 frontend MFA Extranet login flow

Implement a DW10 frontend MFA Extranet login flow

Sebastian Andersen
Reply

Are there any examples or guides on how to implement a frontend MFA extranet login flow?

Currently when we try to log into extranet with a user that has authentication method set to MFA, then we get the error "Login failed for user." on the frontend. We confirmed with Dynamicweb Care that this happens because there is no default MFA login flow for the extranet app on the frontend, and we need to implement that ourselves.

This is on the newest ring 1 release of Dynamicweb 10.22.5.


Replies

 
Nicolai Pedersen Dynamicweb Employee
Nicolai Pedersen
Reply
This post has been marked as an answer

Hi

The curent Swift 2 implementation does not implement MFA/OTP yet.

Be aware that we have the "old" login module (Known as the Extranet module) and the new one called "Users - Authentication" which is documented here:

https://doc.dynamicweb.dev/documentation/implementing/content/apps/users/users-login.html

Only the new one supports MFA/OTP/Magic links

I am just updating the documentation article - below is the new version that is currently in review so it might contain mistakes.

Users - Authentication

The Users – Authentication app renders a frontend authentication UI 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.

Paragraph app settings

Users login

From the paragraph app you have the following settings as an editor:

  • Login template: choose a Razor template from /Templates/Users/UserAuthentication/Login.
  • 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

[!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.

Mental model: one module, several login “tracks”

The module renders a Razor template with a UserAuthenticationViewModel. That model tells you:

  • Which login type is active (Model.LoginType)
  • What happened last postback (Model.Result)
  • Where to send users after successful login (Model.RedirectAfterLogin)
  • Which extra actions exist (create user / set password links, external providers)

This is created server-side in the module and handed to your template.

Supported login methods (frontend)

Dynamicweb 10’s login pipeline supports these frontend experiences:

  • Username + password (“classic login”)
  • OTP (One-time code): username + emailed code (no password)
  • Magic link: username + emailed link (no password)
  • MFA: username + password, then a verification step (code or link)
  • External authentication: e.g. Azure AD / Google etc. (configured providers)

The server decides which one is relevant via the LoginType (globally and/or per user).

Choosing a login method (how the platform decides)

The platform resolves LoginType like this:

  • It can be set globally (system setting), and
  • it can be overridden per user (if the user has a specific login type configured)

That resolution is done in the authentication manager and surfaced to the template through Model.LoginType.

Practical guidance

  • Use Username/Password when you want a familiar login and don’t require step-up security.
  • Use MFA when you want password + second factor (best for employees, admins, high-risk users).
  • Use OTP / Magic link when you want “passwordless” (best for B2B buyers who hate passwords, or when you want to reduce credential stuffing risk).
  • Use External authentication when identity is owned elsewhere (Azure AD, Google Workspace, etc.) or when you want SSO.

The form contract: field names the backend actually listens for

The login middleware looks for specific form keys to decide what to do.

Template location: /Templates/Users/UserAuthentication/Login. Place a .cshtml file in /Templates/Users/UserAuthentication/Login.
The template should inherit ViewModelTemplate<UserAuthenticationViewModel> and import the Dynamicweb.Users.Frontend.UserAuthentication namespace.

Common fields

  • 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)

Special fields that trigger specific flows

Start passwordless (OTP / Magic link) without a password

  • Include username
  • Include dologin (any value)
    Because the middleware only allows “username without password” when dologin exists.

Verify MFA/OTP code

  • Include dologin
  • Include code (the user-entered code)

This calls MFA verification on the backend.

External provider login

  • Post DwExternalLoginProvider = provider id
    This starts the external challenge.

External provider callback

  • Query string contains DwExtranetExternalLogin=true
    The middleware finalizes the external sign-in.

Magic link callback

  • Query string contains DwExtranetMagicLink=<encrypted code>
    The middleware verifies and signs the user in.

Handling outcomes: UserAuthenticationResultType

Your template should treat Model.Result as the canonical “what just happened?” flag.

Notable values (not exhaustive):

  • Success
  • IncorrectLogin
  • PasswordExpired
  • ExceededFailedLogOnLimitLoginLocked
  • External: ExternalProviderAuthenticationFailedExternalProviderEmailNotProvided, …
  • MFA: MfaVerificationRequiredMfaVerificationFailed

A key nuance: “MfaVerificationRequired” is not an error. It means: Step 1 succeeded enough that we’re now waiting for verification.

Implementation pattern: one template that adapts

You can implement this as either:

  • Separate templates per login mode (simple, but duplicated markup), or
  • One template that branches on Model.LoginType and Model.Result (usually best)

Below is a single-template pattern that supports all methods.

The example assumes you already have Bootstrap-ish CSS (adjust freely).

Full example: username/password + OTP + MFA + external providers

@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>

Why this works (and what it maps to server-side):

  • 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.

Method-specific notes and mini examples

1) Username/password (classic)

Minimal form:

<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>

This calls the backend logon directly.

2) MFA (password + verification)

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.

3) OTP (passwordless one-time code)

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. 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).

5) External authentication (Google/Azure AD/etc.)

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 auth “challenge” flow.

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

Email templates (OTP code + magic link)

When verification is required, the module can send:

  • one-time code email, or
  • magic link email

Which one is sent depends on the active login type.

Templates live under:

  • /Templates/Users/UserAuthentication/Email (email templates)

And the module settings include:

  • OneTimeCodeTemplate, sender name/email, subject
  • MagicLinkTemplate, sender name/email, subject

One-time code 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>

Model fields are defined here.

@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>

Model fields are defined here.

Security + UX details you should actually care about

Code lifetime + max attempts

  • Code lifetime is controlled by /Globalsettings/Modules/Extranet/LogOn/CodeLifetimeInSeconds (minimum 10s, default 120s).
  • Verification attempts are capped (max failed code input attempts = 3).

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

  • 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).

Updating an existing Login.cshtml (what to change)

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.

Reference: key types and where they come from

  • UserAuthenticationViewModelLoginTypeMfaCodeExpirationRedirectAfterLoginExternalLoginsResult, links.
  • UserAuthenticationResultType: includes MFA and external-provider outcomes.
  • Middleware contract (form/query keys): usernamepassworddologincodeDwExternalLoginProviderDwExtranetExternalLoginDwExtranetMagicLink.
  • Email support (OTP + magic link): templates + view models.
Votes for this answer: 1
 
Sebastian Andersen
Reply

I see that this app only exists for Swift solutions, would there be a way to have this be available for other non-Swift solutions on the newest DW10 release? We have an older website which was upgraded from Dynamicweb 9 that uses a custom solution (no Swift) and want to be able to also get the benefits of users being able to log in on the newest release without having to convert to Swift.

 
Adrian Ursu Dynamicweb Employee
Adrian Ursu
Reply

Hi Sebastian,

The functionality id DW10, the implementation is Swift. 
I don't think there is an easy way to have a "generic" implementation.
You will have to customize the Swift templates to match your own custom implementation.
If you don't have the resources or knowledge to do it, I would be happy to help. I have a team of experienced developers who can surely take care of this customization.

Adrian

 
Sebastian Andersen
Reply

Hi Adrian,

How would I go about adding a new app similar to Swift for User Authentication if I add the template from Swift 1 inside our solution at Users/UserAuthentication/Login.cshtml and started customising it for our solution? The main problem I have now is adding custom apps similar to how Swift does it, as I can't find any documentation on it.

If I can do that, then hopefully I should be able to do it on my own.

 
Adrian Ursu Dynamicweb Employee
Adrian Ursu
Reply

Hi Sebastian,

The new User apps are not Swift-specific. They are DW10 specific. Which version of DW10 are you on?

You should go to Settings/Administration/Featuremanagement/ and look for Extranet(new) and activate it:
 

And you should be good to go.

Adrian

 
Sebastian Andersen
Reply

Alright that is very helpful. This helped me get the frontend step for MFA login flow implemented now, however the email for the second step verification (MFA) to the frontend login is never sent out.

I checked EmailHandler logs and I have ensured our SMTP settings are correctly configured and read through Nicholai's documentation and saw that he wrote "/Templates/Users/UserAuthentication/Email (email templates)" so I added my own OneTimeCode.cshtml, but there is no app settings to choose the email template and it doesn't seem to send a default one out either.

OneTimeCodeTemplate is the setting i am trying to find which is from Nicholai's text. The email for MFA login types to backend seem to work fine too and have a default Dynamicweb email template. I've been checking through the log files for a while and cannot see any errors or email logs being posted when I try to receive a one time code by email from the frontend.

 
Nicolai Pedersen Dynamicweb Employee
Nicolai Pedersen
Reply

Hi

Yes - it seems like those settings will only show up if you enable login type in user settings:

 

 

That will give you additional settings in the login app:

I aknowledge that is a little secret. Will look into if it is needed to change this a bit.

BR Nicolai

 
Sebastian Andersen
Reply

Hi Nicolai

That is very good to know. I can send out emails just fine now however the code and code life time within the template is not available and I keep getting this error now while using your default one time code email template: An error occurred while attaching module 'UserAuthentication' (Dynamicweb.Frontend.Content).

I tried sending out a bare bones template with TemplateTags() which only returned BaseUrl and another Url as values (missing code and code life time values).

The template I used:

@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>

Sebastian

 
Adrian Ursu Dynamicweb Employee
Adrian Ursu
Reply

Hi Sebastian,

This version of the UserManagement is based on ViewModels and TemplateTags will not work.

You can try @Model.ToJson() if you want to see the whole object.

Adrian 

 
Nicolai Pedersen Dynamicweb Employee
Nicolai Pedersen
Reply

I think the exception comes from the template where the using should probably be more explicit: 

@inherits Dynamicweb.Rendering.ViewModelTemplate<OneTimeCodeEmailViewModel>

Attached is our internal Dynamicweb 10 templates with full markup that can be used as a starting point.

 
Sebastian Andersen
Reply

Hi Nicolai and thank you,

This worked and now I got a working MFA login flow with emails configured correctly.

Now we have a new problem related to the new Users - Authentication app and permissions. I configured the following permissions on a reports page:

When I go to the page “Reports” we get redirected to the login page due to missing permissions (which is correct). I enabled Redirect back to referrer on the new Users - Authentication app, which then redirects us back to reports once we logged into the frontend. In this case I logged in as a user who should not have access to the reports page. When I get redirected back to the reports page after successfully logging in, it immediately redirected me back to login page again which again is correct.

If I manually insert the URL to the reports page, then I can bypass the DW permissions and still get to see and use the page which was meant to be locked to that 1 user even though I am logged out of the backend and only logged into the frontend using a new user with no permissions.

Sebastian

 

 

 

 
Nicolai Pedersen Dynamicweb Employee
Nicolai Pedersen
Reply

Great - the long haul!. Well done.

I am pretty sure permissions work - but we would have to take a look at the setup and see this. Can you either send information to care or I can have them contact you to get the information.

Thanks, Nicolai

 
Sebastian Andersen
Reply

I opened a new ticket with Dynamicweb Care and linked the solution and this thread so we can get it fixed.

Sebastian

 

You must be logged in to post in the forum