rastrillo / native Public

Clone
git clone https://amadan.net/rastrillo/native

Plain git — no account needed to clone.

Download

Download this file

1package nativeauth
2
3import (
4 "crypto/sha256"
5 "encoding/base64"
6 "encoding/json"
7 "github.com/carlosframework/rastrillo/db"
8 "github.com/carlosframework/rastrillo/migrate"
9 "github.com/carlosframework/rastrillo/sessions"
10 "net/http"
11 "net/http/httptest"
12 "path/filepath"
13 "strings"
14 "testing"
15 "time"
16)
17
18// Negative cases independently selected by native_review on 2026-09-12.
19// Mutation-checked that day: removing proof, expiry, consume, approval admission,
20// exchange admission or fresh-context handling each fails; all reverted.
21func TestProofApprovalAndRevocation(t *testing.T) {
22 d, err := db.Open(filepath.Join(t.TempDir(), "sessions.db"), nil)
23 if err != nil {
24 t.Fatal(err)
25 }
26 defer d.Close()
27 if _, err = migrate.Apply(t.Context(), d, sessions.Schema); err != nil {
28 t.Fatal(err)
29 }
30 s, err := sessions.New(sessions.Config{DB: d.Writer(), Origin: "https://app.test"})
31 if err != nil {
32 t.Fatal(err)
33 }
34 admitted := map[string]bool{"alice": true, "bob": true}
35 require := func(next http.Handler) http.Handler {
36 return s.Require(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
37 v, _ := sessions.Current(r)
38 if !admitted[v.Subject] {
39 http.Error(w, "denied", 403)
40 return
41 }
42 next.ServeHTTP(w, r)
43 }))
44 }
45 a := New(s, "https://app.test", "Test", require)
46 alice, _ := s.Mint(sessions.Session{Subject: "alice"})
47 bob, _ := s.Mint(sessions.Session{Subject: "bob"})
48 drive := func(method, path, body, cookie, origin string) *httptest.ResponseRecorder {
49 r := httptest.NewRequest(method, "https://app.test"+path, strings.NewReader(body))
50 r.Header.Set("Content-Type", "application/json")
51 r.Header.Set("Origin", origin)
52 if cookie != "" {
53 r.AddCookie(&http.Cookie{Name: s.CookieName(), Value: cookie})
54 }
55 w := httptest.NewRecorder()
56 s.Middleware(a).ServeHTTP(w, r)
57 return w
58 }
59 begin := func() (string, string) {
60 v := token()
61 hash := sha256.Sum256([]byte(v))
62 w := drive("POST", "/native/request", `{"challenge":"`+base64.RawURLEncoding.EncodeToString(hash[:])+`"}`, "", "https://app.test")
63 if w.Code != 200 {
64 t.Fatalf("begin: %d", w.Code)
65 }
66 var out map[string]string
67 json.Unmarshal(w.Body.Bytes(), &out)
68 return out["id"], v
69 }
70 exchange := func(id, v, cookie string) *httptest.ResponseRecorder {
71 return drive("POST", "/native/exchange", `{"id":"`+id+`","verifier":"`+v+`"}`, cookie, "https://app.test")
72 }
73 approve := func(id, cookie string) *httptest.ResponseRecorder {
74 return drive("POST", "/native/authorize?id="+id, "", cookie, "https://app.test")
75 }
76 id, v := begin()
77 if w := exchange(id, v, ""); w.Code != 202 {
78 t.Fatalf("pending %d", w.Code)
79 }
80 if w := drive("POST", "/native/authorize?id="+id, "", alice, "https://evil.test"); w.Code < 400 {
81 t.Fatal("cross-origin approval accepted")
82 }
83 if w := approve(id, alice); w.Code != 200 {
84 t.Fatalf("approve %d", w.Code)
85 }
86 if w := exchange(id, token(), ""); w.Code != 404 {
87 t.Fatal("wrong proof accepted")
88 }
89 w := exchange(id, v, "")
90 if w.Code != 200 {
91 t.Fatalf("exchange %d", w.Code)
92 }
93 var out map[string]string
94 json.Unmarshal(w.Body.Bytes(), &out)
95 r := httptest.NewRequest("GET", "https://app.test/", nil)
96 r.AddCookie(&http.Cookie{Name: out["name"], Value: out["value"]})
97 got, ok := s.From(r)
98 if !ok || got.Subject != "alice" || out["value"] == alice {
99 t.Fatal("exchange did not mint separate approved identity")
100 }
101 if exchange(id, v, "").Code != 404 {
102 t.Fatal("replay accepted")
103 }
104 id, v = begin()
105 a.pending[id].expires = time.Now().Add(-time.Second)
106 if exchange(id, v, "").Code != 404 {
107 t.Fatal("expired proof accepted")
108 }
109 id, v = begin()
110 if approve(id, alice).Code != 200 {
111 t.Fatal("approval failed")
112 }
113 admitted["alice"] = false
114 // An authenticated exchange caller must never substitute its admission for
115 // the identity approved in the browser (the review's concrete regression).
116 if exchange(id, v, bob).Code != 401 {
117 t.Fatal("revoked approval accepted through another session's context")
118 }
119 id, _ = begin()
120 if approve(id, alice).Code != 403 {
121 t.Fatal("removed member could approve")
122 }
123}
124