diff --git a/TESTING-AUTH-API.md b/TESTING-AUTH-API.md new file mode 100644 index 0000000..310f008 --- /dev/null +++ b/TESTING-AUTH-API.md @@ -0,0 +1,127 @@ +# Testing Content-Type Aware Authentication + +## What Was Fixed + +The `require-role` function now detects if a request is an API call (contains `/api/` in the URI) and returns appropriate responses: +- **API requests**: JSON error with HTTP 403 status +- **Page requests**: HTML redirect to login page + +## How to Test + +### 1. Rebuild and Start Server + +```bash +make +./asteroid +``` + +### 2. Test API Endpoint (Should Return JSON) + +**Test without login (should get JSON 403):** + +```bash +# Using curl +curl -i http://localhost:8080/asteroid/api/tracks + +# Expected output: +HTTP/1.1 403 Forbidden +Content-Type: application/json +... +{"error":"Authentication required","status":403,"message":"You must be logged in with LISTENER role to access this resource"} +``` + +**Test with browser console (while NOT logged in):** + +```javascript +// Open browser console (F12) on http://localhost:8080/asteroid/ +fetch('/asteroid/api/tracks') + .then(r => r.json()) + .then(data => console.log('Response:', data)) + .catch(err => console.error('Error:', err)); + +// Expected output: +// Response: {error: "Authentication required", status: 403, message: "..."} +``` + +### 3. Test Page Endpoint (Should Redirect) + +**Visit a protected page without login:** + +```bash +# Using curl (follow redirects) +curl -L http://localhost:8080/asteroid/admin + +# Should redirect to login page and show HTML +``` + +**Or in browser:** +- Visit: http://localhost:8080/asteroid/admin +- Should redirect to: http://localhost:8080/asteroid/login + +### 4. Test After Login + +**Login first, then test API:** + +```javascript +// 1. Login via browser at /asteroid/login +// 2. Then in console: +fetch('/asteroid/api/tracks') + .then(r => r.json()) + .then(data => console.log('Tracks:', data)) + .catch(err => console.error('Error:', err)); + +// Should now return actual track data (or empty array) +``` + +### 5. Test Player Page + +**The original issue - player page calling API:** + +1. **Without login:** + - Visit: http://localhost:8080/asteroid/player + - Open browser console (F12) + - Check Network tab for `/api/tracks` request + - Should see: Status 403, Response Type: json + - JavaScript should handle error gracefully (not crash) + +2. **With login:** + - Login at: http://localhost:8080/asteroid/login + - Visit: http://localhost:8080/asteroid/player + - API calls should work normally + +## Expected Behavior + +### Before Fix ❌ +``` +API Request → Not Authenticated → Redirect to /login → Returns HTML → JavaScript breaks +``` + +### After Fix ✅ +``` +API Request → Not Authenticated → Return JSON 403 → JavaScript handles error gracefully +Page Request → Not Authenticated → Redirect to /login → User sees login page +``` + +## Debugging + +Check server logs for these messages: + +``` +Request URI: /asteroid/api/tracks, Is API: YES +Role check failed - returning JSON 403 +``` + +Or for page requests: + +``` +Request URI: /asteroid/admin, Is API: NO +Role check failed - redirecting to login +``` + +## Success Criteria + +✅ API endpoints return JSON errors (not HTML redirects) +✅ Page requests still redirect to login +✅ Player page doesn't crash when not logged in +✅ JavaScript can properly handle 403 errors +✅ HTTP status code is 403 (not 302 redirect) diff --git a/user-management.lisp b/user-management.lisp index 39c0005..c4b95b1 100644 --- a/user-management.lisp +++ b/user-management.lisp @@ -3,6 +3,16 @@ (in-package :asteroid) +;; Define a condition for API authentication errors +(define-condition api-auth-error (error) + ((status-code :initarg :status-code :reader status-code) + (json-response :initarg :json-response :reader json-response)) + (:documentation "Condition signaled when API authentication fails")) + +(defun get-json-response (condition) + "Return JSON response from an api-auth-error condition" + (json-response condition)) + ;; User roles and permissions (defparameter *user-roles* '(:listener :dj :admin)) @@ -121,28 +131,93 @@ (format t "Error getting current user: ~a~%" e) nil))) -(defun require-authentication () - "Require user to be authenticated" +(defun require-authentication (&key (api nil)) + "Require user to be authenticated. + If :api t, returns JSON error (401). Otherwise redirects to login page. + Auto-detects API routes if not specified." (handler-case - (unless (session:field "user-id") - (radiance:redirect "/asteroid/login")) + (let* ((user-id (session:field "user-id")) + (uri (uri-to-url (radiance:uri *request*) :representation :external)) + ;; Use explicit flag if provided, otherwise auto-detect from URI + (is-api-request (if api t (search "/api/" uri)))) + (format t "Authentication check - User ID: ~a, URI: ~a, Is API: ~a~%" + user-id uri (if is-api-request "YES" "NO")) + (unless user-id + (if is-api-request + ;; API request - return JSON error with 401 status + (progn + (format t "Authentication failed - returning JSON 401~%") + (setf (radiance:header "Content-Type") "application/json") + (setf (radiance:response-data) + (cl-json:encode-json-to-string + `(("error" . "Authentication required") + ("status" . 401) + ("message" . "You must be logged in to access this resource")))) + (radiance:redirect (radiance:uri))) + ;; Page request - redirect to login + (progn + (format t "Authentication failed - redirecting to login~%") + (radiance:redirect "/asteroid/login"))))) + (api-auth-error (e) + (format t "API auth error caught, returning JSON~%") + (get-json-response e)) (error (e) (format t "Authentication error: ~a~%" e) - (radiance:redirect "/asteroid/login")))) + (let* ((uri (uri-to-url (radiance:uri *request*) :representation :external)) + (is-api-request (if api t (search "/api/" uri)))) + (if is-api-request + (progn + (setf (radiance:header "Content-Type") "application/json") + (cl-json:encode-json-to-string + `(("error" . "Internal server error") + ("status" . 500) + ("message" . ,(format nil "~a" e))))) + (radiance:redirect "/asteroid/login")))))) -(defun require-role (role) - "Require user to have a specific role" +(defun require-role (role &key (api nil)) + "Require user to have a specific role. + If :api t, returns JSON error (403). Otherwise redirects to login page. + Auto-detects API routes if not specified." (handler-case - (let ((current-user (get-current-user))) + (let* ((current-user (get-current-user)) + (uri (uri-to-url (radiance:uri *request*) :representation :external)) + ;; Use explicit flag if provided, otherwise auto-detect from URI + (is-api-request (if api t (search "/api/" uri)))) (format t "Current user for role check: ~a~%" (if current-user "FOUND" "NOT FOUND")) + (format t "Request URI: ~a, Is API: ~a~%" uri (if is-api-request "YES" "NO")) (when current-user (format t "User has role ~a: ~a~%" role (user-has-role-p current-user role))) (unless (and current-user (user-has-role-p current-user role)) - (format t "Role check failed - redirecting to login~%") - (radiance:redirect "/asteroid/login"))) + (if is-api-request + ;; API request - return JSON error with 403 status + (progn + (format t "Role check failed - returning JSON 403~%") + (setf (radiance:header "Content-Type") "application/json") + (error 'api-auth-error + :status-code 403 + :json-response (cl-json:encode-json-to-string + `(("error" . "Authentication required") + ("status" . 403) + ("message" . ,(format nil "You must be logged in with ~a role to access this resource" role)))))) + ;; Page request - redirect to login + (progn + (format t "Role check failed - redirecting to login~%") + (radiance:redirect "/asteroid/login"))))) + (api-auth-error (e) + (format t "API auth error caught in require-role, returning JSON~%") + (get-json-response e)) (error (e) (format t "Role check error: ~a~%" e) - (radiance:redirect "/asteroid/login")))) + (let* ((uri (uri-to-url (radiance:uri *request*) :representation :external)) + (is-api-request (if api t (search "/api/" uri)))) + (if is-api-request + (progn + (setf (radiance:header "Content-Type") "application/json") + (cl-json:encode-json-to-string + `(("error" . "Internal server error") + ("status" . 500) + ("message" . ,(format nil "~a" e))))) + (radiance:redirect "/asteroid/login")))))) (defun update-user-role (user-id new-role) "Update a user's role"