Skip to content

Commit

Permalink
Merge pull request #17 from form3tech-oss/michalfranc-retry-on-429-au…
Browse files Browse the repository at this point in the history
…th0-get-user

Add retry on get user when rate limited
michal-franc authored Oct 3, 2019
2 parents 4da9dd4 + f73b24f commit 36b4ee0
Showing 3 changed files with 197 additions and 32 deletions.
78 changes: 72 additions & 6 deletions auth0/auth0_client.go
Original file line number Diff line number Diff line change
@@ -3,19 +3,72 @@ package auth0
import (
"bytes"
"encoding/json"
"errors"
"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)

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, config *Config) (string, error) {
auth0LoginRequest := &LoginRequest{
ClientId: clientId,
ClientSecret: clientSecret,
Audience: config.apiUri,
GrantType: "client_credentials",
}

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)
}

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)
}

// 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
}

type UserRequest struct {
@@ -112,8 +165,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)
@@ -204,7 +260,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)
@@ -288,7 +348,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)
@@ -380,6 +444,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 {
@@ -412,6 +477,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 {
88 changes: 88 additions & 0 deletions auth0/auth0_client_test.go
Original file line number Diff line number Diff line change
@@ -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 TestAccGetUserByIdIsNotRateLimited(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: "[email protected]",
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()
}
63 changes: 37 additions & 26 deletions auth0/provider.go
Original file line number Diff line number Diff line change
@@ -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 {
@@ -54,39 +69,35 @@ 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) {
log.Println("[INFO] Initializing Auth0 client")

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
}

0 comments on commit 36b4ee0

Please sign in to comment.