11package main
22
33import (
4+ "bytes"
45 "context"
6+ "encoding/json"
7+ "errors"
58 "flag"
69 "fmt"
710 "io"
811 "log"
12+ "net/http"
913 "os"
1014 "runtime/debug"
1115 "strings"
16+ "time"
1217
1318 "github.com/AzureAD/microsoft-authentication-library-for-go/apps/public"
1419)
@@ -82,18 +87,35 @@ func main() {
8287 if verbose {
8388 fmt .Fprintln (os .Stderr , "result:" , result )
8489 }
90+ organization := strings .SplitN (pairs ["path" ], "/" , 2 )[0 ]
91+ var pt PatToken
92+ if organization != "" {
93+ pt , err = getPAT (organization , result .AccessToken )
94+ if err != nil {
95+ fmt .Fprintln (os .Stderr , "error acquiring Personal Access Token" , err )
96+ }
97+ }
8598 var username string
8699 if pairs ["username" ] == "" {
100+ // TODO: check correctness
87101 username = "oauth2"
88102 }
89- output := map [string ]string {
90- "password" : result .AccessToken ,
91- }
103+ output := map [string ]string {}
92104 if username != "" {
93105 output ["username" ] = username
94106 }
95- if ! result .ExpiresOn .IsZero () {
96- output ["password_expiry_utc" ] = fmt .Sprintf ("%d" , result .ExpiresOn .UTC ().Unix ())
107+ var password string
108+ var expiry time.Time
109+ if pt .Token != "" {
110+ password = pt .Token
111+ expiry = pt .ValidTo
112+ } else {
113+ password = result .AccessToken
114+ expiry = result .ExpiresOn
115+ }
116+ output ["password" ] = password
117+ if ! expiry .IsZero () {
118+ output ["password_expiry_utc" ] = fmt .Sprintf ("%d" , expiry .UTC ().Unix ())
97119 }
98120 if verbose {
99121 fmt .Fprintln (os .Stderr , "output:" , output )
@@ -106,11 +128,65 @@ func main() {
106128
107129func authenticate () (public.AuthResult , error ) {
108130 client , err := public .New (
131+ // https://github.com/git-ecosystem/git-credential-manager/blob/8c430c9484c90433ab30c25df7fc1005fe2f4ba4/src/shared/Microsoft.AzureRepos/AzureDevOpsConstants.cs#L15
132+ // magic https://developercommunity.visualstudio.com/t/non-interactive-aad-auth-works-for-visual-studio-a/387853
109133 "872cd9fa-d31f-45e0-9eab-6e460a02d1f1" ,
110134 public .WithAuthority ("https://login.microsoftonline.com/organizations" ))
111135 if err != nil {
112136 return public.AuthResult {}, err
113137 }
138+ // https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/manage-personal-access-tokens-via-api?view=azure-devops
114139 scopes := []string {"499b84ac-1321-427f-aa17-267ca6975798/.default" }
115140 return client .AcquireTokenInteractive (context .Background (), scopes )
116141}
142+
143+ func getPAT (organization , accessToken string ) (PatToken , error ) {
144+ // https://learn.microsoft.com/en-us/rest/api/azure/devops/tokens/pats/create?view=azure-devops-rest-7.1&tabs=HTTP
145+ // sadly https://github.com/microsoft/azure-devops-go-api doesn't have this function
146+ url := fmt .Sprintf ("https://vssps.dev.azure.com/%s/_apis/tokens/pats?api-version=7.1-preview.1" , organization )
147+ j := map [string ]any {
148+ "scopes" : "vso.code_write vso.packaging" ,
149+ }
150+ body , err := json .Marshal (j )
151+ if err != nil {
152+ return PatToken {}, err
153+ }
154+ req , err := http .NewRequest ("POST" , url , bytes .NewReader (body ))
155+ if err != nil {
156+ return PatToken {}, err
157+ }
158+ req .Header .Set ("Content-Type" , "application/json" )
159+ req .Header .Add ("Authorization" , fmt .Sprintf ("Bearer %s" , accessToken ))
160+ response , err := http .DefaultClient .Do (req )
161+ if err != nil {
162+ return PatToken {}, err
163+ }
164+ body , err = io .ReadAll (response .Body )
165+ if err != nil {
166+ return PatToken {}, err
167+ }
168+ if verbose {
169+ fmt .Fprintln (os .Stderr , string (body ))
170+ }
171+ ptr := PatTokenResult {}
172+ err = json .Unmarshal (body , & ptr )
173+ if err != nil {
174+ return PatToken {}, err
175+ }
176+ if ptr .PatTokenError != "" && ptr .PatTokenError != "none" {
177+ return PatToken {}, errors .New (ptr .PatTokenError )
178+ }
179+ return ptr .PatToken , nil
180+ }
181+
182+ // https://learn.microsoft.com/en-us/rest/api/azure/devops/tokens/pats/create?view=azure-devops-rest-7.1&tabs=HTTP#pattokenresult
183+ type PatTokenResult struct {
184+ PatToken PatToken `json:"patToken"`
185+ PatTokenError string `json:"patTokenError"`
186+ }
187+
188+ // https://learn.microsoft.com/en-us/rest/api/azure/devops/tokens/pats/create?view=azure-devops-rest-7.1&tabs=HTTP#pattoken
189+ type PatToken struct {
190+ Token string `json:"token"`
191+ ValidTo time.Time `json:"validTo"`
192+ }
0 commit comments