github.com/jfrog/jfrog-cli-core/v2@v2.51.0/utils/config/tokenrefresh.go (about)

     1  package config
     2  
     3  import (
     4  	"errors"
     5  	"github.com/jfrog/jfrog-client-go/access"
     6  	accessservices "github.com/jfrog/jfrog-client-go/access/services"
     7  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
    12  	"github.com/jfrog/jfrog-cli-core/v2/utils/lock"
    13  	"github.com/jfrog/jfrog-client-go/artifactory"
    14  	"github.com/jfrog/jfrog-client-go/artifactory/services"
    15  	"github.com/jfrog/jfrog-client-go/auth"
    16  	"github.com/jfrog/jfrog-client-go/config"
    17  	"github.com/jfrog/jfrog-client-go/utils/io/httputils"
    18  	"github.com/jfrog/jfrog-client-go/utils/log"
    19  )
    20  
    21  // Internal golang locking for the same process.
    22  var mutex sync.Mutex
    23  
    24  // The serverId used for authentication. Use for reading and writing tokens from/to the config file, and for reading the credentials if needed.
    25  var tokenRefreshServerId string
    26  
    27  const (
    28  	ArtifactoryToken TokenType = "artifactory"
    29  	AccessToken      TokenType = "access"
    30  )
    31  
    32  type TokenType string
    33  
    34  func AccessTokenRefreshPreRequestInterceptor(fields *auth.CommonConfigFields, httpClientDetails *httputils.HttpClientDetails) (err error) {
    35  	return tokenRefreshPreRequestInterceptor(fields, httpClientDetails, AccessToken, auth.RefreshPlatformTokenBeforeExpiryMinutes)
    36  }
    37  
    38  func ArtifactoryTokenRefreshPreRequestInterceptor(fields *auth.CommonConfigFields, httpClientDetails *httputils.HttpClientDetails) (err error) {
    39  	return tokenRefreshPreRequestInterceptor(fields, httpClientDetails, ArtifactoryToken, auth.RefreshArtifactoryTokenBeforeExpiryMinutes)
    40  }
    41  
    42  func tokenRefreshPreRequestInterceptor(fields *auth.CommonConfigFields, httpClientDetails *httputils.HttpClientDetails, tokenType TokenType, refreshBeforeExpiryMinutes int64) (err error) {
    43  	if fields.GetAccessToken() == "" || httpClientDetails.AccessToken == "" {
    44  		return nil
    45  	}
    46  
    47  	timeLeft, err := auth.GetTokenMinutesLeft(httpClientDetails.AccessToken)
    48  	if err != nil || timeLeft > refreshBeforeExpiryMinutes {
    49  		return err
    50  	}
    51  	// Lock to make sure only one thread is trying to refresh
    52  	mutex.Lock()
    53  	defer mutex.Unlock()
    54  	// Refresh only if a new token wasn't acquired (by another thread) while waiting at mutex.
    55  	if fields.AccessToken == httpClientDetails.AccessToken {
    56  		newAccessToken, err := tokenRefreshHandler(httpClientDetails.AccessToken, tokenType)
    57  		if err != nil {
    58  			return err
    59  		}
    60  		if newAccessToken != "" && newAccessToken != httpClientDetails.AccessToken {
    61  			fields.AccessToken = newAccessToken
    62  		}
    63  	}
    64  	// Copy new token from the mutual struct CommonConfigFields to the private struct in httpClientDetails
    65  	httpClientDetails.AccessToken = fields.AccessToken
    66  	return nil
    67  }
    68  
    69  func tokenRefreshHandler(currentAccessToken string, tokenType TokenType) (newAccessToken string, err error) {
    70  	log.Debug("Refreshing token...")
    71  	// Lock config to prevent access from different processes
    72  	lockDirPath, err := coreutils.GetJfrogConfigLockDir()
    73  	if err != nil {
    74  		return
    75  	}
    76  	unlockFunc, err := lock.CreateLock(lockDirPath)
    77  	// Defer the lockFile.Unlock() function before throwing a possible error to avoid deadlock situations.
    78  	defer func() {
    79  		err = errors.Join(err, unlockFunc())
    80  	}()
    81  	if err != nil {
    82  		return
    83  	}
    84  
    85  	serverConfiguration, err := GetSpecificConfig(tokenRefreshServerId, true, false)
    86  	if err != nil {
    87  		return
    88  	}
    89  	if tokenRefreshServerId == "" && serverConfiguration != nil {
    90  		tokenRefreshServerId = serverConfiguration.ServerId
    91  	}
    92  	// If token already refreshed, get new token from config
    93  	if serverConfiguration.AccessToken != "" && serverConfiguration.AccessToken != currentAccessToken {
    94  		log.Debug("Fetched new token from config.")
    95  		newAccessToken = serverConfiguration.AccessToken
    96  		return
    97  	}
    98  
    99  	// If token isn't already expired, Wait to make sure requests using the current token are sent before it is refreshed and becomes invalid
   100  	timeLeft, err := auth.GetTokenMinutesLeft(currentAccessToken)
   101  	if err != nil {
   102  		return
   103  	}
   104  	if timeLeft > 0 {
   105  		time.Sleep(auth.WaitBeforeRefreshSeconds * time.Second)
   106  	}
   107  
   108  	if tokenType == ArtifactoryToken {
   109  		newAccessToken, err = refreshArtifactoryTokenAndWriteToConfig(serverConfiguration, currentAccessToken)
   110  		return
   111  	}
   112  	if tokenType == AccessToken {
   113  		newAccessToken, err = refreshAccessTokenAndWriteToConfig(serverConfiguration, currentAccessToken)
   114  		return
   115  	}
   116  	err = errorutils.CheckErrorf("unsupported refreshable token type: " + string(tokenType))
   117  	return
   118  }
   119  
   120  func refreshArtifactoryTokenAndWriteToConfig(serverConfiguration *ServerDetails, currentAccessToken string) (string, error) {
   121  	refreshToken := serverConfiguration.ArtifactoryRefreshToken
   122  	// Remove previous tokens
   123  	serverConfiguration.AccessToken = ""
   124  	serverConfiguration.ArtifactoryRefreshToken = ""
   125  	// Try refreshing tokens
   126  	newToken, err := refreshArtifactoryExpiredToken(serverConfiguration, currentAccessToken, refreshToken)
   127  
   128  	if err != nil {
   129  		log.Debug("Refresh token failed: " + err.Error())
   130  		log.Debug("Trying to create new tokens...")
   131  
   132  		expirySeconds, err := auth.ExtractExpiryFromAccessToken(currentAccessToken)
   133  		if err != nil {
   134  			return "", err
   135  		}
   136  
   137  		newToken, err = createTokensForConfig(serverConfiguration, expirySeconds)
   138  		if err != nil {
   139  			return "", err
   140  		}
   141  		log.Debug("New token created successfully.")
   142  	} else {
   143  		log.Debug("Token refreshed successfully.")
   144  	}
   145  
   146  	err = writeNewArtifactoryTokens(serverConfiguration, tokenRefreshServerId, newToken.AccessToken, newToken.RefreshToken)
   147  	return newToken.AccessToken, err
   148  }
   149  
   150  func refreshAccessTokenAndWriteToConfig(serverConfiguration *ServerDetails, currentAccessToken string) (string, error) {
   151  	// Try refreshing tokens
   152  	newToken, err := refreshExpiredAccessToken(serverConfiguration, currentAccessToken, serverConfiguration.RefreshToken)
   153  	if err != nil {
   154  		return "", errorutils.CheckErrorf("Refresh access token failed: " + err.Error())
   155  	}
   156  	err = writeNewArtifactoryTokens(serverConfiguration, tokenRefreshServerId, newToken.AccessToken, newToken.RefreshToken)
   157  	return newToken.AccessToken, err
   158  }
   159  
   160  func writeNewArtifactoryTokens(serverConfiguration *ServerDetails, serverId, accessToken, refreshToken string) error {
   161  	serverConfiguration.SetAccessToken(accessToken)
   162  	serverConfiguration.SetArtifactoryRefreshToken(refreshToken)
   163  
   164  	// Get configurations list
   165  	configurations, err := GetAllServersConfigs()
   166  	if err != nil {
   167  		return err
   168  	}
   169  
   170  	// Remove and get the server details from the configurations list
   171  	_, configurations = GetAndRemoveConfiguration(serverId, configurations)
   172  
   173  	// Append the configuration to the configurations list
   174  	configurations = append(configurations, serverConfiguration)
   175  	return SaveServersConf(configurations)
   176  }
   177  
   178  func createTokensForConfig(serverDetails *ServerDetails, expirySeconds int) (auth.CreateTokenResponseData, error) {
   179  	servicesManager, err := createArtifactoryTokensServiceManager(serverDetails)
   180  	if err != nil {
   181  		return auth.CreateTokenResponseData{}, err
   182  	}
   183  
   184  	createTokenParams := services.NewCreateTokenParams()
   185  	createTokenParams.Username = serverDetails.User
   186  	createTokenParams.ExpiresIn = expirySeconds
   187  	// User-scoped token
   188  	createTokenParams.Scope = "member-of-groups:*"
   189  	createTokenParams.Refreshable = true
   190  
   191  	newToken, err := servicesManager.CreateToken(createTokenParams)
   192  	if err != nil {
   193  		return auth.CreateTokenResponseData{}, err
   194  	}
   195  	return newToken, nil
   196  }
   197  
   198  func CreateInitialRefreshableTokensIfNeeded(serverDetails *ServerDetails) (err error) {
   199  	if !(serverDetails.ArtifactoryTokenRefreshInterval > 0 && serverDetails.ArtifactoryRefreshToken == "" && serverDetails.AccessToken == "") ||
   200  		(serverDetails.RefreshToken != "" && serverDetails.AccessToken != "") {
   201  		return nil
   202  	}
   203  	mutex.Lock()
   204  	defer mutex.Unlock()
   205  	lockDirPath, err := coreutils.GetJfrogConfigLockDir()
   206  	if err != nil {
   207  		return
   208  	}
   209  	unlockFunc, err := lock.CreateLock(lockDirPath)
   210  	// Defer the lockFile.Unlock() function before throwing a possible error to avoid deadlock situations.
   211  	defer func() {
   212  		err = errors.Join(err, unlockFunc())
   213  	}()
   214  	if err != nil {
   215  		return
   216  	}
   217  
   218  	newToken, err := createTokensForConfig(serverDetails, serverDetails.ArtifactoryTokenRefreshInterval*60)
   219  	if err != nil {
   220  		return
   221  	}
   222  	// Remove initializing value.
   223  	serverDetails.ArtifactoryTokenRefreshInterval = 0
   224  	err = writeNewArtifactoryTokens(serverDetails, serverDetails.ServerId, newToken.AccessToken, newToken.RefreshToken)
   225  	return
   226  }
   227  
   228  func refreshArtifactoryExpiredToken(serverDetails *ServerDetails, currentAccessToken string, refreshToken string) (auth.CreateTokenResponseData, error) {
   229  	// The tokens passed as parameters are also used for authentication
   230  	noCredsDetails := new(ServerDetails)
   231  	noCredsDetails.ArtifactoryUrl = serverDetails.ArtifactoryUrl
   232  	noCredsDetails.ClientCertPath = serverDetails.ClientCertPath
   233  	noCredsDetails.ClientCertKeyPath = serverDetails.ClientCertKeyPath
   234  	noCredsDetails.ServerId = serverDetails.ServerId
   235  	noCredsDetails.IsDefault = serverDetails.IsDefault
   236  
   237  	servicesManager, err := createArtifactoryTokensServiceManager(noCredsDetails)
   238  	if err != nil {
   239  		return auth.CreateTokenResponseData{}, err
   240  	}
   241  
   242  	refreshTokenParams := services.NewArtifactoryRefreshTokenParams()
   243  	refreshTokenParams.AccessToken = currentAccessToken
   244  	refreshTokenParams.RefreshToken = refreshToken
   245  	return servicesManager.RefreshToken(refreshTokenParams)
   246  }
   247  
   248  func refreshExpiredAccessToken(serverDetails *ServerDetails, currentAccessToken string, refreshToken string) (auth.CreateTokenResponseData, error) {
   249  	// Creating accessTokens service manager without credentials.
   250  	// In case credentials were provided accessTokens refresh mechanism will be operated. That will cause recursive locking mechanism.
   251  	noCredServerDetails := new(ServerDetails)
   252  	noCredServerDetails.Url = serverDetails.Url
   253  	noCredServerDetails.ClientCertPath = serverDetails.ClientCertPath
   254  	noCredServerDetails.ClientCertKeyPath = serverDetails.ClientCertKeyPath
   255  	noCredServerDetails.ServerId = serverDetails.ServerId
   256  	noCredServerDetails.IsDefault = serverDetails.IsDefault
   257  
   258  	servicesManager, err := createAccessTokensServiceManager(noCredServerDetails)
   259  	if err != nil {
   260  		return auth.CreateTokenResponseData{}, err
   261  	}
   262  
   263  	refreshTokenParams := accessservices.CreateTokenParams{}
   264  	refreshTokenParams.AccessToken = currentAccessToken
   265  	refreshTokenParams.RefreshToken = refreshToken
   266  	return servicesManager.RefreshAccessToken(refreshTokenParams)
   267  }
   268  
   269  func createArtifactoryTokensServiceManager(artDetails *ServerDetails) (artifactory.ArtifactoryServicesManager, error) {
   270  	certsPath, err := coreutils.GetJfrogCertsDir()
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  	artAuth, err := artDetails.CreateArtAuthConfig()
   275  	if err != nil {
   276  		return nil, err
   277  	}
   278  	serviceConfig, err := config.NewConfigBuilder().
   279  		SetServiceDetails(artAuth).
   280  		SetCertificatesPath(certsPath).
   281  		SetInsecureTls(artDetails.InsecureTls).
   282  		SetDryRun(false).
   283  		Build()
   284  	if err != nil {
   285  		return nil, err
   286  	}
   287  	return artifactory.New(serviceConfig)
   288  }
   289  
   290  func createAccessTokensServiceManager(serviceDetails *ServerDetails) (*access.AccessServicesManager, error) {
   291  	certsPath, err := coreutils.GetJfrogCertsDir()
   292  	if err != nil {
   293  		return nil, err
   294  	}
   295  	accessAuth, err := serviceDetails.CreateAccessAuthConfig()
   296  	if err != nil {
   297  		return nil, err
   298  	}
   299  	serviceConfig, err := config.NewConfigBuilder().
   300  		SetServiceDetails(accessAuth).
   301  		SetCertificatesPath(certsPath).
   302  		SetInsecureTls(serviceDetails.InsecureTls).
   303  		SetDryRun(false).
   304  		Build()
   305  	if err != nil {
   306  		return nil, err
   307  	}
   308  	return access.New(serviceConfig)
   309  }