mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-08-08 06:08:30 +02:00
feat(plugins): Implement plugin API key management and authentication middleware
The purpose of this is to allow plugins to access certain internal APIs via - Added PluginAPIKey and APIKeyManager for managing API keys associated with plugins. - Introduced PluginAuthMiddleware to handle API key validation for plugin requests. - Updated RouterDef to support plugin accessible endpoints with authentication. - Modified various API registration functions to include plugin accessibility checks. - Enhanced plugin lifecycle management to generate and revoke API keys as needed. - Updated plugin specifications to include permitted API endpoints for access control.
This commit is contained in:
@@ -28,6 +28,8 @@ type AuthAgent struct {
|
||||
Database *db.Database
|
||||
LoginRedirectionHandler func(http.ResponseWriter, *http.Request)
|
||||
Logger *logger.Logger
|
||||
//Plugin related
|
||||
PluginAuthMiddleware *PluginAuthMiddleware //Plugin authentication middleware
|
||||
}
|
||||
|
||||
type AuthEndpoints struct {
|
||||
@@ -39,7 +41,7 @@ type AuthEndpoints struct {
|
||||
}
|
||||
|
||||
// Constructor
|
||||
func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, allowReg bool, systemLogger *logger.Logger, loginRedirectionHandler func(http.ResponseWriter, *http.Request)) *AuthAgent {
|
||||
func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, allowReg bool, systemLogger *logger.Logger, loginRedirectionHandler func(http.ResponseWriter, *http.Request), apiKeyManager *APIKeyManager) *AuthAgent {
|
||||
store := sessions.NewCookieStore(key)
|
||||
err := sysdb.NewTable("auth")
|
||||
if err != nil {
|
||||
@@ -47,6 +49,9 @@ func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database,
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Initialize the plugin authentication middleware
|
||||
pluginAuthMiddleware := NewPluginAuthMiddleware(apiKeyManager)
|
||||
|
||||
//Create a new AuthAgent object
|
||||
newAuthAgent := AuthAgent{
|
||||
SessionName: sessionName,
|
||||
@@ -54,6 +59,7 @@ func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database,
|
||||
Database: sysdb,
|
||||
LoginRedirectionHandler: loginRedirectionHandler,
|
||||
Logger: systemLogger,
|
||||
PluginAuthMiddleware: pluginAuthMiddleware,
|
||||
}
|
||||
|
||||
//Return the authAgent
|
||||
|
152
src/mod/auth/plugin_apikey_manager.go
Normal file
152
src/mod/auth/plugin_apikey_manager.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/plugins/zoraxy_plugin"
|
||||
)
|
||||
|
||||
// PluginAPIKey represents an API key for a plugin
|
||||
type PluginAPIKey struct {
|
||||
PluginID string
|
||||
APIKey string
|
||||
PermittedEndpoints []zoraxy_plugin.PermittedAPIEndpoint // List of permitted API endpoints
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// APIKeyManager manages API keys for plugins
|
||||
type APIKeyManager struct {
|
||||
keys map[string]*PluginAPIKey // key: API key, value: plugin info
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewAPIKeyManager creates a new API key manager
|
||||
func NewAPIKeyManager() *APIKeyManager {
|
||||
return &APIKeyManager{
|
||||
keys: make(map[string]*PluginAPIKey),
|
||||
mutex: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateAPIKey generates a new API key for a plugin
|
||||
func (m *APIKeyManager) GenerateAPIKey(pluginID string, permittedEndpoints []zoraxy_plugin.PermittedAPIEndpoint) (*PluginAPIKey, error) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
// Generate a cryptographically secure random key
|
||||
bytes := make([]byte, 32)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate random bytes: %w", err)
|
||||
}
|
||||
|
||||
// Hash the random bytes to create the API key
|
||||
hash := sha256.Sum256(bytes)
|
||||
apiKey := hex.EncodeToString(hash[:])
|
||||
|
||||
// Create the plugin API key
|
||||
pluginAPIKey := &PluginAPIKey{
|
||||
PluginID: pluginID,
|
||||
APIKey: apiKey,
|
||||
PermittedEndpoints: permittedEndpoints,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Store the API key
|
||||
m.keys[apiKey] = pluginAPIKey
|
||||
|
||||
return pluginAPIKey, nil
|
||||
}
|
||||
|
||||
// ValidateAPIKey validates an API key and returns the associated plugin information
|
||||
func (m *APIKeyManager) ValidateAPIKey(apiKey string) (*PluginAPIKey, error) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
pluginAPIKey, exists := m.keys[apiKey]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("invalid API key")
|
||||
}
|
||||
|
||||
return pluginAPIKey, nil
|
||||
}
|
||||
|
||||
// ValidateAPIKeyForEndpoint validates an API key for a specific endpoint
|
||||
func (m *APIKeyManager) ValidateAPIKeyForEndpoint(endpoint string, method string, apiKey string) (*PluginAPIKey, error) {
|
||||
pluginAPIKey, err := m.ValidateAPIKey(apiKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if the endpoint is permitted
|
||||
for _, permittedEndpoint := range pluginAPIKey.PermittedEndpoints {
|
||||
if permittedEndpoint.Endpoint == endpoint {
|
||||
return pluginAPIKey, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("endpoint not permitted for this API key")
|
||||
}
|
||||
|
||||
// RevokeAPIKey revokes an API key
|
||||
func (m *APIKeyManager) RevokeAPIKey(apiKey string) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
if _, exists := m.keys[apiKey]; !exists {
|
||||
return fmt.Errorf("API key not found")
|
||||
}
|
||||
|
||||
delete(m.keys, apiKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RevokeAPIKeysForPlugin revokes all API keys for a specific plugin
|
||||
func (m *APIKeyManager) RevokeAPIKeysForPlugin(pluginID string) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
keysToRemove := []string{}
|
||||
for apiKey, pluginAPIKey := range m.keys {
|
||||
if pluginAPIKey.PluginID == pluginID {
|
||||
keysToRemove = append(keysToRemove, apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
for _, apiKey := range keysToRemove {
|
||||
delete(m.keys, apiKey)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAPIKeyForPlugin returns the API key for a plugin (if exists)
|
||||
func (m *APIKeyManager) GetAPIKeyForPlugin(pluginID string) (*PluginAPIKey, error) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
for _, pluginAPIKey := range m.keys {
|
||||
if pluginAPIKey.PluginID == pluginID {
|
||||
return pluginAPIKey, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no API key found for plugin")
|
||||
}
|
||||
|
||||
// ListAPIKeys returns all API keys (for debugging purposes)
|
||||
func (m *APIKeyManager) ListAPIKeys() []*PluginAPIKey {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
keys := make([]*PluginAPIKey, 0, len(m.keys))
|
||||
for _, pluginAPIKey := range m.keys {
|
||||
keys = append(keys, pluginAPIKey)
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
77
src/mod/auth/plugin_middleware.go
Normal file
77
src/mod/auth/plugin_middleware.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// Handles the API-Key based authentication for plugins
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PluginAuthMiddleware provides authentication middleware for plugin API requests
|
||||
type PluginAuthMiddleware struct {
|
||||
apiKeyManager *APIKeyManager
|
||||
}
|
||||
|
||||
// NewPluginAuthMiddleware creates a new plugin authentication middleware
|
||||
func NewPluginAuthMiddleware(apiKeyManager *APIKeyManager) *PluginAuthMiddleware {
|
||||
return &PluginAuthMiddleware{
|
||||
apiKeyManager: apiKeyManager,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidatePluginAPIRequest validates an API request with plugin API key for a specific endpoint
|
||||
func (m *PluginAuthMiddleware) ValidatePluginAPIRequest(endpoint string, method string, apiKey string) (*PluginAPIKey, error) {
|
||||
return m.apiKeyManager.ValidateAPIKeyForEndpoint(endpoint, method, apiKey)
|
||||
}
|
||||
|
||||
// WrapHandler wraps an HTTP handler with plugin authentication middleware
|
||||
func (m *PluginAuthMiddleware) WrapHandler(endpoint string, handler http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// First, remove any existing plugin authentication headers
|
||||
r.Header.Del("X-Zoraxy-Plugin-ID")
|
||||
r.Header.Del("X-Zoraxy-Plugin-Auth")
|
||||
|
||||
// Check for API key in the Authorization header
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
// No authorization header, proceed with normal authentication
|
||||
handler(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it's a plugin API key (Bearer token)
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
// Not a Bearer token, proceed with normal authentication
|
||||
handler(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the API key
|
||||
apiKey := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
// Validate the API key for this endpoint
|
||||
pluginAPIKey, err := m.ValidatePluginAPIRequest(endpoint, r.Method, apiKey)
|
||||
if err != nil {
|
||||
// Invalid API key or endpoint not permitted
|
||||
http.Error(w, "Unauthorized: Invalid API key or endpoint not permitted", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Add plugin information to the request context
|
||||
r.Header.Set("X-Zoraxy-Plugin-ID", pluginAPIKey.PluginID)
|
||||
r.Header.Set("X-Zoraxy-Plugin-Auth", "true")
|
||||
|
||||
// Call the original handler
|
||||
handler(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// GetPluginIDFromRequest extracts the plugin ID from the request if authenticated via plugin API key
|
||||
func GetPluginIDFromRequest(r *http.Request) string {
|
||||
return r.Header.Get("X-Plugin-ID")
|
||||
}
|
||||
|
||||
// IsPluginAuthenticated checks if the request is authenticated via plugin API key
|
||||
func IsPluginAuthenticated(r *http.Request) bool {
|
||||
return r.Header.Get("X-Plugin-Auth") == "true"
|
||||
}
|
@@ -25,7 +25,7 @@ func NewManagedHTTPRouter(option RouterOption) *RouterDef {
|
||||
}
|
||||
}
|
||||
|
||||
func (router *RouterDef) HandleFunc(endpoint string, handler func(http.ResponseWriter, *http.Request)) error {
|
||||
func (router *RouterDef) HandleFunc(endpoint string, handler func(http.ResponseWriter, *http.Request), pluginAccessible bool) error {
|
||||
//Check if the endpoint already registered
|
||||
if _, exist := router.endpoints[endpoint]; exist {
|
||||
fmt.Println("WARNING! Duplicated registering of web endpoint: " + endpoint)
|
||||
@@ -34,31 +34,28 @@ func (router *RouterDef) HandleFunc(endpoint string, handler func(http.ResponseW
|
||||
|
||||
authAgent := router.option.AuthAgent
|
||||
|
||||
authWrapper := func(w http.ResponseWriter, r *http.Request) {
|
||||
//Check authentication of the user
|
||||
X_Plugin_Auth := r.Header.Get("X-Zoraxy-Plugin-Auth")
|
||||
if router.option.RequireAuth && !(pluginAccessible && X_Plugin_Auth == "true") {
|
||||
authAgent.HandleCheckAuth(w, r, func(w http.ResponseWriter, r *http.Request) {
|
||||
handler(w, r)
|
||||
})
|
||||
} else {
|
||||
handler(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// if the endpoint is supposed to be plugin accessible, wrap it with plugin authentication middleware
|
||||
if pluginAccessible {
|
||||
authWrapper = router.option.AuthAgent.PluginAuthMiddleware.WrapHandler(endpoint, authWrapper)
|
||||
}
|
||||
|
||||
//OK. Register handler
|
||||
if router.option.TargetMux == nil {
|
||||
http.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) {
|
||||
//Check authentication of the user
|
||||
if router.option.RequireAuth {
|
||||
authAgent.HandleCheckAuth(w, r, func(w http.ResponseWriter, r *http.Request) {
|
||||
handler(w, r)
|
||||
})
|
||||
} else {
|
||||
handler(w, r)
|
||||
}
|
||||
|
||||
})
|
||||
http.HandleFunc(endpoint, authWrapper)
|
||||
} else {
|
||||
router.option.TargetMux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) {
|
||||
//Check authentication of the user
|
||||
if router.option.RequireAuth {
|
||||
authAgent.HandleCheckAuth(w, r, func(w http.ResponseWriter, r *http.Request) {
|
||||
handler(w, r)
|
||||
})
|
||||
} else {
|
||||
handler(w, r)
|
||||
}
|
||||
|
||||
})
|
||||
router.option.TargetMux.HandleFunc(endpoint, authWrapper)
|
||||
}
|
||||
|
||||
router.endpoints[endpoint] = handler
|
||||
|
@@ -41,6 +41,18 @@ func (m *Manager) StartPlugin(pluginID string) error {
|
||||
Port: getRandomPortNumber(),
|
||||
RuntimeConst: *m.Options.SystemConst,
|
||||
}
|
||||
|
||||
// Generate API key if the plugin has permitted endpoints
|
||||
if len(thisPlugin.Spec.PermittedAPIEndpoints) > 0 {
|
||||
apiKey, err := m.Options.APIKeyManager.GenerateAPIKey(thisPlugin.Spec.ID, thisPlugin.Spec.PermittedAPIEndpoints)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pluginConfiguration.APIKey = apiKey.APIKey
|
||||
pluginConfiguration.ZoraxyPort = m.Options.ZoraxyPort
|
||||
m.Log("Generated API key for plugin "+thisPlugin.Spec.Name, nil)
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(pluginConfiguration)
|
||||
|
||||
//Start the plugin with given configuration
|
||||
@@ -270,6 +282,13 @@ func (m *Manager) StopPlugin(pluginID string) error {
|
||||
thisPlugin.Enabled = false
|
||||
thisPlugin.StopAllStaticPathRouters()
|
||||
thisPlugin.StopDynamicForwardRouter()
|
||||
|
||||
//Clean up API key
|
||||
err = m.Options.APIKeyManager.RevokeAPIKeysForPlugin(thisPlugin.Spec.ID)
|
||||
if err != nil {
|
||||
m.Log("Failed to revoke API keys for plugin "+thisPlugin.Spec.Name, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
@@ -42,10 +43,14 @@ type ManagerOptions struct {
|
||||
|
||||
/* Runtime */
|
||||
SystemConst *zoraxyPlugin.RuntimeConstantValue //The system constant value
|
||||
ZoraxyPort int //The port of the Zoraxy instance, used for API calls
|
||||
CSRFTokenGen func(*http.Request) string `json:"-"` //The CSRF token generator function
|
||||
Database *database.Database `json:"-"`
|
||||
Logger *logger.Logger `json:"-"`
|
||||
|
||||
/* API Key Management */
|
||||
APIKeyManager *auth.APIKeyManager `json:"-"` //The API key manager for the plugins
|
||||
|
||||
/* Development */
|
||||
EnableHotReload bool //Check if the plugin file is changed and reload the plugin automatically
|
||||
HotReloadInterval int //The interval for checking the plugin file change, in seconds
|
||||
|
@@ -47,6 +47,12 @@ type RuntimeConstantValue struct {
|
||||
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
|
||||
}
|
||||
|
||||
type PermittedAPIEndpoint struct {
|
||||
Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST)
|
||||
Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access
|
||||
Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
IntroSpect Payload
|
||||
|
||||
@@ -97,7 +103,10 @@ type IntroSpect struct {
|
||||
|
||||
/* Subscriptions Settings */
|
||||
SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered
|
||||
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details
|
||||
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, paired with comments describing how the event is used, see Zoraxy documentation for more details
|
||||
|
||||
/* API Access Control */
|
||||
PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -126,8 +135,10 @@ by the supplied values like starting a web server at given port
|
||||
that listens to 127.0.0.1:port
|
||||
*/
|
||||
type ConfigureSpec struct {
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints
|
||||
ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user