Skip to content

Commit

Permalink
mfa: better OTP registration flow on CLI (#6567)
Browse files Browse the repository at this point in the history
Several improvements:
- show a QR code in the system image viewer
- print an OTP URL in addition to individual fields (some apps accept
  that as input)
- use cluster name as `Issuer` instead of "Teleport"
  • Loading branch information
Andrew Lytvynov committed Apr 28, 2021
1 parent d79eb98 commit a480923
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 16 deletions.
2 changes: 1 addition & 1 deletion api/types/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func (c *AuthPreferenceV2) SetConnectorName(cn string) {
// GetU2F gets the U2F configuration settings.
func (c *AuthPreferenceV2) GetU2F() (*U2F, error) {
if c.Spec.U2F == nil {
return nil, trace.NotFound("U2F configuration not found")
return nil, trace.NotFound("U2F is not configured in this cluster, please contact your administrator and ask them to follow https://goteleport.com/docs/access-controls/guides/u2f/")
}
return c.Spec.U2F, nil
}
Expand Down
8 changes: 5 additions & 3 deletions lib/auth/resetpasswordtoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,11 +253,13 @@ func (s *Server) newTOTPKey(user string) (*otp.Key, *totp.GenerateOpts, error) {
if err != nil {
return nil, nil, trace.Wrap(err)
}
clusterName, err := s.GetClusterName()
if err != nil {
return nil, nil, trace.Wrap(err)
}

opts := totp.GenerateOpts{
// TODO(awly): use proxy public addr as "Issuer", to distinguish
// between TOTP keys for different clusters.
Issuer: "Teleport",
Issuer: clusterName.GetClusterName(),
AccountName: accountName,
Period: 30, // seconds
Digits: otp.DigitsSix,
Expand Down
134 changes: 122 additions & 12 deletions tool/tsh/mfa.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,20 @@ package main

import (
"context"
"encoding/base32"
"fmt"
"image/png"
"io/ioutil"
"os"
"os/exec"
"runtime"
"strings"
"time"

"github.com/gravitational/kingpin"
"github.com/gravitational/trace"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"

"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
Expand Down Expand Up @@ -278,20 +285,74 @@ func promptRegisterChallenge(ctx context.Context, proxyAddr string, c *proto.MFA
}

func promptTOTPRegisterChallenge(c *proto.TOTPRegisterChallenge) (*proto.MFARegisterResponse, error) {
// TODO(awly): mfa: use OS-specific image viewer to show a QR code.
// TODO(awly): mfa: print OTP URL
fmt.Println("Open your TOTP app and create a new manual entry with these fields:")
fmt.Printf("Name: %s\n", c.Account)
fmt.Printf("Issuer: %s\n", c.Issuer)
fmt.Printf("Algorithm: %s\n", c.Algorithm)
fmt.Printf("Number of digits: %d\n", c.Digits)
fmt.Printf("Period: %ds\n", c.PeriodSeconds)
fmt.Printf("Secret: %s\n", c.Secret)
fmt.Println()
secretBin, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(c.Secret)
if err != nil {
return nil, trace.BadParameter("server sent an invalid TOTP secret key %q: %v", c.Secret, err)
}
var algorithm otp.Algorithm
switch strings.ToUpper(c.Algorithm) {
case "SHA1":
algorithm = otp.AlgorithmSHA1
case "SHA256":
algorithm = otp.AlgorithmSHA256
case "SHA512":
algorithm = otp.AlgorithmSHA512
case "MD5":
algorithm = otp.AlgorithmMD5
default:
return nil, trace.BadParameter("server sent an unknown TOTP algorithm %q", c.Algorithm)
}
otpKey, err := totp.Generate(totp.GenerateOpts{
Issuer: c.Issuer,
AccountName: c.Account,
Period: uint(c.PeriodSeconds),
Secret: secretBin,
Digits: otp.Digits(c.Digits),
Algorithm: algorithm,
})
if err != nil {
return nil, trace.BadParameter("server sent invalid TOTP parameters: %v", err)
}

totpCode, err := prompt.Input(os.Stdout, os.Stdin, "Once created, enter an OTP code generated by the app")
// Try to show a QR code in the system image viewer.
// This is not supported on all platforms.
var showingQRCode bool
closeQR, err := showOTPQRCode(otpKey)
if err != nil {
return nil, trace.Wrap(err)
log.WithError(err).Debug("Failed to show QR code")
} else {
showingQRCode = true
defer closeQR()
}

fmt.Println()
if showingQRCode {
fmt.Println("Open your TOTP app and scan the QR code. Alternatively, you can manually enter these fields:")
} else {
fmt.Println("Open your TOTP app and create a new manual entry with these fields:")
}
fmt.Printf(` URL: %s
Account name: %s
Secret key: %s
Issuer: %s
Algorithm: %s
Number of digits: %d
Period: %ds
`, otpKey.URL(), c.Account, c.Secret, c.Issuer, c.Algorithm, c.Digits, c.PeriodSeconds)
fmt.Println()

var totpCode string
// Help the user with typos, don't submit the code until it has the right
// length.
for {
totpCode, err = prompt.Input(os.Stdout, os.Stdin, "Once created, enter an OTP code generated by the app")
if err != nil {
return nil, trace.Wrap(err)
}
if len(totpCode) == int(c.Digits) {
break
}
fmt.Printf("TOTP code must be exactly %d digits long, try again\n", c.Digits)
}
return &proto.MFARegisterResponse{Response: &proto.MFARegisterResponse_TOTP{
TOTP: &proto.TOTPRegisterResponse{Code: totpCode},
Expand Down Expand Up @@ -398,3 +459,52 @@ func (c *mfaRemoveCommand) run(cf *CLIConf) error {
fmt.Printf("MFA device %q removed.\n", c.name)
return nil
}

func showOTPQRCode(k *otp.Key) (cleanup func(), retErr error) {
var imageViewer string
switch runtime.GOOS {
case "linux":
imageViewer = "xdg-open"
case "darwin":
imageViewer = "open"
default:
return func() {}, trace.NotImplemented("showing QR codes is not implemented on %s", runtime.GOOS)
}

otpImage, err := k.Image(456, 456)
if err != nil {
return nil, trace.Wrap(err)
}
imageFile, err := ioutil.TempFile("", "teleport-otp-qr-code-*.png")
if err != nil {
return nil, trace.ConvertSystemError(err)
}
defer func() {
if retErr != nil {
imageFile.Close()
os.Remove(imageFile.Name())
}
}()

if err := png.Encode(imageFile, otpImage); err != nil {
return nil, trace.ConvertSystemError(err)
}
if err := imageFile.Close(); err != nil {
return nil, trace.ConvertSystemError(err)
}
log.Debugf("Wrote OTP QR code to %s", imageFile.Name())

cmd := exec.Command(imageViewer, imageFile.Name())
if err := cmd.Start(); err != nil {
return nil, trace.ConvertSystemError(err)
}
log.Debugf("Opened QR code via %q", imageViewer)
return func() {
if err := os.Remove(imageFile.Name()); err != nil {
log.WithError(err).Debugf("Failed to clean up temporary QR code file %q", imageFile.Name())
}
if err := cmd.Process.Kill(); err != nil {
log.WithError(err).Debug("Failed to stop the QR code image viewer")
}
}, nil
}

0 comments on commit a480923

Please sign in to comment.