Skip to content

Commit a014e9b

Browse files
committed
add --device-flow to login cmd
--device-flow uses oauthdevice to start the OAuth device flow with a well known client on a sourcegraph server
1 parent b76f424 commit a014e9b

File tree

1 file changed

+83
-6
lines changed

1 file changed

+83
-6
lines changed

cmd/src/login.go

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1517
func init() {
1618
usage := `'src login' helps you authenticate 'src' to access a Sourcegraph instance with your user credentials.
1719
1820
Usage:
1921
20-
src login SOURCEGRAPH_URL
22+
src login [flags] SOURCEGRAPH_URL
2123
2224
Examples:
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

Comments
 (0)