Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions cmd/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ Examples:
# Login non-interactively with an API token
$ bk auth login --org my-org --token my-token

# Login on a headless machine or remote shell
$ bk auth login --device
# Login to a specific organization on a headless machine or remote shell
$ bk auth login --device --org my-org

# Login on a headless Linux host using an in-memory /dev/shm credential store
$ bk auth login --device --credential-store shm
Expand Down Expand Up @@ -293,9 +293,6 @@ func (c *LoginCmd) validate(kongCtx *kong.Context) error {
if c.Device && c.Token != "" {
return errors.New("--device cannot be used with --token")
}
if c.Device && c.Org != "" {
return errors.New("--org is not supported with --device; choose an organization on the authorization page")
}
return nil
}

Expand All @@ -312,8 +309,11 @@ func (c *LoginCmd) credentialStoreFlagProvided(kongCtx *kong.Context) bool {
}

func (c *LoginCmd) runDeviceLogin(ctx context.Context, f *factory.Factory, resolvedScopes string, credentialStore oauthTokenStore) error {
orgSlug, orgUUID := organizationIdentifier(c.Org)
cfg := &oauth.Config{
ClientID: oauth.DefaultClientID,
OrgSlug: orgSlug,
OrgUUID: orgUUID,
Scopes: resolvedScopes,
}

Expand Down
13 changes: 9 additions & 4 deletions cmd/auth/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,8 @@ func TestLoginCmdValidateDeviceIncompatibleFlags(t *testing.T) {
wantErr: "--device cannot be used with --token",
},
{
name: "device with org",
cmd: LoginCmd{Device: true, Org: "buildkite"},
wantErr: "--org is not supported with --device; choose an organization on the authorization page",
name: "device with org",
cmd: LoginCmd{Device: true, Org: "buildkite"},
},
{
name: "device only",
Expand Down Expand Up @@ -499,6 +498,12 @@ func TestLoginCmdRunDeviceFlow(t *testing.T) {
if got := r.FormValue("scope"); got != "read_user read_organizations" {
t.Errorf("scope = %q, want requested scopes", got)
}
if got := r.FormValue("organization"); got != "test-org" {
t.Errorf("organization = %q, want test-org", got)
}
if got := r.FormValue("organization_uuid"); got != "" {
t.Errorf("organization_uuid = %q, want empty", got)
}
_ = json.NewEncoder(w).Encode(oauth.DeviceAuthorizationResponse{
DeviceCode: "device-code",
UserCode: "ABCD-EFGH",
Expand Down Expand Up @@ -547,7 +552,7 @@ func TestLoginCmdRunDeviceFlow(t *testing.T) {
t.Setenv("BUILDKITE_HOST", strings.TrimPrefix(server.URL, "https://"))
t.Setenv("BUILDKITE_REST_API_ENDPOINT", server.URL)

cmd := &LoginCmd{Device: true, Scopes: "read_user read_organizations"}
cmd := &LoginCmd{Device: true, Org: "test-org", Scopes: "read_user read_organizations"}
if err := cmd.Run(nil, authStubGlobals{}); err != nil {
t.Fatalf("Run() error = %v", err)
}
Expand Down
126 changes: 75 additions & 51 deletions pkg/oauth/device_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,61 +25,85 @@ func (rt *failingRoundTripper) RoundTrip(req *http.Request) (*http.Response, err
}

func TestRequestDeviceAuthorization(t *testing.T) {
var sawRequest bool
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sawRequest = true
tests := []struct {
name string
orgSlug string
orgUUID string
wantOrganization string
wantUUID string
}{
{name: "without organization"},
{name: "with organization slug", orgSlug: "buildkite", wantOrganization: "buildkite"},
{name: "with organization UUID", orgSlug: "ignored", orgUUID: "018f2f7e-7e99-7d77-b4d3-a95cb01805f4", wantUUID: "018f2f7e-7e99-7d77-b4d3-a95cb01805f4"},
}

if r.Method != "POST" {
t.Errorf("method = %s, want POST", r.Method)
}
if r.URL.Path != "/oauth/device_authorization" {
t.Errorf("path = %s, want /oauth/device_authorization", r.URL.Path)
}
if err := r.ParseForm(); err != nil {
t.Fatalf("ParseForm: %v", err)
}
if got := r.FormValue("client_id"); got != "test-client" {
t.Errorf("client_id = %q, want test-client", got)
}
if got := r.FormValue("scope"); got != "read_user read_organizations" {
t.Errorf("scope = %q, want requested scopes", got)
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var sawRequest bool
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sawRequest = true

w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(DeviceAuthorizationResponse{
DeviceCode: "device-code",
UserCode: "ABCD-EFGH",
VerificationURI: "https://buildkite.example/oauth/device",
VerificationURIComplete: "https://buildkite.example/oauth/device/ABCD-EFGH",
ExpiresIn: 600,
Interval: 5,
})
}))
defer server.Close()
if r.Method != "POST" {
t.Errorf("method = %s, want POST", r.Method)
}
if r.URL.Path != "/oauth/device_authorization" {
t.Errorf("path = %s, want /oauth/device_authorization", r.URL.Path)
}
if err := r.ParseForm(); err != nil {
t.Fatalf("ParseForm: %v", err)
}
if got := r.FormValue("client_id"); got != "test-client" {
t.Errorf("client_id = %q, want test-client", got)
}
if got := r.FormValue("scope"); got != "read_user read_organizations" {
t.Errorf("scope = %q, want requested scopes", got)
}
if got := r.FormValue("organization"); got != tt.wantOrganization {
t.Errorf("organization = %q, want %q", got, tt.wantOrganization)
}
if got := r.FormValue("organization_uuid"); got != tt.wantUUID {
t.Errorf("organization_uuid = %q, want %q", got, tt.wantUUID)
}

origTransport := http.DefaultTransport
http.DefaultTransport = server.Client().Transport
defer func() { http.DefaultTransport = origTransport }()
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(DeviceAuthorizationResponse{
DeviceCode: "device-code",
UserCode: "ABCD-EFGH",
VerificationURI: "https://buildkite.example/oauth/device",
VerificationURIComplete: "https://buildkite.example/oauth/device/ABCD-EFGH",
ExpiresIn: 600,
Interval: 5,
})
}))
defer server.Close()

deviceAuth, err := RequestDeviceAuthorization(context.Background(), &Config{
Host: server.URL[len("https://"):],
ClientID: "test-client",
Scopes: "read_user read_organizations",
})
if err != nil {
t.Fatalf("RequestDeviceAuthorization: %v", err)
}
if !sawRequest {
t.Fatal("server did not receive request")
}
if deviceAuth.DeviceCode != "device-code" {
t.Errorf("DeviceCode = %q, want device-code", deviceAuth.DeviceCode)
}
if deviceAuth.UserCode != "ABCD-EFGH" {
t.Errorf("UserCode = %q, want ABCD-EFGH", deviceAuth.UserCode)
}
if deviceAuth.VerificationURIComplete != "https://buildkite.example/oauth/device/ABCD-EFGH" {
t.Errorf("VerificationURIComplete = %q", deviceAuth.VerificationURIComplete)
origTransport := http.DefaultTransport
http.DefaultTransport = server.Client().Transport
defer func() { http.DefaultTransport = origTransport }()

deviceAuth, err := RequestDeviceAuthorization(context.Background(), &Config{
Host: server.URL[len("https://"):],
ClientID: "test-client",
Scopes: "read_user read_organizations",
OrgSlug: tt.orgSlug,
OrgUUID: tt.orgUUID,
})
if err != nil {
t.Fatalf("RequestDeviceAuthorization: %v", err)
}
if !sawRequest {
t.Fatal("server did not receive request")
}
if deviceAuth.DeviceCode != "device-code" {
t.Errorf("DeviceCode = %q, want device-code", deviceAuth.DeviceCode)
}
if deviceAuth.UserCode != "ABCD-EFGH" {
t.Errorf("UserCode = %q, want ABCD-EFGH", deviceAuth.UserCode)
}
if deviceAuth.VerificationURIComplete != "https://buildkite.example/oauth/device/ABCD-EFGH" {
t.Errorf("VerificationURIComplete = %q", deviceAuth.VerificationURIComplete)
}
})
}
}

Expand Down
5 changes: 5 additions & 0 deletions pkg/oauth/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,11 @@ func RequestDeviceAuthorization(ctx context.Context, cfg *Config) (*DeviceAuthor
"client_id": {cfg.ClientID},
"scope": {cfg.Scopes},
}
if cfg.OrgUUID != "" {
data.Set("organization_uuid", cfg.OrgUUID)
} else if cfg.OrgSlug != "" {
data.Set("organization", cfg.OrgSlug)
}

req, err := http.NewRequestWithContext(ctx, "POST", deviceURL, strings.NewReader(data.Encode()))
if err != nil {
Expand Down