Skip to content

Commit 8f6085a

Browse files
authored
fix: Getting account refresh token from remote OIDC (#4611)
Add more logs especially for better errors handling Make AccountType.RefreshAccount respect the token_mode defined on each account type For token_mode = "basic" send the refresh request as a POST with the Basic Authorization header, mirroring RequestAccessToken For token_mode = "get" issue a query-parameter GET; default path stays a form POST Keeps the response handling unchanged so providers that already worked continue to do so
2 parents f4c446d + 607ab51 commit 8f6085a

File tree

1 file changed

+67
-7
lines changed

1 file changed

+67
-7
lines changed

model/account/type.go

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/cozy/cozy-stack/pkg/consts"
1616
"github.com/cozy/cozy-stack/pkg/couchdb"
1717
"github.com/cozy/cozy-stack/pkg/couchdb/mango"
18+
"github.com/cozy/cozy-stack/pkg/logger"
1819
"github.com/cozy/cozy-stack/pkg/prefixer"
1920
"github.com/labstack/echo/v4"
2021
)
@@ -338,57 +339,116 @@ func (at *AccountType) RequestAccessToken(i *instance.Instance, accessCode, stat
338339
// RefreshAccount requires a new AccessToken using the RefreshToken
339340
// as specified in https://tools.ietf.org/html/rfc6749#section-6
340341
func (at *AccountType) RefreshAccount(a Account) error {
342+
log := logger.WithNamespace("account-refresh")
343+
341344
if a.Oauth == nil {
345+
log.Debugf("Account %s: no OAuth info, cannot refresh", a.ID())
342346
return ErrUnrefreshable
343347
}
344348

345349
// If no endpoint is specified for the account type, the stack just sends
346350
// the client ID and client secret to the konnector and let it fetch the
347351
// token its-self.
348352
if a.Oauth.RefreshToken == "" {
353+
log.Debugf("Account %s (type: %s): no refresh token, delegating to konnector", a.ID(), at.Slug)
349354
a.Oauth.ClientID = at.ClientID
350355
a.Oauth.ClientSecret = at.ClientSecret
351356
return nil
352357
}
353358

354-
res, err := http.PostForm(at.TokenEndpoint, url.Values{
359+
authMode := at.TokenAuthMode
360+
if authMode == "" {
361+
authMode = FormTokenAuthMode
362+
}
363+
log.Debugf("Account %s (type: %s): refreshing OAuth token using %s auth mode", a.ID(), at.Slug, authMode)
364+
365+
params := url.Values{
355366
"grant_type": []string{RefreshToken},
356367
"refresh_token": []string{a.Oauth.RefreshToken},
357-
"client_id": []string{at.ClientID},
358-
"client_secret": []string{at.ClientSecret},
359-
})
368+
}
369+
if authMode != BasicTokenAuthMode {
370+
params.Add("client_id", at.ClientID)
371+
params.Add("client_secret", at.ClientSecret)
372+
}
373+
374+
var res *http.Response
375+
var err error
376+
377+
switch authMode {
378+
case BasicTokenAuthMode:
379+
log.Debugf("Account %s: using Basic authentication mode", a.ID())
380+
var req *http.Request
381+
req, err = http.NewRequest("POST", at.TokenEndpoint, strings.NewReader(params.Encode()))
382+
if err != nil {
383+
log.Errorf("Account %s: failed to create POST request: %s", a.ID(), err)
384+
return err
385+
}
386+
req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationForm)
387+
req.Header.Add(echo.HeaderAccept, echo.MIMEApplicationJSON)
388+
auth := []byte(at.ClientID + ":" + at.ClientSecret)
389+
req.Header.Add(echo.HeaderAuthorization, "Basic "+base64.StdEncoding.EncodeToString(auth))
390+
res, err = accountsClient.Do(req)
391+
case GetTokenAuthMode:
392+
log.Debugf("Account %s: using GET authentication mode", a.ID())
393+
var req *http.Request
394+
req, err = http.NewRequest("GET", at.TokenEndpoint+"?"+params.Encode(), nil)
395+
if err != nil {
396+
log.Errorf("Account %s: failed to create GET request: %s", a.ID(), err)
397+
return err
398+
}
399+
res, err = accountsClient.Do(req)
400+
default:
401+
log.Debugf("Account %s: using form-based authentication mode", a.ID())
402+
res, err = http.PostForm(at.TokenEndpoint, params)
403+
}
360404

361405
if err != nil {
406+
log.Errorf("Account %s: HTTP request to token endpoint failed: %s", a.ID(), err)
407+
return err
408+
}
409+
410+
log.Debugf("Account %s: token endpoint responded with status %d", a.ID(), res.StatusCode)
411+
defer res.Body.Close()
412+
413+
resBody, err := io.ReadAll(res.Body)
414+
if err != nil {
415+
log.Errorf("Account %s: failed to read response body: %s", a.ID(), err)
362416
return err
363417
}
364418

365419
if res.StatusCode != 200 {
366-
resBody, _ := io.ReadAll(res.Body)
367-
return errors.New("oauth services responded with non-200 res: " + string(resBody))
420+
log.Errorf("Account %s: OAuth service error (status %d): %s", a.ID(), res.StatusCode, string(resBody))
421+
return fmt.Errorf("oauth service responded with status %d: %s", res.StatusCode, string(resBody))
368422
}
369423

370424
var out tokenEndpointResponse
371-
err = json.NewDecoder(res.Body).Decode(&out)
425+
err = json.Unmarshal(resBody, &out)
372426
if err != nil {
427+
log.Errorf("Account %s: failed to parse JSON response: %s", a.ID(), err)
373428
return err
374429
}
375430

376431
if out.Error != "" {
432+
log.Errorf("Account %s: OAuth error from service: %s - %s", a.ID(), out.Error, out.ErrorDescription)
377433
return fmt.Errorf("OauthError(%s) %s", out.Error, out.ErrorDescription)
378434
}
379435

380436
if out.AccessToken != "" {
437+
log.Debugf("Account %s: successfully received new access token", a.ID())
381438
a.Oauth.AccessToken = out.AccessToken
382439
}
383440

384441
if out.ExpiresIn != 0 {
385442
a.Oauth.ExpiresAt = time.Now().Add(time.Duration(out.ExpiresIn) * time.Second)
443+
log.Debugf("Account %s: token expires in %d seconds", a.ID(), out.ExpiresIn)
386444
}
387445

388446
if out.RefreshToken != "" {
447+
log.Debugf("Account %s: received new refresh token", a.ID())
389448
a.Oauth.RefreshToken = out.RefreshToken
390449
}
391450

451+
log.Infof("Account %s: successfully refreshed OAuth token", a.ID())
392452
return nil
393453
}
394454

0 commit comments

Comments
 (0)