github.com/htcondor/osdf-client/v6@v6.13.0-rc1.0.20231009141709-766e7b4d1dc8/acquire_token.go (about) 1 package stashcp 2 3 import ( 4 "context" 5 "fmt" 6 "net/url" 7 "path" 8 "strings" 9 "time" 10 11 config "github.com/htcondor/osdf-client/v6/config" 12 namespaces "github.com/htcondor/osdf-client/v6/namespaces" 13 log "github.com/sirupsen/logrus" 14 jwt "github.com/golang-jwt/jwt" 15 oauth2 "github.com/htcondor/osdf-client/v6/oauth2" 16 oauth2_upstream "golang.org/x/oauth2" 17 ) 18 19 func TokenIsAcceptable(jwtSerialized string, osdfPath string, namespace namespaces.Namespace, isWrite bool) bool { 20 parser := jwt.Parser{SkipClaimsValidation: true} 21 token, _, err := parser.ParseUnverified(jwtSerialized, &jwt.MapClaims{}) 22 if err != nil { 23 log.Warningln("Failed to parse token:", err) 24 return false 25 } 26 27 // For now, we'll accept any WLCG token 28 wlcg_ver := (*token.Claims.(*jwt.MapClaims))["wlcg.ver"] 29 if wlcg_ver == nil { 30 return false 31 } 32 33 osdfPathCleaned := path.Clean(osdfPath) 34 if !strings.HasPrefix(osdfPathCleaned, namespace.Path) { 35 return false 36 } 37 38 // For some issuers, the token base path is distinct from the OSDF base path. 39 // Example: 40 // - Issuer base path: `/chtc` 41 // - Namespace path: `/chtc/PROTECTED` 42 // In this case, we want to strip out the issuer base path, not the 43 // namespace one, in order to see if the token has the right privs. 44 45 targetResource := path.Clean("/" + osdfPathCleaned[len(namespace.Path):]) 46 if namespace.CredentialGen != nil && namespace.CredentialGen.BasePath != nil && len(*namespace.CredentialGen.BasePath) > 0 { 47 targetResource = path.Clean("/" + osdfPathCleaned[len(*namespace.CredentialGen.BasePath):]) 48 } 49 50 scopes_iface := (*token.Claims.(*jwt.MapClaims))["scope"] 51 if scopes, ok := scopes_iface.(string); ok { 52 acceptableScope := false 53 for _, scope := range strings.Split(scopes, " ") { 54 scope_info := strings.Split(scope, ":") 55 scopeOK := false 56 if isWrite && (scope_info[0] == "storage.modify" || scope_info[0] == "storage.create") { 57 scopeOK = true 58 } else if scope_info[0] == "storage.read" { 59 scopeOK = true 60 } 61 if !scopeOK { 62 continue 63 } 64 65 if len(scope_info) == 1 { 66 acceptableScope = true 67 break 68 } 69 if strings.HasPrefix(targetResource, scope_info[1]) { 70 acceptableScope = true 71 break 72 } 73 } 74 if acceptableScope { 75 return true 76 } 77 } 78 return false 79 } 80 81 func TokenIsExpired(jwtSerialized string) bool { 82 parser := jwt.Parser{SkipClaimsValidation: true} 83 token, _, err := parser.ParseUnverified(jwtSerialized, &jwt.StandardClaims{}) 84 if err != nil { 85 log.Warningln("Failed to parse token:", err) 86 return true 87 } 88 89 if claims, ok := token.Claims.(*jwt.StandardClaims); ok { 90 return claims.Valid() != nil 91 } 92 return true 93 } 94 95 func RegisterClient(namespace namespaces.Namespace) (*config.PrefixEntry, error) { 96 issuer, err := oauth2.GetIssuerMetadata(*namespace.CredentialGen.Issuer) 97 if err != nil { 98 return nil, err 99 } 100 if issuer.RegistrationURL == "" { 101 return nil, fmt.Errorf("Issuer %s does not support dynamic client registration", *namespace.CredentialGen.Issuer) 102 } 103 104 drcp := oauth2.DCRPConfig{ClientRegistrationEndpointURL: issuer.RegistrationURL, Metadata: oauth2.Metadata{ 105 RedirectURIs: []string{"https://localhost/osdf-client"}, 106 TokenEndpointAuthMethod: "client_secret_basic", 107 GrantTypes: []string{"refresh_token", "urn:ietf:params:oauth:grant-type:device_code"}, 108 ResponseTypes: []string{"code"}, 109 ClientName: "OSDF Command Line Client", 110 Scopes: []string{"offline_access", "wlcg", "storage.read:/", "storage.modify:/", "storage.create:/"}, 111 }} 112 113 resp, err := drcp.Register() 114 if err != nil { 115 return nil, err 116 } 117 newEntry := config.PrefixEntry{ 118 Prefix: namespace.Path, 119 ClientID: resp.ClientID, 120 ClientSecret: resp.ClientSecret, 121 } 122 return &newEntry, nil 123 } 124 125 // Given a URL and a piece of the namespace, attempt to acquire a valid 126 // token for that URL. 127 func AcquireToken(destination *url.URL, namespace namespaces.Namespace, isWrite bool) (string, error) { 128 log.Debugln("Acquiring a token from configuration and OAuth2") 129 130 if namespace.CredentialGen == nil || namespace.CredentialGen.Strategy == nil { 131 return "", fmt.Errorf("Credential generation scheme unknown for prefix %s", namespace.Path) 132 } 133 switch strategy := *namespace.CredentialGen.Strategy; strategy { 134 case "OAuth2": 135 case "Vault": 136 return "", fmt.Errorf("Vault credential generation strategy is not supported") 137 default: 138 return "", fmt.Errorf("Unknown credential generation strategy (%s) for prefix %s", 139 strategy, namespace.Path) 140 } 141 issuer := *namespace.CredentialGen.Issuer 142 if len(issuer) == 0 { 143 return "", fmt.Errorf("Issuer for prefix %s is unknown", namespace.Path) 144 } 145 146 osdfConfig, err := config.GetConfigContents() 147 if err != nil { 148 return "", err 149 } 150 151 prefixIdx := -1 152 for idx, entry := range osdfConfig.OSDF.OauthClient { 153 if entry.Prefix == namespace.Path { 154 prefixIdx = idx 155 break 156 } 157 } 158 var prefixEntry *config.PrefixEntry 159 newEntry := false 160 if prefixIdx < 0 { 161 log.Infof("Prefix configuration for %s not in configuration file; will request new client", namespace.Path) 162 prefixEntry, err = RegisterClient(namespace) 163 if err != nil { 164 return "", err 165 } 166 osdfConfig.OSDF.OauthClient = append(osdfConfig.OSDF.OauthClient, *prefixEntry) 167 prefixEntry = &osdfConfig.OSDF.OauthClient[len(osdfConfig.OSDF.OauthClient) - 1] 168 newEntry = true 169 } else { 170 prefixEntry = &osdfConfig.OSDF.OauthClient[prefixIdx] 171 if len(prefixEntry.ClientID) == 0 || len(prefixEntry.ClientSecret) == 0 { 172 log.Infof("Prefix configuration for %s missing OAuth2 client information", namespace.Path) 173 prefixEntry, err = RegisterClient(namespace) 174 if err != nil { 175 return "", err 176 } 177 osdfConfig.OSDF.OauthClient[prefixIdx] = *prefixEntry 178 newEntry = true 179 } 180 } 181 if newEntry { 182 if err = config.SaveConfigContents(&osdfConfig); err != nil { 183 log.Warningln("Failed to save new token to configuration file:", err) 184 } 185 } 186 187 // For now, a fairly useless token-selection algorithm - take the first in the list. 188 // In the future, we should: 189 // - Check scopes 190 var acceptableToken *config.TokenEntry = nil 191 acceptableUnexpiredToken := "" 192 for idx, token := range prefixEntry.Tokens { 193 if !TokenIsAcceptable(token.AccessToken, destination.Path, namespace, isWrite) { 194 continue 195 } 196 if acceptableToken == nil { 197 acceptableToken = &prefixEntry.Tokens[idx] 198 } else if acceptableUnexpiredToken != "" { 199 // Both tokens are non-empty; let's use them 200 break 201 } 202 if !TokenIsExpired(token.AccessToken) { 203 acceptableUnexpiredToken = token.AccessToken 204 } 205 } 206 if len(acceptableUnexpiredToken) > 0 { 207 log.Debugln("Returning an unexpired token from cache") 208 return acceptableUnexpiredToken, nil 209 } 210 211 if acceptableToken != nil && len(acceptableToken.RefreshToken) > 0 { 212 213 // We have a reasonable token; let's try refreshing it. 214 upstreamToken := oauth2_upstream.Token{ 215 AccessToken: acceptableToken.AccessToken, 216 RefreshToken: acceptableToken.RefreshToken, 217 Expiry: time.Unix(0, 0), 218 } 219 issuerInfo, err := oauth2.GetIssuerMetadata(issuer) 220 if err == nil { 221 upstreamConfig := oauth2_upstream.Config{ 222 ClientID: prefixEntry.ClientID, 223 ClientSecret: prefixEntry.ClientSecret, 224 Endpoint: oauth2_upstream.Endpoint{ 225 AuthURL: issuerInfo.AuthURL, 226 TokenURL: issuerInfo.TokenURL, 227 }} 228 ctx := context.Background() 229 source := upstreamConfig.TokenSource(ctx, &upstreamToken) 230 newToken, err := source.Token() 231 if err != nil { 232 log.Warningln("Failed to renew an expired token:", err) 233 } else { 234 acceptableToken.AccessToken = newToken.AccessToken 235 acceptableToken.Expiration = newToken.Expiry.Unix() 236 if len(newToken.RefreshToken) != 0 { 237 acceptableToken.RefreshToken = newToken.RefreshToken 238 } 239 if err = config.SaveConfigContents(&osdfConfig); err != nil { 240 log.Warningln("Failed to save new token to configuration file:", err) 241 } 242 return newToken.AccessToken, nil 243 } 244 } 245 } 246 247 token, err := oauth2.AcquireToken(issuer, prefixEntry, namespace.CredentialGen, destination.Path, isWrite) 248 if err != nil { 249 return "", err 250 } 251 252 Tokens := &prefixEntry.Tokens 253 *Tokens = append(*Tokens, *token) 254 255 if err = config.SaveConfigContents(&osdfConfig); err != nil { 256 log.Warningln("Failed to save new token to configuration file:", err) 257 } 258 259 return token.AccessToken, nil 260 } 261