mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2026-10-18 04:41:19 +02:00
fix: cookie secret source and header value conversion workflow
Signed-off-by: Jan Larwig <jan@larwig.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user