mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
@@ -0,0 +1,844 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user