Skip to content

Commit

Permalink
chore: revert temporary commit
Browse files Browse the repository at this point in the history
  • Loading branch information
RobertCraigie committed Mar 27, 2024
1 parent 46f8947 commit 4acd051
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 0 deletions.
33 changes: 33 additions & 0 deletions accesstoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ package finchgo

import (
"context"
"errors"
"net/http"

"github.com/Finch-API/finch-api-go/internal/apijson"
"github.com/Finch-API/finch-api-go/internal/param"
"github.com/Finch-API/finch-api-go/internal/requestconfig"
"github.com/Finch-API/finch-api-go/option"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)

// AccessTokenService contains methods and other services that help with
Expand All @@ -32,9 +35,39 @@ func NewAccessTokenService(opts ...option.RequestOption) (r *AccessTokenService)
// Exchange the authorization code for an access token
func (r *AccessTokenService) New(ctx context.Context, body AccessTokenNewParams, opts ...option.RequestOption) (res *CreateAccessTokenResponse, err error) {
opts = append(r.Options[:], opts...)

opts = append(opts[:], func(rc *requestconfig.RequestConfig) error {
bodyClientID := gjson.Get(string(rc.Buffer), "client_id")
if !bodyClientID.Exists() {
if rc.ClientID == "" {
return errors.New("client_id must be provided as an argument or with the FINCH_CLIENT_ID environment variable")
}
updatedBody, err := sjson.Set(string(rc.Buffer), "client_id", rc.ClientID)
if err != nil {
return err
}
rc.Buffer = []byte(updatedBody)
}

bodyClientSecret := gjson.Get(string(rc.Buffer), "client_secret")
if !bodyClientSecret.Exists() {
if rc.ClientSecret == "" {
return errors.New("client_secret must be provided as an argument or with the FINCH_CLIENT_SECRET environment variable")
}
updatedBody, err := sjson.Set(string(rc.Buffer), "client_secret", rc.ClientSecret)
if err != nil {
return err
}
rc.Buffer = []byte(updatedBody)
}

return nil
})

path := "auth/token"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
return

}

type CreateAccessTokenResponse struct {
Expand Down
12 changes: 12 additions & 0 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
- <a href="https://pkg.go.dev/github.com/Finch-API/finch-api-go/internal/shared">shared</a>.<a href="https://pkg.go.dev/github.com/Finch-API/finch-api-go/internal/shared#OperationSupportMatrix">OperationSupportMatrix</a>
- <a href="https://pkg.go.dev/github.com/Finch-API/finch-api-go/internal/shared">shared</a>.<a href="https://pkg.go.dev/github.com/Finch-API/finch-api-go/internal/shared#Paging">Paging</a>

# finchgo

Methods:

- <code>client.<a href="https://pkg.go.dev/github.com/Finch-API/finch-api-go#FinchgoService.GetAuthURL">GetAuthURL</a>(products string, redirectUri string, sandbox bool, opts ...option.RequestOption) (string, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code>client.<a href="https://pkg.go.dev/github.com/Finch-API/finch-api-go#FinchgoService.WithAccessToken">WithAccessToken</a>(accessToken string) (Client, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>

# AccessTokens

Response Types:
Expand Down Expand Up @@ -176,6 +183,11 @@ Response Types:
- <a href="https://pkg.go.dev/github.com/Finch-API/finch-api-go">finchgo</a>.<a href="https://pkg.go.dev/github.com/Finch-API/finch-api-go#PaymentEvent">PaymentEvent</a>
- <a href="https://pkg.go.dev/github.com/Finch-API/finch-api-go">finchgo</a>.<a href="https://pkg.go.dev/github.com/Finch-API/finch-api-go#WebhookEvent">WebhookEvent</a>

Methods:

- <code>client.Webhooks.<a href="https://pkg.go.dev/github.com/Finch-API/finch-api-go#WebhookService.Unwrap">Unwrap</a>(payload []byte, headers http.Header, secret string, now time.Time) (WebhookEvent, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code>client.Webhooks.<a href="https://pkg.go.dev/github.com/Finch-API/finch-api-go#WebhookService.VerifySignature">VerifySignature</a>(payload []byte, headers http.Header, secret string, now time.Time) <a href="https://pkg.go.dev/builtin#error">error</a></code>

# RequestForwarding

Response Types:
Expand Down
86 changes: 86 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@
package finchgo

import (
"context"
"encoding/json"
"errors"
"net/http"
"net/url"
"os"
"strconv"

"github.com/Finch-API/finch-api-go/internal/requestconfig"
"github.com/Finch-API/finch-api-go/option"
)

Expand Down Expand Up @@ -60,3 +67,82 @@ func NewClient(opts ...option.RequestOption) (r *Client) {

return
}

// DEPRECATED: use client.accessTokens().create instead.
func (r *Client) GetAccessToken(ctx context.Context, code string, redirectUri string, opts ...option.RequestOption) (res string, err error) {
opts = append(r.Options[:], opts...)
opts = append(opts[:], option.WithHeaderDel("authorization"))

path := "/auth/token"

var result map[string]string
cfg, err := requestconfig.NewRequestConfig(ctx, http.MethodPost, path, nil, &result, opts...)
if err != nil {
return "", err
}
if cfg.ClientID == "" {
return "", errors.New("expected ClientID to be set in order to call GetAccessToken")
}
if cfg.ClientSecret == "" {
return "", errors.New("expected ClientSecret to be set in order to call GetAccessToken")
}

body := struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Code string `json:"code"`
RedirectURI string `json:"redirect_uri"`
}{
ClientID: cfg.ClientID,
ClientSecret: cfg.ClientSecret,
Code: code,
RedirectURI: redirectUri,
}
cfg.Apply(func(rc *requestconfig.RequestConfig) (err error) {
rc.Buffer, err = json.Marshal(body)
rc.Request.Header.Set("Content-Type", "application/json")
return err
})

err = cfg.Execute()
if err != nil {
return "", err
}
accessToken, ok := result["access_token"]
if !ok {
return "", errors.New("access_token not found in response")
}

return accessToken, nil
}

// Returns the authorization URL which can be visited in order to obtain an
// authorization code from Finch. The authorization code can then be exchanged for
// an access token for the Finch API by calling getAccessToken().
func (r *Client) GetAuthURL(products string, redirectUri string, sandbox bool, opts ...option.RequestOption) (res string, err error) {
opts = append(r.Options[:], opts...)
cfg := requestconfig.RequestConfig{}
cfg.Apply(opts...)

if cfg.ClientID == "" {
return "", errors.New("expected the ClientID to be set in order to call GetAuthUrl")
}
u, err := url.Parse("https://connect.tryfinch.com/authorize")
if err != nil {
return "", err
}
q := u.Query()
q.Set("client_id", cfg.ClientID)
q.Set("products", products)
q.Set("redirect_uri", redirectUri)
q.Set("sandbox", strconv.FormatBool(sandbox))
u.RawQuery = q.Encode()
return u.String(), nil
}

// Returns a copy of the current Finch client with the given access token for
// authentication.
func (r *Client) WithAccessToken(accessToken string) (res Client, err error) {
opts := append(r.Options[:], option.WithAccessToken(accessToken))
return Client{Options: opts}, nil
}
29 changes: 29 additions & 0 deletions examples/auth/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package main

import (
"context"
"fmt"

finch "github.com/Finch-API/finch-api-go"
"github.com/Finch-API/finch-api-go/option"
)

func main() {
client := finch.NewClient(option.WithClientID("foo-client-id"), option.WithClientSecret("foo-client-secret"))

url, err := client.GetAuthURL("products", "https://example.com/redirect", false)
if err != nil {
panic(err.Error())
}
fmt.Printf("auth url: %s\n", url)

accessTokenResponse, err := client.AccessTokens.New(context.TODO(), finch.AccessTokenNewParams{
Code: finch.F("my-code"),
RedirectUri: finch.F("https://example.com/redirect"),
})
if err != nil {
panic(err.Error())
}
fmt.Printf("access token: %s\n", accessTokenResponse.AccessToken)

}
12 changes: 12 additions & 0 deletions finchgo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

package finchgo

type GetAccessTokenParams struct {
}

type GetAuthURLParams struct {
}

type WithAccessTokenParams struct {
}
92 changes: 92 additions & 0 deletions webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@
package finchgo

import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"net/http"
"reflect"
"strconv"
"strings"
"time"

"github.com/Finch-API/finch-api-go/internal/apijson"
"github.com/Finch-API/finch-api-go/internal/shared"
Expand All @@ -28,6 +37,83 @@ func NewWebhookService(opts ...option.RequestOption) (r *WebhookService) {
return
}

// Validates that the given payload was sent by Finch and parses the payload.
func (r *WebhookService) Unwrap(payload []byte, headers http.Header, secret string, now time.Time) (res WebhookEvent, err error) {
err = r.VerifySignature(payload, headers, secret, now)
if err != nil {
return nil, err
}

event := WebhookEvent(nil)
err = apijson.UnmarshalRoot(payload, &event)
if err != nil {
return nil, err
}
return event, nil
}

// Validates whether or not the webhook payload was sent by Finch.
//
// An error will be raised if the webhook payload was not sent by Finch.
func (r *WebhookService) VerifySignature(payload []byte, headers http.Header, secret string, now time.Time) (err error) {
parsedSecret, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
return fmt.Errorf("invalid webhook secret: %s", err)
}

id := headers.Get("finch-event-id")
if len(id) == 0 {
return errors.New("could not find finch-event-id header")
}
sign := headers.Values("finch-signature")
if len(sign) == 0 {
return errors.New("could not find finch-signature header")
}
unixtime := headers.Get("finch-timestamp")
if len(unixtime) == 0 {
return errors.New("could not find finch-timestamp header")
}

timestamp, err := strconv.ParseInt(unixtime, 10, 64)
if err != nil {
return fmt.Errorf("invalid timestamp header: %s, %s", unixtime, err)
}

if timestamp < now.Unix()-300 {
return errors.New("webhook timestamp too old")
}
if timestamp > now.Unix()+300 {
return errors.New("webhook timestamp too new")
}

mac := hmac.New(sha256.New, parsedSecret)
mac.Write([]byte(id))
mac.Write([]byte("."))
mac.Write([]byte(unixtime))
mac.Write([]byte("."))
mac.Write(payload)
expected := mac.Sum(nil)

for _, part := range sign {
parts := strings.Split(part, ",")
if len(parts) != 2 {
continue
}
if parts[0] != "v1" {
continue
}
signature, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
continue
}
if hmac.Equal(signature, expected) {
return nil
}
}

return errors.New("None of the given webhook signatures match the expected signature")
}

type AccountUpdateEvent struct {
Data AccountUpdateEventData `json:"data"`
EventType AccountUpdateEventEventType `json:"event_type"`
Expand Down Expand Up @@ -1630,3 +1716,9 @@ func init() {
},
)
}

type WebhookUnwrapParams struct {
}

type WebhookVerifySignatureParams struct {
}
28 changes: 28 additions & 0 deletions webhook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

package finchgo_test

import (
"net/http"
"testing"
"time"

"github.com/Finch-API/finch-api-go"
)

func TestVerifySignature(t *testing.T) {
secret := "5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH"

payload := `{"company_id":"720be419-0293-4d32-a707-32179b0827ab"}`

header := http.Header{}
header.Add("Finch-Event-Id", "msg_2Lh9KRb0pzN4LePd3XiA4v12Axj")
header.Add("finch-timestamp", "1676312382")
header.Add("finch-signature", "v1,m7y0TV2C+hlHxU42wCieApTSTaA8/047OAplBqxIV/s=")

client := finchgo.NewClient()
err := client.Webhooks.VerifySignature([]byte(payload), header, secret, time.Unix(1676312382, 0))
if err != nil {
t.Fatalf("did not expect error %s", err.Error())
}
}

0 comments on commit 4acd051

Please sign in to comment.