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