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 }