github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/client/auth/auth.go (about) 1 package auth 2 3 import ( 4 "crypto/rand" 5 "encoding/base64" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 "net/http" 11 "net/url" 12 "os" 13 "strings" 14 15 "github.com/cozy/cozy-stack/client/request" 16 build "github.com/cozy/cozy-stack/pkg/config" 17 ) 18 19 type ( 20 // Client describes the data of an OAuth client 21 Client struct { 22 ClientID string `json:"client_id,omitempty"` 23 ClientSecret string `json:"client_secret"` 24 SecretExpiresAt int `json:"client_secret_expires_at"` 25 RegistrationToken string `json:"registration_access_token"` 26 RedirectURIs []string `json:"redirect_uris"` 27 ClientName string `json:"client_name"` 28 ClientKind string `json:"client_kind,omitempty"` 29 ClientURI string `json:"client_uri,omitempty"` 30 LogoURI string `json:"logo_uri,omitempty"` 31 PolicyURI string `json:"policy_uri,omitempty"` 32 SoftwareID string `json:"software_id"` 33 SoftwareVersion string `json:"software_version,omitempty"` 34 } 35 36 // AccessToken describes the content of an access token 37 AccessToken struct { 38 TokenType string `json:"token_type"` 39 AccessToken string `json:"access_token"` 40 RefreshToken string `json:"refresh_token"` 41 Scope string `json:"scope"` 42 } 43 44 // UserAcceptFunc is a function that can be defined by the user of this 45 // library to describe how to ask the user for authorizing the client to 46 // access to its data. 47 // 48 // The method should return the url on which the user has been redirected 49 // which should contain a registering code and state, or an error . 50 UserAcceptFunc func(accessURL string) (*url.URL, error) 51 52 // Request represents an OAuth request with client parameters (*Client) and 53 // list of scopes that the application wants to access. 54 Request struct { 55 ClientParams *Client 56 Scopes []string 57 Domain string 58 Scheme string 59 HTTPClient *http.Client 60 UserAgent string 61 UserAccept UserAcceptFunc 62 Storage Storage 63 64 token *AccessToken 65 client *Client 66 } 67 68 // Error represents a client registration error returned by the OAuth server 69 Error struct { 70 Value string `json:"error"` 71 Description string `json:"error_description,omitempty"` 72 } 73 ) 74 75 func (e *Error) Error() string { 76 return fmt.Sprintf("Authentication error: %s (%s)", e.Description, e.Value) 77 } 78 79 // Clone returns a new Client with cloned values 80 func (c *Client) Clone() *Client { 81 redirects := make([]string, len(c.RedirectURIs)) 82 copy(redirects, c.RedirectURIs) 83 return &Client{ 84 ClientID: c.ClientID, 85 ClientSecret: c.ClientSecret, 86 SecretExpiresAt: c.SecretExpiresAt, 87 RegistrationToken: c.RegistrationToken, 88 RedirectURIs: redirects, 89 ClientName: c.ClientName, 90 ClientKind: c.ClientKind, 91 ClientURI: c.ClientURI, 92 LogoURI: c.LogoURI, 93 PolicyURI: c.PolicyURI, 94 SoftwareID: c.SoftwareID, 95 SoftwareVersion: c.SoftwareVersion, 96 } 97 } 98 99 // Clone returns a new AccessToken with cloned values 100 func (t *AccessToken) Clone() *AccessToken { 101 return &AccessToken{ 102 TokenType: t.TokenType, 103 AccessToken: t.AccessToken, 104 RefreshToken: t.RefreshToken, 105 Scope: t.Scope, 106 } 107 } 108 109 // AuthHeader implements the Tokener interface for the client 110 func (c *Client) AuthHeader() string { 111 return "Bearer " + c.RegistrationToken 112 } 113 114 // AuthHeader implements the Tokener interface for the access token 115 func (t *AccessToken) AuthHeader() string { 116 return "Bearer " + t.AccessToken 117 } 118 119 // RealtimeToken implements the Tokener interface for the access token 120 func (t *AccessToken) RealtimeToken() string { 121 return t.AccessToken 122 } 123 124 // AuthHeader implements the Tokener interface for the request 125 func (r *Request) AuthHeader() string { 126 return r.token.AuthHeader() 127 } 128 129 // RealtimeToken implements the Tokener interface for the access token 130 func (r *Request) RealtimeToken() string { 131 return r.token.RealtimeToken() 132 } 133 134 // defaultClient defaults some values of the given client 135 func defaultClient(c *Client) *Client { 136 if c == nil { 137 c = &Client{} 138 } 139 if c.SoftwareID == "" { 140 c.SoftwareID = "github.com/cozy/cozy-stack" 141 } 142 if c.SoftwareVersion == "" { 143 c.SoftwareVersion = build.Version 144 } 145 if c.ClientName == "" { 146 c.ClientName = "Cozy Go client" 147 } 148 if c.ClientKind == "" { 149 c.ClientKind = "unknown" 150 } 151 return c 152 } 153 154 // Authenticate will start the authentication flow. 155 // 156 // If the storage has a client and token stored, it is reused and no 157 // authentication flow is started. Otherwise, a new client is registered and 158 // the authentication process is started. 159 func (r *Request) Authenticate() error { 160 client, token, err := r.Storage.Load(r.Domain) 161 if err != nil && !os.IsNotExist(err) { 162 return err 163 } 164 if client != nil && token != nil { 165 r.client, r.token = client, token 166 return nil 167 } 168 if client == nil { 169 client, err = r.RegisterClient(defaultClient(r.ClientParams)) 170 if err != nil { 171 return err 172 } 173 } 174 b := make([]byte, 32) 175 if _, err = io.ReadFull(rand.Reader, b); err != nil { 176 return err 177 } 178 state := base64.StdEncoding.EncodeToString(b) 179 if err = r.Storage.Save(r.Domain, client, nil); err != nil { 180 return err 181 } 182 codeURL, err := r.AuthCodeURL(client, state) 183 if err != nil { 184 return err 185 } 186 receivedURL, err := r.UserAccept(codeURL) 187 if err != nil { 188 return err 189 } 190 query := receivedURL.Query() 191 if state != query.Get("state") { 192 return errors.New("Non matching states") 193 } 194 token, err = r.GetAccessToken(client, query.Get("code")) 195 if err != nil { 196 return err 197 } 198 if err = r.Storage.Save(r.Domain, client, token); err != nil { 199 return err 200 } 201 r.client, r.token = client, token 202 return nil 203 } 204 205 // AuthCodeURL returns the url on which the user is asked to authorize the 206 // application. 207 func (r *Request) AuthCodeURL(c *Client, state string) (string, error) { 208 query := url.Values{ 209 "client_id": {c.ClientID}, 210 "redirect_uri": {c.RedirectURIs[0]}, 211 "state": {state}, 212 "response_type": {"code"}, 213 "scope": {strings.Join(r.Scopes, " ")}, 214 } 215 u := url.URL{ 216 Scheme: "https", 217 Host: r.Domain, 218 Path: "/auth/authorize", 219 RawQuery: query.Encode(), 220 } 221 return u.String(), nil 222 } 223 224 // req performs an authentication HTTP request 225 func (r *Request) req(opts *request.Options) (*http.Response, error) { 226 opts.Domain = r.Domain 227 opts.Scheme = r.Scheme 228 opts.Client = r.HTTPClient 229 opts.ParseError = parseError 230 return request.Req(opts) 231 } 232 233 // RegisterClient performs the registration of the specified client. 234 func (r *Request) RegisterClient(c *Client) (*Client, error) { 235 body, err := request.WriteJSON(c) 236 if err != nil { 237 return nil, err 238 } 239 res, err := r.req(&request.Options{ 240 Method: "POST", 241 Path: "/auth/register", 242 Headers: request.Headers{ 243 "Content-Type": "application/json", 244 "Accept": "application/json", 245 }, 246 Body: body, 247 }) 248 if err != nil { 249 return nil, err 250 } 251 return readClient(res.Body) 252 } 253 254 // GetAccessToken fetch the access token using the specified authorization 255 // code. 256 func (r *Request) GetAccessToken(c *Client, code string) (*AccessToken, error) { 257 q := url.Values{ 258 "grant_type": {"authorization_code"}, 259 "code": {code}, 260 "client_id": {c.ClientID}, 261 "client_secret": {c.ClientSecret}, 262 } 263 return r.retrieveToken(c, nil, q) 264 } 265 266 // RefreshToken performs a token refresh using the specified client and current 267 // access token. 268 func (r *Request) RefreshToken(c *Client, t *AccessToken) (*AccessToken, error) { 269 q := url.Values{ 270 "grant_type": {"refresh_token"}, 271 "refresh_token": {t.RefreshToken}, 272 "client_id": {c.ClientID}, 273 "client_secret": {c.ClientSecret}, 274 } 275 return r.retrieveToken(c, t, q) 276 } 277 278 func (r *Request) retrieveToken(c *Client, t *AccessToken, q url.Values) (*AccessToken, error) { 279 opts := &request.Options{ 280 Method: "POST", 281 Path: "/auth/access_token", 282 Body: strings.NewReader(q.Encode()), 283 Headers: request.Headers{ 284 "Content-Type": "application/x-www-form-urlencoded", 285 "Accept": "application/json", 286 }, 287 } 288 if t != nil { 289 opts.Authorizer = t 290 } 291 292 res, err := r.req(opts) 293 if err != nil { 294 return nil, err 295 } 296 token := &AccessToken{} 297 if err := request.ReadJSON(res.Body, token); err != nil { 298 return nil, err 299 } 300 return token, nil 301 } 302 303 func parseError(res *http.Response, b []byte) error { 304 var err Error 305 if err := json.Unmarshal(b, &err); err != nil { 306 return &request.Error{ 307 Status: http.StatusText(res.StatusCode), 308 Title: http.StatusText(res.StatusCode), 309 Detail: string(b), 310 } 311 } 312 return &err 313 } 314 315 func readClient(r io.ReadCloser) (*Client, error) { 316 client := &Client{} 317 if err := request.ReadJSON(r, client); err != nil { 318 return nil, err 319 } 320 return defaultClient(client), nil 321 }