| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- /*
- Copyright 2020 Docker Compose CLI authors
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package login
- import (
- "context"
- "encoding/json"
- "fmt"
- "net/http"
- "net/url"
- "os"
- "time"
- "github.com/docker/compose-cli/pkg/api"
- "github.com/Azure/go-autorest/autorest/adal"
- "github.com/Azure/go-autorest/autorest/azure/auth"
- "github.com/pkg/errors"
- "golang.org/x/oauth2"
- )
- //go login process, derived from code sample provided by MS at https://github.com/devigned/go-az-cli-stuff
- const (
- clientID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" // Azure CLI client id
- )
- type (
- azureToken struct {
- Type string `json:"token_type"`
- Scope string `json:"scope"`
- ExpiresIn int `json:"expires_in"`
- ExtExpiresIn int `json:"ext_expires_in"`
- AccessToken string `json:"access_token"`
- RefreshToken string `json:"refresh_token"`
- Foci string `json:"foci"`
- }
- tenantResult struct {
- Value []tenantValue `json:"value"`
- }
- tenantValue struct {
- TenantID string `json:"tenantId"`
- }
- )
- // AzureLoginService Service to log into azure and get authentifier for azure APIs
- type AzureLoginService interface {
- Login(ctx context.Context, requestedTenantID string, cloudEnvironment string) error
- LoginServicePrincipal(clientID string, clientSecret string, tenantID string, cloudEnvironment string) error
- Logout(ctx context.Context) error
- GetCloudEnvironment() (CloudEnvironment, error)
- GetValidToken() (oauth2.Token, string, error)
- }
- type azureLoginService struct {
- tokenStore tokenStore
- apiHelper apiHelper
- cloudEnvironmentSvc CloudEnvironmentService
- }
- const tokenStoreFilename = "dockerAccessToken.json"
- // NewAzureLoginService creates a NewAzureLoginService
- func NewAzureLoginService() (AzureLoginService, error) {
- return newAzureLoginServiceFromPath(GetTokenStorePath(), azureAPIHelper{}, CloudEnvironments)
- }
- func newAzureLoginServiceFromPath(tokenStorePath string, helper apiHelper, ces CloudEnvironmentService) (*azureLoginService, error) {
- store, err := newTokenStore(tokenStorePath)
- if err != nil {
- return nil, err
- }
- return &azureLoginService{
- tokenStore: store,
- apiHelper: helper,
- cloudEnvironmentSvc: ces,
- }, nil
- }
- // LoginServicePrincipal login with clientId / clientSecret from a service principal.
- // The resulting token does not include a refresh token
- func (login *azureLoginService) LoginServicePrincipal(clientID string, clientSecret string, tenantID string, cloudEnvironment string) error {
- // Tried with auth2.NewUsernamePasswordConfig() but could not make this work with username / password, setting this for CI with clientID / clientSecret
- creds := auth.NewClientCredentialsConfig(clientID, clientSecret, tenantID)
- spToken, err := creds.ServicePrincipalToken()
- if err != nil {
- return errors.Wrapf(api.ErrLoginFailed, "could not login with service principal: %s", err)
- }
- err = spToken.Refresh()
- if err != nil {
- return errors.Wrapf(api.ErrLoginFailed, "could not login with service principal: %s", err)
- }
- token, err := spToOAuthToken(spToken.Token())
- if err != nil {
- return errors.Wrapf(api.ErrLoginFailed, "could not read service principal token expiry: %s", err)
- }
- loginInfo := TokenInfo{TenantID: tenantID, Token: token, CloudEnvironment: cloudEnvironment}
- if err := login.tokenStore.writeLoginInfo(loginInfo); err != nil {
- return errors.Wrapf(api.ErrLoginFailed, "could not store login info: %s", err)
- }
- return nil
- }
- // Logout remove azure token data
- func (login *azureLoginService) Logout(ctx context.Context) error {
- err := login.tokenStore.removeData()
- if os.IsNotExist(err) {
- return errors.New("No Azure login data to be removed")
- }
- return err
- }
- func (login *azureLoginService) getTenantAndValidateLogin(
- ctx context.Context,
- accessToken string,
- refreshToken string,
- requestedTenantID string,
- ce CloudEnvironment,
- ) error {
- bits, statusCode, err := login.apiHelper.queryAPIWithHeader(ctx, ce.GetTenantQueryURL(), fmt.Sprintf("Bearer %s", accessToken))
- if err != nil {
- return errors.Wrapf(api.ErrLoginFailed, "check auth failed: %s", err)
- }
- if statusCode != http.StatusOK {
- return errors.Wrapf(api.ErrLoginFailed, "unable to login status code %d: %s", statusCode, bits)
- }
- var t tenantResult
- if err := json.Unmarshal(bits, &t); err != nil {
- return errors.Wrapf(api.ErrLoginFailed, "unable to unmarshal tenant: %s", err)
- }
- tenantID, err := getTenantID(t.Value, requestedTenantID)
- if err != nil {
- return errors.Wrap(api.ErrLoginFailed, err.Error())
- }
- tToken, err := login.refreshToken(refreshToken, tenantID, ce)
- if err != nil {
- return errors.Wrapf(api.ErrLoginFailed, "unable to refresh token: %s", err)
- }
- loginInfo := TokenInfo{TenantID: tenantID, Token: tToken, CloudEnvironment: ce.Name}
- if err := login.tokenStore.writeLoginInfo(loginInfo); err != nil {
- return errors.Wrapf(api.ErrLoginFailed, "could not store login info: %s", err)
- }
- return nil
- }
- // Login performs an Azure login through a web browser
- func (login *azureLoginService) Login(ctx context.Context, requestedTenantID string, cloudEnvironment string) error {
- ce, err := login.cloudEnvironmentSvc.Get(cloudEnvironment)
- if err != nil {
- return err
- }
- queryCh := make(chan localResponse, 1)
- s, err := NewLocalServer(queryCh)
- if err != nil {
- return err
- }
- s.Serve()
- defer s.Close()
- redirectURL := s.Addr()
- if redirectURL == "" {
- return errors.Wrap(api.ErrLoginFailed, "empty redirect URL")
- }
- deviceCodeFlowCh := make(chan deviceCodeFlowResponse, 1)
- if err = login.apiHelper.openAzureLoginPage(redirectURL, ce); err != nil {
- login.startDeviceCodeFlow(deviceCodeFlowCh, ce)
- }
- select {
- case <-ctx.Done():
- return ctx.Err()
- case dcft := <-deviceCodeFlowCh:
- if dcft.err != nil {
- return errors.Wrapf(api.ErrLoginFailed, "could not get token using device code flow: %s", err)
- }
- token := dcft.token
- return login.getTenantAndValidateLogin(ctx, token.AccessToken, token.RefreshToken, requestedTenantID, ce)
- case q := <-queryCh:
- if q.err != nil {
- return errors.Wrapf(api.ErrLoginFailed, "unhandled local login server error: %s", err)
- }
- code, hasCode := q.values["code"]
- if !hasCode {
- return errors.Wrap(api.ErrLoginFailed, "no login code")
- }
- data := url.Values{
- "grant_type": []string{"authorization_code"},
- "client_id": []string{clientID},
- "code": code,
- "scope": []string{ce.GetTokenScope()},
- "redirect_uri": []string{redirectURL},
- }
- token, err := login.apiHelper.queryToken(ce, data, "organizations")
- if err != nil {
- return errors.Wrapf(api.ErrLoginFailed, "access token request failed: %s", err)
- }
- return login.getTenantAndValidateLogin(ctx, token.AccessToken, token.RefreshToken, requestedTenantID, ce)
- }
- }
- type deviceCodeFlowResponse struct {
- token adal.Token
- err error
- }
- func (login *azureLoginService) startDeviceCodeFlow(deviceCodeFlowCh chan deviceCodeFlowResponse, ce CloudEnvironment) {
- fmt.Println("Could not automatically open a browser, falling back to Azure device code flow authentication")
- go func() {
- token, err := login.apiHelper.getDeviceCodeFlowToken(ce)
- if err != nil {
- deviceCodeFlowCh <- deviceCodeFlowResponse{err: err}
- }
- deviceCodeFlowCh <- deviceCodeFlowResponse{token: token}
- }()
- }
- func getTenantID(tenantValues []tenantValue, requestedTenantID string) (string, error) {
- if requestedTenantID == "" {
- if len(tenantValues) < 1 {
- return "", errors.Errorf("could not find azure tenant")
- }
- return tenantValues[0].TenantID, nil
- }
- for _, tValue := range tenantValues {
- if tValue.TenantID == requestedTenantID {
- return tValue.TenantID, nil
- }
- }
- return "", errors.Errorf("could not find requested azure tenant %s", requestedTenantID)
- }
- func toOAuthToken(token azureToken) oauth2.Token {
- expireTime := time.Now().Add(time.Duration(token.ExpiresIn) * time.Second)
- oauthToken := oauth2.Token{
- RefreshToken: token.RefreshToken,
- AccessToken: token.AccessToken,
- Expiry: expireTime,
- TokenType: token.Type,
- }
- return oauthToken
- }
- func spToOAuthToken(token adal.Token) (oauth2.Token, error) {
- expiresIn, err := token.ExpiresIn.Int64()
- if err != nil {
- return oauth2.Token{}, err
- }
- expireTime := time.Now().Add(time.Duration(expiresIn) * time.Second)
- oauthToken := oauth2.Token{
- RefreshToken: token.RefreshToken,
- AccessToken: token.AccessToken,
- Expiry: expireTime,
- TokenType: token.Type,
- }
- return oauthToken, nil
- }
- // GetValidToken returns an access token and associated tenant ID.
- // Will refresh the token as necessary.
- func (login *azureLoginService) GetValidToken() (oauth2.Token, string, error) {
- loginInfo, err := login.tokenStore.readToken()
- if err != nil {
- return oauth2.Token{}, "", err
- }
- token := loginInfo.Token
- tenantID := loginInfo.TenantID
- if token.Valid() {
- return token, tenantID, nil
- }
- ce, err := login.cloudEnvironmentSvc.Get(loginInfo.CloudEnvironment)
- if err != nil {
- return oauth2.Token{}, "", errors.Wrap(err, "access token request failed--cloud environment could not be determined.")
- }
- token, err = login.refreshToken(token.RefreshToken, tenantID, ce)
- if err != nil {
- return oauth2.Token{}, "", errors.Wrap(err, "access token request failed. Maybe you need to login to Azure again.")
- }
- err = login.tokenStore.writeLoginInfo(TokenInfo{TenantID: tenantID, Token: token, CloudEnvironment: ce.Name})
- if err != nil {
- return oauth2.Token{}, "", err
- }
- return token, tenantID, nil
- }
- // GeCloudEnvironment returns the cloud environment associated with the current authentication token (if we have one)
- func (login *azureLoginService) GetCloudEnvironment() (CloudEnvironment, error) {
- tokenInfo, err := login.tokenStore.readToken()
- if err != nil {
- return CloudEnvironment{}, err
- }
- cloudEnvironment, err := login.cloudEnvironmentSvc.Get(tokenInfo.CloudEnvironment)
- if err != nil {
- return CloudEnvironment{}, err
- }
- return cloudEnvironment, nil
- }
- func (login *azureLoginService) refreshToken(currentRefreshToken string, tenantID string, ce CloudEnvironment) (oauth2.Token, error) {
- data := url.Values{
- "grant_type": []string{"refresh_token"},
- "client_id": []string{clientID},
- "scope": []string{ce.GetTokenScope()},
- "refresh_token": []string{currentRefreshToken},
- }
- token, err := login.apiHelper.queryToken(ce, data, tenantID)
- if err != nil {
- return oauth2.Token{}, err
- }
- return toOAuthToken(token), nil
- }
|