package httpapi import ( "encoding/json" "io" "log/slog" "net/http" "time" ) type refreshReq struct { RefreshToken string `json:"refresh_token"` } type refreshResp struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresIn int `json:"expires_in"` } // handleAuthRefresh proxies a token refresh to the Auth Broker. The SPA holds a // broker-minted access + refresh; when the access expires it POSTs the refresh token // here and cdrop relays it to the broker's /refresh, returning the rotated pair. The // proxy keeps the browser same-origin (no broker CORS) and means cdrop stores no // credential — it only forwards. Public (the access token is expired, which is the // point); Origin-checked for CSRF. func (s *Server) handleAuthRefresh(w http.ResponseWriter, r *http.Request) { if !s.sameOrigin(r) { writeJSON(w, http.StatusForbidden, map[string]string{"error": "bad origin"}) return } var req refreshReq if err := json.NewDecoder(io.LimitReader(r.Body, 8192)).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"}) return } if req.RefreshToken == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing refresh token"}) return } res, err := s.broker.RefreshSession(r.Context(), req.RefreshToken) if err != nil { // A rejected refresh (expired / rotated / revoked) is the client's cue to // re-authenticate; relay it as 401. (A broker outage also lands here — rare, // and re-login is the safe fallback.) slog.Warn("broker refresh failed", "err", err) writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "refresh rejected"}) return } expiresIn := int(res.AccessExpires - time.Now().Unix()) if expiresIn < 0 { expiresIn = 0 } writeJSON(w, http.StatusOK, refreshResp{ AccessToken: res.Access, RefreshToken: res.Refresh, ExpiresIn: expiresIn, }) }