Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions authn/authn.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@ type identityKey struct{}

// Identity is who a request acts as, established by a Provider. Org "" is
// admin (all orgs); Write gates mutating RPCs; Sub is the principal for audit
// trails ("" for static tokens, the JWT sub for priompt-auth tokens).
// trails ("" for static tokens, the JWT sub for priompt-auth tokens). Expires is
// when the credential lapses (zero = never), so long-lived connections that
// authenticate once, such as NATS subscribers, can be closed at that point.
type Identity struct {
Sub string
Org string
Write bool
Sub string
Org string
Write bool
Expires time.Time
}

// Provider authenticates one kind of bearer credential. It receives the raw
Expand Down Expand Up @@ -98,7 +101,7 @@ func (p StaticProvider) Authenticate(_ context.Context, header string) (Identity
if !tok.Expires.IsZero() && time.Now().After(tok.Expires) {
return Identity{}, status.Error(codes.Unauthenticated, "token expired")
}
return Identity{Org: tok.Org, Write: tok.Write}, nil
return Identity{Org: tok.Org, Write: tok.Write, Expires: tok.Expires}, nil
}

// JWKSProvider authenticates JWT-shaped credentials against a priompt-auth
Expand All @@ -123,7 +126,7 @@ func (p JWKSProvider) Authenticate(_ context.Context, header string) (Identity,
if err != nil {
return Identity{}, ErrUnrecognized
}
return Identity{Sub: c.Sub, Org: c.Org, Write: c.RW}, nil
return Identity{Sub: c.Sub, Org: c.Org, Write: c.RW, Expires: time.Unix(c.Exp, 0)}, nil
}

// Interceptor authenticates every RPC through the provider chain and attaches
Expand Down
22 changes: 22 additions & 0 deletions authn/jwks_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package authn

import (
"context"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
Expand Down Expand Up @@ -106,3 +107,24 @@ func TestInterceptorJWT(t *testing.T) {
t.Error("malformed JWT must be rejected")
}
}

// TestIdentityExpires: both providers report when the credential lapses, so a
// connection that authenticated once (a NATS subscriber) can be closed then.
func TestIdentityExpires(t *testing.T) {
ctx := context.Background()
exp := time.Now().Add(time.Hour).Truncate(time.Second)
id, err := NewStatic(map[string]Token{"t1": {Org: "acme", Expires: exp}, "t2": {}}).Authenticate(ctx, "Bearer t1")
if err != nil || !id.Expires.Equal(exp) {
t.Fatalf("static: Expires = %v err=%v, want %v", id.Expires, err, exp)
}
if id, _ := NewStatic(map[string]Token{"t2": {}}).Authenticate(ctx, "Bearer t2"); !id.Expires.IsZero() {
t.Fatalf("static without expiry: Expires = %v, want zero", id.Expires)
}

pub, priv, _ := ed25519.GenerateKey(nil)
p := JWKSProvider{Keys: NewKeySet(jwksServer(t, "k1", pub).URL)}
id, err = p.Authenticate(ctx, "Bearer "+MintJWT(priv, "k1", Claims{Org: "acme", Exp: exp.Unix()}))
if err != nil || !id.Expires.Equal(exp) {
t.Fatalf("jwt: Expires = %v err=%v, want %v", id.Expires, err, exp)
}
}
Loading