Documentation
¶
Overview ¶
Package mfa decides whether enough factors answered, and says which ones.
It holds no cryptography and speaks to no device. A Factor is anything that can be asked "is this person here, now?" and answer yes, no, or "I cannot be asked at the moment" -- Touch ID through go-macos/localauthentication, a security key through go-authn/fido, a passphrase, a code from a phone. What is here is the part that has to be right whatever those are: how many answers, of which kinds, and what to say when some of them fail.
Two of the same thing is one factor ¶
The phrase is multi-FACTOR, not multi-check. A password and a security question are both things you know: someone who learned one has usually learned the other from the same place, and requiring both buys far less than the count suggests. The classification is the point of the exercise, so Policy.DistinctKinds asks for answers of DIFFERENT kinds and is the setting that means what "two-factor" is normally taken to mean.
Policy.Count alone is also offered, because a caller who wants two keys -- two of the same kind, deliberately, so that losing one is survivable -- is asking for something real. It is simply not the same request, and the two are not spelled the same way here.
Index ¶
Constants ¶
This section is empty.
Variables ¶
ErrUnavailable means a factor could not be asked. It is not a failure: a laptop with no fingerprint reader has not refused anyone.
Functions ¶
func Unavailable ¶ added in v0.2.0
Unavailable marks an error as "there was nothing here to ask", keeping the reason: an empty USB port, a person with no authenticator enrolled, a sensor this machine does not have.
It lives here rather than in each adapter because it constructs THIS package's sentinel, and because the second adapter that needed it was about to copy the first one's three lines. Answer.Unavailable is the other half.
Types ¶
type Answer ¶
type Answer struct {
Name string
Kind Kind
// Err is nil when the factor was satisfied, [ErrUnavailable] when it could
// not be asked, and whatever it returned otherwise.
Err error
}
Answer is what one factor said.
func (Answer) Unavailable ¶
Unavailable reports whether the factor could not be asked at all.
type Factor ¶
type Factor interface {
// Name is what a person is told to do: "Touch ID", "your security key".
Name() string
// Kind is what this factor proves.
Kind() Kind
// Verify asks it.
Verify(ctx context.Context) error
}
Factor is one way of asking whether the person is here.
Verify returns nil when the factor is satisfied. It should return ErrUnavailable, wrapped or not, when it cannot be asked at all -- no key plugged in, no sensor on this machine -- which a policy treats differently from a refusal: a factor that was never asked has not failed.
type Kind ¶
type Kind int
Kind is what a factor proves. The three are the classical ones, and the distinction is not decoration: Policy.DistinctKinds counts by it.
const ( // Unknown is a factor that did not say. It never satisfies a policy that // asks for distinct kinds, because an unclassified factor cannot be shown // to differ from another. Unknown Kind = iota // Knowledge is something the person knows: a passphrase, a PIN, a recovery // code. Knowledge // Possession is something they have: a security key, a phone, a smart // card. Possession // Inherence is something they are: a fingerprint, a face, a voice. Inherence )
The kinds of proof.
type Policy ¶
type Policy struct {
// Count is how many factors must be satisfied.
Count int
// DistinctKinds requires the satisfied factors to be of different kinds --
// what "two-factor" is normally taken to mean. With it, two passphrases
// never satisfy a Count of two.
DistinctKinds bool
// StopOnFirstFailure abandons the attempt as soon as one factor refuses,
// instead of asking the rest.
//
// It is false by default, deliberately. Asking every factor and reporting
// all of the answers is what lets a person be told "your key answered, your
// fingerprint did not" -- and asking a security key AFTER a fingerprint has
// already failed costs nothing but a moment, while stopping early tells
// them one thing at a time across as many attempts as they have factors.
StopOnFirstFailure bool
}
Policy is what "enough" means.
The zero Policy is not usable and Verify says so rather than guessing: there is no sensible default for how much proof is enough, and a library that picked one would be deciding something that belongs to whoever is protecting the thing.
type Result ¶
type Result struct {
// Answers is one entry per factor, in the order they were asked. A factor
// not reached because [Policy.StopOnFirstFailure] ended the attempt has no
// entry: it was not asked, and saying it failed would be a lie.
Answers []Answer
// Satisfied is how many answered yes, and Kinds how many DISTINCT kinds
// those were.
Satisfied int
Kinds int
}
Result is what every factor said, and whether that was enough.
func Verify ¶
Verify asks the factors and decides.
The error, when there is one, says what was missing in words a person can act on -- which factor refused, or that two answers of the same kind are not two factors. The Result is returned whether or not the policy was met, because a caller that must tell somebody what to try next needs the detail either way.