You’re working on a local Xperience by Kentico instance, you go to sign in to /admin, and… you don’t remember the password. The built-in “Forgotten password” link is right there — but it sends a reset email, and your local project has no SMTP server configured.

Xperience by Kentico has no official way to reset a locked-out administrator password without email. So here’s a small, reusable maintenance task that does it: set two environment variables, run the project, and it resets the password and exits.

The solution

Add this block to Program.cs, after app.InitKentico() (so the Kentico data layer is ready) and before app.UseKentico() (so it exits before any middleware or the web server starts):

// Maintenance task: reset an administration user's password, then exit without
// starting the web server. Triggered by environment variables.
var resetUser = Environment.GetEnvironmentVariable("KXP_RESET_ADMIN_USER");
var resetPwd = Environment.GetEnvironmentVariable("KXP_RESET_ADMIN_PWD");
if (!string.IsNullOrWhiteSpace(resetUser) && !string.IsNullOrWhiteSpace(resetPwd))
{
    await ResetAdminPasswordAsync(app, resetUser, resetPwd);
    return;
}

And the method itself — a top-level local function further down in Program.cs:

static async Task ResetAdminPasswordAsync(WebApplication app, string user, string pwd)
{
    using var scope = app.Services.CreateScope();
    var userManager = scope.ServiceProvider.GetRequiredService<AdminUserManager>();
    var users = scope.ServiceProvider.GetRequiredService<IInfoProvider<UserInfo>>();

    // Admin users live in CMS_User and are managed by AdminUserManager — not the live-site
    // UserManager<ApplicationUser> (CMS_Member). Look up the user and reset by unambiguous id.
    var info = users.Get()
        .WhereEquals(nameof(UserInfo.UserName), user)
        .TopN(1)
        .FirstOrDefault();

    if (info is null)
    {
        Console.WriteLine($"No CMS_User with UserName '{user}'.");
        return;
    }

    var account = await userManager.FindByIdAsync(info.UserID.ToString());
    if (account is null)
    {
        Console.WriteLine($"Admin identity user not found for id {info.UserID}.");
        return;
    }

    // For an already-registered account, use the reset-token flow (not AddPasswordAsync).
    var token = await userManager.GeneratePasswordResetTokenAsync(account);
    var result = await userManager.ResetPasswordAsync(account, token, pwd);

    Console.WriteLine(result.Succeeded
        ? $"Password for '{info.UserName}' was reset."
        : "Reset failed: " + string.Join("; ", result.Errors.Select(e => e.Description)));
}

The required namespaces are Kentico.Membership (AdminUserManager), CMS.Membership (UserInfo), CMS.DataEngine (IInfoProvider, WhereEquals) and Microsoft.Extensions.DependencyInjection.

Running it

Set the two environment variables and run the project. It resets the password, prints the result, and exits — the web server never starts:

$env:KXP_RESET_ADMIN_USER="administrator"
$env:KXP_RESET_ADMIN_PWD="MyNewPassw0rd!"
dotnet run --no-build
Password for 'administrator' was reset.

It works with any administration username, including email-style ones (jane@example.com). If you get “Reset failed: …” instead, it’s the admin password policy (length/complexity from AdminIdentityOptions) rejecting your choice — the exact validation errors are printed, so pick a stronger password.

Why it’s built this way

Three Xperience by Kentico specifics are baked into that code:

  • Admin users aren’t where you’d expect. Everyone who signs in to /admin lives in CMS_User and is managed by Kentico.Membership.AdminUserManager. The familiar UserManager<ApplicationUser> is for the live site’s members (CMS_Member) and will never find an admin — so inject AdminUserManager.
  • Use the reset-token flow. GeneratePasswordResetTokenAsync + ResetPasswordAsync is the supported way to set the password on an existing account. (AddPasswordAsync only works for accounts still pending registration.)
  • Trigger with environment variables, not --arguments. Xperience runs its own command-line parser during InitKentico() — that’s what powers --kxp-update and --kxp-codegen — and it rejects any unknown --option before your code runs. Environment variables sail right past it.

A note on safety

This is a deliberate maintenance back door, so treat it like one:

  • It only runs when both environment variables are present — normal startup is unaffected.
  • It’s intended for local recovery. In real environments, configure SMTP and use the supported “Forgotten password” flow.
  • If you’d rather it can never run in Production, wrap the trigger in if (app.Environment.IsDevelopment()).

A handy snippet to keep around for the next time you (or a teammate) gets locked out of a local admin. 🔑