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:
1 parent
2f3ea99745
commit
0423723d43
2 files changed
+47
No files matched your search
@@ -144,6 +144,27 @@ func (f *DeviceFlow) Approve(userCode string, approver *Principal) error {
|
||||
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.
|
||||
// The approve UI shows them so the admin consents to exactly what the
|
||||
// companion app will get. Returns (nil, false) for unknown/expired codes.
|
||||
|
||||
@@ -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) {
|
||||
f := NewDeviceFlow(NewLocalJWT("s"), DeviceFlowOptions{BaseURL: "https://admin.example.com"})
|
||||
dc, uc, uri, complete, exp, interval := mustStart(t, f)
|
||||
|
||||
Reference in new issue
Block a user