github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/client/auth/storage.go (about)

     1  package auth
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path"
    10  	"path/filepath"
    11  	"strings"
    12  
    13  	"github.com/adrg/xdg"
    14  	"github.com/cozy/cozy-stack/client/request"
    15  
    16  	"github.com/nightlyone/lockfile"
    17  )
    18  
    19  // Storage is an interface to specify how to store and load authentication
    20  // states.
    21  type Storage interface {
    22  	Load(domain string) (client *Client, token *AccessToken, err error)
    23  	Save(domain string, client *Client, token *AccessToken) error
    24  }
    25  
    26  // FileStorage implements the Storage interface using a simple file.
    27  type FileStorage struct{}
    28  
    29  type authData struct {
    30  	Client *Client      `json:"client,omitempty"`
    31  	Token  *AccessToken `json:"token,omitempty"`
    32  	Domain string       `json:"domain,omitempty"`
    33  }
    34  
    35  // NewFileStorage creates a new *FileStorage
    36  func NewFileStorage() *FileStorage {
    37  	return &FileStorage{}
    38  }
    39  
    40  // Load reads from the OAuth file and the states stored for the specified domain.
    41  func (s *FileStorage) Load(domain string) (client *Client, token *AccessToken, err error) {
    42  	// First check XDG path. If it's not found fallback to the legacy path for retrocompatibility.
    43  	filename, err := xdg.SearchStateFile(path.Join("cozy-stack", "oauth", domain+".json"))
    44  	if err != nil {
    45  		homeDir, err := os.UserHomeDir()
    46  		if err != nil {
    47  			return nil, nil, err
    48  		}
    49  
    50  		filename = filepath.Join(homeDir, fmt.Sprintf(".cozy-oauth-%s", domain))
    51  	}
    52  
    53  	l, err := newFileLock(filename)
    54  	if err != nil {
    55  		return nil, nil, err
    56  	}
    57  
    58  	if err = l.TryLock(); err != nil {
    59  		return nil, nil, err
    60  	}
    61  
    62  	defer func() {
    63  		if err := l.Unlock(); err != nil {
    64  			fmt.Fprintf(os.Stderr, "Error on unlock: %s", err)
    65  		}
    66  	}()
    67  
    68  	f, err := os.Open(filename)
    69  	if err != nil {
    70  		if os.IsNotExist(err) || errors.Is(err, io.EOF) {
    71  			err = nil
    72  		}
    73  		return nil, nil, err
    74  	}
    75  	defer f.Close()
    76  
    77  	data := &authData{}
    78  	if err = request.ReadJSON(f, data); err != nil {
    79  		fmt.Fprintf(os.Stderr, "Authentication file %s is malformed: %s", filename, err.Error())
    80  		return nil, nil, nil
    81  	}
    82  
    83  	if data.Domain != domain {
    84  		return nil, nil, err
    85  	}
    86  
    87  	return data.Client, data.Token, nil
    88  }
    89  
    90  // Save writes the authentication states to a file for the specified domain.
    91  func (s *FileStorage) Save(domain string, client *Client, token *AccessToken) error {
    92  	filename, err := xdg.StateFile(path.Join("cozy-stack", "oauth", domain+".json"))
    93  	if err != nil {
    94  		return err
    95  	}
    96  
    97  	go func() {
    98  		// Remove the legacy if it exists as a new one have been created into the XDG
    99  		// compliant path.
   100  		homeDir, err := os.UserHomeDir()
   101  		if err != nil {
   102  			return
   103  		}
   104  
   105  		_ = os.Remove(filepath.Join(homeDir, fmt.Sprintf(".cozy-oauth-%s", domain)))
   106  	}()
   107  
   108  	l, err := newFileLock(filename)
   109  	if err != nil {
   110  		return err
   111  	}
   112  
   113  	if err = l.TryLock(); err != nil {
   114  		return err
   115  	}
   116  
   117  	defer func() {
   118  		if err := l.Unlock(); err != nil {
   119  			fmt.Fprintf(os.Stderr, "Error on unlock: %s", err)
   120  		}
   121  	}()
   122  
   123  	f, err := os.OpenFile(filename, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600)
   124  	if err != nil {
   125  		return err
   126  	}
   127  	defer f.Close()
   128  
   129  	data := &authData{
   130  		Client: client,
   131  		Token:  token,
   132  		Domain: domain,
   133  	}
   134  
   135  	return json.NewEncoder(f).Encode(data)
   136  }
   137  
   138  func newFileLock(name string) (lockfile.Lockfile, error) {
   139  	lockName := strings.ReplaceAll(name, "/", "_") + ".lock"
   140  	return lockfile.New(filepath.Join(os.TempDir(), lockName))
   141  }