package nativeauth import ( "crypto/sha256" "encoding/base64" "encoding/json" "github.com/carlosframework/rastrillo/db" "github.com/carlosframework/rastrillo/migrate" "github.com/carlosframework/rastrillo/sessions" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "time" ) // Negative cases independently selected by native_review on 2026-09-12. // Mutation-checked that day: removing proof, expiry, consume, approval admission, // exchange admission or fresh-context handling each fails; all reverted. func TestProofApprovalAndRevocation(t *testing.T) { d, err := db.Open(filepath.Join(t.TempDir(), "sessions.db"), nil) if err != nil { t.Fatal(err) } defer d.Close() if _, err = migrate.Apply(t.Context(), d, sessions.Schema); err != nil { t.Fatal(err) } s, err := sessions.New(sessions.Config{DB: d.Writer(), Origin: "https://app.test"}) if err != nil { t.Fatal(err) } admitted := map[string]bool{"alice": true, "bob": true} require := func(next http.Handler) http.Handler { return s.Require(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { v, _ := sessions.Current(r) if !admitted[v.Subject] { http.Error(w, "denied", 403) return } next.ServeHTTP(w, r) })) } a := New(s, "https://app.test", "Test", require) alice, _ := s.Mint(sessions.Session{Subject: "alice"}) bob, _ := s.Mint(sessions.Session{Subject: "bob"}) drive := func(method, path, body, cookie, origin string) *httptest.ResponseRecorder { r := httptest.NewRequest(method, "https://app.test"+path, strings.NewReader(body)) r.Header.Set("Content-Type", "application/json") r.Header.Set("Origin", origin) if cookie != "" { r.AddCookie(&http.Cookie{Name: s.CookieName(), Value: cookie}) } w := httptest.NewRecorder() s.Middleware(a).ServeHTTP(w, r) return w } begin := func() (string, string) { v := token() hash := sha256.Sum256([]byte(v)) w := drive("POST", "/native/request", `{"challenge":"`+base64.RawURLEncoding.EncodeToString(hash[:])+`"}`, "", "https://app.test") if w.Code != 200 { t.Fatalf("begin: %d", w.Code) } var out map[string]string json.Unmarshal(w.Body.Bytes(), &out) return out["id"], v } exchange := func(id, v, cookie string) *httptest.ResponseRecorder { return drive("POST", "/native/exchange", `{"id":"`+id+`","verifier":"`+v+`"}`, cookie, "https://app.test") } approve := func(id, cookie string) *httptest.ResponseRecorder { return drive("POST", "/native/authorize?id="+id, "", cookie, "https://app.test") } id, v := begin() if w := exchange(id, v, ""); w.Code != 202 { t.Fatalf("pending %d", w.Code) } if w := drive("POST", "/native/authorize?id="+id, "", alice, "https://evil.test"); w.Code < 400 { t.Fatal("cross-origin approval accepted") } if w := approve(id, alice); w.Code != 200 { t.Fatalf("approve %d", w.Code) } if w := exchange(id, token(), ""); w.Code != 404 { t.Fatal("wrong proof accepted") } w := exchange(id, v, "") if w.Code != 200 { t.Fatalf("exchange %d", w.Code) } var out map[string]string json.Unmarshal(w.Body.Bytes(), &out) r := httptest.NewRequest("GET", "https://app.test/", nil) r.AddCookie(&http.Cookie{Name: out["name"], Value: out["value"]}) got, ok := s.From(r) if !ok || got.Subject != "alice" || out["value"] == alice { t.Fatal("exchange did not mint separate approved identity") } if exchange(id, v, "").Code != 404 { t.Fatal("replay accepted") } id, v = begin() a.pending[id].expires = time.Now().Add(-time.Second) if exchange(id, v, "").Code != 404 { t.Fatal("expired proof accepted") } id, v = begin() if approve(id, alice).Code != 200 { t.Fatal("approval failed") } admitted["alice"] = false // An authenticated exchange caller must never substitute its admission for // the identity approved in the browser (the review's concrete regression). if exchange(id, v, bob).Code != 401 { t.Fatal("revoked approval accepted through another session's context") } id, _ = begin() if approve(id, alice).Code != 403 { t.Fatal("removed member could approve") } }