Release Please / Release Please (push) Skipped
Release / GoReleaser (push) Skipped
Tests / Secret scan (gitleaks) (push) Skipped
Tests / Test (Go 1.25) (push) Skipped
Tests / Test (Go stable) (push) Skipped
Tests / Secret scan (gitleaks) (pull_request) Successful in 4s
Tests / Test (Go 1.25) (pull_request) Successful in 21s
Tests / Test (Go stable) (pull_request) Successful in 21s
- Route HTTP helpers (getJSON/formRequest/deleteReq/postJSON/putJSON/ multipart upload), Query and AuthenticateContext/ensureToken through the deterministic 429/502/503/504 retry (retryRaw), so the integration suite no longer fails on the shared openresty rate limit under parallel runs. - Fix stale cmd/office/fetch integration test: loader.TaskFields was replaced by loader.DetailForm (broke go vet -tags=integration). - Add unit tests for Transient/DoRetry.
131 lines
4.3 KiB
Go
131 lines
4.3 KiB
Go
package onlyoffice
|
|
|
|
// Authentication primitives: token lifecycle, eager / context-aware auth, and
|
|
// token invalidation. Split out of http.go so auth concerns live together.
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// Auth authenticates using the given credentials and returns a fresh token.
|
|
// Most callers should not call this directly — use Authenticate /
|
|
// AuthenticateContext, or let Query fetch a token lazily. Kept exported for
|
|
// backwards compatibility.
|
|
func (c *Client) Auth(creds *Credentials) (*Token, error) {
|
|
t := &Token{}
|
|
return t, c.Query(Request{
|
|
Uri: "/api/2.0/authentication.json",
|
|
Method: http.MethodPost,
|
|
Body: creds,
|
|
NoAuth: true,
|
|
}, &struct {
|
|
MetaResponse `json:",inline"`
|
|
Response *Token `json:"response"`
|
|
}{
|
|
Response: t,
|
|
})
|
|
}
|
|
|
|
// Authenticate validates credentials and primes the token. Library users may
|
|
// call this eagerly to surface auth errors at startup; otherwise the token
|
|
// is fetched lazily on the first request.
|
|
//
|
|
// Prefer AuthenticateContext in long-running jobs — it honours cancellation.
|
|
func (c *Client) Authenticate() error { return c.ensureToken() }
|
|
|
|
// AuthenticateContext is the context-aware variant of Authenticate. If the
|
|
// cached token is still valid it returns immediately; otherwise it performs
|
|
// a POST to /api/2.0/authentication.json that is cancellable via ctx.
|
|
//
|
|
// Transient answers from the edge (openresty 429/502/503/504) are retried with
|
|
// the same deterministic policy as every other request (see retry.go), because
|
|
// the server rate-limits authentication and the integration suite otherwise
|
|
// fails with a raw HTML 429 page.
|
|
//
|
|
// This is the recommended entry point for long-running syncs (cron,
|
|
// watchers) because it guarantees that a stalled auth call will not block
|
|
// the caller past its deadline.
|
|
func (c *Client) AuthenticateContext(ctx context.Context) error {
|
|
if c.tokenValid() {
|
|
return nil
|
|
}
|
|
return DoRetry(ctx, DefaultRetryPolicy(), func() error {
|
|
return c.authenticateOnce(ctx)
|
|
})
|
|
}
|
|
|
|
// authenticateOnce performs a single authentication POST. Callers must handle
|
|
// retries; use AuthenticateContext.
|
|
func (c *Client) authenticateOnce(ctx context.Context) error {
|
|
body, err := json.Marshal(c.credentials)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal credentials: %w", err)
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL()+"/api/2.0/authentication.json", bytes.NewReader(body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Accept", "application/json")
|
|
resp, err := c.client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("auth request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
raw, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.StatusCode >= 400 {
|
|
return fmt.Errorf("auth: %d %s", resp.StatusCode, truncate(string(raw), 400))
|
|
}
|
|
var env struct {
|
|
Response *Token `json:"response"`
|
|
}
|
|
if err := json.Unmarshal(raw, &env); err != nil {
|
|
return fmt.Errorf("auth decode: %w", err)
|
|
}
|
|
if env.Response == nil || env.Response.Value == "" {
|
|
return fmt.Errorf("auth: empty token in response")
|
|
}
|
|
c.token = env.Response
|
|
return nil
|
|
}
|
|
|
|
// InvalidateToken clears the cached authentication token. The next request
|
|
// (or call to Authenticate / AuthenticateContext) will re-authenticate.
|
|
//
|
|
// Use this to recover from a mid-sync 401 when the server has revoked or
|
|
// rotated the session while the Expires timestamp still looks fresh locally.
|
|
func (c *Client) InvalidateToken() { c.token = nil }
|
|
|
|
// tokenValid reports whether the cached token is present and not expired.
|
|
func (c *Client) tokenValid() bool {
|
|
return c.token != nil && !time.Time(c.token.Expires).Before(time.Now())
|
|
}
|
|
|
|
// ensureToken refreshes the authentication token when missing or expired.
|
|
// Mirrors the logic inline in Query() but is safe to call from helpers that
|
|
// bypass the typed Request abstraction. It shares AuthenticateContext so the
|
|
// transient-retry policy applies to every code path.
|
|
func (c *Client) ensureToken() error {
|
|
if c.tokenValid() {
|
|
return nil
|
|
}
|
|
return c.AuthenticateContext(context.Background())
|
|
}
|
|
|
|
// authHeader returns the value for the Authorization header, ensuring a token.
|
|
func (c *Client) authHeader() (string, error) {
|
|
if err := c.ensureToken(); err != nil {
|
|
return "", err
|
|
}
|
|
return c.token.Value, nil
|
|
}
|