Skip to content
Open
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
12 changes: 10 additions & 2 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,16 @@ import (
type TokenInfo struct {
Scopes []string
Expiration time.Time
// TODO: add standard JWT fields
Extra map[string]any
Extra map[string]any

// Standard JWT fields
// See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
Issuer string `json:"iss,omitempty"`
Subject string `json:"sub,omitempty"`
Audience []string `json:"aud,omitempty"`
NotBefore time.Time `json:"nbf,omitempty"`
IssuedAt time.Time `json:"iat,omitempty"`
JWTID string `json:"jti,omitempty"`
}

// The error that a TokenVerifier should return if the token cannot be verified.
Expand Down
112 changes: 112 additions & 0 deletions auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
)
Expand Down Expand Up @@ -76,3 +77,114 @@ func TestVerify(t *testing.T) {
})
}
}

func TestRequireBearerToken_ClaimsTable(t *testing.T) {
issuedAt := time.Unix(1730000000, 0).UTC()
notBefore := issuedAt.Add(-time.Minute)

verifier := func(_ context.Context, token string, _ *http.Request) (*TokenInfo, error) {
switch token {
case "claims":
return &TokenInfo{
Scopes: []string{"s1"},
Expiration: time.Now().Add(time.Hour),
Issuer: "https://issuer.example",
Subject: "user-123",
Audience: []string{"aud1", "aud2"},
NotBefore: notBefore,
IssuedAt: issuedAt,
JWTID: "jwt-id-abc",
}, nil
case "claims-zero":
return &TokenInfo{Expiration: time.Now().Add(time.Hour)}, nil
default:
return nil, ErrInvalidToken
}
}

for _, tt := range []struct {
name string
header string
checkFunc func(t *testing.T, ti *TokenInfo)
}{
{
name: "claims present",
header: "Bearer claims",
checkFunc: func(t *testing.T, ti *TokenInfo) {
if ti == nil {
t.Fatalf("TokenInfo missing in context")
}
if ti.Issuer != "https://issuer.example" {
t.Fatalf("iss got %q", ti.Issuer)
}
if ti.Subject != "user-123" {
t.Fatalf("sub got %q", ti.Subject)
}
if len(ti.Audience) != 2 || ti.Audience[0] != "aud1" || ti.Audience[1] != "aud2" {
t.Fatalf("aud got %v", ti.Audience)
}
if ti.NotBefore.IsZero() {
t.Fatalf("nbf is zero")
}
if ti.IssuedAt.IsZero() {
t.Fatalf("iat is zero")
}
if ti.JWTID != "jwt-id-abc" {
t.Fatalf("jti got %q", ti.JWTID)
}
if ti.Expiration.IsZero() {
t.Fatalf("exp is zero")
}
},
},
{
name: "claims zero values (except exp)",
header: "Bearer claims-zero",
checkFunc: func(t *testing.T, ti *TokenInfo) {
if ti == nil {
t.Fatalf("TokenInfo missing in context")
}
if ti.Issuer != "" {
t.Fatalf("iss expected empty, got %q", ti.Issuer)
}
if ti.Subject != "" {
t.Fatalf("sub expected empty, got %q", ti.Subject)
}
if len(ti.Audience) != 0 {
t.Fatalf("aud expected empty, got %v", ti.Audience)
}
if !ti.NotBefore.IsZero() {
t.Fatalf("nbf expected zero, got %v", ti.NotBefore)
}
if !ti.IssuedAt.IsZero() {
t.Fatalf("iat expected zero, got %v", ti.IssuedAt)
}
if ti.JWTID != "" {
t.Fatalf("jti expected empty, got %q", ti.JWTID)
}
if ti.Expiration.IsZero() {
t.Fatalf("exp should be set for middleware to pass")
}
},
},
} {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Authorization", tt.header)
rw := httptest.NewRecorder()

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ti := TokenInfoFromContext(r.Context())
// Run the provided check against the token info in context
tt.checkFunc(t, ti)
w.WriteHeader(http.StatusOK)
})

wrapped := RequireBearerToken(verifier, nil)(handler)
wrapped.ServeHTTP(rw, req)
if rw.Result().StatusCode != http.StatusOK {
t.Fatalf("unexpected status: %d", rw.Result().StatusCode)
}
})
}
}