package auth import ( "errors" "net/http" "strings" "testing" "time" ) func TestLocalJWTMintVerify(t *testing.T) { l := NewLocalJWT("test-secret") tok, err := l.Mint("device-1", []string{"mcp"}, time.Hour) if err != nil { t.Fatal(err) } if !strings.HasPrefix(tok, "eyJ") { t.Fatalf("token should start with base64 header: %q", tok[:8]) } p, err := l.Verify(tok) if err != nil { t.Fatal(err) } if p.Subject != "device-1" || p.Kind != KindDevice || !p.HasScope("mcp") { t.Fatalf("principal = %+v", p) } } func TestLocalJWTTampered(t *testing.T) { l := NewLocalJWT("test-secret") tok, _ := l.Mint("d", nil, time.Hour) // Flip one char in the payload segment. parts := strings.Split(tok, ".") payload := []byte(parts[1]) payload[0] ^= 1 tampered := parts[0] + "." + string(payload) + "." + parts[2] if _, err := l.Verify(tampered); err == nil { t.Fatal("tampered token verified") } // Wrong secret. l2 := NewLocalJWT("other-secret") if _, err := l2.Verify(tok); err == nil { t.Fatal("token verified with wrong secret") } } func TestLocalJWTExpiry(t *testing.T) { l := NewLocalJWT("s") // Pin the clock. now := time.Unix(1_700_000_000, 0) l.now = func() time.Time { return now } tok, _ := l.Mint("d", nil, time.Minute) l.now = func() time.Time { return now.Add(2 * time.Minute) } if _, err := l.Verify(tok); !errors.Is(err, ErrTokenExpired) { t.Fatalf("err = %v, want ErrTokenExpired", err) } } func TestLocalJWTDisabled(t *testing.T) { l := NewLocalJWT("") if l.Enabled() { t.Fatal("empty secret should be disabled") } if _, err := l.Mint("d", nil, time.Minute); err == nil { t.Fatal("mint with empty secret should fail") } if _, err := l.Verify("eyJ.eyJ.abc"); !errors.Is(err, ErrNoCredentials) { t.Fatalf("err = %v, want ErrNoCredentials", err) } } func TestLocalJWTAuthenticate(t *testing.T) { l := NewLocalJWT("s") tok, _ := l.Mint("d", nil, time.Minute) req, _ := http.NewRequest(http.MethodGet, "/", nil) req.Header.Set("Authorization", "Bearer "+tok) p, err := l.Authenticate(req) if err != nil || p.Subject != "d" { t.Fatalf("authenticate: %v %+v", err, p) } // No credentials. req2, _ := http.NewRequest(http.MethodGet, "/", nil) if _, err := l.Authenticate(req2); !errors.Is(err, ErrNoCredentials) { t.Fatalf("err = %v, want ErrNoCredentials", err) } } func TestDeviceFlowFull(t *testing.T) { f := NewDeviceFlow(NewLocalJWT("s"), DeviceFlowOptions{BaseURL: "https://admin.example.com"}) dc, uc := mustStartScoped(t, f) // No requested scopes: the approver's scopes become the token's. if err := f.Approve(uc, &Principal{Subject: "user-1", Scopes: []string{"mcp", "write"}}); err != nil { t.Fatal(err) } tok, err := f.Token(dc) if err != nil { t.Fatal(err) } p, err := f.jwt.Verify(tok) if err != nil { t.Fatal(err) } if p.Subject != "user-1" || !p.HasScope("mcp") || !p.HasScope("write") { t.Fatalf("token principal = %+v", p) } // One-shot: second poll fails. if _, err := f.Token(dc); !errors.Is(err, ErrDeviceUnknown) { t.Fatalf("err = %v, want ErrDeviceUnknown after consumption", err) } } func TestDeviceFlowRequestedScopes(t *testing.T) { f := NewDeviceFlow(NewLocalJWT("s"), DeviceFlowOptions{}) dc, uc := startScoped(t, f, "mcp", "write") // Requested scopes show up for the approve page. req, ok := f.RequestedScopes(uc) if !ok || len(req) != 2 { t.Fatalf("requested = %v %v, want [mcp write]", req, ok) } // Approver without scopes (browser admin session) passes requested through. if err := f.Approve(uc, &Principal{Subject: "admin"}); err != nil { t.Fatal(err) } tok, err := f.Token(dc) if err != nil { t.Fatal(err) } p, _ := f.jwt.Verify(tok) if !p.HasScope("mcp") || !p.HasScope("write") { t.Fatalf("requested scopes should pass through: %+v", p) } // No requested scopes: the approver's scopes become the token's. dc0, uc0 := mustStartScoped(t, f) if err := f.Approve(uc0, &Principal{Subject: "admin", Scopes: []string{"read"}}); err != nil { t.Fatal(err) } tok0, err := f.Token(dc0) if err != nil { t.Fatal(err) } p0, _ := f.jwt.Verify(tok0) if !p0.HasScope("read") { t.Fatalf("no requested scopes should use approver scopes: %+v", p0) } // Requested ∩ approver: request [mcp, read], approver [write] → [] (denied // everything the approver can't grant). dc2, uc2 := startScoped(t, f, "mcp", "read") if err := f.Approve(uc2, &Principal{Subject: "admin", Scopes: []string{"write"}}); err != nil { t.Fatal(err) } tok2, err := f.Token(dc2) if err != nil { t.Fatal(err) } p2, _ := f.jwt.Verify(tok2) if len(p2.Scopes) != 0 { t.Fatalf("no overlap should grant nothing: %+v", p2) } // Approver with admin grants any requested scope. dcAdm, ucAdm := startScoped(t, f, "mcp", "read", "write") if err := f.Approve(ucAdm, &Principal{Subject: "boss", Scopes: []string{"admin"}}); err != nil { t.Fatal(err) } tokAdm, err := f.Token(dcAdm) if err != nil { t.Fatal(err) } pAdm, _ := f.jwt.Verify(tokAdm) for _, s := range []string{"mcp", "read", "write"} { if !pAdm.HasScope(s) { t.Fatalf("admin approver should grant %q: %+v", s, pAdm) } } // Requested [mcp] ∩ approver [mcp, read] → [mcp]. dc3, uc3 := startScoped(t, f, "mcp") // RequestedScopes reflects the original request (before consumption). rq, ok := f.RequestedScopes(uc3) if !ok || len(rq) != 1 || rq[0] != "mcp" { t.Fatalf("requested = %v %v", rq, ok) } if err := f.Approve(uc3, &Principal{Subject: "admin", Scopes: []string{"mcp", "read"}}); err != nil { t.Fatal(err) } tok3, err := f.Token(dc3) if err != nil { t.Fatal(err) } p3, _ := f.jwt.Verify(tok3) if len(p3.Scopes) != 1 || !p3.HasScope("mcp") { t.Fatalf("intersection should grant mcp only: %+v", p3) } // Unknown code. if _, ok := f.RequestedScopes("NOPE-XXXX"); ok { t.Fatal("unknown code should not report scopes") } } func TestDeviceFlowGrantInfo(t *testing.T) { now := time.Now() f := NewDeviceFlow(NewLocalJWT("s"), DeviceFlowOptions{TTL: 10 * time.Minute}) f.now = func() time.Time { return now } _, uc := mustStartScoped(t, f, "mcp", "read") scopes, expiresIn, ok := f.GrantInfo(uc) if !ok { t.Fatal("pending grant should report info") } if len(scopes) != 2 || scopes[0] != "mcp" || scopes[1] != "read" { t.Fatalf("scopes = %v, want [mcp read]", scopes) } if expiresIn <= 0 || expiresIn > 600 { t.Fatalf("expiresIn = %d, want 0 < x <= 600", expiresIn) } // Unknown code. if _, _, ok := f.GrantInfo("NOPE-XXXX"); ok { t.Fatal("unknown code should report not found") } // Expired code reports not found (clock advanced past TTL). f.now = func() time.Time { return now.Add(11 * time.Minute) } if _, _, ok := f.GrantInfo(uc); ok { t.Fatal("expired code should report not found") } } func TestDeviceFlowFullLegacy(t *testing.T) { f := NewDeviceFlow(NewLocalJWT("s"), DeviceFlowOptions{BaseURL: "https://admin.example.com"}) dc, uc, uri, complete, exp, interval := mustStart(t, f) if !strings.Contains(uri, "https://admin.example.com/device") { t.Fatalf("verification_uri = %q", uri) } if !strings.Contains(complete, "code="+uc) { t.Fatalf("verification_uri_complete = %q", complete) } if exp <= 0 || interval <= 0 { t.Fatalf("expires_in=%d interval=%d", exp, interval) } // Pending before approval. if _, err := f.Token(dc); !errors.Is(err, ErrAuthorizationPending) { t.Fatalf("err = %v, want pending", err) } // Unknown code. if err := f.Approve("NOPE-XXXX", &Principal{Subject: "u"}); err == nil { t.Fatal("approve with unknown code should fail") } // Approve with a valid code format. if err := f.Approve(uc, &Principal{Subject: "user-1", Scopes: []string{"mcp", "write"}}); err != nil { t.Fatal(err) } tok, err := f.Token(dc) if err != nil { t.Fatal(err) } p, err := f.jwt.Verify(tok) if err != nil { t.Fatal(err) } if p.Subject != "user-1" || !p.HasScope("mcp") || !p.HasScope("write") { t.Fatalf("token principal = %+v", p) } // One-shot: second poll fails. if _, err := f.Token(dc); !errors.Is(err, ErrDeviceUnknown) { t.Fatalf("err = %v, want ErrDeviceUnknown after consumption", err) } } func TestDeviceFlowExpiry(t *testing.T) { now := time.Now() f := NewDeviceFlow(NewLocalJWT("s"), DeviceFlowOptions{}) f.now = func() time.Time { return now } dc, uc, _, _, _, _ := mustStart(t, f) // Advance past TTL. f.now = func() time.Time { return now.Add(11 * time.Minute) } if err := f.Approve(uc, &Principal{Subject: "u"}); !errors.Is(err, ErrDeviceUnknown) { t.Fatalf("approve expired = %v, want ErrDeviceUnknown", err) } if _, err := f.Token(dc); !errors.Is(err, ErrDeviceExpired) { t.Fatalf("token expired = %v, want ErrDeviceExpired", err) } } func TestDeviceFlowTokenDisabled(t *testing.T) { f := NewDeviceFlow(NewLocalJWT(""), DeviceFlowOptions{}) dc, uc, _, _, _, _ := mustStart(t, f) if err := f.Approve(uc, &Principal{Subject: "u"}); err != nil { t.Fatal(err) } if _, err := f.Token(dc); err == nil { t.Fatal("token with disabled jwt should fail") } } func TestDeviceFlowUserCodeFormat(t *testing.T) { f := NewDeviceFlow(NewLocalJWT("s"), DeviceFlowOptions{}) _, uc, _, _, _, _ := mustStart(t, f) if len(uc) != 9 || uc[4] != '-' { t.Fatalf("user code %q must be XXXX-XXXX", uc) } for _, c := range uc { if c == '-' { continue } if !strings.ContainsRune(userCodeAlphabet, c) { t.Fatalf("user code %q contains char outside alphabet", uc) } } } func mustStart(t *testing.T, f *DeviceFlow) (dc, uc, uri, complete string, exp, interval int) { t.Helper() dc, uc, uri, complete, exp, interval, err := f.Start() if err != nil { t.Fatal(err) } return dc, uc, uri, complete, exp, interval } func mustStartScoped(t *testing.T, f *DeviceFlow, scopes ...string) (dc, uc string) { t.Helper() dc, uc, _, _, _, _, err := f.Start(scopes...) if err != nil { t.Fatal(err) } return dc, uc } func startScoped(t *testing.T, f *DeviceFlow, scopes ...string) (dc, uc string) { t.Helper() dc, uc, _, _, _, _, err := f.Start(scopes...) if err != nil { t.Fatal(err) } return dc, uc }