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