dae7bd4cc6
implement GET /identity * GET /identity
366 lines
9.5 KiB
Go
366 lines
9.5 KiB
Go
package cognitoauth
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
// TestJWTAuthMiddleware tests the JWTAuthMiddleware function to ensure it correctly processes
|
|
// authentication tokens from both headers and cookies. It verifies that:
|
|
// - Public paths (/login, /callback, /home) are skipped without authentication
|
|
// - The /logout path properly clears authentication cookies
|
|
// - Requests with Authorization headers are passed through
|
|
// - Requests with auth_token cookies have their tokens moved to Authorization headers
|
|
// - Requests without authentication are redirected to the login page
|
|
func TestJWTAuthMiddleware(t *testing.T) {
|
|
// Setup
|
|
e := echo.New()
|
|
config := &mockConfigProvider{
|
|
region: "us-east-2",
|
|
userPoolID: "us-east-2_testpool",
|
|
}
|
|
|
|
// Setup logger
|
|
logHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
|
Level: slog.LevelDebug,
|
|
})
|
|
logger := slog.New(logHandler)
|
|
config.SetAuthLogger(logger)
|
|
|
|
// Create middleware
|
|
middleware := JWTAuthMiddleware(config)
|
|
|
|
// Test handler that simply returns success
|
|
testHandler := func(c echo.Context) error {
|
|
return c.String(http.StatusOK, "success")
|
|
}
|
|
|
|
// Test cases
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
setupRequest func(*http.Request)
|
|
expectStatus int
|
|
expectLocation string
|
|
expectHeader string
|
|
}{
|
|
{
|
|
name: "skip login path",
|
|
path: "/login",
|
|
setupRequest: func(req *http.Request) {
|
|
// No setup needed
|
|
},
|
|
expectStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "skip callback path",
|
|
path: "/callback",
|
|
setupRequest: func(req *http.Request) {
|
|
// No setup needed
|
|
},
|
|
expectStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "skip home path",
|
|
path: "/home",
|
|
setupRequest: func(req *http.Request) {
|
|
// No setup needed
|
|
},
|
|
expectStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "logout path sets empty cookie",
|
|
path: "/logout",
|
|
setupRequest: func(req *http.Request) {
|
|
// No setup needed
|
|
},
|
|
expectStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "use authorization header",
|
|
path: "/api/test",
|
|
setupRequest: func(req *http.Request) {
|
|
req.Header.Set("Authorization", "Bearer test-token")
|
|
},
|
|
expectStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "use cookie if no authorization header",
|
|
path: "/api/test",
|
|
setupRequest: func(req *http.Request) {
|
|
cookie := &http.Cookie{
|
|
Name: "auth_token",
|
|
Value: "cookie-token",
|
|
Path: "/",
|
|
}
|
|
req.AddCookie(cookie)
|
|
},
|
|
expectStatus: http.StatusOK,
|
|
expectHeader: "Bearer cookie-token",
|
|
},
|
|
{
|
|
name: "continue to next middleware if no token",
|
|
path: "/api/test",
|
|
setupRequest: func(req *http.Request) {
|
|
// No auth header or cookie
|
|
},
|
|
expectStatus: http.StatusOK, // Should continue to test handler
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, tt.path, nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
// Setup request (add headers, cookies, etc.)
|
|
tt.setupRequest(req)
|
|
|
|
// Create Echo context
|
|
c := e.NewContext(req, rec)
|
|
c.SetPath(tt.path)
|
|
|
|
// Run middleware
|
|
err := middleware(testHandler)(c)
|
|
if err != nil {
|
|
t.Errorf("Middleware returned error: %v", err)
|
|
}
|
|
|
|
// Check status code
|
|
if rec.Code != tt.expectStatus {
|
|
t.Errorf("Expected status code %d, got %d", tt.expectStatus, rec.Code)
|
|
}
|
|
|
|
// Check location header (for redirects)
|
|
if tt.expectLocation != "" {
|
|
location := rec.Header().Get("Location")
|
|
if location != tt.expectLocation {
|
|
t.Errorf("Expected Location header %q, got %q", tt.expectLocation, location)
|
|
}
|
|
}
|
|
|
|
// Check Authorization header (if token taken from cookie)
|
|
if tt.expectHeader != "" {
|
|
authHeader := req.Header.Get("Authorization")
|
|
if authHeader != tt.expectHeader {
|
|
t.Errorf("Expected Authorization header %q, got %q", tt.expectHeader, authHeader)
|
|
}
|
|
}
|
|
|
|
// Special case for logout - check cookie
|
|
if tt.path == "/logout" {
|
|
cookies := rec.Result().Cookies()
|
|
found := false
|
|
for _, cookie := range cookies {
|
|
if cookie.Name == "auth_token" {
|
|
found = true
|
|
if cookie.Value != "" {
|
|
t.Errorf("Expected empty auth_token cookie, got %q", cookie.Value)
|
|
}
|
|
if !cookie.Expires.Before(time.Now()) {
|
|
t.Errorf("Expected auth_token cookie to be expired, got %v", cookie.Expires)
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("Expected auth_token cookie to be set for logout")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestTokenValidationMiddleware tests the TokenValidationMiddleware function to verify that it:
|
|
// - Allows access to public paths (/home, /logout) without validation
|
|
// - Properly handles OAuth callbacks by processing code and state parameters
|
|
// - Initiates login by redirecting to Cognito when accessing the login path
|
|
// - Returns 401 Unauthorized for protected paths without Authorization header
|
|
// - Attempts to validate tokens for protected paths with Authorization headers
|
|
func TestTokenValidationMiddleware(t *testing.T) {
|
|
// Setup
|
|
e := echo.New()
|
|
config := &mockConfigProvider{
|
|
region: "us-east-2",
|
|
userPoolID: "us-east-2_testpool",
|
|
}
|
|
|
|
// Setup logger
|
|
logHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
|
Level: slog.LevelDebug,
|
|
})
|
|
logger := slog.New(logHandler)
|
|
config.SetAuthLogger(logger)
|
|
|
|
// Create middleware
|
|
middleware := TokenValidationMiddleware(config)
|
|
|
|
// Test handler that simply returns success
|
|
testHandler := func(c echo.Context) error {
|
|
return c.String(http.StatusOK, "success")
|
|
}
|
|
|
|
// Test cases
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
setupRequest func(*http.Request)
|
|
expectStatus int
|
|
checkBody func(*testing.T, []byte)
|
|
}{
|
|
{
|
|
name: "skip public paths - home",
|
|
path: "/home",
|
|
setupRequest: func(req *http.Request) {
|
|
// No setup needed
|
|
},
|
|
expectStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "skip public paths - logout",
|
|
path: "/logout",
|
|
setupRequest: func(req *http.Request) {
|
|
// No setup needed
|
|
},
|
|
expectStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "handle oauth callback",
|
|
path: "/callback",
|
|
setupRequest: func(req *http.Request) {
|
|
q := req.URL.Query()
|
|
q.Add("code", "test-code")
|
|
q.Add("state", "test-state")
|
|
req.URL.RawQuery = q.Encode()
|
|
},
|
|
// This will attempt to call handleOAuthCallback which will fail
|
|
// but we're just testing the routing logic
|
|
expectStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "handle login initiation",
|
|
path: "/login",
|
|
setupRequest: func(req *http.Request) {
|
|
// No setup needed
|
|
},
|
|
// The login initiation actually redirects to the Cognito login page
|
|
expectStatus: http.StatusFound, // 302 Found (redirect)
|
|
// We can't check the exact URL since it depends on generateRandomString output
|
|
// But we can check that a Location header exists
|
|
checkBody: func(t *testing.T, body []byte) {
|
|
// No body check needed, we'll verify the status code
|
|
},
|
|
},
|
|
{
|
|
name: "missing authorization header",
|
|
path: "/api/test",
|
|
setupRequest: func(req *http.Request) {
|
|
// No auth header
|
|
},
|
|
expectStatus: http.StatusUnauthorized,
|
|
checkBody: func(t *testing.T, body []byte) {
|
|
var response map[string]string
|
|
err := json.Unmarshal(body, &response)
|
|
if err != nil {
|
|
t.Errorf("Failed to parse response: %v", err)
|
|
}
|
|
if response["error"] != "Authorization required" {
|
|
t.Errorf("Expected error 'Authorization required', got %q", response["error"])
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "invalid token",
|
|
path: "/api/test",
|
|
setupRequest: func(req *http.Request) {
|
|
req.Header.Set("Authorization", "Bearer invalid-token")
|
|
},
|
|
expectStatus: http.StatusInternalServerError, // Since we can't mock JWKS fetching
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, tt.path, nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
// Setup request
|
|
tt.setupRequest(req)
|
|
|
|
// Create Echo context
|
|
c := e.NewContext(req, rec)
|
|
c.SetPath(tt.path)
|
|
|
|
// Run middleware
|
|
_ = middleware(testHandler)(c)
|
|
|
|
// Check status code
|
|
if rec.Code != tt.expectStatus {
|
|
t.Errorf("Expected status code %d, got %d", tt.expectStatus, rec.Code)
|
|
}
|
|
|
|
// Check response body if needed
|
|
if tt.checkBody != nil {
|
|
tt.checkBody(t, rec.Body.Bytes())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIsAuthOnlyPath verifies that certain paths bypass authorization while still requiring authentication.
|
|
// The /identity endpoint should be accessible to all authenticated users regardless of their Permit.io roles,
|
|
// allowing them to discover their own roles/permissions.
|
|
func TestIsAuthOnlyPath(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "identity endpoint should bypass authorization",
|
|
path: "/identity",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "api endpoint should require authorization",
|
|
path: "/api/clients",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "documents endpoint should require authorization",
|
|
path: "/documents",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "health endpoint is not auth-only (it's public)",
|
|
path: "/health",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "similar but different path should require authorization",
|
|
path: "/identity/something",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "prefix match should not work",
|
|
path: "/identityinfo",
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := isAuthOnlyPath(tt.path)
|
|
if result != tt.expected {
|
|
t.Errorf("isAuthOnlyPath(%q) = %v, expected %v", tt.path, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|