snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
136
oryx/cookiex/legacy_securecookie.go
Normal file
136
oryx/cookiex/legacy_securecookie.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user