Files
sonr/bridge/handlers/oauth2_delegation.go
T

324 lines
7.9 KiB
Go
Raw Normal View History

2025-10-03 14:45:52 -04:00
package handlers
import (
"encoding/json"
"fmt"
"time"
"github.com/sonr-io/sonr/crypto/ucan"
)
// UCANDelegator handles UCAN token generation for OAuth flows
type UCANDelegator struct {
scopeMapper *ScopeMapper
signer UCANSigner
}
// UCANSigner interface for signing UCAN tokens
type UCANSigner interface {
Sign(token *ucan.Token) (string, error)
GetIssuerDID() string
}
// NewUCANDelegator creates a new UCAN delegator
func NewUCANDelegator(signer UCANSigner) *UCANDelegator {
if signer == nil {
// Use blockchain signer by default
signer, _ = NewBlockchainUCANSigner(nil, "did:sonr:oauth-provider")
}
return &UCANDelegator{
scopeMapper: NewScopeMapper(),
signer: signer,
}
}
// CreateDelegation creates a UCAN token for user-to-client delegation
func (d *UCANDelegator) CreateDelegation(
userDID string,
clientID string,
scopes []string,
expiresAt time.Time,
) (*ucan.Token, error) {
// Build resource context for the user
resourceContext := d.buildResourceContext(userDID)
// Map OAuth scopes to UCAN attenuations
attenuations := d.scopeMapper.MapToUCAN(scopes, userDID, clientID, resourceContext)
if len(attenuations) == 0 {
return nil, fmt.Errorf("no valid attenuations for scopes: %v", scopes)
}
// Create UCAN token
token := &ucan.Token{
Issuer: userDID,
Audience: clientID,
ExpiresAt: expiresAt.Unix(),
NotBefore: time.Now().Unix(),
Attenuations: attenuations,
Facts: []ucan.Fact{
{
Data: d.createOAuthFact(scopes, "user_delegation"),
},
},
}
// Sign the token
signedToken, err := d.signToken(token)
if err != nil {
return nil, fmt.Errorf("failed to sign UCAN token: %w", err)
}
token.Raw = signedToken
return token, nil
}
// CreateServiceDelegation creates a UCAN token for service-to-service delegation
func (d *UCANDelegator) CreateServiceDelegation(
clientID string,
scopes []string,
expiresAt time.Time,
) (*ucan.Token, error) {
// Service delegations use the OAuth provider as issuer
issuerDID := d.signer.GetIssuerDID()
// Build resource context for service
resourceContext := map[string]string{
"service_id": clientID,
"type": "service",
}
// Map OAuth scopes to UCAN attenuations
attenuations := d.scopeMapper.MapToUCAN(scopes, issuerDID, clientID, resourceContext)
if len(attenuations) == 0 {
return nil, fmt.Errorf("no valid attenuations for scopes: %v", scopes)
}
// Create UCAN token
token := &ucan.Token{
Issuer: issuerDID,
Audience: clientID,
ExpiresAt: expiresAt.Unix(),
NotBefore: time.Now().Unix(),
Attenuations: attenuations,
Facts: []ucan.Fact{
{
Data: d.createOAuthFact(scopes, "service_delegation"),
},
},
}
// Sign the token
signedToken, err := d.signToken(token)
if err != nil {
return nil, fmt.Errorf("failed to sign UCAN token: %w", err)
}
token.Raw = signedToken
return token, nil
}
// CreateDelegationChain creates a chain of UCAN tokens for complex delegations
func (d *UCANDelegator) CreateDelegationChain(
userDID string,
intermediaries []string,
finalAudience string,
scopes []string,
expiresAt time.Time,
) ([]*ucan.Token, error) {
chain := make([]*ucan.Token, 0, len(intermediaries)+1)
currentIssuer := userDID
proofs := []ucan.Proof{}
// Create delegation for each intermediary
for _, intermediary := range intermediaries {
token, err := d.createIntermediateDelegation(
currentIssuer,
intermediary,
scopes,
expiresAt,
proofs,
)
if err != nil {
return nil, fmt.Errorf("failed to create intermediate delegation: %w", err)
}
chain = append(chain, token)
proofs = append(proofs, ucan.Proof(token.Raw))
currentIssuer = intermediary
}
// Create final delegation
finalToken, err := d.createFinalDelegation(
currentIssuer,
finalAudience,
scopes,
expiresAt,
proofs,
)
if err != nil {
return nil, fmt.Errorf("failed to create final delegation: %w", err)
}
chain = append(chain, finalToken)
return chain, nil
}
// RevokeDelegation revokes a UCAN delegation
func (d *UCANDelegator) RevokeDelegation(tokenID string) error {
// TODO: Implement delegation revocation
// This would typically involve:
// 1. Adding the token to a revocation list
// 2. Publishing revocation to a public ledger or database
// 3. Notifying relevant parties
return fmt.Errorf("delegation revocation not yet implemented")
}
// ValidateDelegation validates a UCAN token delegation
func (d *UCANDelegator) ValidateDelegation(token *ucan.Token, requiredScopes []string) error {
// Check expiration
if time.Now().Unix() > token.ExpiresAt {
return fmt.Errorf("token expired")
}
// Check not before
if token.NotBefore > 0 && time.Now().Unix() < token.NotBefore {
return fmt.Errorf("token not yet valid")
}
// Verify the token has required capabilities
for _, scope := range requiredScopes {
if !d.tokenGrantsScope(token, scope) {
return fmt.Errorf("token does not grant required scope: %s", scope)
}
}
// TODO: Verify signature
// TODO: Verify delegation chain if proofs exist
return nil
}
// Private helper methods
func (d *UCANDelegator) buildResourceContext(userDID string) map[string]string {
// TODO: Fetch actual resource information for the user
return map[string]string{
"vault_address": fmt.Sprintf("vault:%s", userDID),
"enclave_cid": fmt.Sprintf("cid:%s", userDID),
"dwn_id": fmt.Sprintf("dwn:%s", userDID),
"service_id": fmt.Sprintf("service:%s", userDID),
}
}
func (d *UCANDelegator) createOAuthFact(scopes []string, delegationType string) json.RawMessage {
fact := map[string]any{
"oauth_scopes": scopes,
"delegation_type": delegationType,
"issued_at": time.Now().Unix(),
"oauth_version": "2.0",
}
data, _ := json.Marshal(fact)
return json.RawMessage(data)
}
func (d *UCANDelegator) signToken(token *ucan.Token) (string, error) {
if d.signer == nil {
return "", fmt.Errorf("no signer configured")
}
return d.signer.Sign(token)
}
func (d *UCANDelegator) createIntermediateDelegation(
issuer, audience string,
scopes []string,
expiresAt time.Time,
proofs []ucan.Proof,
) (*ucan.Token, error) {
return d.createDelegationWithType(
issuer,
audience,
scopes,
expiresAt,
proofs,
"intermediate_delegation",
)
}
func (d *UCANDelegator) createFinalDelegation(
issuer, audience string,
scopes []string,
expiresAt time.Time,
proofs []ucan.Proof,
) (*ucan.Token, error) {
return d.createDelegationWithType(
issuer,
audience,
scopes,
expiresAt,
proofs,
"final_delegation",
)
}
// createDelegationWithType creates a delegation token with a specific type
func (d *UCANDelegator) createDelegationWithType(
issuer, audience string,
scopes []string,
expiresAt time.Time,
proofs []ucan.Proof,
delegationType string,
) (*ucan.Token, error) {
resourceContext := d.buildResourceContext(issuer)
attenuations := d.scopeMapper.MapToUCAN(scopes, issuer, audience, resourceContext)
token := &ucan.Token{
Issuer: issuer,
Audience: audience,
ExpiresAt: expiresAt.Unix(),
NotBefore: time.Now().Unix(),
Attenuations: attenuations,
Proofs: proofs,
Facts: []ucan.Fact{
{
Data: d.createOAuthFact(scopes, delegationType),
},
},
}
signedToken, err := d.signToken(token)
if err != nil {
return nil, err
}
token.Raw = signedToken
return token, nil
}
func (d *UCANDelegator) tokenGrantsScope(token *ucan.Token, scope string) bool {
// Get the scope definition
scopeDef, exists := d.scopeMapper.GetScope(scope)
if !exists {
return false
}
// Check if any attenuation grants the required capabilities
for _, attenuation := range token.Attenuations {
if attenuation.Capability.Grants(scopeDef.UCANActions) {
// Also check resource type matches
if d.resourceMatches(attenuation.Resource, scopeDef.ResourceType) {
return true
}
}
}
return false
}
func (d *UCANDelegator) resourceMatches(resource ucan.Resource, resourceType string) bool {
// Simple scheme matching for now
return resource.GetScheme() == resourceType || resource.GetScheme() == "*"
}