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