From 04759628f99519a44ae35c2e35f1147803c03850 Mon Sep 17 00:00:00 2001 From: Michal Franc Date: Mon, 30 Sep 2019 12:04:15 +0100 Subject: [PATCH 1/3] Retry on get user with acceptance test --- auth0/auth0_client.go | 47 ++++++++++++++++++-- auth0/auth0_client_test.go | 88 ++++++++++++++++++++++++++++++++++++++ auth0/provider.go | 57 +++++++++++++----------- 3 files changed, 163 insertions(+), 29 deletions(-) create mode 100644 auth0/auth0_client_test.go diff --git a/auth0/auth0_client.go b/auth0/auth0_client.go index 54dff5d4..7a4c64f2 100644 --- a/auth0/auth0_client.go +++ b/auth0/auth0_client.go @@ -4,18 +4,54 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/parnurzeal/gorequest" + "net/http" ) type AuthClient struct { config *Config } -func NewClient(config *Config) *AuthClient { +func NewClient(clientId string, clientSecret string, config *Config) (*AuthClient, error) { + + token, err := getToken(clientId, clientSecret, config.domain, config.apiUri) + + if err != nil { + return nil, fmt.Errorf("auth0 provider init failed, error: %v", err) + } + + config.accessToken = token + return &AuthClient{ config: config, + }, nil +} + +func getToken(clientId string, clientSecret string, domain string, apiUri string) (string, error) { + auth0LoginRequest := &LoginRequest{ + ClientId: clientId, + ClientSecret: clientSecret, + Audience: apiUri, + GrantType: "client_credentials", } + + res, body, errs := gorequest.New().Post("https://" + domain + "/oauth/token").Send(auth0LoginRequest).End() + + if errs != nil { + return "", fmt.Errorf("could not log in to auth0, error: %v", errs) + } + + if res.StatusCode != 200 { + return "", fmt.Errorf("unsuccesfull token acquisition expected 200 status code got: %v", res.StatusCode) + } + + loginResponse := &LoginResponse{} + err := json.Unmarshal([]byte(body), loginResponse) + if err != nil { + return "", fmt.Errorf("could not parse auth0 login response, error: %v %s", err, body) + } + + return loginResponse.AccessToken, nil } type UserRequest struct { @@ -112,8 +148,11 @@ func (config *Config) getAuthenticationHeader() string { // User func (authClient *AuthClient) GetUserById(id string) (*User, error) { - - resp, body, errs := gorequest.New().Get(authClient.config.apiUri+"users/"+id).Set("Authorization", authClient.config.getAuthenticationHeader()).End() + resp, body, errs := gorequest.New(). + Get(authClient.config.apiUri+"users/"+id). + Set("Authorization", authClient.config.getAuthenticationHeader()). + Retry(authClient.config.maxRetryCount, authClient.config.timeBetweenRetries, http.StatusTooManyRequests). + End() if resp.StatusCode >= 400 && resp.StatusCode != 404 { return nil, fmt.Errorf("bad status code (%d): %s", resp.StatusCode, body) diff --git a/auth0/auth0_client_test.go b/auth0/auth0_client_test.go new file mode 100644 index 00000000..ef12bbe2 --- /dev/null +++ b/auth0/auth0_client_test.go @@ -0,0 +1,88 @@ +package auth0 + +import ( + "github.com/google/uuid" + "os" + "sync" + "testing" + "time" +) + +// 1. Create Token +// 2. Create new user +// 3. Simulate throttled load to GetUserById +// 4. Clean up the created user +func TestAccGetUserByIdIsNotThrottled(t *testing.T) { + auth0RetryCount := 2 + timeBeetwenRetries := time.Second + numberOfRequests := 100 + numberOfGoRoutines := 10 + + domain := os.Getenv("AUTH0_DOMAIN") + if domain == "" { + t.Fatal("AUTH0_DOMAIN must be set for acceptance tests") + } + + clientId := os.Getenv("AUTH0_CLIENT_ID") + if clientId == "" { + t.Fatal("AUTH0_CLIENT_ID must be set for acceptance tests") + } + + clientSecret := os.Getenv("AUTH0_CLIENT_SECRET") + if clientSecret == "" { + t.Fatal("AUTH0_CLIENT_SECRET must be set for acceptance tests") + } + + apiUri := "https://" + domain + "/api/v2/" + + config := &Config{ + domain: domain, + apiUri: apiUri, + maxRetryCount: auth0RetryCount, + timeBetweenRetries: timeBeetwenRetries, + } + + client, err := NewClient(clientId, clientSecret, config) + if err != nil { + t.Fatalf("auth0 test cliend creation failure %v", err) + } + + userRequest := &UserRequest{ + Connection: "Username-Password-Authentication", + Email: "auth0-provider-test@auth0-provider-test.com", + Name: "auth0-provider-test", + Password: uuid.New().String(), + UserMetaData: nil, + EmailVerified: false, + } + + createdUser, err := client.CreateUser(userRequest) + + if err != nil { + t.Fatalf("failed to create test user %v", err) + } + + defer func() { + err := client.DeleteUserById(createdUser.UserId) + if err != nil { + t.Fatalf("Dangling resource! Failed to remove test user with UserId '%v' with error message: %v", createdUser.UserId, err) + } + }() + + var done sync.WaitGroup + + for i := 0; i < numberOfGoRoutines; i++ { + done.Add(1) + go func() { + defer done.Done() + for i := 1; i <= numberOfRequests; i++ { + _, err := client.GetUserById(createdUser.UserId) + if err != nil { + t.Fatalf("failed to get user %v", err) + } + } + }() + } + + done.Wait() +} diff --git a/auth0/provider.go b/auth0/provider.go index 2a3ed82a..a358099b 100644 --- a/auth0/provider.go +++ b/auth0/provider.go @@ -1,12 +1,11 @@ package auth0 import ( - "encoding/json" "fmt" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/terraform" - "github.com/parnurzeal/gorequest" "log" + "time" ) func Provider() terraform.ResourceProvider { @@ -27,6 +26,20 @@ func Provider() terraform.ResourceProvider { Required: true, DefaultFunc: schema.EnvDefaultFunc("AUTH0_CLIENT_SECRET", nil), }, + "auth0_request_max_retry_count": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 2, + Description: "Max retry on requests to Auth0", + }, + "auth0_time_between_retries": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + //second, cannot use time time.Second here as under the hood provider framework users cty.Value dynamic types + //time.Second is translated to 1000000000 (Nanoseconds) which ends up with error panic: can't convert 1000000000 to cty.Value + Default: 1000, + Description: "Time to wait between retried requests to Auth0 (in milliseconds)", + }, }, ResourcesMap: map[string]*schema.Resource{ @@ -41,9 +54,11 @@ func Provider() terraform.ResourceProvider { } type Config struct { - domain string - accessToken string - apiUri string + domain string + accessToken string + apiUri string + maxRetryCount int + timeBetweenRetries time.Duration } type LoginRequest struct { @@ -62,31 +77,23 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { domain := d.Get("domain").(string) apiUri := "https://" + domain + "/api/v2/" + clientId := d.Get("auth0_client_id").(string) + clientSecret := d.Get("auth0_client_secret").(string) + maxRetryCount := d.Get("auth0_request_max_retry_count").(int) + timeBetweenRetries := d.Get("auth0_request_max_retry_count").(int) - auth0LoginRequest := &LoginRequest{ - ClientId: d.Get("auth0_client_id").(string), - ClientSecret: d.Get("auth0_client_secret").(string), - Audience: apiUri, - GrantType: "client_credentials", + config := &Config{ + domain: domain, + apiUri: apiUri, + maxRetryCount: maxRetryCount, + timeBetweenRetries: time.Duration(timeBetweenRetries) * time.Millisecond, } - _, body, errs := gorequest.New().Post("https://" + domain + "/oauth/token").Send(auth0LoginRequest).End() - - if errs != nil { - return nil, fmt.Errorf("could log in to auth0, error: %v", errs) - } + client, err := NewClient(clientId, clientSecret, config) - loginResponse := &LoginResponse{} - err := json.Unmarshal([]byte(body), loginResponse) if err != nil { - return nil, fmt.Errorf("could not parse auth0 login response, error: %v %s", err, body) - } - - config := &Config{ - domain: domain, - accessToken: loginResponse.AccessToken, - apiUri: apiUri, + return nil, fmt.Errorf("auth0 provider configuration failure, error: %v", err) } - return NewClient(config), nil + return client, nil } From 04504b2ad96f4d848091a6cc3d17b1e11a91816b Mon Sep 17 00:00:00 2001 From: Michal Franc Date: Wed, 2 Oct 2019 15:44:16 +0100 Subject: [PATCH 2/3] Added retry on the rest of Get calls --- auth0/auth0_client.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/auth0/auth0_client.go b/auth0/auth0_client.go index 7a4c64f2..95e9d3b2 100644 --- a/auth0/auth0_client.go +++ b/auth0/auth0_client.go @@ -243,7 +243,11 @@ func (authClient *AuthClient) DeleteUserById(id string) error { // Client func (authClient *AuthClient) GetClientById(id string) (*Client, error) { - resp, body, errs := gorequest.New().Get(authClient.config.apiUri+"clients/"+id).Set("Authorization", authClient.config.getAuthenticationHeader()).End() + resp, body, errs := gorequest.New(). + Get(authClient.config.apiUri+"clients/"+id). + Set("Authorization", authClient.config.getAuthenticationHeader()). + Retry(authClient.config.maxRetryCount, authClient.config.timeBetweenRetries, http.StatusTooManyRequests). + End() if resp.StatusCode >= 400 && resp.StatusCode != 404 { return nil, fmt.Errorf("bad status code (%d): %s", resp.StatusCode, body) @@ -327,7 +331,11 @@ func (authClient *AuthClient) DeleteClientById(id string) error { // Api func (authClient *AuthClient) GetApiById(id string) (*Api, error) { - resp, body, errs := gorequest.New().Get(authClient.config.apiUri+"resource-servers/"+id).Set("Authorization", authClient.config.getAuthenticationHeader()).End() + resp, body, errs := gorequest.New(). + Get(authClient.config.apiUri+"resource-servers/"+id). + Set("Authorization", authClient.config.getAuthenticationHeader()). + Retry(authClient.config.maxRetryCount, authClient.config.timeBetweenRetries, http.StatusTooManyRequests). + End() if resp.StatusCode >= 400 && resp.StatusCode != 404 { return nil, fmt.Errorf("bad status code (%d): %s", resp.StatusCode, body) @@ -419,6 +427,7 @@ func (authClient *AuthClient) GetClientGrantById(id string) (*ClientGrant, error _, body, errs := gorequest.New(). Get(authClient.config.apiUri+"client-grants"). Set("Authorization", authClient.config.getAuthenticationHeader()). + Retry(authClient.config.maxRetryCount, authClient.config.timeBetweenRetries, http.StatusTooManyRequests). End() if errs != nil { @@ -451,6 +460,7 @@ func (authClient *AuthClient) GetClientGrantByClientIdAndAudience(clientId strin resp, body, errs := gorequest.New(). Get(authClient.config.apiUri+"client-grants"). Query(queryParams).Set("Authorization", authClient.config.getAuthenticationHeader()). + Retry(authClient.config.maxRetryCount, authClient.config.timeBetweenRetries, http.StatusTooManyRequests). End() if resp.StatusCode >= 400 && resp.StatusCode != 404 { From f73b24fd1dc8a7770aaf1df3273ce2a17cd2de4e Mon Sep 17 00:00:00 2001 From: Michal Franc Date: Wed, 2 Oct 2019 17:22:47 +0100 Subject: [PATCH 3/3] retry and better error handling on token creation This will enable us to see more descrtiptive errors from Auth0 --- auth0/auth0_client.go | 25 +++++++++++++++++++++---- auth0/auth0_client_test.go | 2 +- auth0/provider.go | 6 +++++- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/auth0/auth0_client.go b/auth0/auth0_client.go index 95e9d3b2..e9ffb29d 100644 --- a/auth0/auth0_client.go +++ b/auth0/auth0_client.go @@ -3,6 +3,7 @@ package auth0 import ( "bytes" "encoding/json" + "errors" "fmt" "github.com/parnurzeal/gorequest" "net/http" @@ -14,7 +15,7 @@ type AuthClient struct { func NewClient(clientId string, clientSecret string, config *Config) (*AuthClient, error) { - token, err := getToken(clientId, clientSecret, config.domain, config.apiUri) + token, err := getToken(clientId, clientSecret, config) if err != nil { return nil, fmt.Errorf("auth0 provider init failed, error: %v", err) @@ -27,15 +28,19 @@ func NewClient(clientId string, clientSecret string, config *Config) (*AuthClien }, nil } -func getToken(clientId string, clientSecret string, domain string, apiUri string) (string, error) { +func getToken(clientId string, clientSecret string, config *Config) (string, error) { auth0LoginRequest := &LoginRequest{ ClientId: clientId, ClientSecret: clientSecret, - Audience: apiUri, + Audience: config.apiUri, GrantType: "client_credentials", } - res, body, errs := gorequest.New().Post("https://" + domain + "/oauth/token").Send(auth0LoginRequest).End() + res, body, errs := gorequest.New(). + Post("https://"+config.domain+"/oauth/token"). + Send(auth0LoginRequest). + Retry(config.maxRetryCount, config.timeBetweenRetries, http.StatusTooManyRequests). + End() if errs != nil { return "", fmt.Errorf("could not log in to auth0, error: %v", errs) @@ -51,6 +56,18 @@ func getToken(clientId string, clientSecret string, domain string, apiUri string return "", fmt.Errorf("could not parse auth0 login response, error: %v %s", err, body) } + // Check for Auth0 errors + if loginResponse.Error != "" { + err := fmt.Sprintf("Status: %d, Error: %s", res.StatusCode, loginResponse.Error) + if loginResponse.ErrorDescription != "" { + err += fmt.Sprintf(", Description: %s", loginResponse.ErrorDescription) + } + + err += fmt.Sprintf("\nResponse Body: %s", body) + + return "", errors.New(err) + } + return loginResponse.AccessToken, nil } diff --git a/auth0/auth0_client_test.go b/auth0/auth0_client_test.go index ef12bbe2..443be121 100644 --- a/auth0/auth0_client_test.go +++ b/auth0/auth0_client_test.go @@ -12,7 +12,7 @@ import ( // 2. Create new user // 3. Simulate throttled load to GetUserById // 4. Clean up the created user -func TestAccGetUserByIdIsNotThrottled(t *testing.T) { +func TestAccGetUserByIdIsNotRateLimited(t *testing.T) { auth0RetryCount := 2 timeBeetwenRetries := time.Second numberOfRequests := 100 diff --git a/auth0/provider.go b/auth0/provider.go index a358099b..b3e71ea6 100644 --- a/auth0/provider.go +++ b/auth0/provider.go @@ -69,7 +69,11 @@ type LoginRequest struct { } type LoginResponse struct { - AccessToken string `json:"access_token"` + AccessToken string `json:"access_token"` + IdTokens string `json:"id_token"` + TokenType string `json:"token_type"` + Error string `json:"error"` + ErrorDescription string `json:"description"` } func providerConfigure(d *schema.ResourceData) (interface{}, error) {