@@ -20,8 +20,10 @@ import (
20
20
"fmt"
21
21
"io"
22
22
"strings"
23
+ "time"
23
24
24
25
"connectrpc.com/connect"
26
+ "github.com/bufbuild/buf/private/buf/bufapp"
25
27
"github.com/bufbuild/buf/private/buf/bufcli"
26
28
"github.com/bufbuild/buf/private/bufpkg/bufconnect"
27
29
"github.com/bufbuild/buf/private/gen/proto/connect/buf/alpha/registry/v1alpha1/registryv1alpha1connect"
@@ -31,12 +33,16 @@ import (
31
33
"github.com/bufbuild/buf/private/pkg/connectclient"
32
34
"github.com/bufbuild/buf/private/pkg/netext"
33
35
"github.com/bufbuild/buf/private/pkg/netrc"
36
+ "github.com/bufbuild/buf/private/pkg/oauth2"
37
+ "github.com/bufbuild/buf/private/pkg/transport/http/httpclient"
38
+ "github.com/pkg/browser"
34
39
"github.com/spf13/pflag"
35
40
)
36
41
37
42
const (
38
43
usernameFlagName = "username"
39
44
tokenStdinFlagName = "token-stdin"
45
+ promptFlagName = "prompt"
40
46
)
41
47
42
48
// NewCommand returns a new Command.
@@ -48,9 +54,8 @@ func NewCommand(
48
54
return & appcmd.Command {
49
55
Use : name + " <domain>" ,
50
56
Short : `Log in to the Buf Schema Registry` ,
51
- Long : fmt .Sprintf (`This prompts for your BSR token and updates your %s file with these credentials.
52
- The <domain> argument will default to buf.build if not specified.` , netrc .Filename ),
53
- Args : appcmd .MaximumNArgs (1 ),
57
+ Long : fmt .Sprintf (`This command will open a browser to complete the login process. Use the flags --%s or --%s to complete an alternative login flow. The token is saved to your %s file. The <domain> argument will default to buf.build if not specified.` , promptFlagName , tokenStdinFlagName , netrc .Filename ),
58
+ Args : appcmd .MaximumNArgs (1 ),
54
59
Run : builder .NewRunFunc (
55
60
func (ctx context.Context , container appext.Container ) error {
56
61
return run (ctx , container , flags )
@@ -63,6 +68,7 @@ The <domain> argument will default to buf.build if not specified.`, netrc.Filena
63
68
type flags struct {
64
69
Username string
65
70
TokenStdin bool
71
+ Prompt bool
66
72
}
67
73
68
74
func newFlags () * flags {
@@ -82,7 +88,19 @@ func (f *flags) Bind(flagSet *pflag.FlagSet) {
82
88
& f .TokenStdin ,
83
89
tokenStdinFlagName ,
84
90
false ,
85
- "Read the token from stdin. This command prompts for a token by default" ,
91
+ fmt .Sprintf (
92
+ "Read the token from stdin. This command prompts for a token by default. Exclusive with the flag --%s." ,
93
+ promptFlagName ,
94
+ ),
95
+ )
96
+ flagSet .BoolVar (
97
+ & f .Prompt ,
98
+ promptFlagName ,
99
+ false ,
100
+ fmt .Sprintf (
101
+ "Prompt for the token. The device must be a TTY. Exclusive with the flag --%s." ,
102
+ tokenStdinFlagName ,
103
+ ),
86
104
)
87
105
}
88
106
@@ -140,31 +158,33 @@ func inner(
140
158
return err
141
159
}
142
160
}
143
- // Do not print unless we are prompting
144
- if ! flags .TokenStdin {
145
- if _ , err := fmt .Fprintf (
146
- container .Stdout (),
147
- "Enter the BSR token created at https://%s/settings/user.\n \n " ,
148
- remote ,
149
- ); err != nil {
150
- return err
151
- }
161
+ if flags .TokenStdin && flags .Prompt {
162
+ return appcmd .NewInvalidArgumentErrorf ("cannot use both --%s and --%s flags" , tokenStdinFlagName , promptFlagName )
152
163
}
153
164
var token string
154
165
if flags .TokenStdin {
155
166
data , err := io .ReadAll (container .Stdin ())
156
167
if err != nil {
157
- return err
168
+ return fmt . Errorf ( "unable to read token from stdin: %w" , err )
158
169
}
159
170
token = string (data )
171
+ } else if flags .Prompt {
172
+ var err error
173
+ token , err = doPromptLogin (ctx , container , remote )
174
+ if err != nil {
175
+ return err
176
+ }
160
177
} else {
161
178
var err error
162
- token , err = bufcli . PromptUserForPassword ( container , "Token: " )
179
+ token , err = doBrowserLogin ( ctx , container , remote )
163
180
if err != nil {
164
- if errors .Is (err , bufcli .ErrNotATTY ) {
165
- return errors .New ("cannot perform an interactive login from a non-TTY device" )
181
+ if ! errors .Is (err , oauth2 .ErrUnsupported ) {
182
+ return fmt .Errorf ("unable to complete authorize device grant: %w" , err )
183
+ }
184
+ token , err = doPromptLogin (ctx , container , remote )
185
+ if err != nil {
186
+ return err
166
187
}
167
- return err
168
188
}
169
189
}
170
190
// Remove leading and trailing spaces from user-supplied token to avoid
@@ -219,3 +239,104 @@ func inner(
219
239
}
220
240
return nil
221
241
}
242
+
243
+ // doPromptLogin prompts the user for a token.
244
+ func doPromptLogin (
245
+ _ context.Context ,
246
+ container appext.Container ,
247
+ remote string ,
248
+ ) (string , error ) {
249
+ if _ , err := fmt .Fprintf (
250
+ container .Stdout (),
251
+ "Enter the BSR token created at https://%s/settings/user.\n \n " ,
252
+ remote ,
253
+ ); err != nil {
254
+ return "" , err
255
+ }
256
+ var err error
257
+ token , err := bufcli .PromptUserForPassword (container , "Token: " )
258
+ if err != nil {
259
+ if errors .Is (err , bufcli .ErrNotATTY ) {
260
+ return "" , errors .New ("cannot perform an interactive login from a non-TTY device" )
261
+ }
262
+ return "" , err
263
+ }
264
+ return token , nil
265
+ }
266
+
267
+ // doBrowserLogin performs the device authorization grant flow via the browser.
268
+ func doBrowserLogin (
269
+ ctx context.Context ,
270
+ container appext.Container ,
271
+ remote string ,
272
+ ) (string , error ) {
273
+ baseURL := "https://" + remote
274
+ clientName , err := getClientName ()
275
+ if err != nil {
276
+ return "" , err
277
+ }
278
+ externalConfig := bufapp.ExternalConfig {}
279
+ if err := appext .ReadConfig (container , & externalConfig ); err != nil {
280
+ return "" , err
281
+ }
282
+ appConfig , err := bufapp .NewConfig (container , externalConfig )
283
+ if err != nil {
284
+ return "" , err
285
+ }
286
+ client := httpclient .NewClient (appConfig .TLS )
287
+ oauth2Client := oauth2 .NewClient (baseURL , client )
288
+ // Register the device.
289
+ deviceRegistration , err := oauth2Client .RegisterDevice (ctx , & oauth2.DeviceRegistrationRequest {
290
+ ClientName : clientName ,
291
+ })
292
+ if err != nil {
293
+ var oauth2Err * oauth2.Error
294
+ if errors .As (err , & oauth2Err ) {
295
+ return "" , fmt .Errorf ("authorization failed: %s" , oauth2Err .ErrorDescription )
296
+ }
297
+ return "" , err
298
+ }
299
+ // Request a device authorization code.
300
+ deviceAuthorization , err := oauth2Client .AuthorizeDevice (ctx , & oauth2.DeviceAuthorizationRequest {
301
+ ClientID : deviceRegistration .ClientID ,
302
+ ClientSecret : deviceRegistration .ClientSecret ,
303
+ })
304
+ if err != nil {
305
+ var oauth2Err * oauth2.Error
306
+ if errors .As (err , & oauth2Err ) {
307
+ return "" , fmt .Errorf ("authorization failed: %s" , oauth2Err .ErrorDescription )
308
+ }
309
+ return "" , err
310
+ }
311
+ // Open the browser to the verification URI.
312
+ if err := browser .OpenURL (deviceAuthorization .VerificationURIComplete ); err != nil {
313
+ return "" , fmt .Errorf ("failed to open browser: %w" , err )
314
+ }
315
+ if _ , err := fmt .Fprintf (
316
+ container .Stdout (),
317
+ `Opening your browser to complete authorization process.
318
+
319
+ If your browser doesn't open automatically, please open this URL in a browser to complete the process:
320
+
321
+ %s
322
+ ` ,
323
+ deviceAuthorization .VerificationURIComplete ,
324
+ ); err != nil {
325
+ return "" , err
326
+ }
327
+ // Poll the token endpoint until the user has authorized the device.
328
+ deviceToken , err := oauth2Client .AccessDeviceToken (ctx , & oauth2.DeviceAccessTokenRequest {
329
+ ClientID : deviceRegistration .ClientID ,
330
+ ClientSecret : deviceRegistration .ClientSecret ,
331
+ DeviceCode : deviceAuthorization .DeviceCode ,
332
+ GrantType : oauth2 .DeviceAuthorizationGrantType ,
333
+ }, oauth2 .AccessDeviceTokenWithPollingInterval (time .Duration (deviceAuthorization .Interval )* time .Second ))
334
+ if err != nil {
335
+ var oauth2Err * oauth2.Error
336
+ if errors .As (err , & oauth2Err ) {
337
+ return "" , fmt .Errorf ("authorization failed: %s" , oauth2Err .ErrorDescription )
338
+ }
339
+ return "" , err
340
+ }
341
+ return deviceToken .AccessToken , nil
342
+ }
0 commit comments