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
88 changes: 88 additions & 0 deletions authbridge/authlib/plugins/tokenbroker/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Package client provides an HTTP client for the Token Broker service.
package client

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)

// Client is an HTTP client for the Token Broker service.
type Client struct {
httpClient *http.Client
}

// NewClient creates a new Token Broker client.
func NewClient() *Client {
return &Client{
httpClient: &http.Client{
Timeout: 310 * time.Second, // Longer than Token Broker's 300s timeout
},
}
}

// AcquireToken calls the Token Broker to get a token for the given target server.
// The broker extracts user-id and session-key from the provided JWT token.
// Blocks until a token is available or the context is cancelled.
// If authorizationEndpoint is provided, it will be sent to the broker via X-Authorization-Endpoint header.
// If tokenEndpoint is provided, it will be sent to the broker via X-Token-Endpoint header.
func (c *Client) AcquireToken(ctx context.Context, tokenBrokerURL, token, serverURL, authorizationEndpoint, tokenEndpoint string) (string, error) {
if tokenBrokerURL == "" {
return "", fmt.Errorf("token broker URL cannot be empty")
}

url := fmt.Sprintf("%s/sessions/token", tokenBrokerURL)

req, err := http.NewRequestWithContext(ctx, "POST", url, nil)
if err != nil {
return "", fmt.Errorf("creating request: %w", err)
}

req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-Server-Url", serverURL)
if authorizationEndpoint != "" {
req.Header.Set("X-Authorization-Endpoint", authorizationEndpoint)
}
if tokenEndpoint != "" {
req.Header.Set("X-Token-Endpoint", tokenEndpoint)
}

resp, err := c.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("token broker request failed: %w", err)
}
defer resp.Body.Close()

body, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20)) // 10MB limit
if err != nil {
return "", fmt.Errorf("reading response: %w", err)
}

if resp.StatusCode != http.StatusOK {
var brokerErr struct {
Error string `json:"error"`
Message string `json:"message"` // Token Broker uses "message" instead of "error_description"
}
_ = json.Unmarshal(body, &brokerErr)
return "", &BrokerError{
StatusCode: resp.StatusCode,
OAuthError: brokerErr.Error,
OAuthDescription: brokerErr.Message,
}
}

var result struct {
Token string `json:"token"`
}
if err := json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("parsing token response: %w", err)
}
if result.Token == "" {
return "", fmt.Errorf("token response missing token")
}

return result.Token, nil
}
180 changes: 180 additions & 0 deletions authbridge/authlib/plugins/tokenbroker/client/client_acquire_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package client

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)

// =============================================================================
// Basic Token Acquisition Tests
// =============================================================================

func TestClient_AcquireToken_Success(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/sessions/token" {
t.Errorf("expected /sessions/token, got %s", r.URL.Path)
}
if auth := r.Header.Get("Authorization"); !strings.HasPrefix(auth, "Bearer ") {
t.Errorf("expected Bearer token in Authorization header, got %q", auth)
}
if serverURL := r.Header.Get("X-Server-Url"); serverURL == "" {
t.Error("expected X-Server-Url header")
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"token": "gho_test_token_12345"})
}))
defer srv.Close()

client := NewClient()
token, err := client.AcquireToken(context.Background(), srv.URL, "user-jwt-token", "https://api.github.com", "", "")

if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if token != "gho_test_token_12345" {
t.Errorf("token = %q, want gho_test_token_12345", token)
}
}

func TestClient_AcquireToken_RequestFormat(t *testing.T) {
var capturedMethod, capturedPath, capturedAuth, capturedServerURL string

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
capturedMethod = r.Method
capturedPath = r.URL.Path
capturedAuth = r.Header.Get("Authorization")
capturedServerURL = r.Header.Get("X-Server-Url")

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"token": "test-token"})
}))
defer srv.Close()

client := NewClient()
_, err := client.AcquireToken(context.Background(), srv.URL, "my-jwt-token", "https://target.example.com", "", "")

if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if capturedMethod != "POST" {
t.Errorf("method = %q, want POST", capturedMethod)
}
if capturedPath != "/sessions/token" {
t.Errorf("path = %q, want /sessions/token", capturedPath)
}
if capturedAuth != "Bearer my-jwt-token" {
t.Errorf("Authorization = %q, want Bearer my-jwt-token", capturedAuth)
}
if capturedServerURL != "https://target.example.com" {
t.Errorf("X-Server-Url = %q, want https://target.example.com", capturedServerURL)
}
}

func TestClient_AcquireToken_WithAuthorizationEndpoint(t *testing.T) {
var capturedAuthEndpoint string

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
capturedAuthEndpoint = r.Header.Get("X-Authorization-Endpoint")

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"token": "test-token"})
}))
defer srv.Close()

client := NewClient()

// Test with authorization endpoint
_, err := client.AcquireToken(context.Background(), srv.URL, "my-jwt-token", "https://target.example.com", "https://auth.example.com/oauth/authorize", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if capturedAuthEndpoint != "https://auth.example.com/oauth/authorize" {
t.Errorf("X-Authorization-Endpoint = %q, want %q", capturedAuthEndpoint, "https://auth.example.com/oauth/authorize")
}

// Test without authorization endpoint (empty string)
capturedAuthEndpoint = "should-be-cleared"
_, err = client.AcquireToken(context.Background(), srv.URL, "my-jwt-token", "https://target.example.com", "", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if capturedAuthEndpoint != "" {
t.Errorf("X-Authorization-Endpoint = %q, want empty string", capturedAuthEndpoint)
}
}

func TestClient_AcquireToken_WithTokenEndpoint(t *testing.T) {
var capturedTokenEndpoint string

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
capturedTokenEndpoint = r.Header.Get("X-Token-Endpoint")

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"token": "test-token"})
}))
defer srv.Close()

client := NewClient()

// Test with token endpoint
_, err := client.AcquireToken(context.Background(), srv.URL, "my-jwt-token", "https://target.example.com", "", "https://auth.example.com/oauth/token")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if capturedTokenEndpoint != "https://auth.example.com/oauth/token" {
t.Errorf("X-Token-Endpoint = %q, want %q", capturedTokenEndpoint, "https://auth.example.com/oauth/token")
}

// Test without token endpoint (empty string)
capturedTokenEndpoint = "should-be-cleared"
_, err = client.AcquireToken(context.Background(), srv.URL, "my-jwt-token", "https://target.example.com", "", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if capturedTokenEndpoint != "" {
t.Errorf("X-Token-Endpoint = %q, want empty string", capturedTokenEndpoint)
}
}

func TestClient_AcquireToken_WithBothEndpoints(t *testing.T) {
var capturedAuthEndpoint, capturedTokenEndpoint string

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
capturedAuthEndpoint = r.Header.Get("X-Authorization-Endpoint")
capturedTokenEndpoint = r.Header.Get("X-Token-Endpoint")

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"token": "test-token"})
}))
defer srv.Close()

client := NewClient()

// Test with both endpoints
_, err := client.AcquireToken(context.Background(), srv.URL, "my-jwt-token", "https://target.example.com", "https://auth.example.com/oauth/authorize", "https://auth.example.com/oauth/token")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if capturedAuthEndpoint != "https://auth.example.com/oauth/authorize" {
t.Errorf("X-Authorization-Endpoint = %q, want %q", capturedAuthEndpoint, "https://auth.example.com/oauth/authorize")
}

if capturedTokenEndpoint != "https://auth.example.com/oauth/token" {
t.Errorf("X-Token-Endpoint = %q, want %q", capturedTokenEndpoint, "https://auth.example.com/oauth/token")
}
}
112 changes: 112 additions & 0 deletions authbridge/authlib/plugins/tokenbroker/client/client_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package client

import (
"context"
"net/http"
"strings"
"testing"
)

// =============================================================================
// Benchmark Tests
// =============================================================================

func BenchmarkAcquireToken_Success(b *testing.B) {
helper := NewTestHelper(b)
srv := helper.NewSuccessBroker("bench-token")
defer srv.Close()

client := NewClient()
ctx := context.Background()

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := client.AcquireToken(ctx, srv.URL, "user-token", "https://api.github.com", "", "")
if err != nil {
b.Fatalf("unexpected error: %v", err)
}
}
}

func BenchmarkAcquireToken_Error(b *testing.B) {
helper := NewTestHelper(b)
srv := helper.NewErrorBroker(http.StatusUnauthorized, "unauthorized", "test error")
defer srv.Close()

client := NewClient()
ctx := context.Background()

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = client.AcquireToken(ctx, srv.URL, "user-token", "https://api.github.com", "", "")
}
}

func BenchmarkAcquireToken_LargeToken(b *testing.B) {
largeToken := strings.Repeat("x", 8192) // 8KB token
helper := NewTestHelper(b)
srv := helper.NewSuccessBroker(largeToken)
defer srv.Close()

client := NewClient()
ctx := context.Background()

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := client.AcquireToken(ctx, srv.URL, "user-token", "https://api.github.com", "", "")
if err != nil {
b.Fatalf("unexpected error: %v", err)
}
}
}

func BenchmarkAcquireToken_Parallel(b *testing.B) {
helper := NewTestHelper(b)
srv := helper.NewSuccessBroker("bench-token")
defer srv.Close()

client := NewClient()
ctx := context.Background()

b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _ = client.AcquireToken(ctx, srv.URL, "user-token", "https://api.example.com", "", "")
}
})
}

func BenchmarkAcquireToken_Allocations(b *testing.B) {
helper := NewTestHelper(b)
srv := helper.NewSuccessBroker("alloc-token")
defer srv.Close()

client := NewClient()
ctx := context.Background()

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = client.AcquireToken(ctx, srv.URL, "user-token", "https://api.example.com", "", "")
}
}

func BenchmarkNewClient(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = NewClient()
}
}

func BenchmarkBrokerError_Error(b *testing.B) {
err := &BrokerError{
StatusCode: 401,
OAuthError: "unauthorized",
OAuthDescription: "test error message",
}

b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = err.Error()
}
}
Loading
Loading