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  }