authenticator.go raw

   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