diff --git a/main.go b/main.go index ed2a5d5..e8697f9 100644 --- a/main.go +++ b/main.go @@ -105,6 +105,15 @@ var configByHost = map[string]oauth2.Config{ ClientSecret: "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", Endpoint: endpoints.Google, Scopes: []string{"https://www.googleapis.com/auth/gerritcodereview"}}, + // https://app.vsaex.visualstudio.com/app/view?clientId=3528d4a6-5442-42de-a7ee-01159c916fd9 + "dev.azure.com": { + ClientID: "3528D4A6-5442-42DE-A7EE-01159C916FD9", + Endpoint: oauth2.Endpoint{ + AuthURL: "https://app.vssps.visualstudio.com/oauth2/authorize", + TokenURL: "https://app.vssps.visualstudio.com/oauth2/token"}, + Scopes: []string{"vso.code_write"}, + // server confused by random ports + RedirectURL: "https://127.0.0.1:53119"}, } var ( @@ -327,6 +336,7 @@ func getToken(c oauth2.Config) (*oauth2.Token, error) { c.RedirectURL = server.URL } else { server = httptest.NewUnstartedServer(handler) + server.Listener.Close() url, err := url.Parse(c.RedirectURL) if err != nil { log.Fatalln(err) @@ -336,34 +346,51 @@ func getToken(c oauth2.Config) (*oauth2.Token, error) { log.Fatalln(err) } server.Listener = l - server.Start() - } - defer server.Close() - return authhandler.TokenSourceWithPKCE(context.Background(), &c, state, func(authCodeURL string) (code string, state string, err error) { - defer server.Close() - fmt.Fprintf(os.Stderr, "Please complete authentication in your browser...\n%s\n", authCodeURL) - var open string - switch runtime.GOOS { - case "windows": - open = "start" - case "darwin": - open = "open" - default: - open = "xdg-open" - } - // TODO: wait for server to start before opening browser - if _, err := exec.LookPath(open); err == nil { - err = exec.Command(open, authCodeURL).Run() - if err != nil { - return "", "", err - } + if url.Scheme == "https" { + server.StartTLS() + } else { + server.Start() } - query := <-queries - if verbose { - fmt.Fprintln(os.Stderr, "query:", query) - } - return query.Get("code"), query.Get("state"), nil - }, generatePKCEParams()).Token() + } + pkce := generatePKCEParams() + ao := []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("code_challenge", pkce.Challenge), + oauth2.SetAuthURLParam("code_challenge_method", pkce.ChallengeMethod)} + isAzure := c.Endpoint == configByHost["dev.azure.com"].Endpoint + if isAzure { + ao = append(ao, oauth2.SetAuthURLParam("response_type", "Assertion")) + } + authCodeURL := c.AuthCodeURL(state, ao...) + fmt.Fprintf(os.Stderr, "Please complete authentication in your browser...\n%s\n", authCodeURL) + var open string + switch runtime.GOOS { + case "windows": + open = "start" + case "darwin": + open = "open" + default: + open = "xdg-open" + } + if _, err := exec.LookPath(open); err == nil { + exec.Command(open, authCodeURL).Run() + } + query := <-queries + if verbose { + fmt.Fprintln(os.Stderr, "query:", query) + } + code := query.Get("code") + eo := []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("code_verifier", pkce.Verifier)} + if isAzure { + // following https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/oauth?view=azure-devops + eo = append(eo, + oauth2.SetAuthURLParam("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"), + oauth2.SetAuthURLParam("assertion", code), + oauth2.SetAuthURLParam("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"), + // "client secret" from https://app.vsaex.visualstudio.com/app/view?clientId=3528d4a6-5442-42de-a7ee-01159c916fd9 + oauth2.SetAuthURLParam("client_assertion", "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Im9PdmN6NU1fN3AtSGpJS2xGWHo5M3VfVjBabyJ9.eyJjaWQiOiIzNTI4ZDRhNi01NDQyLTQyZGUtYTdlZS0wMTE1OWM5MTZmZDkiLCJjc2kiOiIxN2Y4N2Y1MC1lOWRhLTRjNmQtYTUxNi1jOGE5YTI3MDRjYWMiLCJuYW1laWQiOiIzNTZlY2VjNC02YjkzLTRmMjctYThlMi1jMzEyNzhhZjlhZDIiLCJpc3MiOiJhcHAudnN0b2tlbi52aXN1YWxzdHVkaW8uY29tIiwiYXVkIjoiYXBwLnZzdG9rZW4udmlzdWFsc3R1ZGlvLmNvbSIsIm5iZiI6MTY2NzI5MjQ0NiwiZXhwIjoxODI1MDU4ODQ1fQ.iOhrMuGIyNOrJHHzkcSvkVGx5216i5-HZwEFerU28yTQDDv-0ttSI2n7TQWgAJ2gdrkQnEV4N8cpRVM3o3bmF4rRgxXOHVpY_Fvi_cFE71AMoU-0ilVCfqeFwTi0Z8g7YHR2aalqcV6MkuiLs1UouSOcwUdeDUCD94yHY5puRnje_Zw2vzb68YGlgFAD4dWIw1R00IeocIGpm3Z7TyQazpAj7EhaR2SdCNpVyEMMbjmyxYtSqjF3Fs-Ja_RFqO9RrAi5Ju1xmx_3_ofw207QNq0PzvTGfQLKP3QioCZxjLbHiT8__7vwvRTjWdg1aDnttKt1qRZwec1RfUv15XG9Nw")) + } + return c.Exchange(context.Background(), code, eo...) } func randomString(n int) string { diff --git a/main_test.go b/main_test.go index b84de2c..1b4974f 100644 --- a/main_test.go +++ b/main_test.go @@ -10,6 +10,9 @@ func TestConfig(t *testing.T) { if key == "android.googlesource.com" { continue } + if key == "dev.azure.com" { + continue + } if !strings.Contains(c.Endpoint.AuthURL, key) { t.Errorf("bad auth url for key %s: %s", key, c.Endpoint.AuthURL) }