Files
sonr/test/integration/oidc_ucan_flow_test.go
T
Prad NukalaandGitHub 13e6c3e84d Master (#1262)
* clear

* feat: Add everything

* fix: Commenht
2025-10-03 14:45:52 -04:00

845 lines
26 KiB
Go

package integration
import (
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/sonr-io/sonr/bridge/handlers"
"github.com/sonr-io/sonr/crypto/ucan"
)
// MockDIDResolver provides mock DID resolution for testing
type MockDIDResolver struct {
keys map[string]ed25519.PublicKey
}
func NewMockDIDResolver() *MockDIDResolver {
resolver := &MockDIDResolver{
keys: make(map[string]ed25519.PublicKey),
}
// Generate test keys for common DIDs
testDIDs := []string{
"did:sonr:oauth-provider",
"did:sonr:user-chain",
"did:sonr:client-a",
"did:sonr:client-b",
"did:sonr:client-c",
"did:sonr:user",
"did:sonr:perf-user",
"did:sonr:user123",
"did:sonr:test-user",
}
for _, did := range testDIDs {
pub, _, _ := ed25519.GenerateKey(rand.Reader)
resolver.keys[did] = pub
}
return resolver
}
func (r *MockDIDResolver) GetPublicKey(did string) (ed25519.PublicKey, error) {
if key, ok := r.keys[did]; ok {
return key, nil
}
// Generate a new key for unknown DIDs
pub, _, _ := ed25519.GenerateKey(rand.Reader)
r.keys[did] = pub
return pub, nil
}
// MockOAuth2Provider provides mock OAuth2 functionality for testing
type MockOAuth2Provider struct {
tokens map[string]string
}
func NewMockOAuth2Provider() *MockOAuth2Provider {
return &MockOAuth2Provider{
tokens: make(map[string]string),
}
}
func (m *MockOAuth2Provider) HandleAuthorize(w http.ResponseWriter, r *http.Request) {
// Mock authorization handler - returns test auth code
code := "test_auth_code_123"
redirectURI := r.URL.Query().Get("redirect_uri")
if redirectURI != "" {
http.Redirect(w, r, redirectURI+"?code="+code, http.StatusFound)
} else {
w.WriteHeader(http.StatusOK)
w.Write([]byte(code))
}
}
func (m *MockOAuth2Provider) HandleToken(w http.ResponseWriter, r *http.Request) {
// Mock token handler - returns test access token with UCAN
// Generate a mock UCAN token (simplified for testing)
mockUCAN := "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6c29ucjpvYXV0aC1wcm92aWRlciIsImF1ZCI6ImRpZDpzb25yOnVzZXIxMjMiLCJhdHQiOlt7ImNhbiI6InZhdWx0OnJlYWQifSx7ImNhbiI6ImR3bjp3cml0ZSJ9XSwiZXhwIjoxNzM2MzY0MDAwLCJubmMiOiJ0ZXN0LW5vbmNlIn0.test_signature"
token := map[string]interface{}{
"access_token": "test_access_token",
"token_type": "Bearer",
"expires_in": 3600,
"ucan_token": mockUCAN,
"scope": "openid vault:read dwn:write",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(token)
}
func (m *MockOAuth2Provider) HandleUserInfo(w http.ResponseWriter, r *http.Request) {
// Mock userinfo handler
userInfo := map[string]interface{}{
"sub": "did:sonr:test-user",
"name": "Test User",
"email": "test@sonr.id",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(userInfo)
}
// OIDCUCANFlowTestSuite tests the complete OIDC → OAuth2 → UCAN flow
// MockBlockchainUCANSigner is a mock implementation for testing
type MockBlockchainUCANSigner struct {
issuerDID string
resolver *MockDIDResolver
}
func NewMockBlockchainUCANSigner(issuerDID string) *MockBlockchainUCANSigner {
return &MockBlockchainUCANSigner{
issuerDID: issuerDID,
resolver: NewMockDIDResolver(),
}
}
// CreateDelegationToken creates a mock UCAN token for testing
func (s *MockBlockchainUCANSigner) CreateDelegationToken(
issuer string,
audience string,
attenuations []ucan.Attenuation,
proofs []ucan.Proof,
expiration time.Duration,
) (string, error) {
// Build attenuations array for claims
var atts []map[string]interface{}
for _, att := range attenuations {
attMap := make(map[string]interface{})
// Handle different capability types
switch cap := att.Capability.(type) {
case *ucan.SimpleCapability:
attMap["can"] = cap.Action
case *ucan.MultiCapability:
// For multi-capability, store all actions as a joined string
if len(cap.Actions) > 0 {
attMap["can"] = strings.Join(cap.Actions, ",")
}
}
// Add resource if present
if att.Resource != nil {
if res, ok := att.Resource.(*handlers.SimpleResource); ok {
attMap["with"] = fmt.Sprintf("%s:%s", res.Scheme, res.Value)
}
}
atts = append(atts, attMap)
}
// Create claims
claims := map[string]interface{}{
"iss": issuer,
"aud": audience,
"exp": time.Now().Add(expiration).Unix(),
"att": atts,
}
claimsJSON, _ := json.Marshal(claims)
// Create a simplified mock UCAN token
token := fmt.Sprintf("eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.%s.mock_signature",
base64.RawURLEncoding.EncodeToString(claimsJSON))
return token, nil
}
// VerifySignature verifies a mock UCAN token
func (s *MockBlockchainUCANSigner) VerifySignature(tokenString string) (*ucan.Token, error) {
// For testing, just check if it's a valid format
parts := strings.Split(tokenString, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid token format")
}
// Decode payload
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, err
}
var claims map[string]interface{}
if err := json.Unmarshal(payload, &claims); err != nil {
return nil, err
}
// Check expiration
if exp, ok := claims["exp"].(float64); ok {
if time.Now().Unix() > int64(exp) {
return nil, fmt.Errorf("token expired")
}
}
// Return a mock token
return &ucan.Token{
Issuer: claims["iss"].(string),
Audience: claims["aud"].(string),
}, nil
}
// ValidateDelegationChain validates a chain of UCAN tokens
func (s *MockBlockchainUCANSigner) ValidateDelegationChain(tokens []string) error {
// For testing, verify each token and check for privilege escalation
var prevCapabilities []string
for i, token := range tokens {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return fmt.Errorf("invalid token format at position %d", i)
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return fmt.Errorf("failed to decode token %d: %w", i, err)
}
var claims map[string]interface{}
if err := json.Unmarshal(payload, &claims); err != nil {
return fmt.Errorf("failed to unmarshal claims for token %d: %w", i, err)
}
// Check for privilege escalation (simplified for testing)
if att, ok := claims["att"].([]interface{}); ok && i > 0 {
for _, a := range att {
if attMap, ok := a.(map[string]interface{}); ok {
if cap, ok := attMap["can"].(string); ok {
// Split comma-separated capabilities
caps := strings.Split(cap, ",")
for _, c := range caps {
c = strings.TrimSpace(c)
// Check if this capability was in the previous token
if !contains(prevCapabilities, c) {
return fmt.Errorf("privilege escalation detected: trying to add '%s' permission", c)
}
}
}
}
}
}
// Store capabilities for next iteration
prevCapabilities = []string{}
if att, ok := claims["att"].([]interface{}); ok {
for _, a := range att {
if attMap, ok := a.(map[string]interface{}); ok {
if cap, ok := attMap["can"].(string); ok {
// Split and store all capabilities
caps := strings.Split(cap, ",")
for _, c := range caps {
prevCapabilities = append(prevCapabilities, strings.TrimSpace(c))
}
}
}
}
}
}
return nil
}
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
type OIDCUCANFlowTestSuite struct {
suite.Suite
oauth2Provider *MockOAuth2Provider
ucanDelegator *handlers.UCANDelegator
tokenExchange *handlers.TokenExchangeHandler
refreshHandler *handlers.RefreshTokenHandler
testServer *httptest.Server
clientStore *MockClientStore
tokenStore *MockTokenStore
mockSigner *MockBlockchainUCANSigner
}
// MockClientStore implements handlers.ClientStore for testing
type MockClientStore struct {
clients map[string]*handlers.OAuth2Client
}
func NewMockClientStore() *MockClientStore {
return &MockClientStore{
clients: make(map[string]*handlers.OAuth2Client),
}
}
func (m *MockClientStore) GetClient(ctx context.Context, clientID string) (*handlers.OAuth2Client, error) {
client, exists := m.clients[clientID]
if !exists {
return nil, handlers.ErrClientNotFound
}
return client, nil
}
func (m *MockClientStore) ValidateClientCredentials(ctx context.Context, clientID, clientSecret string) error {
client, exists := m.clients[clientID]
if !exists || client.ClientSecret != clientSecret {
return handlers.ErrInvalidClientCredentials
}
return nil
}
// MockTokenStore implements handlers.TokenStore for testing
type MockTokenStore struct {
tokens map[string]*handlers.StoredToken
}
func NewMockTokenStore() *MockTokenStore {
return &MockTokenStore{
tokens: make(map[string]*handlers.StoredToken),
}
}
func (m *MockTokenStore) GetToken(ctx context.Context, tokenID string) (*handlers.StoredToken, error) {
token, exists := m.tokens[tokenID]
if !exists {
return nil, handlers.ErrTokenNotFound
}
return token, nil
}
func (m *MockTokenStore) StoreToken(ctx context.Context, token *handlers.StoredToken) error {
m.tokens[token.TokenID] = token
return nil
}
func (m *MockTokenStore) RevokeToken(ctx context.Context, tokenID string) error {
delete(m.tokens, tokenID)
return nil
}
func (suite *OIDCUCANFlowTestSuite) SetupSuite() {
// Initialize stores
suite.clientStore = NewMockClientStore()
suite.tokenStore = NewMockTokenStore()
// Add test client
suite.clientStore.clients["test-client"] = &handlers.OAuth2Client{
ClientID: "test-client",
ClientSecret: "test-secret",
RedirectURIs: []string{"https://example.com/callback"},
AllowedScopes: []string{"openid", "profile", "vault:read", "dwn:write"},
Metadata: map[string]string{
"client_did": "did:sonr:test-client",
},
}
// Initialize handlers with mock signer
suite.mockSigner = NewMockBlockchainUCANSigner("did:sonr:oauth-provider")
// Create a real signer for delegator and handlers (using mock resolver internally)
realSigner, _ := handlers.NewBlockchainUCANSigner(nil, "did:sonr:oauth-provider")
suite.ucanDelegator = handlers.NewUCANDelegator(realSigner)
suite.oauth2Provider = NewMockOAuth2Provider()
suite.tokenExchange = handlers.NewTokenExchangeHandler(suite.ucanDelegator, realSigner, suite.tokenStore, suite.clientStore)
suite.refreshHandler = handlers.NewRefreshTokenHandler(suite.ucanDelegator, realSigner, suite.tokenStore, suite.clientStore)
// Setup test server
mux := http.NewServeMux()
mux.HandleFunc("/.well-known/openid-configuration", suite.handleOIDCDiscovery)
mux.HandleFunc("/oauth/authorize", suite.oauth2Provider.HandleAuthorize)
mux.HandleFunc("/oauth/token", suite.handleToken)
mux.HandleFunc("/oauth/userinfo", suite.oauth2Provider.HandleUserInfo)
suite.testServer = httptest.NewServer(mux)
}
func (suite *OIDCUCANFlowTestSuite) TearDownSuite() {
suite.testServer.Close()
}
func (suite *OIDCUCANFlowTestSuite) handleOIDCDiscovery(w http.ResponseWriter, r *http.Request) {
discovery := map[string]interface{}{
"issuer": suite.testServer.URL,
"authorization_endpoint": suite.testServer.URL + "/oauth/authorize",
"token_endpoint": suite.testServer.URL + "/oauth/token",
"userinfo_endpoint": suite.testServer.URL + "/oauth/userinfo",
"jwks_uri": suite.testServer.URL + "/.well-known/jwks.json",
"scopes_supported": []string{"openid", "profile", "email", "vault:read", "vault:write", "dwn:read", "dwn:write"},
"response_types_supported": []string{"code", "token", "id_token"},
"grant_types_supported": []string{"authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange"},
"token_types_supported": []string{"Bearer", "UCAN"},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(discovery)
}
func (suite *OIDCUCANFlowTestSuite) handleToken(w http.ResponseWriter, r *http.Request) {
grantType := r.FormValue("grant_type")
switch grantType {
case "authorization_code":
suite.oauth2Provider.HandleToken(w, r)
case "refresh_token":
suite.refreshHandler.HandleRefreshToken(w, r)
case "urn:ietf:params:oauth:grant-type:token-exchange":
suite.tokenExchange.HandleTokenExchange(w, r)
default:
http.Error(w, "unsupported_grant_type", http.StatusBadRequest)
}
}
// TestOIDCDiscovery tests OIDC discovery endpoint
func (suite *OIDCUCANFlowTestSuite) TestOIDCDiscovery() {
resp, err := http.Get(suite.testServer.URL + "/.well-known/openid-configuration")
suite.Require().NoError(err)
defer resp.Body.Close()
suite.Equal(http.StatusOK, resp.StatusCode)
var discovery map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&discovery)
suite.Require().NoError(err)
// Verify required OIDC fields
suite.Contains(discovery, "issuer")
suite.Contains(discovery, "authorization_endpoint")
suite.Contains(discovery, "token_endpoint")
suite.Contains(discovery, "jwks_uri")
suite.Contains(discovery, "scopes_supported")
// Verify UCAN-specific extensions
scopes := discovery["scopes_supported"].([]interface{})
suite.Contains(scopes, "vault:read")
suite.Contains(scopes, "dwn:write")
grantTypes := discovery["grant_types_supported"].([]interface{})
suite.Contains(grantTypes, "urn:ietf:params:oauth:grant-type:token-exchange")
}
// TestAuthorizationCodeToUCAN tests authorization code flow that issues UCAN tokens
func (suite *OIDCUCANFlowTestSuite) TestAuthorizationCodeToUCAN() {
// Step 1: Create authorization code
authCode := "test-auth-code"
userDID := "did:sonr:user123"
scopes := []string{"openid", "vault:read", "dwn:write"}
// Store auth code (in real flow, this happens during authorize)
suite.tokenStore.StoreToken(context.Background(), &handlers.StoredToken{
TokenID: authCode,
TokenType: "authorization_code",
ClientID: "test-client",
UserDID: userDID,
Scopes: scopes,
ExpiresAt: time.Now().Add(10 * time.Minute),
})
// Step 2: Exchange authorization code for tokens
req := httptest.NewRequest("POST", "/oauth/token", strings.NewReader(
"grant_type=authorization_code&code="+authCode+"&client_id=test-client",
))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth("test-client", "test-secret")
w := httptest.NewRecorder()
suite.handleToken(w, req)
suite.Equal(http.StatusOK, w.Code)
var tokenResp map[string]interface{}
err := json.NewDecoder(w.Body).Decode(&tokenResp)
suite.Require().NoError(err)
// Verify response contains UCAN token
suite.Contains(tokenResp, "access_token")
suite.Contains(tokenResp, "ucan_token")
suite.Contains(tokenResp, "token_type")
suite.Equal("Bearer", tokenResp["token_type"])
// Step 3: Verify UCAN token structure
ucanTokenStr, ok := tokenResp["ucan_token"].(string)
suite.True(ok, "UCAN token should be a string")
suite.NotEmpty(ucanTokenStr)
// Verify UCAN can be parsed (basic validation)
suite.Contains(ucanTokenStr, ".") // JWT format
}
// TestTokenExchangeFlow tests RFC 8693 token exchange
func (suite *OIDCUCANFlowTestSuite) TestTokenExchangeFlow() {
// Setup: Create an access token
accessToken := "test-access-token"
userDID := "did:sonr:user456"
suite.tokenStore.StoreToken(context.Background(), &handlers.StoredToken{
TokenID: accessToken,
TokenType: "access_token",
AccessToken: accessToken,
ClientID: "test-client",
UserDID: userDID,
Scopes: []string{"vault:read", "dwn:read"},
ExpiresAt: time.Now().Add(time.Hour),
UCANToken: "dummy.ucan.token",
})
// Perform token exchange: Access Token → UCAN
exchangeReq := handlers.TokenExchangeRequest{
GrantType: "urn:ietf:params:oauth:grant-type:token-exchange",
SubjectToken: accessToken,
SubjectTokenType: "urn:ietf:params:oauth:token-type:access_token",
RequestedTokenType: "urn:x-oath:params:oauth:token-type:ucan",
Scope: "vault:read", // Request subset of scopes
}
body, _ := json.Marshal(exchangeReq)
req := httptest.NewRequest("POST", "/oauth/token", strings.NewReader(string(body)))
req.Header.Set("Content-Type", "application/json")
req.SetBasicAuth("test-client", "test-secret")
w := httptest.NewRecorder()
suite.tokenExchange.HandleTokenExchange(w, req)
suite.Equal(http.StatusOK, w.Code)
var exchangeResp handlers.TokenExchangeResponse
err := json.NewDecoder(w.Body).Decode(&exchangeResp)
suite.Require().NoError(err)
// Verify exchange response
suite.NotEmpty(exchangeResp.AccessToken)
suite.Equal("urn:x-oath:params:oauth:token-type:ucan", exchangeResp.IssuedTokenType)
suite.Equal("Bearer", exchangeResp.TokenType)
suite.Equal("vault:read", exchangeResp.Scope)
}
// TestRefreshTokenWithUCANChain tests refresh token flow with UCAN delegation chains
func (suite *OIDCUCANFlowTestSuite) TestRefreshTokenWithUCANChain() {
// Setup: Create refresh token
refreshToken := "test-refresh-token"
userDID := "did:sonr:user789"
suite.tokenStore.StoreToken(context.Background(), &handlers.StoredToken{
TokenID: refreshToken,
TokenType: "refresh_token",
RefreshToken: refreshToken,
ClientID: "test-client",
UserDID: userDID,
Scopes: []string{"vault:read", "vault:write", "dwn:read"},
ExpiresAt: time.Now().Add(30 * 24 * time.Hour),
})
// First refresh - maintain all scopes
req1 := httptest.NewRequest("POST", "/oauth/token", strings.NewReader(
"grant_type=refresh_token&refresh_token="+refreshToken,
))
req1.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req1.SetBasicAuth("test-client", "test-secret")
w1 := httptest.NewRecorder()
suite.handleToken(w1, req1) // Use handleToken instead, which routes to refreshHandler
// Debug: print response if not OK
if w1.Code != http.StatusOK {
suite.T().Logf("Refresh token response: %s", w1.Body.String())
}
suite.Equal(http.StatusOK, w1.Code)
var resp1 handlers.RefreshTokenResponse
err := json.NewDecoder(w1.Body).Decode(&resp1)
suite.Require().NoError(err)
suite.NotEmpty(resp1.AccessToken)
suite.NotEmpty(resp1.UCANToken)
suite.Contains(resp1.Scope, "vault:read")
suite.Contains(resp1.Scope, "vault:write")
// Second refresh - attenuate scopes (reduce permissions)
newRefreshToken := resp1.RefreshToken
if newRefreshToken == "" {
newRefreshToken = refreshToken // Use original if not rotated
}
req2 := httptest.NewRequest("POST", "/oauth/token", strings.NewReader(
"grant_type=refresh_token&refresh_token="+newRefreshToken+"&scope=vault:read",
))
req2.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req2.SetBasicAuth("test-client", "test-secret")
w2 := httptest.NewRecorder()
suite.handleToken(w2, req2) // Use handleToken instead
// Debug: print response if not OK
if w2.Code != http.StatusOK {
suite.T().Logf("Second refresh response: %s", w2.Body.String())
}
// Should succeed with reduced scopes
suite.Equal(http.StatusOK, w2.Code)
var resp2 handlers.RefreshTokenResponse
err = json.NewDecoder(w2.Body).Decode(&resp2)
suite.Require().NoError(err)
suite.Equal("vault:read", resp2.Scope) // Only requested scope
suite.NotContains(resp2.Scope, "vault:write") // Write permission removed
}
// TestCrossModuleAuthorization tests authorization across multiple modules
func (suite *OIDCUCANFlowTestSuite) TestCrossModuleAuthorization() {
// Create UCAN token with cross-module capabilities
userDID := "did:sonr:user-cross"
clientDID := "did:sonr:client-cross"
// Map OAuth scopes to multiple module capabilities
scopes := []string{"vault:read", "dwn:write", "service:manage", "did:write"}
// Create delegation with cross-module permissions
ucanToken, err := suite.ucanDelegator.CreateDelegation(
userDID,
clientDID,
scopes,
time.Now().Add(time.Hour),
)
suite.Require().NoError(err)
suite.NotNil(ucanToken)
// Verify token contains attenuations for all modules
suite.Require().NotEmpty(ucanToken.Attenuations)
// Check that each module has appropriate capabilities
moduleCapabilities := make(map[string][]string)
for _, att := range ucanToken.Attenuations {
scheme := att.Resource.GetScheme()
actions := att.Capability.GetActions()
moduleCapabilities[scheme] = append(moduleCapabilities[scheme], actions...)
}
// Verify all modules are represented
suite.Contains(moduleCapabilities, "vault")
suite.Contains(moduleCapabilities, "dwn")
suite.Contains(moduleCapabilities, "service")
suite.Contains(moduleCapabilities, "did")
// Verify appropriate actions per module
suite.Contains(moduleCapabilities["vault"], "read")
suite.Contains(moduleCapabilities["dwn"], "write")
suite.Contains(moduleCapabilities["did"], "write")
}
// TestDelegationChainValidation tests validation of UCAN delegation chains
func (suite *OIDCUCANFlowTestSuite) TestDelegationChainValidation() {
signer := suite.mockSigner
// Create initial delegation: User → Client A
userDID := "did:sonr:user-chain"
clientA := "did:sonr:client-a"
token1, err := signer.CreateDelegationToken(
userDID,
clientA,
[]ucan.Attenuation{
{
Capability: &ucan.MultiCapability{Actions: []string{"read", "write"}},
Resource: &handlers.SimpleResource{Scheme: "vault", Value: userDID},
},
},
nil, // No proofs for initial delegation
time.Hour,
)
suite.Require().NoError(err)
// Create second delegation: Client A → Client B (with attenuation)
clientB := "did:sonr:client-b"
token2, err := signer.CreateDelegationToken(
clientA,
clientB,
[]ucan.Attenuation{
{
Capability: &ucan.SimpleCapability{Action: "read"}, // Reduced permissions
Resource: &handlers.SimpleResource{Scheme: "vault", Value: userDID},
},
},
[]ucan.Proof{ucan.Proof(token1)}, // Include proof
time.Hour,
)
suite.Require().NoError(err)
// Validate delegation chain
err = signer.ValidateDelegationChain([]string{token1, token2})
suite.NoError(err, "Valid delegation chain should pass validation")
// Test invalid chain (trying to escalate privileges)
invalidToken, err := signer.CreateDelegationToken(
clientB,
"did:sonr:client-c",
[]ucan.Attenuation{
{
Capability: &ucan.MultiCapability{Actions: []string{"read", "write", "delete"}}, // Escalation!
Resource: &handlers.SimpleResource{Scheme: "vault", Value: userDID},
},
},
[]ucan.Proof{ucan.Proof(token2)},
time.Hour,
)
suite.Require().NoError(err) // Token creation succeeds
// But validation should fail due to privilege escalation
err = signer.ValidateDelegationChain([]string{token1, token2, invalidToken})
suite.Error(err, "Chain with privilege escalation should fail validation")
}
// TestPerformanceBenchmark tests authorization performance
func (suite *OIDCUCANFlowTestSuite) TestPerformanceBenchmark() {
iterations := 100
maxDuration := 50 * time.Millisecond // Target: < 50ms per operation
userDID := "did:sonr:perf-user"
clientDID := "did:sonr:perf-client"
scopes := []string{"vault:read", "dwn:write"}
// Benchmark UCAN token creation
start := time.Now()
for i := 0; i < iterations; i++ {
_, err := suite.ucanDelegator.CreateDelegation(
userDID,
clientDID,
scopes,
time.Now().Add(time.Hour),
)
suite.Require().NoError(err)
}
avgCreation := time.Since(start) / time.Duration(iterations)
suite.Less(avgCreation, maxDuration,
"Average UCAN creation time (%v) should be less than %v", avgCreation, maxDuration)
// Benchmark token validation
signer := suite.mockSigner
testToken, _ := signer.CreateDelegationToken(
userDID,
clientDID,
[]ucan.Attenuation{
{
Capability: &ucan.SimpleCapability{Action: "read"},
Resource: &handlers.SimpleResource{Scheme: "vault", Value: userDID},
},
},
nil,
time.Hour,
)
start = time.Now()
for i := 0; i < iterations; i++ {
_, err := signer.VerifySignature(testToken)
suite.Require().NoError(err)
}
avgValidation := time.Since(start) / time.Duration(iterations)
suite.Less(avgValidation, maxDuration,
"Average UCAN validation time (%v) should be less than %v", avgValidation, maxDuration)
// Log performance metrics
suite.T().Logf("Performance Metrics:")
suite.T().Logf(" UCAN Creation: %v avg", avgCreation)
suite.T().Logf(" UCAN Validation: %v avg", avgValidation)
}
// TestSecurityAudit performs security validation of delegation chains
func (suite *OIDCUCANFlowTestSuite) TestSecurityAudit() {
signer := suite.mockSigner
// Test 1: Expired token rejection
expiredToken, err := signer.CreateDelegationToken(
"did:sonr:user",
"did:sonr:client",
[]ucan.Attenuation{
{
Capability: &ucan.SimpleCapability{Action: "read"},
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
},
},
nil,
-1*time.Hour, // Already expired
)
suite.Require().NoError(err)
_, err = signer.VerifySignature(expiredToken)
suite.Error(err, "Expired token should fail verification")
// Test 2: Malformed token rejection
malformedToken := "not.a.valid.token"
_, err = signer.VerifySignature(malformedToken)
suite.Error(err, "Malformed token should fail verification")
// Test 3: Token replay protection
validToken, err := signer.CreateDelegationToken(
"did:sonr:user",
"did:sonr:client",
[]ucan.Attenuation{
{
Capability: &ucan.SimpleCapability{Action: "read"},
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "test"},
},
},
nil,
time.Hour,
)
suite.Require().NoError(err)
// First use should succeed
_, err = signer.VerifySignature(validToken)
suite.NoError(err)
// Multiple uses should also succeed (tokens are bearer tokens)
// But in production, nonce/jti tracking would prevent replay
_, err = signer.VerifySignature(validToken)
suite.NoError(err)
// Test 4: Scope boundary enforcement
err = suite.ucanDelegator.ValidateDelegation(
&ucan.Token{
Issuer: "did:sonr:user",
Audience: "did:sonr:client",
ExpiresAt: time.Now().Add(time.Hour).Unix(),
Attenuations: []ucan.Attenuation{
{
Capability: &ucan.SimpleCapability{Action: "read"},
Resource: &handlers.SimpleResource{Scheme: "vault", Value: "user"},
},
},
},
[]string{"vault:write"}, // Requesting more than granted
)
suite.Error(err, "Should reject request for unpermitted scope")
}
func TestOIDCUCANFlowSuite(t *testing.T) {
suite.Run(t, new(OIDCUCANFlowTestSuite))
}