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 }