Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
137 lines
5.0 KiB
Go
137 lines
5.0 KiB
Go
// Copyright © 2026 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package cookiex
|
|
|
|
// This file isolates the gorilla/securecookie dependency. It exists so that
|
|
// cookies sealed by the previous securecookie-based stores keep decoding
|
|
// during the migration, and (behind WithLegacyEncode) so that freshly minted
|
|
// cookies stay readable by old pods during the stage-1 rollout. Delete it,
|
|
// and the securecookie dependency, once ory_legacy_cookie_decodes_total has
|
|
// flatlined fleet-wide for a full max-cookie-lifespan window.
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/gorilla/securecookie"
|
|
"github.com/pkg/errors"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
)
|
|
|
|
// legacyDecodes counts cookies that were successfully decoded via the legacy
|
|
// securecookie fallback. It is registered on the Prometheus default
|
|
// registerer, which Kratos and Hydra expose on /metrics/prometheus.
|
|
var legacyDecodes = promauto.NewCounterVec(prometheus.CounterOpts{
|
|
Name: "ory_legacy_cookie_decodes_total",
|
|
Help: "Number of cookies successfully decoded via the legacy securecookie fallback, by codec purpose.",
|
|
}, []string{"purpose"})
|
|
|
|
// LegacySecureCookieKeyPairs derives the (hashKey, blockKey) pairs the
|
|
// securecookie-based cookie stores used: each secret signs, and its SHA-256
|
|
// digest encrypts.
|
|
func LegacySecureCookieKeyPairs(secrets [][]byte) [][]byte {
|
|
pairs := make([][]byte, 0, 2*len(secrets))
|
|
for _, secret := range secrets {
|
|
block := sha256.Sum256(secret)
|
|
pairs = append(pairs, secret, block[:])
|
|
}
|
|
return pairs
|
|
}
|
|
|
|
// WithLegacyKeyPairs enables decoding cookies in the legacy securecookie
|
|
// format, sealed under the given (hashKey, blockKey) pairs. While this option
|
|
// is in use, T must be unmarshalable from a flat JSON object with string
|
|
// values; WithLegacyEncode additionally requires T to marshal to one. Remove
|
|
// once all cookies sealed in the legacy format have expired.
|
|
func WithLegacyKeyPairs(pairs ...[]byte) Option {
|
|
return func(c *config) { c.legacyKeyPairs = pairs }
|
|
}
|
|
|
|
type legacyState struct {
|
|
codecs []securecookie.Codec
|
|
encode bool
|
|
}
|
|
|
|
func newLegacyState(cfg config, maxAge time.Duration) legacyState {
|
|
codecs := securecookie.CodecsFromPairs(cfg.legacyKeyPairs...)
|
|
for _, codec := range codecs {
|
|
if sc, ok := codec.(*securecookie.SecureCookie); ok {
|
|
sc.MaxAge(int(maxAge / time.Second))
|
|
}
|
|
}
|
|
return legacyState{codecs: codecs, encode: cfg.legacyEncode}
|
|
}
|
|
|
|
// openLegacy decodes a securecookie-sealed value and bridges it into T via
|
|
// its JSON representation: legacy payloads are flat string-to-string maps, so
|
|
// string, time.Time, and UUID fields all round-trip through their JSON string
|
|
// forms.
|
|
func (c *Codec[T]) openLegacy(name, value string) (T, error) {
|
|
var zero T
|
|
if len(c.legacy.codecs) == 0 {
|
|
return zero, errors.WithStack(ErrInvalidCookie)
|
|
}
|
|
values := make(map[any]any)
|
|
if err := securecookie.DecodeMulti(name, value, &values, c.legacy.codecs...); err != nil {
|
|
return zero, errors.WithStack(ErrInvalidCookie)
|
|
}
|
|
flat := make(map[string]string, len(values))
|
|
for k, v := range values {
|
|
key, keyOK := k.(string)
|
|
val, valOK := v.(string)
|
|
if !keyOK || !valOK {
|
|
return zero, errors.WithStack(ErrInvalidCookie)
|
|
}
|
|
flat[key] = val
|
|
}
|
|
bridge, err := json.Marshal(flat)
|
|
if err != nil {
|
|
return zero, errors.Wrap(err, "cookiex: cannot bridge legacy cookie")
|
|
}
|
|
var out T
|
|
if err := json.Unmarshal(bridge, &out); err != nil {
|
|
return zero, errors.WithStack(ErrInvalidCookie)
|
|
}
|
|
legacyDecodes.WithLabelValues(c.purpose).Inc()
|
|
return out, nil
|
|
}
|
|
|
|
// WithLegacyEncode makes Set seal in the legacy securecookie format under the
|
|
// first legacy key pair, so pods that only understand the legacy format can
|
|
// read freshly minted cookies during a rolling deploy. Requires
|
|
// WithLegacyKeyPairs. This is stage 1 of the rollout; a follow-up removes the
|
|
// option, flipping encoding to the v1 format. JSON null values inside the
|
|
// payload are coerced to empty strings by the bridge; do not use pointer-typed
|
|
// fields while legacy encode is enabled.
|
|
func WithLegacyEncode() Option {
|
|
return func(c *config) { c.legacyEncode = true }
|
|
}
|
|
|
|
// sealLegacy bridges T through its JSON representation into the flat
|
|
// string-to-string map that the securecookie stores used.
|
|
func (c *Codec[T]) sealLegacy(name string, value T) (string, error) {
|
|
buf, err := json.Marshal(value)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "cookiex: cannot marshal cookie value")
|
|
}
|
|
var flat map[string]string
|
|
if err := json.Unmarshal(buf, &flat); err != nil {
|
|
return "", errors.Wrap(err, "cookiex: payload must be a flat JSON object with string values while legacy encode is enabled")
|
|
}
|
|
if flat == nil {
|
|
return "", errors.New("cookiex: payload must be a flat JSON object with string values while legacy encode is enabled")
|
|
}
|
|
values := make(map[any]any, len(flat))
|
|
for k, v := range flat {
|
|
values[k] = v
|
|
}
|
|
encoded, err := securecookie.EncodeMulti(name, values, c.legacy.codecs[0])
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "cookiex: cannot encode legacy cookie")
|
|
}
|
|
return encoded, nil
|
|
}
|