Files
kmsvc-manage/internal/api/interceptors/auth_test.go
T
riotpiaole ef7b54710d feat(auth): add Authentik JWT validation shared by gRPC and REST
JWKS caching via lestrrat-go/jwx, signature/iss/aud/exp validation, and
a single gRPC interceptor that grpc-gateway's forwarded headers make
work identically for REST callers.
2026-06-21 18:55:23 -07:00

109 lines
2.9 KiB
Go

package interceptors
import (
"context"
"errors"
"testing"
"github.com/lestrrat-go/jwx/v2/jwt"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
type fakeValidator struct {
token jwt.Token
err error
}
func (f fakeValidator) Validate(ctx context.Context, tokenString string) (jwt.Token, error) {
return f.token, f.err
}
func contextWithAuthHeader(value string) context.Context {
if value == "" {
return context.Background()
}
return metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", value))
}
func TestUnaryServerInterceptorRejectsMissingOrMalformedHeader(t *testing.T) {
validator := fakeValidator{token: jwt.New()}
interceptor := UnaryServerInterceptor(validator)
handlerCalled := false
handler := func(ctx context.Context, req any) (any, error) {
handlerCalled = true
return "ok", nil
}
tests := []struct {
name string
header string
}{
{name: "missing header", header: ""},
{name: "malformed header (no Bearer prefix)", header: "sometoken"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
handlerCalled = false
ctx := contextWithAuthHeader(tc.header)
_, err := interceptor(ctx, nil, &grpc.UnaryServerInfo{}, handler)
if err == nil {
t.Fatalf("expected error, got none")
}
if status.Code(err) != codes.Unauthenticated {
t.Fatalf("code = %v, want Unauthenticated", status.Code(err))
}
if handlerCalled {
t.Fatalf("handler must not be called when auth fails")
}
})
}
}
func TestUnaryServerInterceptorRejectsInvalidToken(t *testing.T) {
validator := fakeValidator{err: errors.New("bad token")}
interceptor := UnaryServerInterceptor(validator)
handler := func(ctx context.Context, req any) (any, error) {
t.Fatalf("handler must not be called when validation fails")
return nil, nil
}
ctx := contextWithAuthHeader("Bearer not-a-real-token")
_, err := interceptor(ctx, nil, &grpc.UnaryServerInfo{}, handler)
if status.Code(err) != codes.Unauthenticated {
t.Fatalf("code = %v, want Unauthenticated", status.Code(err))
}
}
func TestUnaryServerInterceptorAllowsValidTokenAndInjectsClaims(t *testing.T) {
wantToken := jwt.New()
_ = wantToken.Set(jwt.SubjectKey, "test-subject")
validator := fakeValidator{token: wantToken}
interceptor := UnaryServerInterceptor(validator)
var gotClaims jwt.Token
handler := func(ctx context.Context, req any) (any, error) {
claims, ok := ClaimsFromContext(ctx)
if !ok {
t.Fatalf("expected claims in context")
}
gotClaims = claims
return "ok", nil
}
ctx := contextWithAuthHeader("Bearer a-valid-token")
resp, err := interceptor(ctx, nil, &grpc.UnaryServerInfo{}, handler)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp != "ok" {
t.Fatalf("resp = %v, want ok", resp)
}
if gotClaims.Subject() != "test-subject" {
t.Fatalf("subject = %q, want test-subject", gotClaims.Subject())
}
}