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:
- Render the right inputs for the currently active login mode
- Post the expected field names back to the platform
- React to
Model.Resultto 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. This means that it's possible to have several "login tracks" active on a solution at the same time.
App settings
How you set up this app depends heavily on the login type selected for the solution; if any of the options except normal are selected you will have access to other app settings.
If the normal login type is selected, you have access to these settings:
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
ForgotPasswordLinkeditor value is obsolete. Use the Page to set password setting instead.
Templates & login forms
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.
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 (useModel.RedirectAfterLogin)Autologin(checkbox):"keep me signed in" (persistent cookie)usernamepassword(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):
SuccessIncorrectLoginPasswordExpiredExceededFailedLogOnLimit,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).
Magic link
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).
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/passwordtriggers normal auth (or MFA step 1) - Posting
username+dologintriggers 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 + codeverifies - Posting
DwExternalLoginProviderstarts 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>
Updating an existing login template
If you have an existing template like the current one (username/password + external providers), you mainly need to add:
- A branch that detects
Model.Result == MfaVerificationRequired(and failed) and renders the code-entry form - A passwordless "send code" mode (username + hidden
dologin) whenModel.LoginTypeis passwordless - Add handling for
MfaVerificationFailedin 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.
Email templates
When verification via email is required - for example for the Onetime password and magic link login methods - the module can be set up to send out emails, i.e. one-time code email 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.
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>
Magic link email template example
@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
Code lifetime + max attempts
Code lifetime is controlled by /Globalsettings/Modules/Extranet/LogOn/CodeLifetimeInSeconds (minimum 10s, default 120s) or via the Login settings.
Verification attempts are capped (max failed code input attempts = 3).
So in your UI:
- Show an "expires at" hint using
Model.MfaCodeExpirationwhen 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
redirectvalue 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)
