Auth Middleware

Description

IBEX Hub authentication is based on the double JWT system.

This means that every system based on top of it should call the Refresh Access Token endpoint once any endpoint returns a 401 on a request, and call the Sign in endpoint every time a request to refresh-access-token returns a 401.

Code example

package ibexhubclient

import (
	"bytes"
	"encoding/json"
	"errors"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/hashicorp/go-retryablehttp"
)

type IbexHubClient struct {
	ibexHubUrl      string
	ibexHubUser     string
	ibexHubPassword string
	client          *retryablehttp.Client
	accessToken     string
	refreshToken    string
}

type NewProviderInput struct {
  IbexHubUrl      string
	IbexHubUser     string
	IbexHubPassword string
}

func NewPovider(input *NewProviderInput) (*IbexHubClient, error) {
	client := &IbexHubClient{
		ibexHubUrl:      input.IbexHubUrl,
		ibexHubUser:     input.IbexHubUser,
		ibexHubPassword: input.IbexHubPassword,
		client:          newHttpClient(),
	}

	if err := client.signIn(); err != nil {
		return nil, err
	}

	return client, nil
}

func newHttpClient() *retryablehttp.Client {
	retryClient := retryablehttp.NewClient()
	retryClient.RetryMax = 6
	retryClient.RetryWaitMin = time.Second * 3
	retryClient.RetryWaitMax = time.Second * 10

	return retryClient
}
package ibexhubclient

import (
	"bytes"
	"encoding/json"
	"errors"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/hashicorp/go-retryablehttp"
)

func (client *IbexHubClient) call(method string, url string, body interface{}) (*http.Response, error) {
	var request *retryablehttp.Request
	var err error

	if method == http.MethodGet || method == http.MethodDelete {
		request, err = retryablehttp.NewRequest(method, url, nil)
		if err != nil {
			return nil, err
		}
	} else {
		requestBody, err := marshalBody(body)
		if err != nil {
			return nil, err
		}

		request, err = retryablehttp.NewRequest(method, url, requestBody)
		if err != nil {
			return nil, err
		}
	}

	request.Header.Set("Authorization", client.accessToken)
  
  response, err := client.client.Do(request)
	if err != nil {
		return nil, err
	}
	return client.checkUnAuthorized(request, response)
}

func marshalBody(body interface{}) (*bytes.Buffer, error) {
	var requestBody *bytes.Buffer = nil

	if body != nil {
		marshaledBytes, err := json.Marshal(body)
		if err != nil {
			return nil, err
		}
		requestBody = bytes.NewBuffer(marshaledBytes)
	}

	return requestBody, nil
}

// check unauthorized

func (client *IbexHubClient) checkUnAuthorized(req *retryablehttp.Request, response *http.Response) (*http.Response, error) {
	if response.StatusCode != http.StatusUnauthorized {
		return response, nil
	}

	refreshAccessTokenResponse, err := client.refreshAccessToken()
	if err != nil {
		return nil, err
	} else if refreshAccessTokenResponse.StatusCode == http.StatusUnauthorized {
		if err := client.signIn(); err != nil {
			return nil, err
		}
	}

	req.Header.Set("Authorization", client.accessToken)

	resp, err := client.client.Do(req)
	if err != nil {
		return nil, err
	}

	return resp, nil
}
package ibexhubclient

const (
	accountGroup  = "/account"
)

// Create Account

func (client *IbexHubClient) CreateAccount(name string, currencyID int) (*forms.AccountResponse, error) {
	requestBody := &CreateAccountBody{Name: name, CurrencyID: currencyID}

	response, err := client.call(http.MethodPost, client.ibexHubUrl+accountGroup+"/create", requestBody)
	if err != nil {
		return nil, err
	}

	defer response.Body.Close()

	if response.StatusCode > 299 {
		var errResponse forms.ErrorResponse
		if err := json.NewDecoder(response.Body).Decode(&errResponse); err != nil {
			return nil, err
		}
		return nil, errors.New(errResponse.Error)
	}

	createAccountResponse := &forms.AccountResponse{}
	err = json.NewDecoder(response.Body).Decode(createAccountResponse)

	return createAccountResponse, err
}
package ibexhubclient


type HubError struct {
	ErrorResponse *ErrorResponse
	StatusCode    int
}

type ErrorResponse struct {
	Error string `json:"error"`
}

type CreateAccountBody struct {
	Name       string `json:"name" type:"string" binding:"required" sensitive:"true"`
	CurrencyID int    `json:"currencyId"`
}

type SignUpBody struct {
	Email             string `json:"email" type:"string" binding:"required" sensitive:"true"`
	TemporaryPassword string `json:"temporaryPassword" type:"string" binding:"required" sensitive:"true"`
	NewPassword       string `json:"newPassword" type:"string" binding:"required" sensitive:"true"`
}

type SignInBody struct {
	Email    string `json:"email" type:"string" binding:"required" sensitive:"true"`
	Password string `json:"password" type:"string" binding:"required" sensitive:"true"`
}

type TokensResponse struct {
	AccessToken           string `json:"accessToken" type:"string" sensitive:"true"`
	AccessTokenExpiresAt  int64  `json:"accessTokenExpiresAt" type:"integer"`
	RefreshToken          string `json:"refreshToken" type:"string" sensitive:"true"`
	RefreshTokenExpiresAt int64  `json:"refreshTokenExpiresAt" type:"integer"`
}

type RefreshTokenBody struct {
	RefreshToken string `json:"refreshToken" type:"string" binding:"required" sensitive:"true"`
}

type RefreshAccessTokenResponse struct {
	AccessToken string `json:"accessToken" type:"string" sensitive:"true"`
	ExpiresAt   int64  `json:"expiresAt" type:"integer"`
}
package ibexhubclient

import (
	"encoding/json"
	"errors"
	"net/http"

	"github.com/IBEXDWM/IBEXHub-Client/ibexhubclient/forms"
	"github.com/hashicorp/go-retryablehttp"
)

const (
	authGroup = "/auth"
)

func (client *IbexHubClient) signIn() error {
	requestBody := forms.SignInBody{
		Email:    client.ibexHubUser,
		Password: client.ibexHubPassword,
	}

	response, err := client.call(http.MethodPost, client.ibexHubUrl+authGroup+"/signin", requestBody)
	if err != nil {
		return err
	}

	defer response.Body.Close()

	if response.StatusCode != http.StatusOK {
		errorResponse := &forms.ErrorResponse{}
		if err = json.NewDecoder(response.Body).Decode(errorResponse); err != nil {
			return err
		}
		return errors.New(errorResponse.Error)
	}

	tokensResponse := &TokensResponse{}
	if err = json.NewDecoder(response.Body).Decode(tokensResponse); err != nil {
		return err
	}

	client.accessToken = tokensResponse.AccessToken
	client.refreshToken = tokensResponse.RefreshToken

	return nil
}

// Refresh Access Token

func (client *IbexHubClient) refreshAccessToken() (*http.Response, error) {
	requestBody := forms.RefreshTokenBody{RefreshToken: client.refreshToken}

	bodyMarshaled, err := marshalBody(requestBody)
	if err != nil {
		return nil, err
	}
	response, err := http.Post(client.ibexHubUrl+authGroup+"/refresh-access-token", "application/json", bodyMarshaled)
	if err != nil {
		return nil, err
	}

	defer response.Body.Close()

	refreshTokensResponse := &RefreshAccessTokenResponse{}
	err = json.NewDecoder(response.Body).Decode(refreshTokensResponse)
	if err != nil {
		return nil, err
	}

	client.accessToken = refreshTokensResponse.AccessToken

	return response, nil
}

Usage

package main

func main() {
  config := ibexhubclient.NewProviderInput{
    IbexHubUrl: "",
    IbexHubUser: "",
    IbexHubPassword: "",
  }
  hubClient, err := ibexhubclient.NewProvider()
  if err != nil {
    log.Println(err.Error())
    return
  }
  
  newAccount, err := hubClient.CreateAccount("zozo", 0)
   if err != nil {
    log.Println(err.Error())
    return
  }
  log.Println(newAccount)
}