From b24e274e9931093ee65304e32ca1f309d2f01234 Mon Sep 17 00:00:00 2001 From: mo khan Date: Tue, 15 Apr 2025 13:53:16 -0600 Subject: feat: store tokens in a session cookie --- app/controllers/sessions/controller.go | 32 +++++---- app/controllers/sessions/controller_test.go | 100 +++++++++++++++++++++------- 2 files changed, 91 insertions(+), 41 deletions(-) (limited to 'app') diff --git a/app/controllers/sessions/controller.go b/app/controllers/sessions/controller.go index a4ba092..70d5631 100644 --- a/app/controllers/sessions/controller.go +++ b/app/controllers/sessions/controller.go @@ -1,10 +1,10 @@ package sessions import ( - "fmt" + "encoding/base64" + "encoding/json" "net/http" - "github.com/xlgmokha/x/pkg/serde" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/oidc" "golang.org/x/oauth2" ) @@ -31,25 +31,23 @@ 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")) if err != nil { - fmt.Printf("%v\n", err) + return } - err = serde.ToJSON(w, token) - if err != nil { - fmt.Printf("%v\n", err) - return + tokens := map[string]interface{}{ + "access_token": token.AccessToken, + "token_type": token.TokenType, + "refresh_token": token.RefreshToken, + "expiry": token.Expiry, + "expires_in": token.ExpiresIn, } if rawIDToken, ok := token.Extra("id_token").(string); ok { - idToken, err := oidc.NewIDToken(rawIDToken) - if err != nil { - fmt.Printf("%v\n", err) - return - } - err = serde.ToJSON(w, idToken) - if err != nil { - fmt.Printf("%v\n", err) - return - } + tokens["id_token"] = rawIDToken } + + data, err := json.Marshal(tokens) + encoded := base64.URLEncoding.EncodeToString(data) + http.SetCookie(w, &http.Cookie{Name: "session", Value: encoded}) + http.Redirect(w, r, "/dashboard", http.StatusFound) } diff --git a/app/controllers/sessions/controller_test.go b/app/controllers/sessions/controller_test.go index d90896e..1c4637d 100644 --- a/app/controllers/sessions/controller_test.go +++ b/app/controllers/sessions/controller_test.go @@ -1,6 +1,8 @@ package sessions import ( + "encoding/base64" + "encoding/json" "net/http" "net/http/httptest" "net/url" @@ -10,6 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/xlgmokha/x/pkg/serde" + "github.com/xlgmokha/x/pkg/x" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/oidc" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/test" ) @@ -18,27 +21,47 @@ func TestSessions(t *testing.T) { srv := httptest.NewServer(nil) srv.Config = &http.Server{ Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, "/.well-known/openid-configuration", r.URL.Path) - require.NoError(t, serde.ToJSON(w, &oidc.Metadata{ - AuthorizationEndpoint: srv.URL + "/oauth/authorize", - ClaimsSupported: []string{"aud"}, - CodeChallengeMethodsSupported: []string{"plain"}, - DeviceAuthorizationEndpoint: srv.URL + "/device/authorize", - IDTokenSigningAlgValuesSupported: []string{"RS256"}, - Issuer: srv.URL, - JWKSURI: srv.URL + "/jwks", - MFAChallengeEndpoint: srv.URL + "/mfa", - RegistrationEndpoint: srv.URL + "/users/new", - RequestURIParameterSupported: false, - ResponseModesSupported: []string{"query"}, - ResponseTypeSupported: []string{"code"}, - RevocationEndpoint: srv.URL + "/revoke", - ScopesSupported: []string{"oidc"}, - SubjectTypesSupported: []string{"public"}, - TokenEndpoint: srv.URL + "/token", - TokenEndpointAuthMethodsSupported: []string{"client_secret_post"}, - UserInfoEndpoint: srv.URL + "/users/me", - })) + switch r.URL.Path { + case "/.well-known/openid-configuration": + require.NoError(t, serde.ToJSON(w, &oidc.Metadata{ + AuthorizationEndpoint: srv.URL + "/oauth/authorize", + ClaimsSupported: []string{"aud"}, + CodeChallengeMethodsSupported: []string{"plain"}, + DeviceAuthorizationEndpoint: srv.URL + "/device/authorize", + IDTokenSigningAlgValuesSupported: []string{"RS256"}, + Issuer: srv.URL, + JWKSURI: srv.URL + "/jwks", + MFAChallengeEndpoint: srv.URL + "/mfa", + RegistrationEndpoint: srv.URL + "/users/new", + RequestURIParameterSupported: false, + ResponseModesSupported: []string{"query"}, + ResponseTypeSupported: []string{"code"}, + RevocationEndpoint: srv.URL + "/revoke", + ScopesSupported: []string{"oidc"}, + SubjectTypesSupported: []string{"public"}, + TokenEndpoint: srv.URL + "/token", + TokenEndpointAuthMethodsSupported: []string{"client_secret_post"}, + UserInfoEndpoint: srv.URL + "/users/me", + })) + case "/token": + if err := r.ParseForm(); err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + if r.Form["grant_type"][0] == "authorization_code" && r.Form["code"][0] == "code" { + w.Header().Add("Content-Type", "application/json") + require.NoError(t, serde.ToJSON(w, map[string]string{ + "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", + })) + } + default: + t.Logf("404: %v", r.URL.Path) + w.WriteHeader(http.StatusNotFound) + } }), } defer srv.Close() @@ -81,9 +104,38 @@ func TestSessions(t *testing.T) { t.Run("with an invalid csrf token", func(t *testing.T) {}) t.Run("with an invalid authorization code grant", func(t *testing.T) {}) t.Run("with a valid authorization code grant", func(t *testing.T) { - t.Run("stores the id token in a session cookie", func(t *testing.T) {}) - t.Run("stores the access token in a session cookie", func(t *testing.T) {}) - t.Run("redirects to the homepage", func(t *testing.T) {}) + r, w := test.RequestResponse("GET", "/session/callback?code=code") + + mux.ServeHTTP(w, r) + + cookies, err := http.ParseCookie(w.Header().Get("Set-Cookie")) + require.NoError(t, err) + cookie := x.Find(cookies, func(item *http.Cookie) bool { + return item.Name == "session" + }) + require.NotZero(t, cookie) + data, err := base64.URLEncoding.DecodeString(cookie.Value) + require.NoError(t, err) + tokens := map[string]interface{}{} + require.NoError(t, json.Unmarshal(data, &tokens)) + + t.Run("stores the id token in a session cookie", func(t *testing.T) { + assert.Equal(t, "eyJ0eXAiOiJKV1QiLCJraWQiOiJ0ZDBTbWRKUTRxUGg1cU5Lek0yNjBDWHgyVWgtd2hHLU1Eam9PS1dmdDhFIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMCIsInN1YiI6IjEiLCJhdWQiOiJlMzFlMWRhMGI4ZjZiNmUzNWNhNzBjNzkwYjEzYzA0MDZlNDRhY2E2YjJiZjY3ZjU1ZGU3MzU1YTk3OWEyMjRmIiwiZXhwIjoxNzQ0NzM3NDI3LCJpYXQiOjE3NDQ3MzczMDcsImF1dGhfdGltZSI6MTc0NDczNDY0OSwic3ViX2xlZ2FjeSI6IjI0NzRjZjBiMjIxMTY4OGE1NzI5N2FjZTBlMjYwYTE1OTQ0NzU0ZDE2YjFiZDQyYzlkNjc3OWM5MDAzNjc4MDciLCJuYW1lIjoiQWRtaW5pc3RyYXRvciIsIm5pY2tuYW1lIjoicm9vdCIsInByZWZlcnJlZF91c2VybmFtZSI6InJvb3QiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByb2ZpbGUiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMC9yb290IiwicGljdHVyZSI6Imh0dHBzOi8vd3d3LmdyYXZhdGFyLmNvbS9hdmF0YXIvMjU4ZDhkYzkxNmRiOGNlYTJjYWZiNmMzY2QwY2IwMjQ2ZWZlMDYxNDIxZGJkODNlYzNhMzUwNDI4Y2FiZGE0Zj9zPTgwJmQ9aWRlbnRpY29uIiwiZ3JvdXBzX2RpcmVjdCI6WyJ0b29sYm94IiwiZ2l0bGFiLW9yZyIsImdudXdnZXQiLCJDb21taXQ0NTEiLCJqYXNoa2VuYXMiLCJmbGlnaHRqcyIsInR3aXR0ZXIiLCJnaXRsYWItZXhhbXBsZXMiLCJnaXRsYWItZXhhbXBsZXMvc2VjdXJpdHkiLCI0MTI3MDgiLCJnaXRsYWItZXhhbXBsZXMvZGVtby1ncm91cCIsImN1c3RvbS1yb2xlcy1yb290LWdyb3VwIiwiNDM0MDQ0LWdyb3VwLTEiLCI0MzQwNDQtZ3JvdXAtMiIsImdpdGxhYi1vcmcxIiwiZ2l0bGFiLW9yZy9zZWN1cmUiLCJnaXRsYWItb3JnL3NlY3VyZS9tYW5hZ2VycyIsImdpdGxhYi1vcmcvc2VjdXJpdHktcHJvZHVjdHMiLCJnaXRsYWItb3JnL3NlY3VyaXR5LXByb2R1Y3RzL2FuYWx5emVycyIsImN1c3RvbS1yb2xlcy1yb290LWdyb3VwL2FhIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAvYWEvYWFhIiwibWFzc19pbnNlcnRfZ3JvdXBfXzBfMTAwIl19.SZu_l7tQ2Kkeogq0z8cRaDWPfv52JTo-RkiExbnud_lrfrXXneS77BIzaGKX_bzq4SM_oO_Q63AzK66B1r6Gp7ACo4DjOUEIWETg7ZBKcDzEZnresB7kmI_MJ5rfIJTmnH75GOfc_pl5l8T896TbaShN6zSpaXXIVEfhyUrflSWb4hhA7Hbwy2b6laXiaDv0qpcn1udPVYMTsll8I5ni_2yzuEPSVRgrcQoQ46OwVDZIi9tlfdT2qNVjH6FxJ3mkBcxtIVjf3_JYAawFEscg2uvQYwFWj9T6LleMknAh3QFJJMrS6mPqlXJGPUE5pTQgsBInfEikfm9PXxezA-IY6g", tokens["id_token"]) + }) + + t.Run("stores the access token in a session cookie", func(t *testing.T) { + assert.Equal(t, "14fa6e71afaabbe5e31ef2b47ccab7ca7a3c26f8dfdb74acce3eca30099af028", tokens["access_token"]) + assert.Equal(t, "Bearer", tokens["token_type"]) + }) + + t.Run("stores the refresh token in a session cookie", func(t *testing.T) { + assert.Equal(t, "365b261d4b25ba37e7c1e14e6501902aeecfb7fffc4602c44d6ac22b4c715b0f", tokens["refresh_token"]) + }) + + t.Run("redirects to the homepage", func(t *testing.T) { + require.Equal(t, http.StatusFound, w.Code) + assert.Equal(t, "/dashboard", w.Header().Get("Location")) + }) }) }) } -- cgit v1.2.3