153 lines
5.4 KiB
Go
153 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},
|
|
nil,
|
|
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)
|
|
}
|