go-authn

Authentication in pure Go — the factors that prove somebody is present, and the directory that says who they are and what can prove them.

CGO_ENABLED=0 transport-pluggable CTAPHID · CTAP2 · COSE knowledge · possession · inherence SQL · LDAP · a file verified against a real security key read by OpenLDAP's own ldapsearch
Documentation GitHub

The phrase is multi-factor, not multi-check. A passphrase and a recovery code are both things you know: whoever learned one has usually learned the other from the same place, and requiring both buys far less than the count suggests. So the classification is the point — Policy{Count: 2, DistinctKinds: true} is what two-factor is normally taken to mean, and two passphrases never satisfy it.

And a factor answers a different question from a directory. Is this person here, now? is not who is this person, and what would prove them? A site's people are already written down somewhere — a database, an LDAP server, a file — and what proves each of them is not one thing: NTLMv2 needs the password or its MD4(UTF16LE(password)), so an LDAP bind cannot authenticate an SMB session, and neither can a bcrypt. directory is one model over the three sources that carries what proves somebody rather than a password field, and authnd serves it back out over LDAP, because almost everything speaks LDAP.

Nothing here is platform-specific. A Transport moves 64-byte reports to and from an authenticator, and where those come from — IOKit on macOS, hidraw on Linux, WebHID in a browser — is somebody else's problem. The macOS one is go-macos/fido: four calls to IOKit, no framework, no driver, no cgo. A FIDO key opens with no entitlement and no user consent, which is what makes the whole path reachable from Go at all.

The reference was read first, and it earned its keep. In pure Go, client-side, there was nothing to reuse: the reference of the field is Yubico's libfido2, written in C, whose one serious Go binding goes through cgo. So libfido2 and the CTAP specification are read here as documentation. That reading caught two faults a key would never have shown, because a short ping comes back the same either way: CTAPHID_KEEPALIVE is not an answer, and the message limit is the framing's rather than the length field's. CBOR, by contrast, does have a Go reference — fxamacker/cbor — so it is used rather than rewritten.

Repositories

fido libprotocol

v0.5.0 · github.com/go-authn/fido

The FIDO client-to-authenticator protocol over any transport: CTAPHID framing, the handshake, GetInfo, MakeCredential, GetAssertion, ClientPIN, and the authenticator data parsed down to a *ecdsa.PublicKey.

CI coverage 100%

mfa libpolicy

v0.2.0 · github.com/go-authn/mfa

How many factors, of which kinds, and what to say when some refuse. A factor that could not be asked has not failed; a factor not reached is not reported as failed; and there is no default policy.

CI coverage 100%

keyfactor libfactor

v0.1.0 · github.com/go-authn/keyfactor

The piece between the two: a security key as an mfa.Factor, over any transport. It lived in a platform package until a second copy was about to be written that differed in one line.

CI coverage 100%

totp libfactor

v0.1.0 · github.com/go-authn/totp

Time-based one-time passwords (RFC 6238), and the factor a policy asks. A code is valid for a whole step, so the RFC requires the verifier to refuse a second use of one – and every step up to the last accepted, not merely the same one.

CI coverage ≥95% — the floor CI enforces; the standard is 100%

directory libidentity

v0.4.0 · github.com/go-authn/directory

Who somebody is and what proves them, over a database (sqldir), an LDAP server (ldapdir) or a file (hcldir). What proves somebody is not one thing: an LDAP bind cannot authenticate an SMB session, and neither can a bcrypt.

CI coverage ≥85% — the floor CI enforces; the standard is 100%

oidc libidentity

v0.1.0 · github.com/go-authn/oidc

An OpenID Connect token verified into an identity: discovery, JWKS, and the refusals that matter. For the things that speak a token instead of a directory.

CI coverage ≥90% — the floor CI enforces; the standard is 100%

authnd toolserver

v0.2.0 · github.com/go-authn/authnd

An LDAP server for people who are somewhere else – a SQL database, a configuration file, another directory – because almost everything speaks LDAP. The second factor goes on the end of the password, which is the only field a simple bind has.

CI coverage ≥80% — the floor CI enforces; the standard is 100%

krb5 libprotocol

v0.1.0 · github.com/go-authn/krb5

The acceptor half of Kerberos, and only that half: an AP-REQ verified against a keytab and answered with the AP-REP gokrb5 refuses to build – without which a GSS-API client asking for mutual authentication completes no context at all. No KDC, no tickets issued. sec=krb5 and sec=krb5i, not sec=krb5p.

CI

Every factor answers one question — is this person here, now? — and the policy counts the answers by KIND, because two of the same thing is one factor. A directory answers the other one, and carries what proves each person rather than a password field. Pure Go with CGO_ENABLED=0, gated in CI on Linux, macOS and Windows. The standard is 100% statement coverage: fido, mfa and keyfactor are gated on it and hold it, and the newer repos carry a floor CI will not let them fall below, with the shortfall printed on every run. The protocol is proven against a real security key: registered, asserted, and the signature verified with the public key the key itself handed out — which is what proves the parsing rather than asserting it, since a COSE key could not verify anything if its extent were wrong by a byte. The LDAP side is proven the same way, by something that is not this code: OpenLDAP's own ldapsearch reads the server, and is refused with Invalid credentials (49) for a wrong password and for the empty one. BSD-3-Clause throughout. Consumers: go-macos/fido for the transport, and go-fileshare/fileshare, which describes its people with the same users block.