umbraco-mfa-login-provider
SKILL.md
Umbraco MFA Login Provider
What is it?
An MFA Login Provider is the UI component for Two-Factor Authentication (2FA) in Umbraco. It provides the interface for users to enable/disable and configure their 2FA provider (e.g., Google Authenticator, SMS codes). The backend ITwoFactorProvider must be configured separately in C# - this extension type handles the frontend setup and configuration UI.
Documentation
Always fetch the latest docs before implementing:
- Two-Factor Authentication: https://docs.umbraco.com/umbraco-cms/reference/security/two-factor-authentication
- Extension Types: https://docs.umbraco.com/umbraco-cms/customizing/extending-overview/extension-types
- Foundation: https://docs.umbraco.com/umbraco-cms/customizing/foundation
Workflow
- Fetch docs - Use WebFetch on the URLs above
- Ask questions - What 2FA method? QR code setup? Custom validation UI?
- Configure backend - Set up C# ITwoFactorProvider first
- Generate frontend files - Create manifest + configuration element
- Explain - Show what was created and how to test
Minimal Examples
Manifest (umbraco-package.json)
{
"name": "My MFA Provider",
"extensions": [
{
"type": "mfaLoginProvider",
"alias": "My.MfaProvider.Authenticator",
"name": "Authenticator App MFA",
"forProviderName": "Umbraco.GoogleAuthenticator",
"element": "/App_Plugins/MyMfa/mfa-setup.js",
"meta": {
"label": "Authenticator App"
}
}
]
}
Manifest (TypeScript)
import type { ManifestMfaLoginProvider } from '@umbraco-cms/backoffice/extension-registry';
const manifest: ManifestMfaLoginProvider = {
type: 'mfaLoginProvider',
alias: 'My.MfaProvider.Authenticator',
name: 'Authenticator MFA Provider',
forProviderName: 'Umbraco.GoogleAuthenticator', // Must match backend provider name
element: () => import('./mfa-setup.element.js'),
meta: {
label: 'Authenticator App',
},
};
export const manifests = [manifest];
MFA Setup Element (mfa-setup.element.ts)
import { html, css, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
import type { UmbMfaProviderConfigurationElementProps } from '@umbraco-cms/backoffice/user';
@customElement('my-mfa-setup')
export class MyMfaSetupElement extends UmbLitElement implements UmbMfaProviderConfigurationElementProps {
@property({ type: String })
providerName = '';
@property({ type: String })
displayName = '';
@property({ attribute: false })
callback!: (providerName: string, code: string, secret: string) => Promise<{ error?: string }>;
@property({ attribute: false })
close!: () => void;
@state()
private _loading = true;
@state()
private _secret = '';
@state()
private _qrCodeUrl = '';
@state()
private _code = '';
@state()
private _submitting = false;
#notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE;
constructor() {
super();
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (context) => {
this.#notificationContext = context;
});
}
async connectedCallback() {
super.connectedCallback();
await this.#loadSetupData();
}
async #loadSetupData() {
try {
// Fetch setup data from API
const response = await fetch(`/umbraco/management/api/v1/user/2fa/${this.providerName}/setup`);
const data = await response.json();
this._secret = data.secret;
this._qrCodeUrl = data.qrCodeSetupImageUrl;
} catch (error) {
this.#notificationContext?.peek('danger', { data: { message: 'Failed to load setup data' } });
} finally {
this._loading = false;
}
}
async #handleSubmit(e: SubmitEvent) {
e.preventDefault();
if (!this._code || this._code.length !== 6) {
this.#notificationContext?.peek('warning', { data: { message: 'Please enter a 6-digit code' } });
return;
}
this._submitting = true;
try {
const result = await this.callback(this.providerName, this._code, this._secret);
if (result.error) {
this.#notificationContext?.peek('danger', { data: { message: result.error } });
} else {
this.#notificationContext?.peek('positive', { data: { message: '2FA enabled successfully' } });
this.close();
}
} finally {
this._submitting = false;
}
}
render() {
if (this._loading) {
return html`<uui-loader></uui-loader>`;
}
return html`
<uui-form>
<form @submit=${this.#handleSubmit}>
<umb-body-layout headline="Set up ${this.displayName}">
<div id="main">
<p>Scan this QR code with your authenticator app:</p>
<div class="qr-container">
<img src=${this._qrCodeUrl} alt="QR Code" />
</div>
<p>Or enter this secret manually: <code>${this._secret}</code></p>
<uui-form-layout-item>
<uui-label slot="label" for="code" required>Verification Code</uui-label>
<uui-input
id="code"
type="text"
inputmode="numeric"
pattern="[0-9]*"
maxlength="6"
placeholder="000000"
.value=${this._code}
@input=${(e: InputEvent) => (this._code = (e.target as HTMLInputElement).value)}
required
></uui-input>
</uui-form-layout-item>
</div>
<div slot="actions">
<uui-button
label="Cancel"
look="secondary"
@click=${this.close}
></uui-button>
<uui-button
type="submit"
label="Enable"
look="primary"
color="positive"
?state=${this._submitting ? 'waiting' : undefined}
></uui-button>
</div>
</umb-body-layout>
</form>
</uui-form>
`;
}
static styles = css`
#main {
max-width: 400px;
margin: 0 auto;
text-align: center;
}
.qr-container {
margin: var(--uui-size-space-5) 0;
}
.qr-container img {
max-width: 200px;
border: 1px solid var(--uui-color-border);
border-radius: var(--uui-border-radius);
}
code {
display: block;
margin: var(--uui-size-space-3) 0;
padding: var(--uui-size-space-2);
background: var(--uui-color-surface-alt);
border-radius: var(--uui-border-radius);
font-family: monospace;
word-break: break-all;
}
`;
}
export default MyMfaSetupElement;
declare global {
interface HTMLElementTagNameMap {
'my-mfa-setup': MyMfaSetupElement;
}
}
Using Default MFA Element
// If you don't need custom UI, you can use the built-in default element
// Just register the manifest without a custom element - Umbraco provides a default
const manifest: ManifestMfaLoginProvider = {
type: 'mfaLoginProvider',
alias: 'My.MfaProvider.Default',
name: 'Default MFA Provider',
forProviderName: 'Umbraco.GoogleAuthenticator',
// No element specified - uses default
meta: {
label: 'Authenticator App',
},
};
Backend C# Configuration (for reference)
// ITwoFactorProvider implementation
public class GoogleAuthenticatorProvider : ITwoFactorProvider
{
public string ProviderName => "Umbraco.GoogleAuthenticator";
public Task<bool> ValidateTwoFactorPIN(string secret, string code)
{
var twoFactorAuthenticator = new TwoFactorAuthenticator();
return Task.FromResult(
twoFactorAuthenticator.ValidateTwoFactorPIN(secret, code)
);
}
public Task<bool> ValidateTwoFactorSetup(string secret, string code)
{
return ValidateTwoFactorPIN(secret, code);
}
public async Task<object> GetSetupDataAsync(Guid userKey, IMemberService memberService)
{
var secret = Base32Encoding.ToString(Guid.NewGuid().ToByteArray());
var authenticator = new TwoFactorAuthenticator();
var setupInfo = authenticator.GenerateSetupCode(
"My App",
userKey.ToString(),
secret,
false
);
return new
{
secret,
qrCodeSetupImageUrl = setupInfo.QrCodeSetupImageUrl
};
}
}
// Register in Composer
public class MfaComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
var identityBuilder = new BackOfficeIdentityBuilder(builder.Services);
identityBuilder.AddTwoFactorProvider<GoogleAuthenticatorProvider>(
GoogleAuthenticatorProvider.Name
);
}
}
Element Props Interface
| Property | Type | Description |
|---|---|---|
providerName |
string |
The provider identifier |
displayName |
string |
Human-readable provider name |
callback |
Function |
Call with code/secret to validate |
close |
Function |
Close the setup modal |
Callback Function
callback(providerName: string, code: string, secret: string): Promise<{ error?: string }>
Returns an object with an error property if validation failed, or empty object on success.
That's it! Always fetch fresh docs, keep examples minimal, generate complete working code.
Weekly Installs
49
Repository
umbraco/umbraco…e-skillsGitHub Stars
13
First Seen
Feb 4, 2026
Security Audits
Installed on
github-copilot36
opencode19
codex19
gemini-cli17
amp17
kimi-cli17