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.

Next

  • Sign in — how MFA fits into the OIDC flow.
  • Users — the identity that owns MFA devices.
  • API → MFA — HTTP-level reference.