From 850067cb415d5859223cfa7c9172dc00437d74bb Mon Sep 17 00:00:00 2001 From: Christopher Taylor Date: Tue, 14 Oct 2025 20:44:11 +0200 Subject: [PATCH 1/2] getDeviceToken: return error instead of calling Fatalln() the call site in main() exits on error, so this seems unnecessary here --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 2148e5d..ffd31e9 100644 --- a/main.go +++ b/main.go @@ -494,7 +494,7 @@ func getToken(ctx context.Context, c oauth2.Config, authURLSuffix string) (*oaut func getDeviceToken(ctx context.Context, c oauth2.Config) (*oauth2.Token, error) { deviceAuth, err := c.DeviceAuth(ctx) if err != nil { - log.Fatalln(err) + return nil, err } if verbose { fmt.Fprintf(os.Stderr, "%+v\n", deviceAuth) From 39759458a5543ddd427c8734b5709f35efc00346 Mon Sep 17 00:00:00 2001 From: Christopher Taylor Date: Tue, 14 Oct 2025 20:44:11 +0200 Subject: [PATCH 2/2] display QR codes for device flow authentication use package rsc.io/qr to generate QR codes and display them in the terminal using ANSI escape codes. --- go.mod | 2 ++ go.sum | 2 ++ main.go | 43 ++++++++++++++++++++++++++++++++++++++++++- main_test.go | 11 +++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 888fb8d..6ffd7c0 100644 --- a/go.mod +++ b/go.mod @@ -5,3 +5,5 @@ go 1.23.0 toolchain go1.24.1 require golang.org/x/oauth2 v0.30.0 + +require rsc.io/qr v0.2.0 diff --git a/go.sum b/go.sum index c05b468..628f91d 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= +rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/main.go b/main.go index ffd31e9..77021dd 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ package main import ( + "bytes" "context" "flag" "fmt" @@ -31,6 +32,7 @@ import ( "golang.org/x/oauth2" "golang.org/x/oauth2/endpoints" + "rsc.io/qr" ) // configByHost lists default config for several public hosts. @@ -499,10 +501,49 @@ func getDeviceToken(ctx context.Context, c oauth2.Config) (*oauth2.Token, error) if verbose { fmt.Fprintf(os.Stderr, "%+v\n", deviceAuth) } - fmt.Fprintf(os.Stderr, "Please enter code %s at %s\n", deviceAuth.UserCode, deviceAuth.VerificationURI) + if deviceAuth.VerificationURIComplete != "" { + fmt.Fprintf(os.Stderr, "Please scan the QR code or enter code %s at %s\n", deviceAuth.UserCode, deviceAuth.VerificationURI) + fmt.Fprintln(os.Stderr) + writeQRCode(os.Stderr, deviceAuth.VerificationURIComplete) + } else { + fmt.Fprintf(os.Stderr, "Please enter code %s at %s\n", deviceAuth.UserCode, deviceAuth.VerificationURI) + } return c.DeviceAccessToken(ctx, deviceAuth) } +func writeQRCode(w io.Writer, data string) error { + // use low redundancy to generate small QR codes for terminal output. + // we assume the user is sitting in front of their screen so image quality shouldn't be an issue + code, err := qr.Encode(data, qr.L) + if err != nil { + return err + } + + var ( + black = "\033[40m \033[0m" + white = "\033[107m \033[0m" + whiteLine = strings.Repeat(white, code.Size+2) + "\n" + buf bytes.Buffer + ) + buf.WriteString(whiteLine) + for y := range code.Size { + buf.WriteString(white) + for x := range code.Size { + if code.Black(x, y) { + buf.WriteString(black) + } else { + buf.WriteString(white) + } + } + buf.WriteString(white) + buf.WriteRune('\n') + } + buf.WriteString(whiteLine) + + _, err = buf.WriteTo(w) + return err +} + func replaceHost(e oauth2.Endpoint, host string) oauth2.Endpoint { e.AuthURL = replaceHostInURL(e.AuthURL, host) e.TokenURL = replaceHostInURL(e.TokenURL, host) diff --git a/main_test.go b/main_test.go index fa64d59..26c3264 100644 --- a/main_test.go +++ b/main_test.go @@ -1,6 +1,7 @@ package main import ( + "os" "strings" "testing" ) @@ -22,6 +23,16 @@ func TestConfig(t *testing.T) { } } +func TestQR(t *testing.T) { + msg := os.Getenv("QR_MSG") + if msg == "" { + t.Skip("no QR_MSG set, skipping") + } + if err := writeQRCode(os.Stdout, msg); err != nil { + t.Fatal(err) + } +} + func FuzzParse(f *testing.F) { f.Add("key=value") f.Add("key=")