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  }