* clear

* feat: Add everything

* fix: Commenht
This commit is contained in:
Prad Nukala
2025-10-03 14:45:52 -04:00
committed by GitHub
parent 43b4a11c06
commit 13e6c3e84d
1935 changed files with 655061 additions and 40058 deletions
+563
View File
@@ -0,0 +1,563 @@
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/sonr-io/sonr/crypto/ucan"
)
// RefreshTokenHandler handles OAuth2 refresh token flows with UCAN chains
type RefreshTokenHandler struct {
delegator *UCANDelegator
signer *BlockchainUCANSigner
tokenStore TokenStore
clientStore ClientStore
}
// RefreshTokenRequest represents an OAuth2 refresh token request
type RefreshTokenRequest struct {
GrantType string `json:"grant_type"`
RefreshToken string `json:"refresh_token"`
Scope string `json:"scope,omitempty"`
ClientID string `json:"client_id,omitempty"`
ClientSecret string `json:"client_secret,omitempty"`
}
// RefreshTokenResponse represents an OAuth2 refresh token response
type RefreshTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token,omitempty"`
Scope string `json:"scope,omitempty"`
UCANToken string `json:"ucan_token,omitempty"`
}
// UCANRefreshMetadata stores metadata for UCAN refresh chains
type UCANRefreshMetadata struct {
OriginalIssuer string `json:"original_issuer"`
DelegationChain []string `json:"delegation_chain"`
RefreshCount int `json:"refresh_count"`
MaxRefreshCount int `json:"max_refresh_count"`
CreatedAt time.Time `json:"created_at"`
LastRefreshedAt time.Time `json:"last_refreshed_at"`
AttenuationPath []Attenuation `json:"attenuation_path"`
}
// Attenuation represents scope reduction in the delegation chain
type Attenuation struct {
FromScopes []string `json:"from_scopes"`
ToScopes []string `json:"to_scopes"`
Timestamp time.Time `json:"timestamp"`
Reason string `json:"reason,omitempty"`
}
// NewRefreshTokenHandler creates a new refresh token handler
func NewRefreshTokenHandler(
delegator *UCANDelegator,
signer *BlockchainUCANSigner,
tokenStore TokenStore,
clientStore ClientStore,
) *RefreshTokenHandler {
return &RefreshTokenHandler{
delegator: delegator,
signer: signer,
tokenStore: tokenStore,
clientStore: clientStore,
}
}
// HandleRefreshToken handles OAuth2 refresh token requests
func (h *RefreshTokenHandler) HandleRefreshToken(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse request (handle both JSON and form-encoded)
var req RefreshTokenRequest
contentType := r.Header.Get("Content-Type")
if strings.Contains(contentType, "application/json") {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.sendError(w, "invalid_request", "Failed to parse JSON request body")
return
}
} else {
// Parse form data
if err := r.ParseForm(); err != nil {
h.sendError(w, "invalid_request", "Failed to parse form data")
return
}
req.GrantType = r.FormValue("grant_type")
req.RefreshToken = r.FormValue("refresh_token")
req.Scope = r.FormValue("scope")
req.ClientID = r.FormValue("client_id")
req.ClientSecret = r.FormValue("client_secret")
}
// Validate grant type
if req.GrantType != "refresh_token" {
h.sendError(w, "unsupported_grant_type", "Only refresh_token grant type is supported")
return
}
// Validate refresh token
if req.RefreshToken == "" {
h.sendError(w, "invalid_request", "Missing refresh_token parameter")
return
}
// Authenticate client
clientID, clientSecret := h.extractClientCredentials(r, &req)
ctx := r.Context()
if err := h.clientStore.ValidateClientCredentials(ctx, clientID, clientSecret); err != nil {
h.sendError(w, "invalid_client", "Client authentication failed")
return
}
// Get client information
client, err := h.clientStore.GetClient(ctx, clientID)
if err != nil {
h.sendError(w, "invalid_client", "Client not found")
return
}
// Process refresh token
response, err := h.processRefreshToken(ctx, &req, client)
if err != nil {
h.sendError(w, "invalid_grant", err.Error())
return
}
// Send response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Pragma", "no-cache")
json.NewEncoder(w).Encode(response)
}
// processRefreshToken processes the refresh token and returns new tokens
func (h *RefreshTokenHandler) processRefreshToken(
ctx context.Context,
req *RefreshTokenRequest,
client *OAuth2Client,
) (*RefreshTokenResponse, error) {
// Retrieve stored refresh token
storedToken, err := h.tokenStore.GetToken(ctx, req.RefreshToken)
if err != nil {
return nil, fmt.Errorf("invalid refresh token")
}
// Validate token type
if storedToken.TokenType != "refresh_token" {
return nil, fmt.Errorf("token is not a refresh token")
}
// Validate client binding
if storedToken.ClientID != client.ClientID {
return nil, fmt.Errorf("refresh token was issued to a different client")
}
// Check if refresh token has expired
if time.Now().After(storedToken.ExpiresAt) {
return nil, fmt.Errorf("refresh token has expired")
}
// Get refresh metadata
metadata, err := h.getRefreshMetadata(ctx, req.RefreshToken)
if err != nil {
// Initialize metadata for first refresh
metadata = &UCANRefreshMetadata{
OriginalIssuer: storedToken.UserDID,
DelegationChain: []string{},
RefreshCount: 0,
MaxRefreshCount: 10, // Default max refresh count
CreatedAt: time.Now(),
AttenuationPath: []Attenuation{},
}
}
// Check refresh count limit
if metadata.RefreshCount >= metadata.MaxRefreshCount {
return nil, fmt.Errorf("refresh token has reached maximum refresh count")
}
// Parse requested scopes
requestedScopes := storedToken.Scopes
if req.Scope != "" {
requestedScopes = strings.Split(req.Scope, " ")
// Validate scope reduction (attenuate permissions)
if !h.validateScopeAttenuation(requestedScopes, storedToken.Scopes) {
return nil, fmt.Errorf("requested scopes exceed refresh token scopes")
}
// Record attenuation
if !h.scopesEqual(requestedScopes, storedToken.Scopes) {
metadata.AttenuationPath = append(metadata.AttenuationPath, Attenuation{
FromScopes: storedToken.Scopes,
ToScopes: requestedScopes,
Timestamp: time.Now(),
Reason: "Client requested scope reduction",
})
}
}
// Create new UCAN token with delegation chain
newUCANToken, err := h.createRefreshedUCANToken(
ctx,
metadata,
storedToken,
client,
requestedScopes,
)
if err != nil {
return nil, fmt.Errorf("failed to create refreshed UCAN token: %w", err)
}
// Generate new access token
accessTokenID := h.generateTokenID()
accessToken := &StoredToken{
TokenID: accessTokenID,
TokenType: "access_token",
AccessToken: accessTokenID,
ExpiresAt: time.Now().Add(time.Hour),
Scopes: requestedScopes,
ClientID: client.ClientID,
UserDID: storedToken.UserDID,
UCANToken: newUCANToken,
}
if err := h.tokenStore.StoreToken(ctx, accessToken); err != nil {
return nil, fmt.Errorf("failed to store new access token: %w", err)
}
// Update refresh metadata
metadata.RefreshCount++
metadata.LastRefreshedAt = time.Now()
metadata.DelegationChain = append(metadata.DelegationChain, newUCANToken)
// Optionally rotate refresh token
newRefreshTokenID := ""
if h.shouldRotateRefreshToken(metadata) {
newRefreshTokenID = h.generateTokenID()
newRefreshToken := &StoredToken{
TokenID: newRefreshTokenID,
TokenType: "refresh_token",
RefreshToken: newRefreshTokenID,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour), // 30 days
Scopes: requestedScopes,
ClientID: client.ClientID,
UserDID: storedToken.UserDID,
}
if err := h.tokenStore.StoreToken(ctx, newRefreshToken); err != nil {
// Non-fatal, continue with existing refresh token
newRefreshTokenID = ""
} else {
// Revoke old refresh token
h.tokenStore.RevokeToken(ctx, req.RefreshToken)
// Store metadata for new refresh token
h.storeRefreshMetadata(ctx, newRefreshTokenID, metadata)
}
} else {
// Update metadata for existing refresh token
h.storeRefreshMetadata(ctx, req.RefreshToken, metadata)
}
// Build response
response := &RefreshTokenResponse{
AccessToken: accessTokenID,
TokenType: "Bearer",
ExpiresIn: 3600,
Scope: strings.Join(requestedScopes, " "),
UCANToken: newUCANToken,
}
if newRefreshTokenID != "" {
response.RefreshToken = newRefreshTokenID
}
return response, nil
}
// createRefreshedUCANToken creates a new UCAN token with proper delegation chain
func (h *RefreshTokenHandler) createRefreshedUCANToken(
ctx context.Context,
metadata *UCANRefreshMetadata,
storedToken *StoredToken,
client *OAuth2Client,
scopes []string,
) (string, error) {
// Build proof chain from previous delegations
proofs := make([]ucan.Proof, 0, len(metadata.DelegationChain))
for _, tokenStr := range metadata.DelegationChain {
proofs = append(proofs, ucan.Proof(tokenStr))
}
// Add original token as proof if exists
if storedToken.UCANToken != "" {
proofs = append([]ucan.Proof{ucan.Proof(storedToken.UCANToken)}, proofs...)
}
// Determine issuer and audience
issuer := metadata.OriginalIssuer
if issuer == "" {
issuer = storedToken.UserDID
}
audience := client.ClientID
if did, ok := client.Metadata["client_did"]; ok {
audience = did
}
// Create resource context with refresh metadata
resourceContext := map[string]string{
"refresh_count": fmt.Sprintf("%d", metadata.RefreshCount),
"original_issuer": metadata.OriginalIssuer,
"delegation_type": "refresh_token",
"client_id": client.ClientID,
}
// Map OAuth scopes to UCAN attenuations
attenuations := h.delegator.scopeMapper.MapToUCAN(scopes, issuer, audience, resourceContext)
// Create UCAN token with delegation chain
ucanToken := &ucan.Token{
Issuer: issuer,
Audience: audience,
ExpiresAt: time.Now().Add(time.Hour).Unix(),
NotBefore: time.Now().Unix(),
Attenuations: attenuations,
Proofs: proofs,
Facts: []ucan.Fact{
{
Data: h.createRefreshFact(metadata, scopes),
},
},
}
// Sign the token
signedToken, err := h.signer.Sign(ucanToken)
if err != nil {
return "", fmt.Errorf("failed to sign UCAN token: %w", err)
}
// Validate the delegation chain
if len(metadata.DelegationChain) > 0 {
allTokens := append([]string{storedToken.UCANToken}, metadata.DelegationChain...)
allTokens = append(allTokens, signedToken)
if err := h.signer.ValidateDelegationChain(allTokens); err != nil {
return "", fmt.Errorf("invalid delegation chain: %w", err)
}
}
return signedToken, nil
}
// validateScopeAttenuation validates that requested scopes are properly attenuated
func (h *RefreshTokenHandler) validateScopeAttenuation(requested, allowed []string) bool {
// Build allowed scope map
allowedMap := make(map[string]bool)
for _, scope := range allowed {
allowedMap[scope] = true
}
// Check each requested scope
for _, scope := range requested {
if !allowedMap[scope] {
// Check if a parent scope allows this
found := false
for _, allowedScope := range allowed {
if h.delegator.scopeMapper.IsHierarchicalScope(allowedScope, scope) {
found = true
break
}
}
if !found {
return false
}
}
}
return true
}
// shouldRotateRefreshToken determines if refresh token should be rotated
func (h *RefreshTokenHandler) shouldRotateRefreshToken(metadata *UCANRefreshMetadata) bool {
// Rotate on every use for maximum security
// Could be configured based on policy
return true
}
// extractClientCredentials extracts client credentials from request
func (h *RefreshTokenHandler) extractClientCredentials(
r *http.Request,
req *RefreshTokenRequest,
) (string, string) {
// Try Basic Auth first
if clientID, clientSecret, ok := r.BasicAuth(); ok {
return clientID, clientSecret
}
// Fall back to request body
return req.ClientID, req.ClientSecret
}
// getRefreshMetadata retrieves refresh metadata from storage
func (h *RefreshTokenHandler) getRefreshMetadata(
ctx context.Context,
refreshTokenID string,
) (*UCANRefreshMetadata, error) {
// In production, this would retrieve from persistent storage
// For now, return error to initialize new metadata
return nil, fmt.Errorf("metadata not found")
}
// storeRefreshMetadata stores refresh metadata
func (h *RefreshTokenHandler) storeRefreshMetadata(
ctx context.Context,
refreshTokenID string,
metadata *UCANRefreshMetadata,
) error {
// In production, this would persist to storage
// For now, just return success
return nil
}
// createRefreshFact creates a fact for refresh token
func (h *RefreshTokenHandler) createRefreshFact(
metadata *UCANRefreshMetadata,
scopes []string,
) json.RawMessage {
fact := map[string]any{
"type": "refresh_token",
"refresh_count": metadata.RefreshCount,
"original_issuer": metadata.OriginalIssuer,
"scopes": scopes,
"refreshed_at": time.Now().Unix(),
"delegation_length": len(metadata.DelegationChain),
}
// Add attenuation info if present
if len(metadata.AttenuationPath) > 0 {
fact["attenuations"] = len(metadata.AttenuationPath)
lastAttenuation := metadata.AttenuationPath[len(metadata.AttenuationPath)-1]
fact["last_attenuation"] = map[string]any{
"from": strings.Join(lastAttenuation.FromScopes, " "),
"to": strings.Join(lastAttenuation.ToScopes, " "),
"at": lastAttenuation.Timestamp.Unix(),
}
}
data, _ := json.Marshal(fact)
return json.RawMessage(data)
}
// scopesEqual checks if two scope slices are equal
func (h *RefreshTokenHandler) scopesEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
aMap := make(map[string]bool)
for _, scope := range a {
aMap[scope] = true
}
for _, scope := range b {
if !aMap[scope] {
return false
}
}
return true
}
// generateTokenID generates a unique token identifier
func (h *RefreshTokenHandler) generateTokenID() string {
// In production, use a proper UUID or secure random generator
return fmt.Sprintf("tok_%d_%s", time.Now().UnixNano(), h.randomString(16))
}
// randomString generates a random string
func (h *RefreshTokenHandler) randomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
result := make([]byte, length)
for i := range result {
result[i] = charset[time.Now().UnixNano()%int64(len(charset))]
}
return string(result)
}
// sendError sends an OAuth error response
func (h *RefreshTokenHandler) sendError(w http.ResponseWriter, errorCode, errorDescription string) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Pragma", "no-cache")
w.WriteHeader(http.StatusBadRequest)
response := map[string]string{
"error": errorCode,
"error_description": errorDescription,
}
json.NewEncoder(w).Encode(response)
}
// HandleUCANRefresh handles UCAN-specific refresh requests
func (h *RefreshTokenHandler) HandleUCANRefresh(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse UCAN token from Authorization header
authHeader := r.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
h.sendError(w, "invalid_request", "Missing or invalid Authorization header")
return
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
// Verify the UCAN token
ucanToken, err := h.signer.VerifySignature(tokenString)
if err != nil {
h.sendError(w, "invalid_grant", "Invalid UCAN token")
return
}
// Check if token can be refreshed (not expired beyond grace period)
gracePeriod := int64(300) // 5 minutes grace period
if time.Now().Unix() > ucanToken.ExpiresAt+gracePeriod {
h.sendError(w, "invalid_grant", "Token expired beyond grace period")
return
}
// Create refreshed token with extended expiration
newToken, err := h.signer.RefreshToken(tokenString, time.Hour)
if err != nil {
h.sendError(w, "server_error", "Failed to refresh token")
return
}
// Send response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
json.NewEncoder(w).Encode(map[string]any{
"ucan_token": newToken,
"token_type": "UCAN",
"expires_in": 3600,
})
}