github.com/google/osv-scalibr@v0.4.1/veles/secrets/gcpoauth2access/validator.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package gcpoauth2access 16 17 import ( 18 "encoding/json" 19 "errors" 20 "fmt" 21 "io" 22 "net/http" 23 "strconv" 24 "time" 25 26 "github.com/google/osv-scalibr/veles" 27 "github.com/google/osv-scalibr/veles/secrets/common/simplevalidate" 28 ) 29 30 const ( 31 // endpoint is the URL of Google's OAuth2 tokeninfo endpoint. 32 // https://developers.google.com/identity/protocols/oauth2 33 endpoint = "https://www.googleapis.com/oauth2/v3/tokeninfo" 34 ) 35 36 // NewValidator creates a new Validator for GCP OAuth2 access tokens. 37 func NewValidator() *simplevalidate.Validator[Token] { 38 return &simplevalidate.Validator[Token]{ 39 EndpointFunc: func(t Token) (string, error) { 40 if t.Token == "" { 41 return "", errors.New("OAuth2 token is empty") 42 } 43 return fmt.Sprintf("%s?access_token=%s", endpoint, t.Token), nil 44 }, 45 HTTPMethod: http.MethodGet, 46 InvalidResponseCodes: []int{http.StatusBadRequest}, 47 StatusFromResponseBody: statusFromResponseBody, 48 HTTPC: &http.Client{Timeout: 10 * time.Second}, 49 } 50 } 51 52 // statusFromResponseBody extracts the validation status from the HTTP response body. 53 // It checks if the token has any scopes and if it's expired based on 54 // 'expires_in' or 'exp' fields from the token info. 55 // The token is considered valid if it contains any scopes and is not expired, 56 // invalid if it has no scopes or is expired, and validation fails if the 57 // expiration status cannot be determined. 58 func statusFromResponseBody(body io.Reader) (veles.ValidationStatus, error) { 59 bodyBytes, err := io.ReadAll(body) 60 if err != nil { 61 return veles.ValidationFailed, fmt.Errorf("failed to read response: %w", err) 62 } 63 64 var tokenInfo response 65 if err := json.Unmarshal(bodyBytes, &tokenInfo); err != nil { 66 return veles.ValidationFailed, fmt.Errorf("failed to parse response: %w", err) 67 } 68 69 // Token is recognized. Check scopes and expiration. 70 if tokenInfo.Scope == "" { 71 // Token does not have access to any scopes. 72 return veles.ValidationInvalid, nil 73 } 74 75 expiresIn, err := strconv.ParseInt(tokenInfo.ExpiresIn, 10, 64) 76 if err == nil { 77 if expiresIn > 0 { 78 return veles.ValidationValid, nil 79 } 80 return veles.ValidationInvalid, nil 81 } 82 83 expiresAt, err := strconv.ParseInt(tokenInfo.Expiry, 10, 64) 84 if err == nil && expiresAt > 0 { 85 expire := time.Unix(expiresAt, 0) 86 if time.Now().Before(expire) { 87 return veles.ValidationValid, nil 88 } 89 return veles.ValidationInvalid, nil 90 } 91 92 // If we can't determine expiration, consider validation failed. 93 return veles.ValidationFailed, errors.New("failed to determine token expiration") 94 } 95 96 // response represents the response from Google's OAuth2 token endpoint. 97 // https://developers.google.com/identity/protocols/oauth2 98 type response struct { 99 // Expiry is the expiration time of the token in Unix time. 100 Expiry string `json:"exp"` 101 // ExpiresIn is the number of seconds until the token expires. 102 ExpiresIn string `json:"expires_in"` 103 // Scope is a space-delimited list that identify the resources that your application could access 104 // https://developers.google.com/identity/protocols/oauth2/scopes 105 Scope string `json:"scope"` 106 }