factors

package module
v0.1.1-0...-378997a Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 5, 2026 License: BSD-3-Clause Imports: 4 Imported by: 0

README

factors

Go Reference License CI

Windows's authentication factors, as factors — mfa.Factor values a policy can ask. Pure Go, CGO_ENABLED=0.

r, err := mfa.Verify(ctx, mfa.Policy{Count: 2},
    factors.WindowsHello("unlock the vault"),        // go-mswin/winrt
    factors.SecurityKey("example.test", credID),     // go-mswin/webauthn
)

The two factors come from two different packages, which is the shape of Windows: winrt asks Windows Hello through UserConsentVerifier, and webauthn asks a security key through webauthn.dll. Neither binding knows what a policy is, and neither should — a program that only wants a Hello prompt must not end up importing one.

⛔ Windows cannot offer an inherence factor

This is the finding that shapes the package, and it is worth stating plainly rather than papering over.

Windows Hello accepts a face, a fingerprint, or a PIN, and the person chooses. Nothing in UserConsentVerifier or in a WebAuthn assertion reports which — the assertion's "user verified" bit is set the same way for all three. A PIN is something known; a face is something one is. A factor claiming Inherence here would be claiming to know something Windows never said.

So WindowsHello reports mfa.Unknown, and a policy asking for distinct kinds will never count it towards them. That is not this package working around an API; it is the API declining to say, reported faithfully.

The consequence, worth knowing before designing around it: mfa.Policy{Count: 2, DistinctKinds: true} cannot be satisfied on Windows by these two factors alone. A caller who needs two kinds must supply the knowledge factor themselves — and then it is theirs to be honest about. A test holds this, so nobody quietly "fixes" it.

macOS can do better: LocalAuthentication has a biometrics-only policy, so go-macos/factors offers a real inherence factor. The difference between the two platforms is real, not an oversight here.

What earns the possession claim

SecurityKey constrains the request to a cross-platform authenticator, so Windows Hello cannot answer it. Without that constraint Windows would be free to satisfy the request with the machine itself, and something you have would mean the computer already in front of the person.

And the constraint is a request. Assertion.Transport is an observation, and the two can differ — so the answer is read back, and an authenticator that reports itself as internal is refused even though it was asked not to be. Silence is not a contradiction: an older Windows reports no transport at all, and that is left alone rather than guessed at.

What it refuses to confuse

  • Unavailable is not refused. winrt.Verify is used rather than RequireUserConsent, because the latter folds every outcome into a boolean. DeviceNotPresent, NotConfiguredForUser, DisabledByPolicy and DeviceBusy are reported as mfa.ErrUnavailable; Canceled and RetriesExhausted came from somebody who was there and are refusals.
  • A PIN on the key is not a second factor. VerifiedSecurityKey is still Possession: whatever the key asked for never reaches this machine and identifies nobody to us — it protects the key. Counting it separately would let one object masquerade as two factors.
  • The key must say a person was there. The UP flag is checked, and UV when verification was asked for. Windows answering is not the same as somebody touching something.
  • The challenge is random. This factor does not verify the signature, so there is no protocol to bind a challenge to — and a fixed one would let a recorded assertion be replayed here for ever.

Coverage

Everything portable is covered to 100%, on Linux, with no hardware: the classification, the kinds, the incomplete-request refusals, the origin default and the absent-versus-refused split all go through the askHello/askKey seams.

The two platform functions are not covered, and the gate says so rather than pretending. They raise a Windows dialog and wait for a person; a runner has nobody to touch a key and no Hello enrolled, and a test that popped a prompt would be a test nobody could run twice. They have not been run against a real machine yet — the packages underneath them have not either, and both say so.

Documentation

Overview

Package factors makes Windows's authentication factors usable by github.com/go-authn/mfa.

Its two factors come from two different packages, which is the whole shape of Windows: go-mswin/winrt asks Windows Hello, and go-mswin/webauthn asks a security key through webauthn.dll. Neither binding knows what a policy is, and neither should — a program that only wants a Hello prompt must not end up importing one.

r, err := mfa.Verify(ctx, mfa.Policy{Count: 2},
    factors.WindowsHello("unlock the vault"),
    factors.SecurityKey("example.test", credentialID),
)

⛔ Windows cannot offer an inherence factor

This is the finding that shapes the package, and it is worth stating plainly rather than papering over.

Windows Hello accepts a face, a fingerprint, or a **PIN**, and the person chooses. Nothing in UserConsentVerifier or in a WebAuthn assertion reports WHICH — the assertion's "user verified" bit is set the same way for all three. A PIN is something known; a face is something one is. So a factor that claimed mfa.Inherence here would be claiming to know something Windows never said.

WindowsHello therefore reports mfa.Unknown, and a policy asking for distinct KINDS will never count it towards them. That is not a limitation of this package working around an API; it is the API declining to say, reported faithfully. macOS can do better — LocalAuthentication has a biometrics-only policy, so github.com/go-macos/factors offers a real inherence factor — and the difference between the two platforms is real, not an oversight here.

The consequence is worth knowing before designing around it: mfa.Policy{Count: 2, DistinctKinds: true} cannot be satisfied on Windows by these two factors alone. A caller who needs two kinds must supply the knowledge factor themselves, and then it is theirs to be honest about.

Index

Constants

This section is empty.

Variables

View Source
var ErrUnsupported = errors.New("factors: these are Windows factors")

ErrUnsupported is what both factors report off Windows. It is wrapped as unavailable rather than as a refusal: a Mac has not failed anyone's Windows Hello, it has no Windows Hello. The macOS adapters are go-macos/factors.

Functions

func SecurityKey

func SecurityKey(rpID string, credentialID []byte) mfa.Factor

SecurityKey is a registered credential on a carried authenticator.

The request is constrained to a cross-platform authenticator, so Windows Hello cannot answer it. That constraint is what makes mfa.Possession a true statement: without it, Windows would be free to satisfy the request with the machine itself, and "something you have" would mean the computer already in front of the person.

credentialID is what a registration returned. Registering is not done here; see go-mswin/webauthn.

func VerifiedSecurityKey

func VerifiedSecurityKey(rpID string, credentialID []byte) mfa.Factor

VerifiedSecurityKey is the same, with the key asked to establish who holds it.

The kind does not change. Whatever the key asked for -- its own PIN, its own fingerprint reader -- never reaches this machine and identifies nobody to us; it protects the key. Counting it as a second factor would let one object masquerade as two.

func WindowsHello

func WindowsHello(reason string) mfa.Factor

WindowsHello is the authenticator built into this machine, as a factor.

reason is what Windows shows the person in its own prompt, so it should say what is being unlocked.

Its kind is mfa.Unknown, honestly: see the package documentation. What it proves is that somebody satisfied this machine's own check, which is worth having — it is simply not a KIND that can be shown to differ from a passphrase.

func WithOrigin

func WithOrigin(f mfa.Factor, origin string) mfa.Factor

WithOrigin overrides the origin sent in the client data.

It defaults to "https://" + rpID, which is what a program that is not a web page wants. A browser-like caller with a real page has a real origin and should say so, because Windows checks that the two agree.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL