Files
sonr/bridge/handlers/oauth2_register.go
T
Prad NukalaandGitHub 13e6c3e84d Master (#1262)
* clear

* feat: Add everything

* fix: Commenht
2025-10-03 14:45:52 -04:00

384 lines
12 KiB
Go

package handlers
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"net/http"
"strings"
"time"
)
// DynamicClientRegistrationRequest represents a client registration request per RFC 7591
type DynamicClientRegistrationRequest struct {
ClientName string `json:"client_name"`
RedirectURIs []string `json:"redirect_uris"`
GrantTypes []string `json:"grant_types,omitempty"`
ResponseTypes []string `json:"response_types,omitempty"`
Scope string `json:"scope,omitempty"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
ApplicationType string `json:"application_type,omitempty"`
Contacts []string `json:"contacts,omitempty"`
LogoURI string `json:"logo_uri,omitempty"`
ClientURI string `json:"client_uri,omitempty"`
PolicyURI string `json:"policy_uri,omitempty"`
TosURI string `json:"tos_uri,omitempty"`
JwksURI string `json:"jwks_uri,omitempty"`
SoftwareID string `json:"software_id,omitempty"`
SoftwareVersion string `json:"software_version,omitempty"`
}
// DynamicClientRegistrationResponse represents the response for client registration
type DynamicClientRegistrationResponse struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
ClientName string `json:"client_name"`
RedirectURIs []string `json:"redirect_uris"`
GrantTypes []string `json:"grant_types"`
ResponseTypes []string `json:"response_types"`
Scope string `json:"scope"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
ApplicationType string `json:"application_type"`
ClientIDIssuedAt int64 `json:"client_id_issued_at"`
ClientSecretExpiresAt int64 `json:"client_secret_expires_at,omitempty"`
LogoURI string `json:"logo_uri,omitempty"`
ClientURI string `json:"client_uri,omitempty"`
PolicyURI string `json:"policy_uri,omitempty"`
TosURI string `json:"tos_uri,omitempty"`
JwksURI string `json:"jwks_uri,omitempty"`
}
// HandleDynamicClientRegistration handles dynamic client registration per RFC 7591
func (s *OAuth2Provider) HandleDynamicClientRegistration(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse registration request
var req DynamicClientRegistrationRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid registration request", http.StatusBadRequest)
return
}
// Validate required fields
if req.ClientName == "" {
http.Error(w, "client_name is required", http.StatusBadRequest)
return
}
if len(req.RedirectURIs) == 0 {
http.Error(w, "redirect_uris is required", http.StatusBadRequest)
return
}
// Validate redirect URIs
for _, uri := range req.RedirectURIs {
if !isValidRedirectURI(uri) {
http.Error(w, "Invalid redirect URI: "+uri, http.StatusBadRequest)
return
}
}
// Set defaults if not provided
if len(req.GrantTypes) == 0 {
req.GrantTypes = []string{"authorization_code"}
}
if len(req.ResponseTypes) == 0 {
req.ResponseTypes = []string{"code"}
}
if req.TokenEndpointAuthMethod == "" {
// Default based on application type
if req.ApplicationType == "native" || req.ApplicationType == "browser" {
req.TokenEndpointAuthMethod = "none" // Public client
} else {
req.TokenEndpointAuthMethod = "client_secret_basic"
}
}
if req.ApplicationType == "" {
req.ApplicationType = "web"
}
// Validate grant types and response types
if !validateGrantTypes(req.GrantTypes) {
http.Error(w, "Invalid grant types", http.StatusBadRequest)
return
}
if !validateResponseTypes(req.ResponseTypes) {
http.Error(w, "Invalid response types", http.StatusBadRequest)
return
}
// Validate scopes if scope mapper is available
if req.Scope != "" && s.scopeMapper != nil {
scopes := strings.Split(req.Scope, " ")
for _, scope := range scopes {
// Check if scope is valid using the scope mapper
if _, exists := s.scopeMapper.GetScope(scope); !exists {
http.Error(w, "Invalid scope: "+scope, http.StatusBadRequest)
return
}
}
}
// Generate client credentials
clientID := generateDynamicClientID()
var clientSecret string
var clientSecretExpiresAt int64
// Only generate secret for confidential clients
if req.TokenEndpointAuthMethod != "none" {
clientSecret = generateDynamicClientSecret()
// Client secrets expire in 1 year by default
clientSecretExpiresAt = time.Now().Add(365 * 24 * time.Hour).Unix()
}
// Create OAuth2 client
client := &OAuth2Client{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURIs: req.RedirectURIs,
AllowedScopes: strings.Split(req.Scope, " "),
AllowedGrants: req.GrantTypes,
TokenLifetime: time.Hour, // Default 1 hour
RequirePKCE: false,
TrustedClient: false,
RequiresConsent: true,
Metadata: map[string]string{
"client_name": req.ClientName,
"application_type": req.ApplicationType,
"logo_uri": req.LogoURI,
"client_uri": req.ClientURI,
"policy_uri": req.PolicyURI,
"tos_uri": req.TosURI,
"jwks_uri": req.JwksURI,
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Set client type based on auth method
if req.TokenEndpointAuthMethod == "none" {
client.ClientType = "public"
} else {
client.ClientType = "confidential"
}
// Determine if client requires PKCE
if req.ApplicationType == "native" || req.ApplicationType == "browser" {
client.RequirePKCE = true
}
// Store client in the registry
if s.clientRegistry != nil {
if err := s.clientRegistry.RegisterClient(client); err != nil {
http.Error(w, "Failed to register client", http.StatusInternalServerError)
return
}
}
// Build response
resp := DynamicClientRegistrationResponse{
ClientID: clientID,
ClientSecret: clientSecret,
ClientName: req.ClientName,
RedirectURIs: req.RedirectURIs,
GrantTypes: req.GrantTypes,
ResponseTypes: req.ResponseTypes,
Scope: req.Scope,
TokenEndpointAuthMethod: req.TokenEndpointAuthMethod,
ApplicationType: req.ApplicationType,
ClientIDIssuedAt: time.Now().Unix(),
ClientSecretExpiresAt: clientSecretExpiresAt,
LogoURI: req.LogoURI,
ClientURI: req.ClientURI,
PolicyURI: req.PolicyURI,
TosURI: req.TosURI,
JwksURI: req.JwksURI,
}
// Return registration response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(resp)
}
// HandleClientConfiguration handles client configuration retrieval
func (s *OAuth2Provider) HandleClientConfiguration(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Extract client ID from path or query
clientID := r.URL.Query().Get("client_id")
if clientID == "" {
// Try to extract from path (e.g., /register/{client_id})
parts := strings.Split(r.URL.Path, "/")
if len(parts) > 2 {
clientID = parts[len(parts)-1]
}
}
if clientID == "" {
http.Error(w, "client_id is required", http.StatusBadRequest)
return
}
// Validate access token for client management
authHeader := r.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
w.Header().Set("WWW-Authenticate", `Bearer realm="client_configuration"`)
http.Error(w, "Access token required", http.StatusUnauthorized)
return
}
token := strings.TrimPrefix(authHeader, "Bearer ")
// Validate token in the access token store
if s.accessTokenStore == nil {
http.Error(w, "Token store not available", http.StatusInternalServerError)
return
}
// Check token validity (simplified for now)
// In production, this should validate the token properly
if token == "" {
http.Error(w, "Invalid or expired access token", http.StatusUnauthorized)
return
}
// Get client from registry
if s.clientRegistry == nil {
http.Error(w, "Client registry not available", http.StatusInternalServerError)
return
}
client, err := s.clientRegistry.GetClient(clientID)
if err != nil {
http.Error(w, "Client not found", http.StatusNotFound)
return
}
// Build response
resp := DynamicClientRegistrationResponse{
ClientID: client.ClientID,
ClientName: client.Metadata["client_name"],
RedirectURIs: client.RedirectURIs,
GrantTypes: client.AllowedGrants,
ResponseTypes: []string{"code", "token"}, // Default response types
Scope: strings.Join(client.AllowedScopes, " "),
TokenEndpointAuthMethod: getTokenEndpointAuthMethod(client),
ApplicationType: client.Metadata["application_type"],
ClientIDIssuedAt: client.CreatedAt.Unix(),
LogoURI: client.Metadata["logo_uri"],
ClientURI: client.Metadata["client_uri"],
PolicyURI: client.Metadata["policy_uri"],
TosURI: client.Metadata["tos_uri"],
JwksURI: client.Metadata["jwks_uri"],
}
// Return client configuration
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
json.NewEncoder(w).Encode(resp)
}
// getTokenEndpointAuthMethod determines the auth method from client type
func getTokenEndpointAuthMethod(client *OAuth2Client) string {
if client.ClientType == "public" {
return "none"
}
return "client_secret_basic"
}
// Helper functions for dynamic registration
func generateDynamicClientID() string {
// Generate a random client ID for dynamic registration
b := make([]byte, 16)
_, _ = rand.Read(b)
return "dyn_client_" + base64.RawURLEncoding.EncodeToString(b)
}
func generateDynamicClientSecret() string {
// Generate a secure random secret for dynamic registration
b := make([]byte, 32)
_, _ = rand.Read(b)
return base64.RawURLEncoding.EncodeToString(b)
}
func isValidRedirectURI(uri string) bool {
// Basic validation - in production, this should be more comprehensive
if uri == "" {
return false
}
// Allow localhost for development
if strings.HasPrefix(uri, "http://localhost") || strings.HasPrefix(uri, "http://127.0.0.1") {
return true
}
// Require HTTPS for production URIs
if !strings.HasPrefix(uri, "https://") {
// Allow custom schemes for native apps
if strings.Contains(uri, "://") {
return true
}
return false
}
return true
}
func validateGrantTypes(grantTypes []string) bool {
validGrants := map[string]bool{
"authorization_code": true,
"implicit": true,
"refresh_token": true,
"client_credentials": true,
"password": true,
}
for _, grant := range grantTypes {
if !validGrants[grant] {
return false
}
}
return true
}
func validateResponseTypes(responseTypes []string) bool {
validTypes := map[string]bool{
"code": true,
"token": true,
"id_token": true,
}
for _, respType := range responseTypes {
// Handle composite types like "code id_token"
parts := strings.Split(respType, " ")
for _, part := range parts {
if !validTypes[part] {
return false
}
}
}
return true
}
func hasScope(scopes []string, requiredScope string) bool {
for _, scope := range scopes {
if scope == requiredScope {
return true
}
}
return false
}