| 1 | package auth |
| 2 | |
| 3 | import ( |
| 4 | "encoding/base64" |
| 5 | "strings" |
| 6 | "sync" |
| 7 | "sync/atomic" |
| 8 | "testing" |
| 9 | "time" |
| 10 | |
| 11 | "github.com/FelineStateMachine/tinyrelay/protocol/nostr" |
| 12 | ) |
| 13 | |
| 14 | func TestNIP98ReplayWindowBoundary(t *testing.T) { |
| 15 | now := time.Unix(1700000000, 0) |
| 16 | v := NewValidator(func() time.Time { return now }) |
| 17 | // A future-dated proof is still fresh after its first replay window ends. |
| 18 | e := signedEvent(t, 27235, now.Unix()+60, "" , [][]string{{"u" , "https://relay.example/api" }, {"method" , "GET" }}) |
| 19 | header := token(t, e) |
| 20 | if _, err := v.VerifyNIP98(header, "https://relay.example/api" , "GET" , "" ); err != nil { |
| 21 | t.Fatal(err) |
| 22 | } |
| 23 | now = now.Add(time.Minute) |
| 24 | if _, err := v.VerifyNIP98(header, "https://relay.example/api" , "GET" , "" ); err == nil || !strings.Contains(err.Error(), "replay" ) { |
| 25 | t.Fatalf("proof must remain seen at exactly one minute: %v" , err) |
| 26 | } |
| 27 | now = now.Add(time.Second) |
| 28 | if _, err := v.VerifyNIP98(header, "https://relay.example/api" , "GET" , "" ); err != nil { |
| 29 | t.Fatalf("expired replay entry should be pruned: %v" , err) |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | func TestNIP98AcceptsAllExistingBase64Forms(t *testing.T) { |
| 34 | now := time.Unix(1700000000, 0) |
| 35 | e := signedEvent(t, 27235, now.Unix(), "\u083e\u083f" , [][]string{{"u" , "https://relay.example/api" }, {"method" , "GET" }}) |
| 36 | raw, err := nostr.Canonical(e) |
| 37 | if err != nil { |
| 38 | t.Fatal(err) |
| 39 | } |
| 40 | for _, encoding := range []*base64.Encoding{base64.StdEncoding, base64.RawStdEncoding, base64.URLEncoding, base64.RawURLEncoding} { |
| 41 | v := NewValidator(func() time.Time { return now }) |
| 42 | header := "nOsTr " + encoding.EncodeToString(raw) |
| 43 | if _, err := v.VerifyNIP98(header, "https://relay.example/api" , "GET" , "" ); err != nil { |
| 44 | t.Fatalf("base64 form rejected: %v" , err) |
| 45 | } |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | func TestNIP42ChallengeExpiresAtOneMinute(t *testing.T) { |
| 50 | now := time.Unix(1700000000, 0) |
| 51 | m := NewChallengeManager("wss://relay.example" , func() time.Time { return now }) |
| 52 | challenge, err := m.Issue() |
| 53 | if err != nil { |
| 54 | t.Fatal(err) |
| 55 | } |
| 56 | e := signedEvent(t, 22242, now.Unix(), "" , [][]string{{"relay" , "wss://relay.example" }, {"challenge" , challenge}}) |
| 57 | now = now.Add(time.Minute) |
| 58 | if err := m.Verify(e); err == nil || !strings.Contains(err.Error(), "challenge" ) { |
| 59 | t.Fatalf("challenge must expire even while timestamp remains fresh: %v" , err) |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | func TestConcurrentProofConsumptionIsSingleUse(t *testing.T) { |
| 64 | now := time.Unix(1700000000, 0) |
| 65 | v := NewValidator(func() time.Time { return now }) |
| 66 | e := signedEvent(t, 27235, now.Unix(), "" , [][]string{{"u" , "https://relay.example/api" }, {"method" , "GET" }}) |
| 67 | header := token(t, e) |
| 68 | m := NewChallengeManager("wss://relay.example" , func() time.Time { return now }) |
| 69 | challenge, err := m.Issue() |
| 70 | if err != nil { |
| 71 | t.Fatal(err) |
| 72 | } |
| 73 | authEvent := signedEvent(t, 22242, now.Unix(), "" , [][]string{{"relay" , "wss://relay.example" }, {"challenge" , challenge}}) |
| 74 | for name, verify := range map[string]func() error{ |
| 75 | "NIP-98" : func() error { _, err := v.VerifyNIP98(header, "https://relay.example/api" , "GET" , "" ); return err }, |
| 76 | "NIP-42" : func() error { return m.Verify(authEvent) }, |
| 77 | } { |
| 78 | t.Run(name, func(t *testing.T) { |
| 79 | var successes atomic.Int32 |
| 80 | var wg sync.WaitGroup |
| 81 | for range 16 { |
| 82 | wg.Add(1) |
| 83 | go func() { |
| 84 | defer wg.Done() |
| 85 | if verify() == nil { |
| 86 | successes.Add(1) |
| 87 | } |
| 88 | }() |
| 89 | } |
| 90 | wg.Wait() |
| 91 | if got := successes.Load(); got != 1 { |
| 92 | t.Fatalf("successful consumers = %d, want 1" , got) |
| 93 | } |
| 94 | }) |
| 95 | } |
| 96 | } |