Files
sonr/bridge/handlers/oauth2_token_exchange.go
T

651 lines
18 KiB
Go
Raw Normal View History

2025-10-03 14:45:52 -04:00
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/sonr-io/sonr/crypto/ucan"
)
// TokenExchangeHandler implements RFC 8693 OAuth 2.0 Token Exchange
type TokenExchangeHandler struct {
delegator *UCANDelegator
signer *BlockchainUCANSigner
tokenStore TokenStore
clientStore ClientStore
}
// TokenStore interface for token persistence
type TokenStore interface {
GetToken(ctx context.Context, tokenID string) (*StoredToken, error)
StoreToken(ctx context.Context, token *StoredToken) error
RevokeToken(ctx context.Context, tokenID string) error
}
// ClientStore interface for OAuth client information
type ClientStore interface {
GetClient(ctx context.Context, clientID string) (*OAuth2Client, error)
ValidateClientCredentials(ctx context.Context, clientID, clientSecret string) error
}
// StoredToken represents a stored OAuth token
type StoredToken struct {
TokenID string `json:"token_id"`
TokenType string `json:"token_type"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresAt time.Time `json:"expires_at"`
Scopes []string `json:"scopes"`
ClientID string `json:"client_id"`
UserDID string `json:"user_did,omitempty"`
UCANToken string `json:"ucan_token"`
}
// TokenExchangeRequest represents an RFC 8693 token exchange request
type TokenExchangeRequest struct {
GrantType string `json:"grant_type"`
Resource string `json:"resource,omitempty"`
Audience string `json:"audience,omitempty"`
Scope string `json:"scope,omitempty"`
RequestedTokenType string `json:"requested_token_type,omitempty"`
SubjectToken string `json:"subject_token"`
SubjectTokenType string `json:"subject_token_type"`
ActorToken string `json:"actor_token,omitempty"`
ActorTokenType string `json:"actor_token_type,omitempty"`
}
// TokenExchangeResponse represents an RFC 8693 token exchange response
type TokenExchangeResponse struct {
AccessToken string `json:"access_token"`
IssuedTokenType string `json:"issued_token_type"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in,omitempty"`
Scope string `json:"scope,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
}
// Token type identifiers from RFC 8693
const (
TokenTypeAccessToken = "urn:ietf:params:oauth:token-type:access_token"
TokenTypeRefreshToken = "urn:ietf:params:oauth:token-type:refresh_token"
TokenTypeIDToken = "urn:ietf:params:oauth:token-type:id_token"
TokenTypeSAML1 = "urn:ietf:params:oauth:token-type:saml1"
TokenTypeSAML2 = "urn:ietf:params:oauth:token-type:saml2"
TokenTypeJWT = "urn:ietf:params:oauth:token-type:jwt"
TokenTypeUCAN = "urn:x-oath:params:oauth:token-type:ucan"
)
// NewTokenExchangeHandler creates a new token exchange handler
func NewTokenExchangeHandler(
delegator *UCANDelegator,
signer *BlockchainUCANSigner,
tokenStore TokenStore,
clientStore ClientStore,
) *TokenExchangeHandler {
return &TokenExchangeHandler{
delegator: delegator,
signer: signer,
tokenStore: tokenStore,
clientStore: clientStore,
}
}
// HandleTokenExchange handles RFC 8693 token exchange requests
func (h *TokenExchangeHandler) HandleTokenExchange(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse request
var req TokenExchangeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.sendError(w, "invalid_request", "Failed to parse request body")
return
}
// Validate grant type
if req.GrantType != "urn:ietf:params:oauth:grant-type:token-exchange" {
h.sendError(w, "unsupported_grant_type", "Only token-exchange grant type is supported")
return
}
// Validate required parameters
if req.SubjectToken == "" || req.SubjectTokenType == "" {
h.sendError(w, "invalid_request", "Missing required parameters")
return
}
// Authenticate client
clientID, clientSecret, ok := r.BasicAuth()
if !ok {
h.sendError(w, "invalid_client", "Client authentication required")
return
}
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 token exchange based on token types
response, err := h.processTokenExchange(ctx, &req, client)
if err != nil {
h.sendError(w, "invalid_request", 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)
}
// processTokenExchange processes the token exchange based on token types
func (h *TokenExchangeHandler) processTokenExchange(
ctx context.Context,
req *TokenExchangeRequest,
client *OAuth2Client,
) (*TokenExchangeResponse, error) {
// Determine requested token type (default to access token)
requestedType := req.RequestedTokenType
if requestedType == "" {
requestedType = TokenTypeAccessToken
}
// Handle different subject token types
switch req.SubjectTokenType {
case TokenTypeAccessToken:
return h.exchangeAccessToken(ctx, req, client, requestedType)
case TokenTypeRefreshToken:
return h.exchangeRefreshToken(ctx, req, client, requestedType)
case TokenTypeJWT:
return h.exchangeJWT(ctx, req, client, requestedType)
case TokenTypeUCAN:
return h.exchangeUCAN(ctx, req, client, requestedType)
default:
return nil, fmt.Errorf("unsupported subject token type: %s", req.SubjectTokenType)
}
}
// exchangeAccessToken exchanges an access token for a new token
func (h *TokenExchangeHandler) exchangeAccessToken(
ctx context.Context,
req *TokenExchangeRequest,
client *OAuth2Client,
requestedType string,
) (*TokenExchangeResponse, error) {
// Retrieve the subject token
storedToken, err := h.tokenStore.GetToken(ctx, req.SubjectToken)
if err != nil {
return nil, fmt.Errorf("invalid subject token")
}
// Validate token hasn't expired
if time.Now().After(storedToken.ExpiresAt) {
return nil, fmt.Errorf("subject token has expired")
}
// Parse requested scopes (default to original scopes)
requestedScopes := storedToken.Scopes
if req.Scope != "" {
requestedScopes = strings.Split(req.Scope, " ")
// Validate requested scopes are subset of original
if !h.isScopeSubset(requestedScopes, storedToken.Scopes) {
return nil, fmt.Errorf("requested scopes exceed original token scopes")
}
}
// Determine audience (default to requested audience or client ID)
audience := req.Audience
if audience == "" {
// Try to extract DID from client metadata
if clientDID, ok := client.Metadata["client_did"]; ok {
audience = clientDID
} else {
audience = client.ClientID
}
}
// Create new UCAN delegation based on requested type
switch requestedType {
case TokenTypeUCAN:
return h.createUCANResponse(
ctx,
storedToken.UserDID,
audience,
requestedScopes,
storedToken.UCANToken,
)
case TokenTypeAccessToken:
return h.createAccessTokenResponse(ctx, storedToken.UserDID, audience, requestedScopes)
default:
return nil, fmt.Errorf("unsupported requested token type: %s", requestedType)
}
}
// exchangeRefreshToken exchanges a refresh token for new tokens
func (h *TokenExchangeHandler) exchangeRefreshToken(
ctx context.Context,
req *TokenExchangeRequest,
client *OAuth2Client,
requestedType string,
) (*TokenExchangeResponse, error) {
// Retrieve the refresh token
storedToken, err := h.tokenStore.GetToken(ctx, req.SubjectToken)
if err != nil {
return nil, fmt.Errorf("invalid refresh token")
}
// Validate it's actually a refresh token
if storedToken.TokenType != "refresh_token" {
return nil, fmt.Errorf("token is not a refresh token")
}
// Create new tokens with same scopes
clientDID := client.ClientID
if did, ok := client.Metadata["client_did"]; ok {
clientDID = did
}
return h.createAccessTokenResponse(ctx, storedToken.UserDID, clientDID, storedToken.Scopes)
}
// exchangeJWT exchanges a JWT for a UCAN token
func (h *TokenExchangeHandler) exchangeJWT(
ctx context.Context,
req *TokenExchangeRequest,
client *OAuth2Client,
requestedType string,
) (*TokenExchangeResponse, error) {
// Verify the JWT
ucanToken, err := h.signer.VerifySignature(req.SubjectToken)
if err != nil {
return nil, fmt.Errorf("invalid JWT: %w", err)
}
// Extract scopes from JWT claims
scopes := h.extractScopesFromUCAN(ucanToken)
// Create response based on requested type
clientDID := client.ClientID
if did, ok := client.Metadata["client_did"]; ok {
clientDID = did
}
switch requestedType {
case TokenTypeUCAN:
return h.createUCANResponse(ctx, ucanToken.Issuer, clientDID, scopes, req.SubjectToken)
case TokenTypeAccessToken:
return h.createAccessTokenResponse(ctx, ucanToken.Issuer, clientDID, scopes)
default:
return nil, fmt.Errorf("unsupported requested token type: %s", requestedType)
}
}
// exchangeUCAN exchanges a UCAN token for another token type
func (h *TokenExchangeHandler) exchangeUCAN(
ctx context.Context,
req *TokenExchangeRequest,
client *OAuth2Client,
requestedType string,
) (*TokenExchangeResponse, error) {
// Verify the UCAN token
ucanToken, err := h.signer.VerifySignature(req.SubjectToken)
if err != nil {
return nil, fmt.Errorf("invalid UCAN token: %w", err)
}
// Validate delegation chain if actor token is provided
if req.ActorToken != "" {
actorToken, err := h.signer.VerifySignature(req.ActorToken)
if err != nil {
return nil, fmt.Errorf("invalid actor token: %w", err)
}
// Validate actor can act on behalf of subject
if actorToken.Audience != ucanToken.Issuer {
return nil, fmt.Errorf("actor token audience doesn't match subject issuer")
}
}
// Extract scopes from UCAN
scopes := h.extractScopesFromUCAN(ucanToken)
// Handle impersonation/delegation if actor token is present
issuer := ucanToken.Issuer
clientDID := client.ClientID
if did, ok := client.Metadata["client_did"]; ok {
clientDID = did
}
if req.ActorToken != "" {
// Actor is performing action on behalf of subject
issuer = clientDID // Actor becomes the new issuer
}
// Create response based on requested type
switch requestedType {
case TokenTypeAccessToken:
return h.createAccessTokenResponse(ctx, issuer, clientDID, scopes)
case TokenTypeUCAN:
// Create delegated UCAN with proof chain
proofs := []ucan.Proof{ucan.Proof(req.SubjectToken)}
if req.ActorToken != "" {
proofs = append(proofs, ucan.Proof(req.ActorToken))
}
return h.createDelegatedUCANResponse(ctx, issuer, clientDID, scopes, proofs)
default:
return nil, fmt.Errorf("unsupported requested token type: %s", requestedType)
}
}
// createUCANResponse creates a UCAN token response
func (h *TokenExchangeHandler) createUCANResponse(
ctx context.Context,
issuer, audience string,
scopes []string,
proof string,
) (*TokenExchangeResponse, error) {
// Create UCAN token with delegation
ucanToken, err := h.delegator.CreateDelegation(
issuer,
audience,
scopes,
time.Now().Add(time.Hour),
)
if err != nil {
return nil, fmt.Errorf("failed to create UCAN token: %w", err)
}
// Add proof if provided
if proof != "" {
ucanToken.Proofs = []ucan.Proof{ucan.Proof(proof)}
}
// Sign the token
signedToken, err := h.signer.Sign(ucanToken)
if err != nil {
return nil, fmt.Errorf("failed to sign UCAN token: %w", err)
}
return &TokenExchangeResponse{
AccessToken: signedToken,
IssuedTokenType: TokenTypeUCAN,
TokenType: "Bearer",
ExpiresIn: 3600,
Scope: strings.Join(scopes, " "),
}, nil
}
// createDelegatedUCANResponse creates a delegated UCAN token with proof chain
func (h *TokenExchangeHandler) createDelegatedUCANResponse(
ctx context.Context,
issuer, audience string,
scopes []string,
proofs []ucan.Proof,
) (*TokenExchangeResponse, error) {
// Build resource context
resourceContext := map[string]string{
"delegation_type": "token_exchange",
"issued_at": fmt.Sprintf("%d", time.Now().Unix()),
}
// Map scopes to attenuations
attenuations := h.delegator.scopeMapper.MapToUCAN(scopes, issuer, audience, resourceContext)
// Create UCAN token with proof 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.createTokenExchangeFact(scopes),
},
},
}
// Sign the token
signedToken, err := h.signer.Sign(ucanToken)
if err != nil {
return nil, fmt.Errorf("failed to sign delegated UCAN token: %w", err)
}
return &TokenExchangeResponse{
AccessToken: signedToken,
IssuedTokenType: TokenTypeUCAN,
TokenType: "Bearer",
ExpiresIn: 3600,
Scope: strings.Join(scopes, " "),
}, nil
}
// createAccessTokenResponse creates a standard OAuth access token response
func (h *TokenExchangeHandler) createAccessTokenResponse(
ctx context.Context,
userDID, clientID string,
scopes []string,
) (*TokenExchangeResponse, error) {
// Create UCAN-backed access token
ucanToken, err := h.delegator.CreateDelegation(
userDID,
clientID,
scopes,
time.Now().Add(time.Hour),
)
if err != nil {
return nil, fmt.Errorf("failed to create access token: %w", err)
}
// Generate token ID
tokenID := h.generateTokenID()
// Store token
storedToken := &StoredToken{
TokenID: tokenID,
TokenType: "access_token",
AccessToken: tokenID,
ExpiresAt: time.Now().Add(time.Hour),
Scopes: scopes,
ClientID: clientID,
UserDID: userDID,
UCANToken: ucanToken.Raw,
}
if err := h.tokenStore.StoreToken(ctx, storedToken); err != nil {
return nil, fmt.Errorf("failed to store token: %w", err)
}
// Generate refresh token
refreshTokenID := h.generateTokenID()
refreshToken := &StoredToken{
TokenID: refreshTokenID,
TokenType: "refresh_token",
RefreshToken: refreshTokenID,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour), // 30 days
Scopes: scopes,
ClientID: clientID,
UserDID: userDID,
}
if err := h.tokenStore.StoreToken(ctx, refreshToken); err != nil {
// Non-fatal, continue without refresh token
refreshTokenID = ""
}
response := &TokenExchangeResponse{
AccessToken: tokenID,
IssuedTokenType: TokenTypeAccessToken,
TokenType: "Bearer",
ExpiresIn: 3600,
Scope: strings.Join(scopes, " "),
}
if refreshTokenID != "" {
response.RefreshToken = refreshTokenID
}
return response, nil
}
// extractScopesFromUCAN extracts OAuth scopes from UCAN attenuations
func (h *TokenExchangeHandler) extractScopesFromUCAN(token *ucan.Token) []string {
scopeMap := make(map[string]bool)
for _, att := range token.Attenuations {
scheme := att.Resource.GetScheme()
actions := att.Capability.GetActions()
// Map UCAN capabilities back to OAuth scopes
for _, action := range actions {
scope := h.mapUCANToScope(scheme, action)
if scope != "" {
scopeMap[scope] = true
}
}
}
scopes := make([]string, 0, len(scopeMap))
for scope := range scopeMap {
scopes = append(scopes, scope)
}
return scopes
}
// mapUCANToScope maps UCAN capability to OAuth scope
func (h *TokenExchangeHandler) mapUCANToScope(scheme, action string) string {
// Reverse mapping from UCAN to OAuth scopes
switch scheme {
case "vault":
switch action {
case "read":
return "vault:read"
case "write":
return "vault:write"
case "sign":
return "vault:sign"
case "*", "admin":
return "vault:admin"
}
case "service", "svc":
switch action {
case "read":
return "service:read"
case "write":
return "service:write"
case "*", "admin":
return "service:manage"
}
case "did":
switch action {
case "read":
return "did:read"
case "write", "update":
return "did:write"
}
case "dwn":
switch action {
case "read":
return "dwn:read"
case "write":
return "dwn:write"
}
}
// Default mapping
return fmt.Sprintf("%s:%s", scheme, action)
}
// isScopeSubset checks if requested scopes are subset of allowed scopes
func (h *TokenExchangeHandler) isScopeSubset(requested, allowed []string) bool {
allowedMap := make(map[string]bool)
for _, scope := range allowed {
allowedMap[scope] = true
}
for _, scope := range requested {
if !allowedMap[scope] {
// Check if parent scope is allowed
if !h.isParentScopeAllowed(scope, allowed) {
return false
}
}
}
return true
}
// isParentScopeAllowed checks if a parent scope grants the requested scope
func (h *TokenExchangeHandler) isParentScopeAllowed(requested string, allowed []string) bool {
for _, scope := range allowed {
if h.delegator.scopeMapper.IsHierarchicalScope(scope, requested) {
return true
}
}
return false
}
// createTokenExchangeFact creates a fact for token exchange
func (h *TokenExchangeHandler) createTokenExchangeFact(scopes []string) json.RawMessage {
fact := map[string]any{
"type": "token_exchange",
"scopes": scopes,
"issued_at": time.Now().Unix(),
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
}
data, _ := json.Marshal(fact)
return json.RawMessage(data)
}
// generateTokenID generates a unique token identifier
func (h *TokenExchangeHandler) generateTokenID() string {
// In production, use a proper UUID or random generator
return fmt.Sprintf("tok_%d_%s", time.Now().UnixNano(), h.randomString(16))
}
// randomString generates a random string of specified length
func (h *TokenExchangeHandler) 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 *TokenExchangeHandler) 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)
}