login.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. package login
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "io/ioutil"
  7. "log"
  8. "math/rand"
  9. "net"
  10. "net/http"
  11. "net/url"
  12. "os/exec"
  13. "path/filepath"
  14. "runtime"
  15. "strconv"
  16. "strings"
  17. "time"
  18. "github.com/docker/api/errdefs"
  19. "github.com/Azure/go-autorest/autorest"
  20. "github.com/Azure/go-autorest/autorest/adal"
  21. "github.com/Azure/go-autorest/autorest/azure/cli"
  22. "github.com/Azure/go-autorest/autorest/date"
  23. "golang.org/x/oauth2"
  24. "github.com/pkg/errors"
  25. )
  26. func init() {
  27. rand.Seed(time.Now().Unix())
  28. }
  29. //go login process, derived from code sample provided by MS at https://github.com/devigned/go-az-cli-stuff
  30. const (
  31. authorizeFormat = "https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize?response_type=code&client_id=%s&redirect_uri=%s&state=%s&prompt=select_account&response_mode=query&scope=%s"
  32. tokenEndpoint = "https://login.microsoftonline.com/%s/oauth2/v2.0/token"
  33. authorizationURL = "https://management.azure.com/tenants?api-version=2019-11-01"
  34. // scopes for a multi-tenant app works for openid, email, other common scopes, but fails when trying to add a token
  35. // v1 scope like "https://management.azure.com/.default" for ARM access
  36. scopes = "offline_access https://management.azure.com/.default"
  37. clientID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" // Azure CLI client id
  38. )
  39. type (
  40. azureToken struct {
  41. Type string `json:"token_type"`
  42. Scope string `json:"scope"`
  43. ExpiresIn int `json:"expires_in"`
  44. ExtExpiresIn int `json:"ext_expires_in"`
  45. AccessToken string `json:"access_token"`
  46. RefreshToken string `json:"refresh_token"`
  47. Foci string `json:"foci"`
  48. }
  49. tenantResult struct {
  50. Value []tenantValue `json:"value"`
  51. }
  52. tenantValue struct {
  53. TenantID string `json:"tenantId"`
  54. }
  55. )
  56. // AzureLoginService Service to log into azure and get authentifier for azure APIs
  57. type AzureLoginService struct {
  58. tokenStore tokenStore
  59. apiHelper apiHelper
  60. }
  61. const tokenStoreFilename = "dockerAccessToken.json"
  62. func getTokenStorePath() string {
  63. cliPath, _ := cli.AccessTokensPath()
  64. return filepath.Join(filepath.Dir(cliPath), tokenStoreFilename)
  65. }
  66. // NewAzureLoginService creates a NewAzureLoginService
  67. func NewAzureLoginService() (AzureLoginService, error) {
  68. return newAzureLoginServiceFromPath(getTokenStorePath(), azureAPIHelper{})
  69. }
  70. func newAzureLoginServiceFromPath(tokenStorePath string, helper apiHelper) (AzureLoginService, error) {
  71. store, err := newTokenStore(tokenStorePath)
  72. if err != nil {
  73. return AzureLoginService{}, err
  74. }
  75. return AzureLoginService{
  76. tokenStore: store,
  77. apiHelper: helper,
  78. }, nil
  79. }
  80. type apiHelper interface {
  81. queryToken(data url.Values, tenantID string) (azureToken, error)
  82. openAzureLoginPage(redirectURL string)
  83. queryAuthorizationAPI(authorizationURL string, authorizationHeader string) ([]byte, int, error)
  84. }
  85. type azureAPIHelper struct{}
  86. //Login perform azure login through browser
  87. func (login AzureLoginService) Login(ctx context.Context) error {
  88. queryCh := make(chan url.Values, 1)
  89. serverPort, err := startLoginServer(queryCh)
  90. if err != nil {
  91. return err
  92. }
  93. redirectURL := "http://localhost:" + strconv.Itoa(serverPort)
  94. login.apiHelper.openAzureLoginPage(redirectURL)
  95. select {
  96. case <-ctx.Done():
  97. return nil
  98. case qsValues := <-queryCh:
  99. errorMsg, hasError := qsValues["error"]
  100. if hasError {
  101. return fmt.Errorf("login failed : %s", errorMsg)
  102. }
  103. code, hasCode := qsValues["code"]
  104. if !hasCode {
  105. return errdefs.ErrLoginFailed
  106. }
  107. data := url.Values{
  108. "grant_type": []string{"authorization_code"},
  109. "client_id": []string{clientID},
  110. "code": code,
  111. "scope": []string{scopes},
  112. "redirect_uri": []string{redirectURL},
  113. }
  114. token, err := login.apiHelper.queryToken(data, "organizations")
  115. if err != nil {
  116. return errors.Wrap(err, "Access token request failed")
  117. }
  118. bits, statusCode, err := login.apiHelper.queryAuthorizationAPI(authorizationURL, fmt.Sprintf("Bearer %s", token.AccessToken))
  119. if err != nil {
  120. return errors.Wrap(err, "login failed")
  121. }
  122. if statusCode == 200 {
  123. var tenantResult tenantResult
  124. if err := json.Unmarshal(bits, &tenantResult); err != nil {
  125. return errors.Wrap(err, "login failed")
  126. }
  127. tenantID := tenantResult.Value[0].TenantID
  128. tenantToken, err := login.refreshToken(token.RefreshToken, tenantID)
  129. if err != nil {
  130. return errors.Wrap(err, "login failed")
  131. }
  132. loginInfo := TokenInfo{TenantID: tenantID, Token: tenantToken}
  133. err = login.tokenStore.writeLoginInfo(loginInfo)
  134. if err != nil {
  135. return errors.Wrap(err, "login failed")
  136. }
  137. fmt.Println("Login Succeeded")
  138. return nil
  139. }
  140. return fmt.Errorf("login failed : " + string(bits))
  141. }
  142. }
  143. func startLoginServer(queryCh chan url.Values) (int, error) {
  144. mux := http.NewServeMux()
  145. mux.HandleFunc("/", queryHandler(queryCh))
  146. listener, err := net.Listen("tcp", ":0")
  147. if err != nil {
  148. return 0, err
  149. }
  150. availablePort := listener.Addr().(*net.TCPAddr).Port
  151. server := &http.Server{Handler: mux}
  152. go func() {
  153. if err := server.Serve(listener); err != nil {
  154. queryCh <- url.Values{
  155. "error": []string{fmt.Sprintf("error starting http server with: %v", err)},
  156. }
  157. }
  158. }()
  159. return availablePort, nil
  160. }
  161. func (helper azureAPIHelper) openAzureLoginPage(redirectURL string) {
  162. state := randomString("", 10)
  163. authURL := fmt.Sprintf(authorizeFormat, clientID, redirectURL, state, scopes)
  164. openbrowser(authURL)
  165. }
  166. func (helper azureAPIHelper) queryAuthorizationAPI(authorizationURL string, authorizationHeader string) ([]byte, int, error) {
  167. req, err := http.NewRequest(http.MethodGet, authorizationURL, nil)
  168. if err != nil {
  169. return nil, 0, err
  170. }
  171. req.Header.Add("Authorization", authorizationHeader)
  172. res, err := http.DefaultClient.Do(req)
  173. if err != nil {
  174. return nil, 0, err
  175. }
  176. bits, err := ioutil.ReadAll(res.Body)
  177. if err != nil {
  178. return nil, 0, err
  179. }
  180. return bits, res.StatusCode, nil
  181. }
  182. func queryHandler(queryCh chan url.Values) func(w http.ResponseWriter, r *http.Request) {
  183. queryHandler := func(w http.ResponseWriter, r *http.Request) {
  184. _, hasCode := r.URL.Query()["code"]
  185. if hasCode {
  186. _, err := w.Write([]byte(successfullLoginHTML))
  187. if err != nil {
  188. queryCh <- url.Values{
  189. "error": []string{err.Error()},
  190. }
  191. } else {
  192. queryCh <- r.URL.Query()
  193. }
  194. } else {
  195. _, err := w.Write([]byte(loginFailedHTML))
  196. if err != nil {
  197. queryCh <- url.Values{
  198. "error": []string{err.Error()},
  199. }
  200. } else {
  201. queryCh <- r.URL.Query()
  202. }
  203. }
  204. }
  205. return queryHandler
  206. }
  207. func (helper azureAPIHelper) queryToken(data url.Values, tenantID string) (azureToken, error) {
  208. res, err := http.Post(fmt.Sprintf(tokenEndpoint, tenantID), "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
  209. if err != nil {
  210. return azureToken{}, err
  211. }
  212. if res.StatusCode != 200 {
  213. return azureToken{}, errors.Errorf("error while renewing access token, status : %s", res.Status)
  214. }
  215. bits, err := ioutil.ReadAll(res.Body)
  216. if err != nil {
  217. return azureToken{}, err
  218. }
  219. token := azureToken{}
  220. if err := json.Unmarshal(bits, &token); err != nil {
  221. return azureToken{}, err
  222. }
  223. return token, nil
  224. }
  225. func toOAuthToken(token azureToken) oauth2.Token {
  226. expireTime := time.Now().Add(time.Duration(token.ExpiresIn) * time.Second)
  227. oauthToken := oauth2.Token{
  228. RefreshToken: token.RefreshToken,
  229. AccessToken: token.AccessToken,
  230. Expiry: expireTime,
  231. TokenType: token.Type,
  232. }
  233. return oauthToken
  234. }
  235. // NewAuthorizerFromLogin creates an authorizer based on login access token
  236. func NewAuthorizerFromLogin() (autorest.Authorizer, error) {
  237. login, err := NewAzureLoginService()
  238. if err != nil {
  239. return nil, err
  240. }
  241. oauthToken, err := login.GetValidToken()
  242. if err != nil {
  243. return nil, err
  244. }
  245. token := adal.Token{
  246. AccessToken: oauthToken.AccessToken,
  247. Type: oauthToken.TokenType,
  248. ExpiresIn: json.Number(strconv.Itoa(int(time.Until(oauthToken.Expiry).Seconds()))),
  249. ExpiresOn: json.Number(strconv.Itoa(int(oauthToken.Expiry.Sub(date.UnixEpoch()).Seconds()))),
  250. RefreshToken: "",
  251. Resource: "",
  252. }
  253. return autorest.NewBearerAuthorizer(&token), nil
  254. }
  255. // GetValidToken returns an access token. Refresh token if needed
  256. func (login AzureLoginService) GetValidToken() (oauth2.Token, error) {
  257. loginInfo, err := login.tokenStore.readToken()
  258. if err != nil {
  259. return oauth2.Token{}, err
  260. }
  261. token := loginInfo.Token
  262. if token.Valid() {
  263. return token, nil
  264. }
  265. tenantID := loginInfo.TenantID
  266. token, err = login.refreshToken(token.RefreshToken, tenantID)
  267. if err != nil {
  268. return oauth2.Token{}, errors.Wrap(err, "access token request failed. Maybe you need to login to azure again.")
  269. }
  270. err = login.tokenStore.writeLoginInfo(TokenInfo{TenantID: tenantID, Token: token})
  271. if err != nil {
  272. return oauth2.Token{}, err
  273. }
  274. return token, nil
  275. }
  276. func (login AzureLoginService) refreshToken(currentRefreshToken string, tenantID string) (oauth2.Token, error) {
  277. data := url.Values{
  278. "grant_type": []string{"refresh_token"},
  279. "client_id": []string{clientID},
  280. "scope": []string{scopes},
  281. "refresh_token": []string{currentRefreshToken},
  282. }
  283. token, err := login.apiHelper.queryToken(data, tenantID)
  284. if err != nil {
  285. return oauth2.Token{}, err
  286. }
  287. return toOAuthToken(token), nil
  288. }
  289. func openbrowser(url string) {
  290. var err error
  291. switch runtime.GOOS {
  292. case "linux":
  293. err = exec.Command("xdg-open", url).Start()
  294. case "windows":
  295. err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
  296. case "darwin":
  297. err = exec.Command("open", url).Start()
  298. default:
  299. err = fmt.Errorf("unsupported platform")
  300. }
  301. if err != nil {
  302. log.Fatal(err)
  303. }
  304. }
  305. var (
  306. letterRunes = []rune("abcdefghijklmnopqrstuvwxyz123456789")
  307. )
  308. func randomString(prefix string, length int) string {
  309. b := make([]rune, length)
  310. for i := range b {
  311. b[i] = letterRunes[rand.Intn(len(letterRunes))]
  312. }
  313. return prefix + string(b)
  314. }
  315. const loginFailedHTML = `
  316. <!DOCTYPE html>
  317. <html>
  318. <head>
  319. <meta charset="utf-8" />
  320. <title>Login failed</title>
  321. </head>
  322. <body>
  323. <h4>Some failures occurred during the authentication</h4>
  324. <p>You can log an issue at <a href="https://github.com/azure/azure-cli/issues">Azure CLI GitHub Repository</a> and we will assist you in resolving it.</p>
  325. </body>
  326. </html>
  327. `
  328. const successfullLoginHTML = `
  329. <!DOCTYPE html>
  330. <html>
  331. <head>
  332. <meta charset="utf-8" />
  333. <meta http-equiv="refresh" content="10;url=https://docs.microsoft.com/cli/azure/">
  334. <title>Login successfully</title>
  335. </head>
  336. <body>
  337. <h4>You have logged into Microsoft Azure!</h4>
  338. <p>You can close this window, or we will redirect you to the <a href="https://docs.microsoft.com/cli/azure/">Azure CLI documents</a> in 10 seconds.</p>
  339. </body>
  340. </html>
  341. `