mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
564 lines
16 KiB
Go
564 lines
16 KiB
Go
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,
|
||
|
|
})
|
||
|
|
}
|