github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/xsuaa/xsuaa.go (about) 1 package xsuaa 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "net/http" 8 "net/url" 9 "time" 10 11 "github.com/pkg/errors" 12 ) 13 14 const authHeaderKey = "Authorization" 15 const oneHourInSeconds = 3600.0 16 17 // XSUAA contains the fields to authenticate to a xsuaa service instance on BTP to retrieve a access token 18 // It also caches the latest retrieved access token 19 type XSUAA struct { 20 OAuthURL string 21 ClientID string 22 ClientSecret string 23 CachedAuthToken AuthToken 24 } 25 26 // AuthToken provides a structure for the XSUAA auth token to be marshalled into 27 type AuthToken struct { 28 TokenType string `json:"token_type"` 29 AccessToken string `json:"access_token"` 30 ExpiresIn time.Duration `json:"expires_in"` 31 ExpiresAt time.Time 32 } 33 34 // SetAuthHeaderIfNotPresent retrieves a XSUAA bearer token and sets the 'Authorization' header on a given http.Header. 35 // If another 'Authorization' header is already present, no change is done to the given header. 36 func (x *XSUAA) SetAuthHeaderIfNotPresent(header *http.Header) error { 37 if len(header.Get(authHeaderKey)) > 0 { 38 return nil 39 } 40 if len(x.OAuthURL) == 0 || 41 len(x.ClientID) == 0 || 42 len(x.ClientSecret) == 0 { 43 return errors.Errorf("OAuthURL, ClientID and ClientSecret have to be set on the xsuaa instance") 44 } 45 46 secondsOfValidityLeft := x.CachedAuthToken.ExpiresAt.Sub(time.Now()).Seconds() 47 if len(x.CachedAuthToken.AccessToken) == 0 || 48 (secondsOfValidityLeft > 0 && secondsOfValidityLeft < oneHourInSeconds) { 49 token, err := x.GetBearerToken() 50 if err != nil { 51 return err 52 } 53 x.CachedAuthToken = token 54 } 55 header.Add(authHeaderKey, fmt.Sprintf("%s %s", x.CachedAuthToken.TokenType, x.CachedAuthToken.AccessToken)) 56 return nil 57 } 58 59 // GetBearerToken authenticates to and retrieves the auth information from the provided XSUAA oAuth base url. The following path 60 // and query is always used: /oauth/token?grant_type=client_credentials&response_type=token. The gotten JSON string is marshalled 61 // into an AuthToken struct and returned. If no 'access_token' field was present in the JSON response, an error is returned. 62 func (x *XSUAA) GetBearerToken() (authToken AuthToken, err error) { 63 const method = http.MethodGet 64 const urlPathAndQuery = "oauth/token?grant_type=client_credentials&response_type=token" 65 66 oauthBaseURL, err := url.Parse(x.OAuthURL) 67 if err != nil { 68 return 69 } 70 entireURL := fmt.Sprintf("%s://%s/%s", oauthBaseURL.Scheme, oauthBaseURL.Host, urlPathAndQuery) 71 72 httpClient := http.Client{} 73 74 request, err := http.NewRequest(method, entireURL, nil) 75 if err != nil { 76 return 77 } 78 request.Header.Add("Accept", "application/json") 79 request.SetBasicAuth(x.ClientID, x.ClientSecret) 80 81 response, httpErr := httpClient.Do(request) 82 if httpErr != nil { 83 err = errors.Wrapf(httpErr, "fetching an access token failed: HTTP %s request to %s failed", 84 method, entireURL) 85 return 86 } 87 88 bodyText, err := readResponseBody(response) 89 if err != nil { 90 return 91 } 92 93 if response.StatusCode != http.StatusOK { 94 err = errors.Errorf("fetching an access token failed: HTTP %s request to %s failed: "+ 95 "expected response code 200, got '%d', response body: '%s'", 96 method, entireURL, response.StatusCode, bodyText) 97 return 98 } 99 100 parsingErr := json.Unmarshal(bodyText, &authToken) 101 if err != nil { 102 err = errors.Wrapf(parsingErr, "HTTP response body could not be parsed as JSON: %s", bodyText) 103 return 104 } 105 106 if authToken.AccessToken == "" { 107 err = errors.Errorf("expected authToken field 'access_token' in json response: got response body: '%s'", 108 bodyText) 109 return 110 } 111 if authToken.TokenType == "" { 112 authToken.TokenType = "bearer" 113 } 114 if authToken.ExpiresIn > 0 { 115 authToken.ExpiresAt = setExpireTime(time.Now(), authToken.ExpiresIn) 116 } 117 118 return 119 } 120 121 func setExpireTime(now time.Time, secondsValid time.Duration) time.Time { 122 return now.Add(time.Second * secondsValid) 123 } 124 125 func readResponseBody(response *http.Response) ([]byte, error) { 126 if response == nil { 127 return nil, errors.Errorf("did not retrieve an HTTP response") 128 } 129 if response.Body != nil { 130 defer response.Body.Close() 131 } 132 bodyText, readErr := io.ReadAll(response.Body) 133 if readErr != nil { 134 return nil, errors.Wrap(readErr, "HTTP response body could not be read") 135 } 136 return bodyText, nil 137 }