1 package authenticator
2 3 import (
4 "crypto/rand"
5 "errors"
6 "fmt"
7 "io"
8 "net/http"
9 "time"
10 11 "github.com/transip/gotransip/v6/jwt"
12 "github.com/transip/gotransip/v6/rest"
13 )
14 15 const (
16 // this is the header key we will add the signature to
17 signatureHeader = "Signature"
18 // this prefix will be used to name tokens we requested
19 // customers are able to see this in their control panel
20 labelPrefix = "gotransip-client"
21 // authenticationPath is the endpoint that the authenticator
22 // will communicate with
23 authenticationPath = "/auth"
24 // a requested Token expires after a day by default
25 // will be used if Authenticator.TokenExpiration is not set
26 defaultTokenExpiration = "1 day"
27 // DemoToken can be used to test with the api without using your own account
28 DemoToken = `eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImN3MiFSbDU2eDNoUnkjelM4YmdOIn0.` +
29 `eyJpc3MiOiJhcGkudHJhbnNpcC5ubCIsImF1ZCI6ImFwaS50cmFuc2lwLm5sIiwianRpIjoiY3cy` +
30 `IVJsNTZ4M2hSeSN6UzhiZ04iLCJpYXQiOjE1ODIyMDE1NTAsIm5iZiI6MTU4MjIwMTU1MCwiZXhw` +
31 `IjoyMTE4NzQ1NTUwLCJjaWQiOiI2MDQ0OSIsInJvIjpmYWxzZSwiZ2siOmZhbHNlLCJrdiI6dHJ1` +
32 `ZX0.fYBWV4O5WPXxGuWG-vcrFWqmRHBm9yp0PHiYh_oAWxWxCaZX2Rf6WJfc13AxEeZ67-lY0TA2` +
33 `kSaOCp0PggBb_MGj73t4cH8gdwDJzANVxkiPL1Saqiw2NgZ3IHASJnisUWNnZp8HnrhLLe5ficvb` +
34 `1D9WOUOItmFC2ZgfGObNhlL2y-AMNLT4X7oNgrNTGm-mespo0jD_qH9dK5_evSzS3K8o03gu6p19` +
35 `jxfsnIh8TIVRvNdluYC2wo4qDl5EW5BEZ8OSuJ121ncOT1oRpzXB0cVZ9e5_UVAEr9X3f26_Eomg` +
36 `52-PjrgcRJ_jPIUYbrlo06KjjX2h0fzMr21ZE023Gw`
37 )
38 39 var (
40 // ErrTokenExpired will be throwed when the static token that has been set by the client is expired
41 // and we cannot request a new one
42 ErrTokenExpired = errors.New("token expired and no private key is set")
43 )
44 45 // Authenticator is used to store,retrieve and request new tokens on every request.
46 // It checks the expiry date of a Token and if it is expired it will request a new one
47 type Authenticator struct {
48 // this contains a []byte representation of the the private key of the customer
49 // this key will be used to sign a new Token request
50 PrivateKeyBody []byte
51 // this is Token, that is filled with a static Token that a customer provides
52 // or a Token that we got from a Token request
53 Token jwt.Token
54 // this is the http client to do auth requests with
55 HTTPClient *http.Client
56 // this would be the auth path, thus where we will get new tokens from
57 BasePath string
58 // this would be the account name of customer
59 Login string
60 // When this is set to true the requested tokens can only be used with the 'ip' we are requesting with
61 Whitelisted bool
62 // Whether or not we want to request read only Tokens, that can only only be used to retrieve information
63 // not to create, modify or delete it
64 ReadOnly bool
65 // TokenCache is used to retrieve previously acquired tokens and saving new ones
66 // If not set we do not use a cache to store the tokens
67 TokenCache TokenCache
68 // TokenExpiration defines lifetime of generated tokens.
69 // If unspecified, the default is 1 day.
70 // Has no effect for tokens provided via the Token field
71 TokenExpiration time.Duration
72 }
73 74 // AuthRequest will be transformed and send in order to request a new Token
75 // for more information, see: https://api.transip.nl/rest/docs.html#header-authentication
76 type AuthRequest struct {
77 // Account name
78 Login string `json:"login"`
79 // Unique number for this request
80 Nonce string `json:"nonce"`
81 // Custom name to give this Token, you can see your tokens in the transip control panel
82 Label string `json:"label,omitempty"`
83 // Enable read only mode
84 ReadOnly bool `json:"read_only"`
85 // Unix time stamp of when this Token should expire
86 ExpirationTime string `json:"expiration_time"`
87 // Whether this key can be used from everywhere, e.g should not be whitelisted to the current requesting ip
88 GlobalKey bool `json:"global_key"`
89 }
90 91 // GetToken will return the current Token if it is not expired.
92 // If it is expired it will try to request a new Token, set and return that.
93 func (a *Authenticator) GetToken() (jwt.Token, error) {
94 // If token is not set, and we have a token cache,
95 // try to retrieve it from the token cache
96 if a.Token.ExpiryDate == 0 && a.TokenCache != nil {
97 if err := a.retrieveTokenFromCache(); err != nil {
98 return jwt.Token{}, err
99 }
100 }
101 102 if a.Token.Expired() && a.PrivateKeyBody == nil {
103 return jwt.Token{}, ErrTokenExpired
104 }
105 if a.Token.Expired() {
106 var err error
107 a.Token, err = a.requestNewToken()
108 109 if err != nil {
110 return jwt.Token{}, err
111 }
112 113 // if a TokenCache is set we want to write acquired tokens to the cache
114 if a.TokenCache != nil {
115 if err = a.TokenCache.Set(a.getTokenCacheKey(), a.Token); err != nil {
116 return jwt.Token{}, fmt.Errorf("error writing token to cache: %w", err)
117 }
118 }
119 }
120 121 return a.Token, nil
122 }
123 124 // retrieveTokenFromCache gets the token from the cache
125 func (a *Authenticator) retrieveTokenFromCache() error {
126 var err error
127 a.Token, err = a.TokenCache.Get(a.getTokenCacheKey())
128 if err != nil {
129 return fmt.Errorf("error getting token from cache: %w", err)
130 }
131 132 return nil
133 }
134 135 // requestNewToken will request a new Token using the http client
136 // creating a new AuthRequest, converting it to json and sending that to the api auth url
137 // on error it will pass this back
138 func (a *Authenticator) requestNewToken() (jwt.Token, error) {
139 restRequest, err := a.getAuthRequest()
140 if err != nil {
141 return jwt.Token{}, fmt.Errorf("error during auth request creation: %w", err)
142 }
143 144 getMethod := rest.PostMethod
145 146 httpRequest, err := restRequest.GetHTTPRequest(a.BasePath, getMethod.Method)
147 if err != nil {
148 return jwt.Token{}, fmt.Errorf("error constructing token http request: %w", err)
149 }
150 bodyToSign, err := restRequest.GetJSONBody()
151 if err != nil {
152 return jwt.Token{}, fmt.Errorf("error marshalling token request: %w", err)
153 }
154 signature, err := signWithKey(bodyToSign, a.PrivateKeyBody)
155 if err != nil {
156 return jwt.Token{}, err
157 }
158 httpRequest.Header.Add(signatureHeader, signature)
159 160 httpResponse, err := a.HTTPClient.Do(httpRequest)
161 if err != nil {
162 return jwt.Token{}, fmt.Errorf("error requesting token: %w", err)
163 }
164 165 defer httpResponse.Body.Close()
166 167 // read entire response body
168 b, err := io.ReadAll(httpResponse.Body)
169 if err != nil {
170 return jwt.Token{}, fmt.Errorf("error requesting token: %w", err)
171 }
172 173 restResponse := rest.Response{
174 Body: b,
175 StatusCode: httpResponse.StatusCode,
176 Method: getMethod,
177 }
178 179 var tokenToReturn tokenResponse
180 err = restResponse.ParseResponse(&tokenToReturn)
181 if err != nil {
182 return jwt.Token{}, fmt.Errorf("error requesting token: %w", err)
183 }
184 185 return jwt.New(tokenToReturn.Token)
186 }
187 188 // tokenResponse is used to extract a Token from the api server response
189 type tokenResponse struct {
190 Token string `json:"Token"`
191 }
192 193 // getNonce returns a random 16 character length string nonce
194 // each time it is called
195 func (a *Authenticator) getNonce() (string, error) {
196 randomBytes := make([]byte, 8)
197 198 if _, err := rand.Read(randomBytes); err != nil {
199 return "", fmt.Errorf("error when getting random data for new nonce: %w", err)
200 }
201 202 // convert to hex
203 return fmt.Sprintf("%02x", randomBytes), nil
204 }
205 206 // getAuthRequest returns a rest.Request filled with a new AuthRequest
207 func (a *Authenticator) getAuthRequest() (rest.Request, error) {
208 labelPostFix := time.Now().UnixNano()
209 210 nonce, err := a.getNonce()
211 if err != nil {
212 return rest.Request{}, err
213 }
214 215 authRequest := AuthRequest{
216 Login: a.Login,
217 Nonce: nonce,
218 Label: fmt.Sprintf("%s-%d", labelPrefix, labelPostFix),
219 ReadOnly: a.ReadOnly,
220 ExpirationTime: a.getTokenExpirationString(),
221 GlobalKey: !a.Whitelisted,
222 }
223 224 return rest.Request{
225 Endpoint: authenticationPath,
226 Body: authRequest,
227 }, nil
228 }
229 230 // getTokenCacheKey returns a name for the given Login and our authenticator name
231 func (a *Authenticator) getTokenCacheKey() string {
232 return fmt.Sprintf("%s-%s-token", labelPrefix, a.Login)
233 }
234 235 // getTokenExpirationString returns the requested or default expiration in string format for the API
236 func (a *Authenticator) getTokenExpirationString() string {
237 if a.TokenExpiration != time.Duration(0) {
238 return fmt.Sprintf("%0.0f seconds", a.TokenExpiration.Seconds())
239 }
240 return defaultTokenExpiration
241 }
242