MFA
MFA (multi-factor authentication) is the second-factor proof Huudis can require before issuing a session. The Go SDK exposes the enrolment and verification dance behind client.MFA. Devices come in three flavours: TOTP (default), WebAuthn (hardware keys, platform authenticators), and SMS. For the HTTP surface see API → MFA.
Field on the Client
client.MFA — type *huudis.MfaResource. Enrolment is two-step: Enroll allocates a pending device and (for TOTP) returns the secret; VerifyEnrollment confirms the user can produce a valid code.
Methods
Enroll
Signature. func (r *MfaResource) Enroll(ctx context.Context, in MfaEnrollInput, auth RequestAuth) (*MfaEnrollment, error)
Allocates a pending device. For Type: "totp", the response carries Secret (base32) and OtpAuthURL (an otpauth:// URI you'd render as a QR code). For webauthn or sms, you get the device ID and finish the dance elsewhere.
enr, err := client.MFA.Enroll(ctx, huudis.MfaEnrollInput{
Type: "totp",
Label: "Phone (iPhone 13)",
}, huudis.RequestAuth{})
if err != nil {
return err
}
// Render enr.OtpAuthURL as a QR code, or hand enr.Secret to the user.
VerifyEnrollment
Signature. func (r *MfaResource) VerifyEnrollment(ctx context.Context, in MfaVerifyEnrollmentInput, auth RequestAuth) (*MfaDevice, error)
Completes enrolment by checking the code. On success the device flips to active; on failure the pending row stays — retry up to five times before purge.
dev, err := client.MFA.VerifyEnrollment(ctx, huudis.MfaVerifyEnrollmentInput{
DeviceID: enr.DeviceID,
Code: "123456",
}, huudis.RequestAuth{})
if err != nil {
return err
}
log.Println("Enrolled:", dev.ID, dev.Type)
VerifyLogin
Signature. func (r *MfaResource) VerifyLogin(ctx context.Context, in MfaVerifyLoginInput, auth RequestAuth) (*MfaVerifyLoginResult, error)
Used during sign-in: when Huudis issues a token without the mfa_verified claim (the user has MFA enrolled but hasn't proven the second factor yet), this endpoint accepts the code and returns an upgraded token. Pass the existing pending token via RequestAuth.AuthToken.
res, err := client.MFA.VerifyLogin(ctx,
huudis.MfaVerifyLoginInput{Code: code},
huudis.RequestAuth{AuthToken: pendingToken})
if err != nil {
return err
}
// Replace the pending token; subsequent calls now satisfy require_mfa middleware.
session.SetAccessToken(res.AccessToken)
ListDevices
Signature. func (r *MfaResource) ListDevices(ctx context.Context, auth RequestAuth) ([]MfaDevice, error)
Lists every active MFA device on the calling user's identity. Doesn't include pending enrolments.
devices, _ := client.MFA.ListDevices(ctx, huudis.RequestAuth{})
for _, d := range devices {
label := d.Label
if label == "" {
label = "(unlabelled)"
}
last := d.LastUsedAt
if last == "" {
last = "never"
}
fmt.Println(d.ID, d.Type, label, last)
}
DeleteDevice
Signature. func (r *MfaResource) DeleteDevice(ctx context.Context, id string, auth RequestAuth) error
Removes a device. If it's the user's only device and their workspace requires MFA, the call returns state_conflict — enrol another one first.
if err := client.MFA.DeleteDevice(ctx, "mfa_01HX...", huudis.RequestAuth{}); err != nil {
return err
}
Types
type MfaDevice struct {
ID string `json:"id"` // "mfa_..."
Type string `json:"type"` // "totp" | "webauthn" | "sms"
Label string `json:"label,omitempty"`
CreatedAt string `json:"createdAt"`
LastUsedAt string `json:"lastUsedAt,omitempty"`
}
type MfaEnrollment struct {
DeviceID string `json:"deviceId"`
Secret string `json:"secret,omitempty"` // base32, TOTP only
OtpAuthURL string `json:"otpAuthUrl,omitempty"` // "otpauth://...", TOTP only
QrCodePng string `json:"qrCodePng,omitempty"` // base64 PNG, TOTP only
}
type MfaVerifyLoginResult struct {
AccessToken string `json:"accessToken"`
}
See API → MFA for per-field rules.
Common patterns
Full TOTP enrolment flow (net/http)
http.HandleFunc("/settings/mfa/start", func(w http.ResponseWriter, r *http.Request) {
enr, err := client.MFA.Enroll(r.Context(), huudis.MfaEnrollInput{
Type: "totp",
Label: r.URL.Query().Get("label"),
}, huudis.RequestAuth{})
if err != nil {
http.Error(w, err.Error(), 500)
return
}
// Stash enr.DeviceID in a short-lived signed cookie or session.
json.NewEncoder(w).Encode(map[string]string{
"qrPng": enr.QrCodePng,
"secret": enr.Secret,
})
})
http.HandleFunc("/settings/mfa/confirm", func(w http.ResponseWriter, r *http.Request) {
deviceID := r.FormValue("device_id")
code := r.FormValue("code")
dev, err := client.MFA.VerifyEnrollment(r.Context(), huudis.MfaVerifyEnrollmentInput{
DeviceID: deviceID,
Code: code,
}, huudis.RequestAuth{})
if err != nil {
http.Error(w, "bad_code", 400)
return
}
json.NewEncoder(w).Encode(dev)
})
Step-up authentication
A sensitive action wants mfa_verified even if the user signed in long ago. Verify with RequireMFA: true and redirect to a step-up page on failure.
func requireFreshMfa(c *huudis.Client) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := c.VerifyAccessToken(r.Context(), r.Header.Get("Authorization"), true)
if err != nil {
var he *huudis.Error
if errors.As(err, &he) && he.Code == "MFA_REQUIRED" {
http.Redirect(w, r, "/auth/step-up?return="+r.URL.Path, http.StatusFound)
return
}
http.Error(w, err.Error(), 401)
return
}
next.ServeHTTP(w, r)
})
}
}
Migrate users to WebAuthn
Encourage WebAuthn while keeping TOTP as fallback.
func inviteWebauthn(ctx context.Context, c *huudis.Client, userTokens map[string]string) error {
for uid, tok := range userTokens {
devices, err := c.MFA.ListDevices(ctx, huudis.RequestAuth{AuthToken: tok})
if err != nil {
return err
}
hasWebauthn := false
for _, d := range devices {
if d.Type == "webauthn" {
hasWebauthn = true
break
}
}
if !hasWebauthn {
emailUser(uid, "Add a passkey to your Huudis account")
}
}
return nil
}
Errors
Code |
Status |
Cause |
|---|---|---|
validation_error |
400 | Bad device Type, label too long. |
bad_code |
401 | TOTP code mismatched or outside the 30-second window. |
not_found |
404 | Device doesn't exist on this identity. |
state_conflict |
409 | Deleting the last device on a workspace that requires MFA; verifying an already-active device. |
rate_limited |
429 | Too many failed VerifyLogin attempts — back off ~30 seconds. |