Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add api communication #11

Merged
merged 4 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions api/authorization_challenge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package api

import (
"context"
"errors"
"time"
)

// AuthorisationChallengeResponse defines the authorization challenge response
type AuthorisationChallengeResponse struct {
Timestamp time.Time `json:"timestamp"`
Challenge string `json:"challenge"`
}

// AuthorisationChallengeRequest defines the structure of the token session initialization
type AuthorisationChallengeRequest struct {
ContextIdentifier *ContextIdentifier `json:"contextIdentifier"`
}

// ContextIdentifier defines the ContextIdentifier part of the authorization challenge
type ContextIdentifier struct {
Identifier string `json:"identifier"`
Type string `json:"type"`
}

func fetchChallenge(ctx context.Context, c *Client) (*AuthorisationChallengeResponse, error) {
response := &AuthorisationChallengeResponse{}
var errorResponse ErrorResponse

request := &AuthorisationChallengeRequest{
ContextIdentifier: &ContextIdentifier{
Identifier: c.ID,
Type: "onip",
},
}

resp, err := c.Client.R().
SetResult(response).
SetBody(request).
SetContext(ctx).
Post(c.URL + "/api/online/Session/AuthorisationChallenge")
if err != nil {
return nil, err
}
if resp.IsError() {
return nil, errors.New(errorResponse.Exception.ExceptionDetailList[0].ExceptionDescription)
}
return response, nil
}
142 changes: 142 additions & 0 deletions api/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Package api used for communication with the KSeF API
package api

import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"os"

"github.com/go-resty/resty/v2"
)

// ClientOptFunc defines function for customizing the KSeF client
type ClientOptFunc func(*ClientOpts)

// ClientOpts defines the client parameters
type ClientOpts struct {
Client *resty.Client
URL string
ID string
Token string
SessionToken string
SessionReference string
KeyPath string
}

func defaultClientOpts() ClientOpts {
return ClientOpts{
Client: resty.New(),
URL: "https://ksef-test.mf.gov.pl",
torrocus marked this conversation as resolved.
Show resolved Hide resolved
ID: "",
Token: "",
SessionToken: "",
SessionReference: "",
KeyPath: "",
}
}

// Client defines KSeF client
type Client struct {
ClientOpts
}

// WithClient allows to customize the http client used for making the requests
func WithClient(client *resty.Client) ClientOptFunc {
return func(o *ClientOpts) {
o.Client = client
}
}

// WithID allows customizing the Polish tax id number (NIP)
func WithID(id string) ClientOptFunc {
return func(o *ClientOpts) {
o.ID = id
}
}

// WithToken allows customizing the KSeF authorization token
func WithToken(token string) ClientOptFunc {
return func(o *ClientOpts) {
o.Token = token
}
}

// WithKeyPath allows customizing the public key for KSeF API authorization
func WithKeyPath(keyPath string) ClientOptFunc {
return func(o *ClientOpts) {
o.KeyPath = keyPath
}
}

// WithProductionURL sets the client url to KSeF production
func WithProductionURL(o *ClientOpts) {
o.URL = "https://ksef.mf.gov.pl"
}

// WithDemoURL sets the client url to KSeF demo
func WithDemoURL(o *ClientOpts) {
o.URL = "https://ksef-demo.mf.gov.pl"
}

// NewClient returns a KSeF API client
func NewClient(opts ...ClientOptFunc) *Client {
o := defaultClientOpts()
for _, fn := range opts {
fn(&o)
}
o.Client.SetDebug(true)
return &Client{
ClientOpts: o,
}
}

// FetchSessionToken requests new session token
func FetchSessionToken(ctx context.Context, c *Client) error {
challenge, err := fetchChallenge(ctx, c)
if err != nil {
return err
}

encryptedToken, err := encryptToken(c, challenge)
if err != nil {
return fmt.Errorf("cannot encrypt token: %v", err)
}

sessionToken, err := initTokenSession(ctx, c, encryptedToken, challenge.Challenge)
if err != nil {
return fmt.Errorf("cannot init session token: %v", err)
}

c.SessionToken = sessionToken.SessionToken.Token
c.SessionReference = sessionToken.ReferenceNumber

return nil
}

func publicKey(keyPath string) (*rsa.PublicKey, error) {
key, err := os.ReadFile(keyPath)
if err != nil {
return nil, fmt.Errorf("cannot read key file %s: %v", keyPath, err)
}
block, _ := pem.Decode(key)
parsedKey, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("cannot parse public key: %v", err)
}
return parsedKey.(*rsa.PublicKey), nil
}

func encryptToken(c *Client, challenge *AuthorisationChallengeResponse) ([]byte, error) {
rawToken := fmt.Sprintf("%s|%d", c.Token, challenge.Timestamp.UnixMilli())

publicKey, err := publicKey(c.KeyPath)
if err != nil {
return nil, err
}

return rsa.EncryptPKCS1v15(rand.Reader, publicKey, []byte(rawToken))
}
19 changes: 19 additions & 0 deletions api/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package api_test

import (
"testing"

api_test "github.com/invopop/gobl.ksef/api/test"
"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/assert"
)

func TestFetchSessionToken(t *testing.T) {
t.Run("should get session token", func(t *testing.T) {
client, err := api_test.Client()
defer httpmock.DeactivateAndReset()
assert.NoError(t, err)

assert.Equal(t, client.SessionToken, "exampleSessionToken")
})
}
16 changes: 16 additions & 0 deletions api/error_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package api

// ErrorResponse parses error responses
type ErrorResponse struct {
Exception struct {
ServiceCtx string `json:"serviceCtx"`
ServiceCode string `json:"serviceCode"`
ServiceName string `json:"serviceName"`
Timestamp string `json:"timestamp"`
ReferenceNumber string `json:"referenceNumber"`
ExceptionDetailList []struct {
ExceptionCode int `json:"exceptionCode"`
ExceptionDescription string `json:"exceptionDescription"`
} `json:"exceptionDetailList"`
} `json:"exception"`
}
136 changes: 136 additions & 0 deletions api/init_token_session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package api

import (
"context"
"encoding/base64"
"encoding/xml"
"errors"
)

// InitSessionTokenResponse defines the token session initialization response structure
type InitSessionTokenResponse struct {
Timestamp string `json:"timestamp"`
ReferenceNumber string `json:"referenceNumber"`
SessionToken *SessionToken `json:"sessionToken"`
}

// SessionToken defines the session token part of the token session initialization response
type SessionToken struct {
Token string `json:"token"`
Context struct {
ContextIdentifier struct {
Type string `json:"type"`
Identifier string `json:"identifier"`
} `json:"contextIdentifier"`
ContextName struct {
Type string `json:"type"`
TradeName string `json:"tradeName"`
FullName string `json:"fullName"`
} `json:"contextName"`
CredentialsRoleList []struct {
Type string `json:"type"`
RoleType string `json:"roleType"`
RoleDescription string `json:"roleDescription"`
} `json:"credentialsRoleList"`
} `json:"context"`
}

// InitSessionTokenRequest defines the structure of the token session initialization
type InitSessionTokenRequest struct {
Context *InitSessionTokenContext `xml:"ns3:Context"`
XMLName xml.Name
XMLNamespace string `xml:"xmlns,attr"`
XMLNamespace2 string `xml:"xmlns:ns2,attr"`
XMLNamespace3 string `xml:"xmlns:ns3,attr"`
}

// InitSessionTokenContext defines the Context part of the token session initialization
type InitSessionTokenContext struct {
Challenge string `xml:"Challenge"`
Identifier *InitSessionTokenIdentifier `xml:"Identifier"`
DocumentType *InitSessionTokenDocumentType `xml:"DocumentType"`
Token string `xml:"Token"`
}

// InitSessionTokenIdentifier defines the Identifier part of the token session initialization
type InitSessionTokenIdentifier struct {
Identifier string `xml:"ns2:Identifier"`
Type string `xml:"xsi:type,attr"`
Namespace string `xml:"xmlns:xsi,attr"`
}

// InitSessionTokenDocumentType defines the DocumentType part of the token session initialization
type InitSessionTokenDocumentType struct {
Service string `xml:"ns2:Service"`
FormCode *InitSessionTokenFormCode `xml:"ns2:FormCode"`
}

// InitSessionTokenFormCode defines the FormCode part of the token session initialization
type InitSessionTokenFormCode struct {
SystemCode string `xml:"ns2:SystemCode"`
SchemaVersion string `xml:"ns2:SchemaVersion"`
TargetNamespace string `xml:"ns2:TargetNamespace"`
Value string `xml:"ns2:Value"`
}

const (
// XMLNamespace namespace setting for token initialization XML
XMLNamespace = "http://ksef.mf.gov.pl/schema/gtw/svc/online/types/2021/10/01/0001"
// XMLNamespace2 namespace setting for token initialization XML
XMLNamespace2 = "http://ksef.mf.gov.pl/schema/gtw/svc/types/2021/10/01/0001"
// XMLNamespace3 namespace setting for token initialization XML
XMLNamespace3 = "http://ksef.mf.gov.pl/schema/gtw/svc/online/auth/request/2021/10/01/0001"
// XSIType namespace setting for token initialization XML
XSIType = "ns2:SubjectIdentifierByCompanyType"
// XSINamespace namespace setting for token initialization XML
XSINamespace = "http://www.w3.org/2001/XMLSchema-instance"
// RootElementName root element name for token initialization XML
RootElementName = "ns3:InitSessionTokenRequest"
)

func initTokenSession(ctx context.Context, c *Client, token []byte, challenge string) (*InitSessionTokenResponse, error) {
response := &InitSessionTokenResponse{}
var errorResponse ErrorResponse

request := &InitSessionTokenRequest{
XMLNamespace: XMLNamespace,
XMLNamespace2: XMLNamespace2,
XMLNamespace3: XMLNamespace3,
XMLName: xml.Name{Local: RootElementName},
Context: &InitSessionTokenContext{
Identifier: &InitSessionTokenIdentifier{
Namespace: XSINamespace,
Type: XSIType,
Identifier: c.ID,
},
Challenge: challenge,
DocumentType: &InitSessionTokenDocumentType{
Service: "KSeF",
FormCode: &InitSessionTokenFormCode{
SystemCode: "FA (2)",
SchemaVersion: "1-0E",
TargetNamespace: "http://crd.gov.pl/wzor/2023/06/29/12648",
Value: "FA",
},
},
Token: base64.StdEncoding.EncodeToString(token),
},
}
bytes, _ := bytes(*request)

resp, err := c.Client.R().
SetResult(response).
SetError(&errorResponse).
SetBody(bytes).
SetContext(ctx).
SetHeader("Content-Type", "application/octet-stream; charset=utf-8").
Post(c.URL + "/api/online/Session/InitToken")

if err != nil {
return nil, err
}
if resp.IsError() {
return nil, errors.New(errorResponse.Exception.ExceptionDetailList[0].ExceptionDescription)
}
return response, nil
}
Loading
Loading