MFA with Go SDK

Hello everyone,

I’m trying to auth with MFA using the Go SDK. I’m able to enable MFA on the identity. It passes the VerifyZitiMfa(code)function call and returns the recovery codes etc.

When I try to auth, it’s returning:

error verifying MFA TOTP:  [POST /authenticate/mfa][401] authenticateMfaUnauthorized  &{Data:<nil> Meta:0x140002ead00}

I modeled the code after the zssh repo.

package main

import (
	"bufio"
	"fmt"
	"net/url"
	"os"
	"strings"

	"github.com/openziti/edge-api/rest_model"
	edgeapis "github.com/openziti/sdk-golang/edge-apis"
	"github.com/openziti/sdk-golang/ziti"
	"github.com/skip2/go-qrcode"
)

func main() {
	if len(os.Args) < 2 {
		fmt.Println("Usage: mfa <enable|remove>")
		return
	}

	cmd := os.Args[1]

	switch cmd {
	case "auth":
		AuthMFA()
	case "enable":
		if err := EnableMFA(); err != nil {
			fmt.Printf("Failed to enable MFA: %v,", err)
		}
	case "remove":
		if err := RemoveMfa(); err != nil {
			fmt.Printf("Failed to remove MFA: %v,", err)
		}
	default:
		fmt.Println("Usage: mfa <enable|remove>")
	}
}

func AuthMFA() error {
	ctx, err := NewContext()
	if err != nil {
		return fmt.Errorf("error creating ziti context: %w", err)
	}
	Auth(ctx)
	return nil
}

func EnableMFA() error {
	ctx, err := NewContext()
	if err != nil {
		return fmt.Errorf("error creating ziti context: %w", err)
	}
	Auth(ctx)

	if deet, err := ctx.EnrollZitiMfa(); err != nil {
		fmt.Println("Attempting to enroll for MFA TOTP failed.")
		fmt.Println("This identity is likely already enrolled or is in the process of being enrolled.")
		fmt.Println("To continue the MFA TOTP enrollment process you must \"remove\" MFA TOTP first.")
		fmt.Println("Run \"mfa remove\" to clear the current state, then try again.")
	} else {
		parsedURL, err := url.Parse(deet.ProvisioningURL)
		if err != nil {
			panic(err)
		}

		params := parsedURL.Query()
		secret := params.Get("secret")
		fmt.Println()
		fmt.Println("Generate and enter the correct code to continue.")
		fmt.Println("Add this secret to your TOTP generator and verify the code.")
		fmt.Println()
		fmt.Println("  MFA TOTP Secret: ", secret)

		var q *qrcode.QRCode
		q, err = qrcode.New(fmt.Sprintf("otpauth://totp/zsshlabel?secret=%s&issuer=zssh", secret), qrcode.Highest)
		if err != nil {
			return fmt.Errorf("Failed to generate QR Code for MFA enrollment: %w", err)
		}
		art := q.ToString(false)
		fmt.Println(art)

		fmt.Println()

		code := ReadCode(false)

		if err := ctx.VerifyZitiMfa(code); err != nil {
			return fmt.Errorf("verifying ziti mfa: %w", err)
		}

		fmt.Println()
		fmt.Println("Code verified. These are your recovery codes. Save these codes somewhere safe.")
		fmt.Println("If you lose your TOTP generator, these codes can be used to verify")
		fmt.Println("your MFA TOTP to generate a new code.")
		fmt.Println()
		recoveryCodes := deet.RecoveryCodes

		fmt.Println("┌────────┬────────┬────────┬────────┬────────┐")

		for i := 0; i < len(recoveryCodes); i += 5 {
			for j := 0; j < 5 && i+j < len(recoveryCodes); j++ {
				fmt.Printf("│ %6s ", recoveryCodes[i+j])
			}
			fmt.Println("│")
			if i+5 < len(recoveryCodes) {
				fmt.Println("├────────┼────────┼────────┼────────┼────────┤")
			}
		}

		fmt.Println("└────────┴────────┴────────┴────────┴────────┘")
	}
	return nil
}

func RemoveMfa() error {
	ctx, err := NewContext()
	if err != nil {
		return fmt.Errorf("error creating ziti context: %w", err)
	}
	done := make(chan bool)
	ctx.Events().AddAuthenticationStateFullListener(func(context ziti.Context, session edgeapis.ApiSession) {
		go func() {
			fmt.Println()
			fmt.Println("If MFA TOTP has been successfully enrolled, you must enter a valid code or a valid recovery code,")
			fmt.Println("otherwise, enter any value to continue.")
			fmt.Println()
			code := ReadCode(true)
			if err := ctx.RemoveZitiMfa(code); err != nil {
				fmt.Printf("error removing MFA TOTP: %v", err)
				return
			}
			done <- true
			fmt.Println("MFA TOTP removed")
		}()
	})
	Auth(ctx)
	<-done
	return nil
}

func NewContext() (ziti.Context, error) {
	conf, err := ziti.NewConfigFromFile("identity.json")
	if err != nil {
		return nil, fmt.Errorf("failed to load ziti identity (%v): %w", "identity.json", err)
	}
	fmt.Println("Loaded ziti identity")

	ztx, err := ziti.NewContext(conf)
	if err != nil {
		return nil, fmt.Errorf("creating ziti context: %w", err)
	}

	ztx.Events().AddMfaTotpCodeListener(func(c ziti.Context, detail *rest_model.AuthQueryDetail, response ziti.MfaCodeResponse) {
		ok := false
		for !ok {
			fmt.Println("MFA TOTP required to fully authenticate")
			code := ReadCode(false)
			if err := response(code); err != nil {
				fmt.Println("error verifying MFA TOTP: ", err)
			} else {
				ok = true
			}
		}
	})

	return ztx, nil
}

func Auth(ztx ziti.Context) {
	if err := ztx.Authenticate(); err != nil {
		fmt.Printf("error creating ziti context: %v", err)
		fmt.Printf("could not authenticate. verify your identity is correct and matches all necessary authentication conditions.")
	}
}

func ReadCode(allowEmpty bool) string {
	code := ""
	reader := bufio.NewReader(os.Stdin)
	for code == "" {
		fmt.Print("MFA TOTP code: ")
		code, _ = reader.ReadString('\n')
		code = strings.TrimSpace(code)
		if allowEmpty {
			break
		}
	}
	return code
}

Any help would be greatly appreciated.

Thanks,

rja

Hi @rja, welcome to the community and to OpenZiti!

I was looking into this on Friday and a tiny bit over the weekend but I don't have an answer for you yet. I'm hoping to get around to this some time later today.

Hi @rja, yeah that code apparently isn't working any more for me either. I'll have to talk to the team and try to sort out the zssh code and follow up later in the week

Hiya. Thanks for taking the time to check into this. Happy it wasn’t just me. :wink:

-rja