diff options
| -rw-r--r-- | app/controllers/sessions/controller.go | 17 | ||||
| -rw-r--r-- | app/controllers/sessions/controller_test.go | 8 | ||||
| -rw-r--r-- | app/init.go | 23 | ||||
| -rw-r--r-- | pkg/web/cookie/cookie_test.go | 29 | ||||
| -rw-r--r-- | pkg/web/cookie/new.go | 22 | ||||
| -rw-r--r-- | pkg/web/cookie/reset.go | 36 |
6 files changed, 125 insertions, 10 deletions
diff --git a/app/controllers/sessions/controller.go b/app/controllers/sessions/controller.go index d526a86..1ceb9ec 100644 --- a/app/controllers/sessions/controller.go +++ b/app/controllers/sessions/controller.go @@ -1,18 +1,24 @@ package sessions import ( + "context" "net/http" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/oidc" + "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/web/cookie" "golang.org/x/oauth2" ) type Controller struct { - cfg *oidc.OpenID + cfg *oidc.OpenID + http *http.Client } -func New(cfg *oidc.OpenID) *Controller { - return &Controller{cfg: cfg} +func New(cfg *oidc.OpenID, http *http.Client) *Controller { + return &Controller{ + cfg: cfg, + http: http, + } } func (c *Controller) MountTo(mux *http.ServeMux) { @@ -27,7 +33,8 @@ func (c *Controller) New(w http.ResponseWriter, r *http.Request) { } func (c *Controller) Create(w http.ResponseWriter, r *http.Request) { - token, err := c.cfg.Config.Exchange(r.Context(), r.URL.Query().Get("code")) + ctx := context.WithValue(r.Context(), oauth2.HTTPClient, c.http) + token, err := c.cfg.Config.Exchange(ctx, r.URL.Query().Get("code")) if err != nil { w.WriteHeader(http.StatusBadRequest) return @@ -44,6 +51,6 @@ func (c *Controller) Create(w http.ResponseWriter, r *http.Request) { return } - http.SetCookie(w, &http.Cookie{Name: "session", Value: encoded}) + http.SetCookie(w, cookie.New("session", encoded, tokens.Expiry)) http.Redirect(w, r, "/dashboard", http.StatusFound) } diff --git a/app/controllers/sessions/controller_test.go b/app/controllers/sessions/controller_test.go index bbe82bc..71f9311 100644 --- a/app/controllers/sessions/controller_test.go +++ b/app/controllers/sessions/controller_test.go @@ -8,6 +8,7 @@ import ( "net/url" "strings" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -54,8 +55,9 @@ func TestSessions(t *testing.T) { "access_token": "14fa6e71afaabbe5e31ef2b47ccab7ca7a3c26f8dfdb74acce3eca30099af028", "token_type": "Bearer", "refresh_token": "365b261d4b25ba37e7c1e14e6501902aeecfb7fffc4602c44d6ac22b4c715b0f", - "expiry": "2025-04-15T12:30:50.498991929-06:00", - "id_token": "eyJ0eXAiOiJKV1QiLCJraWQiOiJ0ZDBTbWRKUTRxUGg1cU5Lek0yNjBDWHgyVWgtd2hHLU1Eam9PS1dmdDhFIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMCIsInN1YiI6IjEiLCJhdWQiOiJlMzFlMWRhMGI4ZjZiNmUzNWNhNzBjNzkwYjEzYzA0MDZlNDRhY2E2YjJiZjY3ZjU1ZGU3MzU1YTk3OWEyMjRmIiwiZXhwIjoxNzQ0NzM3NDI3LCJpYXQiOjE3NDQ3MzczMDcsImF1dGhfdGltZSI6MTc0NDczNDY0OSwic3ViX2xlZ2FjeSI6IjI0NzRjZjBiMjIxMTY4OGE1NzI5N2FjZTBlMjYwYTE1OTQ0NzU0ZDE2YjFiZDQyYzlkNjc3OWM5MDAzNjc4MDciLCJuYW1lIjoiQWRtaW5pc3RyYXRvciIsIm5pY2tuYW1lIjoicm9vdCIsInByZWZlcnJlZF91c2VybmFtZSI6InJvb3QiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByb2ZpbGUiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMC9yb290IiwicGljdHVyZSI6Imh0dHBzOi8vd3d3LmdyYXZhdGFyLmNvbS9hdmF0YXIvMjU4ZDhkYzkxNmRiOGNlYTJjYWZiNmMzY2QwY2IwMjQ2ZWZlMDYxNDIxZGJkODNlYzNhMzUwNDI4Y2FiZGE0Zj9zPTgwJmQ9aWRlbnRpY29uIiwiZ3JvdXBzX2RpcmVjdCI6WyJ0b29sYm94IiwiZ2l0bGFiLW9yZyIsImdudXdnZXQiLCJDb21taXQ0NTEiLCJqYXNoa2VuYXMiLCJmbGlnaHRqcyIsInR3aXR0ZXIiLCJnaXRsYWItZXhhbXBsZXMiLCJnaXRsYWItZXhhbXBsZXMvc2VjdXJpdHkiLCI0MTI3MDgiLCJnaXRsYWItZXhhbXBsZXMvZGVtby1ncm91cCIsImN1c3RvbS1yb2xlcy1yb290LWdyb3VwIiwiNDM0MDQ0LWdyb3VwLTEiLCI0MzQwNDQtZ3JvdXAtMiIsImdpdGxhYi1vcmcxIiwiZ2l0bGFiLW9yZy9zZWN1cmUiLCJnaXRsYWItb3JnL3NlY3VyZS9tYW5hZ2VycyIsImdpdGxhYi1vcmcvc2VjdXJpdHktcHJvZHVjdHMiLCJnaXRsYWItb3JnL3NlY3VyaXR5LXByb2R1Y3RzL2FuYWx5emVycyIsImN1c3RvbS1yb2xlcy1yb290LWdyb3VwL2FhIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAvYWEvYWFhIiwibWFzc19pbnNlcnRfZ3JvdXBfXzBfMTAwIl19.SZu_l7tQ2Kkeogq0z8cRaDWPfv52JTo-RkiExbnud_lrfrXXneS77BIzaGKX_bzq4SM_oO_Q63AzK66B1r6Gp7ACo4DjOUEIWETg7ZBKcDzEZnresB7kmI_MJ5rfIJTmnH75GOfc_pl5l8T896TbaShN6zSpaXXIVEfhyUrflSWb4hhA7Hbwy2b6laXiaDv0qpcn1udPVYMTsll8I5ni_2yzuEPSVRgrcQoQ46OwVDZIi9tlfdT2qNVjH6FxJ3mkBcxtIVjf3_JYAawFEscg2uvQYwFWj9T6LleMknAh3QFJJMrS6mPqlXJGPUE5pTQgsBInfEikfm9PXxezA-IY6g", + // "expiry": "2025-04-15T19:16:38.78960504-06:00" + "expiry": time.Now().Add(1 * time.Hour).Format(time.RFC3339), + "id_token": "eyJ0eXAiOiJKV1QiLCJraWQiOiJ0ZDBTbWRKUTRxUGg1cU5Lek0yNjBDWHgyVWgtd2hHLU1Eam9PS1dmdDhFIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMCIsInN1YiI6IjEiLCJhdWQiOiJlMzFlMWRhMGI4ZjZiNmUzNWNhNzBjNzkwYjEzYzA0MDZlNDRhY2E2YjJiZjY3ZjU1ZGU3MzU1YTk3OWEyMjRmIiwiZXhwIjoxNzQ0NzM3NDI3LCJpYXQiOjE3NDQ3MzczMDcsImF1dGhfdGltZSI6MTc0NDczNDY0OSwic3ViX2xlZ2FjeSI6IjI0NzRjZjBiMjIxMTY4OGE1NzI5N2FjZTBlMjYwYTE1OTQ0NzU0ZDE2YjFiZDQyYzlkNjc3OWM5MDAzNjc4MDciLCJuYW1lIjoiQWRtaW5pc3RyYXRvciIsIm5pY2tuYW1lIjoicm9vdCIsInByZWZlcnJlZF91c2VybmFtZSI6InJvb3QiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByb2ZpbGUiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMC9yb290IiwicGljdHVyZSI6Imh0dHBzOi8vd3d3LmdyYXZhdGFyLmNvbS9hdmF0YXIvMjU4ZDhkYzkxNmRiOGNlYTJjYWZiNmMzY2QwY2IwMjQ2ZWZlMDYxNDIxZGJkODNlYzNhMzUwNDI4Y2FiZGE0Zj9zPTgwJmQ9aWRlbnRpY29uIiwiZ3JvdXBzX2RpcmVjdCI6WyJ0b29sYm94IiwiZ2l0bGFiLW9yZyIsImdudXdnZXQiLCJDb21taXQ0NTEiLCJqYXNoa2VuYXMiLCJmbGlnaHRqcyIsInR3aXR0ZXIiLCJnaXRsYWItZXhhbXBsZXMiLCJnaXRsYWItZXhhbXBsZXMvc2VjdXJpdHkiLCI0MTI3MDgiLCJnaXRsYWItZXhhbXBsZXMvZGVtby1ncm91cCIsImN1c3RvbS1yb2xlcy1yb290LWdyb3VwIiwiNDM0MDQ0LWdyb3VwLTEiLCI0MzQwNDQtZ3JvdXAtMiIsImdpdGxhYi1vcmcxIiwiZ2l0bGFiLW9yZy9zZWN1cmUiLCJnaXRsYWItb3JnL3NlY3VyZS9tYW5hZ2VycyIsImdpdGxhYi1vcmcvc2VjdXJpdHktcHJvZHVjdHMiLCJnaXRsYWItb3JnL3NlY3VyaXR5LXByb2R1Y3RzL2FuYWx5emVycyIsImN1c3RvbS1yb2xlcy1yb290LWdyb3VwL2FhIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAvYWEvYWFhIiwibWFzc19pbnNlcnRfZ3JvdXBfXzBfMTAwIl19.SZu_l7tQ2Kkeogq0z8cRaDWPfv52JTo-RkiExbnud_lrfrXXneS77BIzaGKX_bzq4SM_oO_Q63AzK66B1r6Gp7ACo4DjOUEIWETg7ZBKcDzEZnresB7kmI_MJ5rfIJTmnH75GOfc_pl5l8T896TbaShN6zSpaXXIVEfhyUrflSWb4hhA7Hbwy2b6laXiaDv0qpcn1udPVYMTsll8I5ni_2yzuEPSVRgrcQoQ46OwVDZIi9tlfdT2qNVjH6FxJ3mkBcxtIVjf3_JYAawFEscg2uvQYwFWj9T6LleMknAh3QFJJMrS6mPqlXJGPUE5pTQgsBInfEikfm9PXxezA-IY6g", })) } default: @@ -68,7 +70,7 @@ func TestSessions(t *testing.T) { cfg, err := oidc.New(t.Context(), srv.URL, "client_id", "client_secret", "callback_url") require.NoError(t, err) - controller := New(cfg) + controller := New(cfg, http.DefaultClient) mux := http.NewServeMux() controller.MountTo(mux) diff --git a/app/init.go b/app/init.go index bbfb6da..f35c2d6 100644 --- a/app/init.go +++ b/app/init.go @@ -2,6 +2,7 @@ package app import ( "context" + "fmt" "log" "net/http" @@ -12,8 +13,20 @@ import ( "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/controllers/sparkles" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/db" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/oidc" + "golang.org/x/oauth2" ) +type transport struct { +} + +func (r *transport) RoundTrip(request *http.Request) (*http.Response, error) { + response, err := http.DefaultTransport.RoundTrip(request) + if err == nil { + fmt.Printf("%v: %v %v\n", response.StatusCode, request.Method, request.URL) + } + return response, err +} + func init() { ioc.RegisterSingleton[db.Repository](ioc.Default, func() db.Repository { return db.NewRepository() @@ -27,9 +40,12 @@ func init() { ioc.Register[*health.Controller](ioc.Default, func() *health.Controller { return health.New() }) + ioc.RegisterSingleton[*http.Client](ioc.Default, func() *http.Client { + return &http.Client{Transport: &transport{}} + }) ioc.RegisterSingleton[*oidc.OpenID](ioc.Default, func() *oidc.OpenID { item, err := oidc.New( - context.Background(), + context.WithValue(context.Background(), oauth2.HTTPClient, ioc.MustResolve[*http.Client](ioc.Default)), env.Fetch("OIDC_ISSUER", "https://gitlab.com"), env.Fetch("OAUTH_CLIENT_ID", "client_id"), env.Fetch("OAUTH_CLIENT_SECRET", "client_secret"), @@ -41,6 +57,9 @@ func init() { return item }) ioc.Register[*sessions.Controller](ioc.Default, func() *sessions.Controller { - return sessions.New(ioc.MustResolve[*oidc.OpenID](ioc.Default)) + return sessions.New( + ioc.MustResolve[*oidc.OpenID](ioc.Default), + ioc.MustResolve[*http.Client](ioc.Default), + ) }) } diff --git a/pkg/web/cookie/cookie_test.go b/pkg/web/cookie/cookie_test.go new file mode 100644 index 0000000..17e3d88 --- /dev/null +++ b/pkg/web/cookie/cookie_test.go @@ -0,0 +1,29 @@ +package cookie + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/xlgmokha/x/pkg/env" +) + +func TestCookie(t *testing.T) { + t.Run("New", func(t *testing.T) { + t.Run("returns a cookie pinned to the HOST", func(t *testing.T) { + env.With(env.Vars{"HOST": "sparkle.example.com"}, func() { + cookie := New("name", "value", time.Now().Add(1*time.Minute)) + assert.Equal(t, cookie.Domain, "sparkle.example.com") + }) + }) + }) + + t.Run("Reset", func(t *testing.T) { + t.Run("returns an expired cookie", func(t *testing.T) { + result := Reset("example") + + assert.Equal(t, -1, result.MaxAge) + assert.Equal(t, time.Unix(0, 0), result.Expires) + }) + }) +} diff --git a/pkg/web/cookie/new.go b/pkg/web/cookie/new.go new file mode 100644 index 0000000..2809640 --- /dev/null +++ b/pkg/web/cookie/new.go @@ -0,0 +1,22 @@ +package cookie + +import ( + "net/http" + "time" + + "github.com/xlgmokha/x/pkg/env" +) + +func New(name, value string, expires time.Time) *http.Cookie { + return &http.Cookie{ + Name: name, + Value: value, // TODO:: digitally sign the value + Expires: expires, + MaxAge: int(time.Until(expires).Seconds()), + Path: "/", + // HttpOnly: true, + // Secure: true, + SameSite: http.SameSiteDefaultMode, + Domain: env.Fetch("HOST", "localhost"), + } +} diff --git a/pkg/web/cookie/reset.go b/pkg/web/cookie/reset.go new file mode 100644 index 0000000..1686343 --- /dev/null +++ b/pkg/web/cookie/reset.go @@ -0,0 +1,36 @@ +package cookie + +import ( + "net/http" + "time" + + "github.com/xlgmokha/x/pkg/env" +) + +func Reset(name string) *http.Cookie { + return Clear(&http.Cookie{ + Name: name, + }) +} + +func Expire(w http.ResponseWriter, r *http.Request, name string) { + cookie, err := r.Cookie(name) + if err != nil { + http.SetCookie(w, Reset(name)) + } else { + Clear(cookie) + http.SetCookie(w, cookie) + } +} + +func Clear(cookie *http.Cookie) *http.Cookie { + cookie.Value = "" + cookie.Expires = time.Unix(0, 0) + cookie.MaxAge = -1 + cookie.Path = "/" + cookie.HttpOnly = true + cookie.Secure = true + cookie.SameSite = http.SameSiteNoneMode + cookie.Domain = env.Fetch("HOST", "localhost") + return cookie +} |
