Files
upstream-ory-kratos/oryx/cookiex/legacy_securecookie.go
upstream-mirror c60cc7ca75
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
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

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
}