Skip to content

Commit

Permalink
Detect when the token is refreshed
Browse files Browse the repository at this point in the history
- Move all OAuth2 related code to oauth2 subpackage.
- Duplicate part of the golang.org/x/oauth2/internal package for
  implementing a custom TokenSource that calls a custom handler on token
  change/refresh.
- Only provide the TokenSource instead of the oauth2.Config to the Client.
  • Loading branch information
printesoi committed Apr 9, 2024
1 parent c07f4d4 commit 1781233
Show file tree
Hide file tree
Showing 14 changed files with 954 additions and 237 deletions.
41 changes: 29 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,18 @@ This package can be use both for interacting with (calling) the ANAF e-factura
API via the Client object and for generating an Invoice UBL XML.

```go
import "github.com/printesoi/e-factura-go"
import (
"github.com/printesoi/e-factura-go"
efactura_oauth2 "github.com/printesoi/e-factura-go/oauth2"
)
```

Construct the required OAuth2 config needed for the Client:

```go
oauth2Cfg, err := efactura.MakeOAuth2Config(
efactura.OAuth2ConfigCredentials(anafAppClientID, anafApplientSecret),
efactura.OAuth2ConfigRedirectURL(anafAppRedirectURL),
oauth2Cfg, err := efactura_oauth2.MakeConfig(
efactura_oauth2.ConfigCredentials(anafAppClientID, anafApplientSecret),
efactura_oauth2.ConfigRedirectURL(anafAppRedirectURL),
)
if err != nil {
// Handle error
Expand All @@ -69,7 +72,7 @@ to the redirect URL):

```go
// Assuming the oauth2Cfg is built as above
initialToken, err := oauth2Cfg.Exchange(ctx, authorizationCode)
token, err := oauth2Cfg.Exchange(ctx, authorizationCode)
if err != nil {
// Handle error
}
Expand All @@ -81,7 +84,7 @@ will also receive the `state` parameter with `code`.
Parse the initial token from JSON:

```go
initialToken, err := efactura.TokenFromJSON([]byte(tokenJSON))
token, err := efactura.TokenFromJSON([]byte(tokenJSON))
if err != nil {
// Handle error
}
Expand All @@ -93,7 +96,26 @@ Construct a new client:
client, err := efactura.NewClient(
context.Background(),
efactura.ClientOAuth2Config(oauth2Cfg),
efactura.ClientOAuth2InitialToken(initialToken),
efactura.ClientOAuth2TokenSource(efactura_oauth2.TokenSource(token)),
efactura.ClientProductionEnvironment(false), // false for test, true for production mode
)
if err != nil {
// Handle error
}
```

If you want to store the token in a store/db and update it everytime it
refreshes use `efactura_oauth2.TokenSourceWithChangedHandler`:

```go
onTokenChanged := func(ctx context.Context, token *xoauth.Token) error {
fmt.Printf("Token changed...")
return nil
}
client, err := efactura.NewClient(
context.Background(),
efactura.ClientOAuth2Config(oauth2Cfg),
efactura.ClientOAuth2TokenSource(efactura_oauth2.TokenSourceWithChangedHandler(token, onTokenChanged)),
efactura.ClientProductionEnvironment(false), // false for test, true for production mode
)
if err != nil {
Expand Down Expand Up @@ -311,11 +333,6 @@ cannot unmarshal a struct like efactura.Invoice due to namespace prefixes!
XML (maybe checking with the tools provided by mfinante).
- [ ] Godoc and more code examples.
- [ ] Test coverage
- [ ] Support full OAuth2 authentication flow for the client, not just passing
the initial token. This however will be tricky to implement properly since
the OAuth2 app registered in the ANAF developer profile must have a fixed
list of HTTPS redirect URLs and the redirect URL used for creating the OAuth2
config must exactly matche one of the URLs.
## Contributing ##
Expand Down
25 changes: 7 additions & 18 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,7 @@ import (
"net/url"
"strings"

"golang.org/x/oauth2"
)

var (
ErrInvalidClientOAuth2Config = errors.New("Invalid OAuth2Config provided")
ErrInvalidClientOAuth2Token = errors.New("Invalid Auth token provided")
xoauth2 "golang.org/x/oauth2"
)

const (
Expand Down Expand Up @@ -68,10 +63,8 @@ type Client struct {
apiPublicBaseURL *url.URL
userAgent string

oauth2Cfg OAuth2Config
initialToken *oauth2.Token

apiClient *http.Client
tokenSource xoauth2.TokenSource
apiClient *http.Client
}

// NewClient creates a new client using the provided config options.
Expand All @@ -93,18 +86,14 @@ func NewClient(ctx context.Context, opts ...ClientConfigOption) (*Client, error)
apiPublicBaseURL = apiPublicBaseProd
}

if !cfg.OAuth2Config.Valid() {
return nil, ErrInvalidClientOAuth2Config
}
if !cfg.InitialToken.Valid() {
return nil, ErrInvalidClientOAuth2Token
if cfg.TokenSource == nil {
return nil, errors.New("invalid token source for client")
}

client := new(Client)
client.userAgent = defaultUserAgent
client.oauth2Cfg = cfg.OAuth2Config
client.initialToken = cfg.InitialToken
client.apiClient = cfg.OAuth2Config.Client(ctx, cfg.InitialToken)
client.tokenSource = cfg.TokenSource
client.apiClient = xoauth2.NewClient(ctx, client.tokenSource)
if cfg.UserAgent != nil {
client.userAgent = *cfg.UserAgent
}
Expand Down
58 changes: 36 additions & 22 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ import (
"testing"
"time"

"github.com/printesoi/e-factura-go/oauth2"
"github.com/stretchr/testify/assert"
"golang.org/x/oauth2"
xoauth2 "golang.org/x/oauth2"
)

// setupTestEnvOAuth2Config creates a OAuth2Config from the environment.
Expand All @@ -35,7 +36,7 @@ import (
// EFACTURA_TEST_REDIRECT_URL are not set, this method returns an error.
// If skipIfEmptyEnv is set to true and the env vars
// are not set, this method returns a nil config.
func setupTestEnvOAuth2Config(skipIfEmptyEnv bool) (oauth2Cfg *OAuth2Config, err error) {
func setupTestEnvOAuth2Config(skipIfEmptyEnv bool) (oauth2Cfg *oauth2.Config, err error) {
clientID := os.Getenv("EFACTURA_TEST_CLIENT_ID")
clientSecret := os.Getenv("EFACTURA_TEST_CLIENT_SECRET")
if clientID == "" || clientSecret == "" {
Expand All @@ -52,9 +53,9 @@ func setupTestEnvOAuth2Config(skipIfEmptyEnv bool) (oauth2Cfg *OAuth2Config, err
return
}

if cfg, er := MakeOAuth2Config(
OAuth2ConfigCredentials(clientID, clientSecret),
OAuth2ConfigRedirectURL(redirectURL),
if cfg, er := oauth2.MakeConfig(
oauth2.ConfigCredentials(clientID, clientSecret),
oauth2.ConfigRedirectURL(redirectURL),
); er != nil {
err = er
return
Expand All @@ -64,9 +65,13 @@ func setupTestEnvOAuth2Config(skipIfEmptyEnv bool) (oauth2Cfg *OAuth2Config, err
return
}

func getTestCIF() string {
return os.Getenv("EFACTURA_TEST_CIF")
}

// setupRealClient creates a real sandboxed Client (a client that talks to the
// ANAF TEST APIs).
func setupRealClient(skipIfEmptyEnv bool, oauth2Cfg *OAuth2Config) (*Client, error) {
func setupRealClient(skipIfEmptyEnv bool, oauth2Cfg *oauth2.Config) (*Client, error) {
if oauth2Cfg == nil {
cfg, err := setupTestEnvOAuth2Config(skipIfEmptyEnv)
if err != nil {
Expand All @@ -83,23 +88,33 @@ func setupRealClient(skipIfEmptyEnv bool, oauth2Cfg *OAuth2Config) (*Client, err
return nil, errors.New("Invalid initial token json")
}

token, err := TokenFromJSON([]byte(tokenJSON))
token, err := oauth2.TokenFromJSON([]byte(tokenJSON))
if err != nil {
return nil, err
}

client, err := NewClient(
context.Background(),
ClientOAuth2Config(*oauth2Cfg),
ClientOAuth2InitialToken(token),
ClientSandboxEnvironment(true),
sandbox := true
if os.Getenv("EFACTURA_TEST_PRODUCTION") == getTestCIF() {
sandbox = false
}

onTokenChanged := func(ctx context.Context, token *xoauth2.Token) error {
tokenJSON, _ := json.Marshal(token)
fmt.Printf("[E-FACTURA] token changed: %s\n", string(tokenJSON))
return nil
}

ctx := context.Background()
client, err := NewClient(ctx,
ClientOAuth2TokenSource(oauth2Cfg.TokenSourceWithChangedHandler(ctx, token, onTokenChanged)),
ClientSandboxEnvironment(sandbox),
)
return client, err
}

// setupTestOAuth2Config sets up a test HTTP server along with a OAuth2Config
// that is configured to talk to that test server.
func setupTestOAuth2Config(clientID, clientSecret string) (oauth2Cfg OAuth2Config, mux *http.ServeMux, serverURL string, teardown func(), err error) {
func setupTestOAuth2Config(clientID, clientSecret string) (oauth2Cfg oauth2.Config, mux *http.ServeMux, serverURL string, teardown func(), err error) {
// mux is the HTTP request multiplexer used with the test server.
mux = http.NewServeMux()

Expand All @@ -119,13 +134,13 @@ func setupTestOAuth2Config(clientID, clientSecret string) (oauth2Cfg OAuth2Confi
if err != nil {
return
}
oauth2Cfg, err = MakeOAuth2Config(
OAuth2ConfigCredentials(clientID, clientSecret),
OAuth2ConfigRedirectURL(redirectURL),
OAuth2ConfigEndpoint(oauth2.Endpoint{
oauth2Cfg, err = oauth2.MakeConfig(
oauth2.ConfigCredentials(clientID, clientSecret),
oauth2.ConfigRedirectURL(redirectURL),
oauth2.ConfigEndpoint(xoauth2.Endpoint{
AuthURL: authorizeURL,
TokenURL: tokenURL,
AuthStyle: oauth2.AuthStyleInHeader,
AuthStyle: xoauth2.AuthStyleInHeader,
}),
)
if err != nil {
Expand All @@ -141,7 +156,7 @@ func setupTestOAuth2Config(clientID, clientSecret string) (oauth2Cfg OAuth2Confi
// setupTestClient sets up a test HTTP server along with a Client that is
// configured to talk to that test server. Tests should register handlers on
// mux which provide mock responses for the API method being tested.
func setupTestClient(oauth2Cfg OAuth2Config, initialToken *oauth2.Token) (client *Client, mux *http.ServeMux, serverURL string, teardown func(), err error) {
func setupTestClient(token *xoauth2.Token) (client *Client, mux *http.ServeMux, serverURL string, teardown func(), err error) {
// mux is the HTTP request multiplexer used with the test server.
mux = http.NewServeMux()

Expand All @@ -157,8 +172,7 @@ func setupTestClient(oauth2Cfg OAuth2Config, initialToken *oauth2.Token) (client
serverURL = server.URL
client, err = NewClient(
context.Background(),
ClientOAuth2Config(oauth2Cfg),
ClientOAuth2InitialToken(initialToken),
ClientOAuth2TokenSource(xoauth2.StaticTokenSource(token)),
ClientSandboxEnvironment(true),
ClientBaseURL(serverURL+apiBasePathSandbox),
ClientBasePublicURL(serverURL+apiPublicBasePathProd),
Expand Down Expand Up @@ -261,7 +275,7 @@ func TestClientAuth(t *testing.T) {
return
}

client, clientMux, serverURL, clientTeardown, err := setupTestClient(oauth2Cfg, token)
client, clientMux, serverURL, clientTeardown, err := setupTestClient(token)
if clientTeardown != nil {
defer clientTeardown()
}
Expand Down
22 changes: 6 additions & 16 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,15 @@
package efactura

import (
"golang.org/x/oauth2"
xoauth2 "golang.org/x/oauth2"
)

// ClientConfig is the config used to create a Client
type ClientConfig struct {
// OAuth2Config is the OAuth2 config used for creating the http.Client that
// autorefreshes the Token.
OAuth2Config OAuth2Config
// Token is the starting oauth2 Token (including the refresh token).
// TokenSource is the token source used for generating OAuth2 tokens.
// Until this library will support authentication with the SPV certificate,
// this must always be provided.
InitialToken *oauth2.Token
TokenSource xoauth2.TokenSource
// Unless BaseURL is set, Sandbox controlls whether to use production
// endpoints (if set to false) or test endpoints (if set to true).
Sandbox bool
Expand All @@ -47,17 +44,10 @@ type ClientConfig struct {
// ClientConfigOption allows gradually modifying a ClientConfig
type ClientConfigOption func(*ClientConfig)

// ClientOAuth2Config sets the OAuth2 config
func ClientOAuth2Config(oauth2Cfg OAuth2Config) ClientConfigOption {
// ClientOAuth2TokenSource sets the token source to use.
func ClientOAuth2TokenSource(tokenSource xoauth2.TokenSource) ClientConfigOption {
return func(c *ClientConfig) {
c.OAuth2Config = oauth2Cfg
}
}

// ClientOAuth2InitialToken sets the initial OAuth2 Token
func ClientOAuth2InitialToken(token *oauth2.Token) ClientConfigOption {
return func(c *ClientConfig) {
c.InitialToken = token
c.TokenSource = tokenSource
}
}

Expand Down
Loading

0 comments on commit 1781233

Please sign in to comment.