diff options
| author | mo khan <mo@mokhan.ca> | 2025-05-07 10:30:59 -0700 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-05-07 10:30:59 -0700 |
| commit | f0fbdab72254d68d0a3a4a49a4a1646f89f0f913 (patch) | |
| tree | a6ede6841714a16fa9ac61ae28575a1f749ad547 | |
| parent | 61598cf8c8a2dbae368f3f8b15393c70d2e4fa9d (diff) | |
feat: digitally sign and verify cookie using randomly generated key
| -rw-r--r-- | app/controllers/sessions/controller_test.go | 2 | ||||
| -rw-r--r-- | app/controllers/sessions/service.go | 3 | ||||
| -rw-r--r-- | app/middleware/token_parser.go | 4 | ||||
| -rw-r--r-- | go.mod | 2 | ||||
| -rw-r--r-- | go.sum | 2 | ||||
| -rw-r--r-- | pkg/web/cookie.go | 44 |
6 files changed, 52 insertions, 5 deletions
diff --git a/app/controllers/sessions/controller_test.go b/app/controllers/sessions/controller_test.go index a29041e..a1158da 100644 --- a/app/controllers/sessions/controller_test.go +++ b/app/controllers/sessions/controller_test.go @@ -127,7 +127,7 @@ func TestSessions(t *testing.T) { cookie, err := http.ParseSetCookie(w.Header().Get("Set-Cookie")) require.NoError(t, err) require.NotZero(t, cookie) - data, err := base64.URLEncoding.DecodeString(cookie.Value) + data, err := base64.URLEncoding.DecodeString(web.CookieValueFrom(cookie)) require.NoError(t, err) tokens := map[string]interface{}{} require.NoError(t, json.Unmarshal(data, &tokens)) diff --git a/app/controllers/sessions/service.go b/app/controllers/sessions/service.go index 0ee692a..af1512c 100644 --- a/app/controllers/sessions/service.go +++ b/app/controllers/sessions/service.go @@ -7,6 +7,7 @@ import ( "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/oidc" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/pls" + "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/web" "golang.org/x/oauth2" ) @@ -38,7 +39,7 @@ func (svc *Service) Exchange(r *http.Request) (*oidc.Tokens, error) { } state := r.URL.Query().Get("state") - if state != cookies[0].Value { + if state != web.CookieValueFrom(cookies[0]) { return nil, errors.New("Invalid CSRF token") } diff --git a/app/middleware/token_parser.go b/app/middleware/token_parser.go index 8d81aab..6047a94 100644 --- a/app/middleware/token_parser.go +++ b/app/middleware/token_parser.go @@ -5,6 +5,7 @@ import ( "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/oidc" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/pls" + "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/web" ) type TokenParser func(*http.Request) oidc.RawToken @@ -16,7 +17,8 @@ func IDTokenFromSessionCookie(r *http.Request) oidc.RawToken { return "" } - tokens, err := oidc.TokensFromBase64String(cookies[0].Value) + value := web.CookieValueFrom(cookies[0]) + tokens, err := oidc.TokensFromBase64String(value) if err != nil { pls.LogError(r.Context(), err) return "" @@ -9,7 +9,7 @@ require ( github.com/rs/zerolog v1.34.0 github.com/stretchr/testify v1.10.0 github.com/testcontainers/testcontainers-go v0.36.0 - github.com/xlgmokha/x v0.0.0-20250507143633-104ccfbf81d1 + github.com/xlgmokha/x v0.0.0-20250507172007-444d6e509eb4 golang.org/x/oauth2 v0.30.0 ) @@ -140,6 +140,8 @@ github.com/xlgmokha/x v0.0.0-20250430185455-b691eda27477 h1:oeKrn9BSDSic5MTXKhQe github.com/xlgmokha/x v0.0.0-20250430185455-b691eda27477/go.mod h1:axGPKzoJCNTmPJxYqN5l+Z9gGbPe0yolkT61a5p3QiI= github.com/xlgmokha/x v0.0.0-20250507143633-104ccfbf81d1 h1:iW9a6GiZ8kYduHkUXl6v81CpKyBzSqBMh/fBMqBStKk= github.com/xlgmokha/x v0.0.0-20250507143633-104ccfbf81d1/go.mod h1:FhvU8e/Zcpo0Lw8n5WkPtqQwzGrLD7FhFEi0MbEBLGk= +github.com/xlgmokha/x v0.0.0-20250507172007-444d6e509eb4 h1:5o6TNNvh9RH+/W/XdlNETOgPyUvw9JYESz6IBhjWTKw= +github.com/xlgmokha/x v0.0.0-20250507172007-444d6e509eb4/go.mod h1:FhvU8e/Zcpo0Lw8n5WkPtqQwzGrLD7FhFEi0MbEBLGk= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/pkg/web/cookie.go b/pkg/web/cookie.go index 1377274..4795f96 100644 --- a/pkg/web/cookie.go +++ b/pkg/web/cookie.go @@ -1,18 +1,28 @@ package web import ( + "crypto/sha256" + "encoding/base64" + "fmt" "net/http" + "strings" "github.com/xlgmokha/x/pkg/cookie" + "github.com/xlgmokha/x/pkg/crypt" "github.com/xlgmokha/x/pkg/env" + "github.com/xlgmokha/x/pkg/pls" "github.com/xlgmokha/x/pkg/x" ) +var key []byte = x.Must(pls.GenerateRandomBytes(32)) // TODO:: Read fix key from environment variable +var Signer *crypt.HMACSigner = x.New[*crypt.HMACSigner](crypt.WithKey(key), crypt.WithAlgorithm(sha256.New)) +var delimiter string = "--" + func NewCookie(name, value string, options ...x.Option[*http.Cookie]) *http.Cookie { return x.New[*http.Cookie](x.Prepend[x.Option[*http.Cookie]]( options, cookie.WithName(name), - cookie.WithValue(value), // TODO:: digitally sign the value + withSignedValue(value), cookie.WithPath("/"), cookie.WithHttpOnly(true), cookie.WithSecure(true), @@ -21,6 +31,19 @@ func NewCookie(name, value string, options ...x.Option[*http.Cookie]) *http.Cook )...) } +func withSignedValue(value string) x.Option[*http.Cookie] { + signature, err := Signer.Sign([]byte(value)) + if err != nil { + return nil + } + return cookie.WithValue(fmt.Sprintf( + "%v%v%v", + value, + delimiter, + base64.URLEncoding.EncodeToString(signature), + )) +} + func ExpireCookie(w http.ResponseWriter, name string) { cookie.Expire(w, name, cookie.WithPath("/"), @@ -29,3 +52,22 @@ func ExpireCookie(w http.ResponseWriter, name string) { cookie.WithSecure(true), ) } + +func CookieValueFrom(c *http.Cookie) string { + segments := strings.SplitN(c.Value, delimiter, 2) + if len(segments) != 2 { + return "" + } + + data := segments[0] + signature, err := base64.URLEncoding.DecodeString(segments[1]) + if err != nil { + return "" + } + + if !Signer.Verify([]byte(data), []byte(signature)) { + return "" + } + + return data +} |
