@@ -7,17 +7,19 @@ import (
77 "io"
88 "os"
99 "strings"
10+ "time"
1011
1112 "github.com/sourcegraph/src-cli/internal/api"
1213 "github.com/sourcegraph/src-cli/internal/cmderrors"
14+ "github.com/sourcegraph/src-cli/internal/oauthdevice"
1315)
1416
1517func init () {
1618 usage := `'src login' helps you authenticate 'src' to access a Sourcegraph instance with your user credentials.
1719
1820Usage:
1921
20- src login SOURCEGRAPH_URL
22+ src login [flags] SOURCEGRAPH_URL
2123
2224Examples:
2325
@@ -28,6 +30,10 @@ Examples:
2830 Authenticate to Sourcegraph.com:
2931
3032 $ src login https://sourcegraph.com
33+
34+ Use OAuth device flow to authenticate (opens a browser):
35+
36+ $ src login --device-flow https://sourcegraph.com
3137`
3238
3339 flagSet := flag .NewFlagSet ("login" , flag .ExitOnError )
@@ -37,7 +43,8 @@ Examples:
3743 }
3844
3945 var (
40- apiFlags = api .NewFlags (flagSet )
46+ apiFlags = api .NewFlags (flagSet )
47+ useDeviceFlow = flagSet .Bool ("device-flow" , false , "Use OAuth device flow to obtain an access token interactively" )
4148 )
4249
4350 handler := func (args []string ) error {
@@ -54,7 +61,15 @@ Examples:
5461
5562 client := cfg .apiClient (apiFlags , io .Discard )
5663
57- return loginCmd (context .Background (), cfg , client , endpoint , os .Stdout )
64+ return loginCmd (context .Background (), loginParams {
65+ cfg : cfg ,
66+ client : client ,
67+ endpoint : endpoint ,
68+ out : os .Stdout ,
69+ useDeviceFlow : * useDeviceFlow ,
70+ apiFlags : apiFlags ,
71+ deviceFlowClient : oauthdevice .NewClient (),
72+ })
5873 }
5974
6075 commands = append (commands , & command {
@@ -64,8 +79,21 @@ Examples:
6479 })
6580}
6681
67- func loginCmd (ctx context.Context , cfg * config , client api.Client , endpointArg string , out io.Writer ) error {
68- endpointArg = cleanEndpoint (endpointArg )
82+ type loginParams struct {
83+ cfg * config
84+ client api.Client
85+ endpoint string
86+ out io.Writer
87+ useDeviceFlow bool
88+ apiFlags * api.Flags
89+ deviceFlowClient oauthdevice.Client
90+ }
91+
92+ func loginCmd (ctx context.Context , p loginParams ) error {
93+ endpointArg := cleanEndpoint (p .endpoint )
94+ cfg := p .cfg
95+ client := p .client
96+ out := p .out
6997
7098 printProblem := func (problem string ) {
7199 fmt .Fprintf (out , "❌ Problem: %s\n " , problem )
@@ -86,7 +114,19 @@ func loginCmd(ctx context.Context, cfg *config, client api.Client, endpointArg s
86114
87115 noToken := cfg .AccessToken == ""
88116 endpointConflict := endpointArg != cfg .Endpoint
89- if noToken || endpointConflict {
117+
118+ if p .useDeviceFlow {
119+ token , err := runDeviceFlow (ctx , endpointArg , out , p .deviceFlowClient )
120+ if err != nil {
121+ printProblem (fmt .Sprintf ("Device flow authentication failed: %s" , err ))
122+ fmt .Fprintln (out , createAccessTokenMessage )
123+ return cmderrors .ExitCode1
124+ }
125+
126+ cfg .AccessToken = token
127+ cfg .Endpoint = endpointArg
128+ client = cfg .apiClient (p .apiFlags , out )
129+ } else if noToken || endpointConflict {
90130 fmt .Fprintln (out )
91131 switch {
92132 case noToken :
@@ -122,6 +162,43 @@ func loginCmd(ctx context.Context, cfg *config, client api.Client, endpointArg s
122162 }
123163 fmt .Fprintln (out )
124164 fmt .Fprintf (out , "✔️ Authenticated as %s on %s\n " , result .CurrentUser .Username , endpointArg )
165+
166+ if p .useDeviceFlow {
167+ fmt .Fprintln (out )
168+ fmt .Fprintf (out , "To use this access token, set the following environment variables in your terminal:\n \n " )
169+ fmt .Fprintf (out , " export SRC_ENDPOINT=%s\n " , endpointArg )
170+ fmt .Fprintf (out , " export SRC_ACCESS_TOKEN=%s\n " , cfg .AccessToken )
171+ }
172+
125173 fmt .Fprintln (out )
126174 return nil
127175}
176+
177+ func runDeviceFlow (ctx context.Context , endpoint string , out io.Writer , client oauthdevice.Client ) (string , error ) {
178+ authResp , err := client .Start (ctx , endpoint , nil )
179+ if err != nil {
180+ return "" , err
181+ }
182+
183+ fmt .Fprintln (out )
184+ fmt .Fprintf (out , "🔐 To authenticate, visit %s and enter the code: %s\n " , authResp .VerificationURI , authResp .UserCode )
185+ if authResp .VerificationURIComplete != "" {
186+ fmt .Fprintln (out )
187+ fmt .Fprintf (out , " Alternatively, you can open: %s\n " , authResp .VerificationURIComplete )
188+ }
189+ fmt .Fprintln (out )
190+ fmt .Fprint (out , "Waiting for authorization..." )
191+ defer fmt .Fprintf (out , "DONE\n \n " )
192+
193+ interval := time .Duration (authResp .Interval ) * time .Second
194+ if interval <= 0 {
195+ interval = 5 * time .Second
196+ }
197+
198+ tokenResp , err := client .Poll (ctx , endpoint , authResp .DeviceCode , interval , authResp .ExpiresIn )
199+ if err != nil {
200+ return "" , err
201+ }
202+
203+ return tokenResp .AccessToken , nil
204+ }
0 commit comments