github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/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  
    76  	data := &authData{}
    77  	if err = request.ReadJSON(f, data); err != nil {
    78  		fmt.Fprintf(os.Stderr, "Authentication file %s is malformed: %s", filename, err.Error())
    79  		return nil, nil, nil
    80  	}
    81  
    82  	if data.Domain != domain {
    83  		return nil, nil, err
    84  	}
    85  
    86  	return data.Client, data.Token, nil
    87  }
    88  
    89  // Save writes the authentication states to a file for the specified domain.
    90  func (s *FileStorage) Save(domain string, client *Client, token *AccessToken) error {
    91  	filename, err := xdg.StateFile(path.Join("cozy-stack", "oauth", domain+".json"))
    92  	if err != nil {
    93  		return err
    94  	}
    95  
    96  	go func() {
    97  		// Remove the legacy if it exists as a new one have been created into the XDG
    98  		// compliant path.
    99  		homeDir, err := os.UserHomeDir()
   100  		if err != nil {
   101  			return
   102  		}
   103  
   104  		_ = os.Remove(filepath.Join(homeDir, fmt.Sprintf(".cozy-oauth-%s", domain)))
   105  	}()
   106  
   107  	l, err := newFileLock(filename)
   108  	if err != nil {
   109  		return err
   110  	}
   111  
   112  	if err = l.TryLock(); err != nil {
   113  		return err
   114  	}
   115  
   116  	defer func() {
   117  		if err := l.Unlock(); err != nil {
   118  			fmt.Fprintf(os.Stderr, "Error on unlock: %s", err)
   119  		}
   120  	}()
   121  
   122  	f, err := os.OpenFile(filename, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600)
   123  	if err != nil {
   124  		return err
   125  	}
   126  	defer f.Close()
   127  
   128  	data := &authData{
   129  		Client: client,
   130  		Token:  token,
   131  		Domain: domain,
   132  	}
   133  
   134  	return json.NewEncoder(f).Encode(data)
   135  }
   136  
   137  func newFileLock(name string) (lockfile.Lockfile, error) {
   138  	lockName := strings.ReplaceAll(name, "/", "_") + ".lock"
   139  	return lockfile.New(filepath.Join(os.TempDir(), lockName))
   140  }