Files
sxchnge/backend/product/auth/http/public/refresh_test.go
T

152 lines
5.4 KiB
Go

package public
import (
"context"
customermodel "git.sendico.io/tech/sxchnge/product/customers/model"
sessionmodel "git.sendico.io/tech/sxchnge/product/sessions/model"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"git.sendico.io/tech/sxchnge/product/auth/config"
"git.sendico.io/tech/sxchnge/product/sessions/browser"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)
func newBrowserRefreshRouter(t *testing.T, expiresAt time.Time) (*PublicRouter, *testRefreshTokenDB) {
t.Helper()
account := testActiveAccount(t)
clientID := "web"
deviceID := "browser-1"
rtdb := &testRefreshTokenDB{
client: &sessionmodel.Client{ClientID: clientID},
tokens: []*sessionmodel.RefreshToken{
{
ClientRefreshToken: sessionmodel.ClientRefreshToken{
SessionIdentifier: sessionmodel.SessionIdentifier{
ClientID: clientID,
DeviceID: deviceID,
SessionVersion: "version-1",
},
RefreshToken: "cookie-refresh",
},
AccountRef: account.GetID(),
ExpiresAt: expiresAt,
},
},
}
config := middleware.TokenConfig{
Expiration: middleware.TokenExpiration{Account: 1, Refresh: 720},
Length: 32,
}
signature := middleware.Signature{
Algorithm: "HS256",
PrivateKey: []byte("browser-refresh-test-secret"),
}
return NewRouter(
zap.NewNop(),
"/api/v1",
&testAccountDB{result: account},
rtdb,
nil,
chi.NewRouter(),
&config,
&signature,
middleware.LoginRateLimits{},
), rtdb
}
func executeBrowserRefresh(t *testing.T, router *PublicRouter) *httptest.ResponseRecorder {
t.Helper()
body := `{"clientId":"web","deviceId":"browser-1","token":"body-must-be-ignored"}`
request := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "https://app.sendico.io/api/v1/accounts/refresh", strings.NewReader(body))
request.Header.Set(browser.TransportHeader, browser.TransportCookie)
request.AddCookie(&http.Cookie{Name: browser.RefreshCookie, Value: "cookie-refresh"})
response := httptest.NewRecorder()
router.refreshAccessToken(request)(response, request)
return response
}
func executeNativeRefresh(t *testing.T, router *PublicRouter, token string) *httptest.ResponseRecorder {
t.Helper()
body := `{"clientId":"web","deviceId":"browser-1","token":"` + token + `"}`
request := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "https://app.sendico.io/api/v1/accounts/refresh", strings.NewReader(body))
response := httptest.NewRecorder()
router.refreshAccessToken(request)(response, request)
return response
}
func TestBrowserRefreshRestoresAccessWithoutExposingRefreshCredential(t *testing.T) {
router, _ := newBrowserRefreshRouter(t, time.Now().Add(30*24*time.Hour))
response := executeBrowserRefresh(t, router)
require.Equal(t, http.StatusOK, response.Code)
require.NotContains(t, response.Body.String(), "cookie-refresh")
require.NotEmpty(t, decodeLoginResponse(t, response.Body.Bytes()).Data.AccessToken.Token)
require.Empty(t, response.Header().Get("Set-Cookie"))
}
func TestBrowserRefreshRotatesExpiringCookieServerSide(t *testing.T) {
t.Setenv("API_ENDPOINT", "/api/v1")
router, rtdb := newBrowserRefreshRouter(t, time.Now().Add(24*time.Hour))
response := executeBrowserRefresh(t, router)
require.Equal(t, http.StatusOK, response.Code)
require.NotContains(t, response.Body.String(), "cookie-refresh")
cookies := response.Result().Cookies()
require.Len(t, cookies, 1)
require.Equal(t, browser.RefreshCookie, cookies[0].Name)
require.NotEqual(t, "cookie-refresh", cookies[0].Value)
require.Empty(t, decodeLoginResponse(t, response.Body.Bytes()).Data.RefreshToken.Token)
require.Equal(t, cookies[0].Value, rtdb.tokens[0].RefreshToken)
}
func TestBrowserRefreshRejectsStaleCookieAndClearsIt(t *testing.T) {
t.Setenv("API_ENDPOINT", "/api/v1")
router, rtdb := newBrowserRefreshRouter(t, time.Now().Add(30*24*time.Hour))
rtdb.tokens = nil
response := executeBrowserRefresh(t, router)
require.Equal(t, http.StatusUnauthorized, response.Code)
require.Contains(t, response.Body.String(), `"error":"unauthorized"`)
require.NotContains(t, response.Body.String(), "cookie-refresh")
cookies := response.Result().Cookies()
require.Len(t, cookies, 1)
require.Equal(t, browser.RefreshCookie, cookies[0].Name)
require.Empty(t, cookies[0].Value)
require.Equal(t, -1, cookies[0].MaxAge)
require.True(t, cookies[0].HttpOnly)
require.True(t, cookies[0].Secure)
}
func TestNativeRefreshRejectsStaleTokenWithoutCookieMutation(t *testing.T) {
router, _ := newBrowserRefreshRouter(t, time.Now().Add(30*24*time.Hour))
response := executeNativeRefresh(t, router, "stale-refresh")
require.Equal(t, http.StatusUnauthorized, response.Code)
require.Contains(t, response.Body.String(), `"error":"unauthorized"`)
require.Empty(t, response.Header().Get("Set-Cookie"))
}
func TestRefreshRejectsInactiveCustomerAndClientMismatch(t *testing.T) {
for _, status := range []customermodel.AccountStatus{customermodel.AccountBlocked, customermodel.AccountPendingVerification} {
t.Run(string(status), func(t *testing.T) {
router, _ := newBrowserRefreshRouter(t, time.Now().Add(24*time.Hour))
router.db.(*testAccountDB).result.Status = status
require.Equal(t, http.StatusUnauthorized, executeBrowserRefresh(t, router).Code)
})
}
router, sessions := newBrowserRefreshRouter(t, time.Now().Add(24*time.Hour))
other := testActiveAccount(t)
sessions.client.AccountRef = &other.ID
require.Equal(t, http.StatusUnauthorized, executeBrowserRefresh(t, router).Code)
}