feat(auth): GrantInfo — expose pending grant scopes + remaining TTL

The approve UI needs to show what a user code asks for and how long it
is valid; GrantInfo returns (scopes, secondsLeft, ok) without consuming
the grant (consumption stays one-shot in Token).
This commit is contained in:
mats committed 2026-08-16 14:29:22 +02:00
1 parent 2f3ea99745
commit 0423723d43
2 files changed
+47

No files matched your search

+21
View File
@@ -144,6 +144,27 @@ func (f *DeviceFlow) Approve(userCode string, approver *Principal) error {
return nil return nil
} }
// GrantInfo returns what a pending grant's client asked for (scopes) and
// how long the code is still valid (seconds). The approve UI shows both so
// the admin consents to exactly what the companion app will get and knows
// how long they have. Returns (nil, 0, false) for unknown/expired codes.
func (f *DeviceFlow) GrantInfo(userCode string) (scopes []string, expiresIn int, ok bool) {
if f == nil {
return nil, 0, false
}
f.mu.Lock()
defer f.mu.Unlock()
dc, ok := f.byCode[userCode]
if !ok {
return nil, 0, false
}
s, ok := f.pending[dc]
if !ok || f.now().After(s.expiresAt) {
return nil, 0, false
}
return append([]string(nil), s.requested...), int(s.expiresAt.Sub(f.now()) / time.Second), true
}
// RequestedScopes returns the scopes a pending grant's client asked for. // RequestedScopes returns the scopes a pending grant's client asked for.
// The approve UI shows them so the admin consents to exactly what the // The approve UI shows them so the admin consents to exactly what the
// companion app will get. Returns (nil, false) for unknown/expired codes. // companion app will get. Returns (nil, false) for unknown/expired codes.
+26
View File
@@ -195,6 +195,32 @@ func TestDeviceFlowRequestedScopes(t *testing.T) {
} }
} }
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) { func TestDeviceFlowFullLegacy(t *testing.T) {
f := NewDeviceFlow(NewLocalJWT("s"), DeviceFlowOptions{BaseURL: "https://admin.example.com"}) f := NewDeviceFlow(NewLocalJWT("s"), DeviceFlowOptions{BaseURL: "https://admin.example.com"})
dc, uc, uri, complete, exp, interval := mustStart(t, f) dc, uc, uri, complete, exp, interval := mustStart(t, f)