fix: cookie secret source and header value conversion workflow

Signed-off-by: Jan Larwig <jan@larwig.com>
This commit is contained in:
Jan Larwig
2026-03-26 22:24:21 +01:00
parent d0c125d173
commit df0cf59f97
9 changed files with 196 additions and 32 deletions
+45
View File
@@ -0,0 +1,45 @@
package options
import "fmt"
// ClaimSource allows loading a header value from a claim within the session
type ClaimSource struct {
// Claim is the name of the claim in the session that the value should be
// loaded from. Available claims: `access_token` `id_token` `created_at`
// `expires_on` `refresh_token` `email` `user` `groups` `preferred_username`.
Claim string `yaml:"claim,omitempty"`
// Prefix is an optional prefix that will be prepended to the value of the
// claim if it is non-empty.
Prefix string `yaml:"prefix,omitempty"`
// BasicAuthPassword converts this claim into a basic auth header.
// Note the value of claim will become the basic auth username and the
// basicAuthPassword will be used as the password value.
BasicAuthPassword *SecretSource `yaml:"basicAuthPassword,omitempty"`
}
// MarshalYAML implements the yaml.Marshaler interface for ClaimSource.
// This is only necessary for the conversion workflow from toml to yaml
func (c *ClaimSource) MarshalYAML() (interface{}, error) {
if c == nil {
return nil, nil
}
mapped := make(map[string]interface{})
if c.Claim != "" {
mapped["claim"] = c.Claim
}
if c.Prefix != "" {
mapped["prefix"] = c.Prefix
}
if c.BasicAuthPassword != nil {
basicAuthPasswordYAML, err := c.BasicAuthPassword.MarshalYAML()
if err != nil {
return nil, fmt.Errorf("error marshaling basicAuthPassword for ClaimSource: %w", err)
}
mapped["basicAuthPassword"] = basicAuthPasswordYAML
}
return mapped, nil
}
+4
View File
@@ -15,6 +15,8 @@ const (
DefaultCSRFPerRequest bool = false
)
// SameSiteMode is an enum representing the different SameSite modes for cookies
// Available modes are "lax", "strict", "none", and "" (default browser behavior)
type SameSiteMode string
const (
@@ -24,6 +26,8 @@ const (
SameSiteDefault SameSiteMode = ""
)
// ScriptAccess is an enum representing whether a cookie is accessible to JavaScript
// Available modes are "allow", "deny" (default behavior)
type ScriptAccess string
const (
+3 -18
View File
@@ -1,6 +1,8 @@
package options
import "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr"
import (
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr"
)
const (
// DefaultHeaderPreserveRequestValue is the default value for Header.PreserveRequestValue
@@ -46,23 +48,6 @@ type HeaderValue struct {
*ClaimSource `yaml:"claimSource,omitempty"`
}
// ClaimSource allows loading a header value from a claim within the session
type ClaimSource struct {
// Claim is the name of the claim in the session that the value should be
// loaded from. Available claims: `access_token` `id_token` `created_at`
// `expires_on` `refresh_token` `email` `user` `groups` `preferred_username`.
Claim string `yaml:"claim,omitempty"`
// Prefix is an optional prefix that will be prepended to the value of the
// claim if it is non-empty.
Prefix string `yaml:"prefix,omitempty"`
// BasicAuthPassword converts this claim into a basic auth header.
// Note the value of claim will become the basic auth username and the
// basicAuthPassword will be used as the password value.
BasicAuthPassword *SecretSource `yaml:"basicAuthPassword,omitempty"`
}
// EnsureDefaults sets any default values for Header fields.
func (h *Header) EnsureDefaults() {
if h.PreserveRequestValue == nil {
+27
View File
@@ -62,6 +62,10 @@ func (ss *SecretSource) GetSecretValue() ([]byte, error) {
return nil, fmt.Errorf("failed retrieving secret value: no source defined")
}
if len(ss.Value) == 0 && ss.FromFile != "" {
return value, nil
}
decoded := make([]byte, base64.URLEncoding.DecodedLen(len(value)))
if _, err := base64.URLEncoding.Decode(decoded, value); err != nil {
return nil, fmt.Errorf("error decoding secret value: %w", err)
@@ -69,6 +73,29 @@ func (ss *SecretSource) GetSecretValue() ([]byte, error) {
return decoded, nil
}
// MarshalYAML implements the yaml.Marshaler interface for SecretSource.
// This is only necessary for the conversion workflow from toml to yaml
func (ss *SecretSource) MarshalYAML() (interface{}, error) {
if ss == nil {
return nil, nil
}
if ss.FromFile != "" {
return map[string]string{
"fromFile": ss.FromFile,
}, nil
}
encodedValue, err := ss.GetRawSecretValue()
if err != nil {
return nil, fmt.Errorf("error getting raw secret value for marshaling: %w", err)
}
return map[string]string{
"value": string(encodedValue),
}, nil
}
// EnsureDefaults sets any default values for SecretSource fields.
func (ss *SecretSource) EnsureDefaults() {
// No defaults to set currently
+79
View File
@@ -0,0 +1,79 @@
package options
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
const (
testSecretValue = "bXktc2VjcmV0" // base64 for "my-secret"
testFileSecretContent = "file-secret"
)
func TestSecretSourceGetSecretValue(t *testing.T) {
t.Run("returns secret when Value is set", func(t *testing.T) {
ss := &SecretSource{
Value: []byte(testSecretValue),
FromFile: "",
}
secret, err := ss.GetSecretValue()
assert.NoError(t, err)
assert.Equal(t, "my-secret", string(secret))
})
t.Run("returns secret when both Value and FromFile are set", func(t *testing.T) {
ss := &SecretSource{
Value: []byte(testSecretValue),
FromFile: "/some/file",
}
secret, err := ss.GetSecretValue()
assert.NoError(t, err)
assert.Equal(t, "my-secret", string(secret))
})
t.Run("reads from file when only FromFile is set", func(t *testing.T) {
// Create a temporary file
tmpfile, err := os.CreateTemp("", "secret-source-test")
assert.NoError(t, err)
defer os.Remove(tmpfile.Name())
_, err = tmpfile.Write([]byte(testFileSecretContent))
assert.NoError(t, err)
tmpfile.Close()
ss := &SecretSource{
FromFile: tmpfile.Name(),
}
secret, err := ss.GetSecretValue()
assert.NoError(t, err)
assert.Equal(t, "file-secret", string(secret))
})
t.Run("returns error when file does not exist", func(t *testing.T) {
ss := &SecretSource{
FromFile: "/nonexistent/file",
}
secret, err := ss.GetSecretValue()
assert.Error(t, err)
assert.Nil(t, secret)
})
t.Run("returns error when no source is defined", func(t *testing.T) {
ss := &SecretSource{}
secret, err := ss.GetSecretValue()
assert.Error(t, err)
assert.Nil(t, secret)
})
t.Run("returns error when Value is not valid base64", func(t *testing.T) {
ss := &SecretSource{
Value: []byte("invalid-base64"),
FromFile: "",
}
secret, err := ss.GetSecretValue()
assert.Error(t, err)
assert.Nil(t, secret)
})
}