network/pkg/gateway/handlers/auth/wallet_handler.go
2026-01-20 10:12:33 +02:00

445 lines
14 KiB
Go

package auth
import (
"encoding/json"
"fmt"
"net/http"
"strings"
authsvc "github.com/DeBrosOfficial/network/pkg/gateway/auth"
)
// WhoamiHandler returns the authenticated user's identity and method.
// This endpoint shows whether the request is authenticated via JWT or API key,
// and provides details about the authenticated principal.
//
// GET /v1/auth/whoami
// Response: { "authenticated", "method", "subject", "namespace", ... }
func (h *Handlers) WhoamiHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Determine namespace (may be overridden by auth layer)
ns := h.defaultNS
if v := ctx.Value(CtxKeyNamespaceOverride); v != nil {
if s, ok := v.(string); ok && s != "" {
ns = s
}
}
// Prefer JWT if present
if v := ctx.Value(CtxKeyJWT); v != nil {
if claims, ok := v.(*authsvc.JWTClaims); ok && claims != nil {
writeJSON(w, http.StatusOK, map[string]any{
"authenticated": true,
"method": "jwt",
"subject": claims.Sub,
"issuer": claims.Iss,
"audience": claims.Aud,
"issued_at": claims.Iat,
"not_before": claims.Nbf,
"expires_at": claims.Exp,
"namespace": ns,
})
return
}
}
// Fallback: API key identity
var key string
if v := ctx.Value(CtxKeyAPIKey); v != nil {
if s, ok := v.(string); ok {
key = s
}
}
writeJSON(w, http.StatusOK, map[string]any{
"authenticated": key != "",
"method": "api_key",
"api_key": key,
"namespace": ns,
})
}
// RegisterHandler registers a new application/client after wallet signature verification.
// This allows wallets to register applications and obtain client credentials.
//
// POST /v1/auth/register
// Request body: RegisterRequest
// Response: { "client_id", "app": { ... }, "signature_verified" }
func (h *Handlers) RegisterHandler(w http.ResponseWriter, r *http.Request) {
if h.authService == nil {
writeError(w, http.StatusServiceUnavailable, "auth service not initialized")
return
}
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
var req RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if strings.TrimSpace(req.Wallet) == "" || strings.TrimSpace(req.Nonce) == "" || strings.TrimSpace(req.Signature) == "" {
writeError(w, http.StatusBadRequest, "wallet, nonce and signature are required")
return
}
ctx := r.Context()
verified, err := h.authService.VerifySignature(ctx, req.Wallet, req.Nonce, req.Signature, req.ChainType)
if err != nil || !verified {
writeError(w, http.StatusUnauthorized, "signature verification failed")
return
}
// Mark nonce used
nsID, _ := h.resolveNamespace(ctx, req.Namespace)
h.markNonceUsed(ctx, nsID, strings.ToLower(req.Wallet), req.Nonce)
// In a real app we'd derive the public key from the signature, but for simplicity here
// we just use a placeholder or expect it in the request if needed.
// For Ethereum, we can recover it.
publicKey := "recovered-pk"
appID, err := h.authService.RegisterApp(ctx, req.Wallet, req.Namespace, req.Name, publicKey)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusCreated, map[string]any{
"client_id": appID,
"app": map[string]any{
"app_id": appID,
"name": req.Name,
"namespace": req.Namespace,
"wallet": strings.ToLower(req.Wallet),
},
"signature_verified": true,
})
}
// LoginPageHandler serves the wallet authentication login page.
// This provides an interactive HTML page for wallet-based authentication
// using MetaMask or other Web3 wallet providers.
//
// GET /v1/auth/login?callback=<url>
// Query params: callback (required) - URL to redirect after successful auth
// Response: HTML page with wallet connection UI
func (h *Handlers) LoginPageHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
callbackURL := r.URL.Query().Get("callback")
if callbackURL == "" {
writeError(w, http.StatusBadRequest, "callback parameter is required")
return
}
// Get default namespace
ns := strings.TrimSpace(h.defaultNS)
if ns == "" {
ns = "default"
}
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
html := fmt.Sprintf(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DeBros Network - Wallet Authentication</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%%, #764ba2 100%%);
margin: 0;
padding: 20px;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
padding: 40px;
max-width: 500px;
width: 100%%;
text-align: center;
}
.logo {
font-size: 32px;
font-weight: bold;
color: #667eea;
margin-bottom: 10px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
}
.step {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
text-align: left;
}
.step-number {
background: #667eea;
color: white;
border-radius: 50%%;
width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 10px;
}
button {
background: #667eea;
color: white;
border: none;
border-radius: 8px;
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin: 10px;
}
button:hover {
background: #5a67d8;
transform: translateY(-1px);
}
button:disabled {
background: #cbd5e0;
cursor: not-allowed;
transform: none;
}
.error {
background: #fed7d7;
color: #e53e3e;
padding: 12px;
border-radius: 8px;
margin: 20px 0;
display: none;
}
.success {
background: #c6f6d5;
color: #2f855a;
padding: 12px;
border-radius: 8px;
margin: 20px 0;
display: none;
}
.loading {
display: none;
margin: 20px 0;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0%% { transform: rotate(0deg); }
100%% { transform: rotate(360deg); }
}
.namespace-info {
background: #e6fffa;
border: 1px solid #81e6d9;
border-radius: 8px;
padding: 15px;
margin: 20px 0;
}
.code {
font-family: 'Monaco', 'Menlo', monospace;
background: #f7fafc;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<div class="logo">🌐 DeBros Network</div>
<p class="subtitle">Secure Wallet Authentication</p>
<div class="namespace-info">
<strong>📁 Namespace:</strong> <span class="code">%s</span>
</div>
<div class="step">
<div><span class="step-number">1</span><strong>Connect Your Wallet</strong></div>
<p>Click the button below to connect your Ethereum wallet (MetaMask, WalletConnect, etc.)</p>
</div>
<div class="step">
<div><span class="step-number">2</span><strong>Sign Authentication Message</strong></div>
<p>Your wallet will prompt you to sign a message to prove your identity. This is free and secure.</p>
</div>
<div class="step">
<div><span class="step-number">3</span><strong>Get Your API Key</strong></div>
<p>After signing, you'll receive an API key to access the DeBros Network.</p>
</div>
<div class="error" id="error"></div>
<div class="success" id="success"></div>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Processing authentication...</p>
</div>
<button onclick="connectWallet()" id="connectBtn">🔗 Connect Wallet</button>
<button onclick="window.close()" style="background: #718096;">❌ Cancel</button>
</div>
<script>
const callbackURL = '%s';
const namespace = '%s';
let walletAddress = null;
async function connectWallet() {
const btn = document.getElementById('connectBtn');
const loading = document.getElementById('loading');
const error = document.getElementById('error');
const success = document.getElementById('success');
try {
btn.disabled = true;
loading.style.display = 'block';
error.style.display = 'none';
success.style.display = 'none';
// Check if MetaMask is available
if (typeof window.ethereum === 'undefined') {
throw new Error('Please install MetaMask or another Ethereum wallet');
}
// Request account access
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
if (accounts.length === 0) {
throw new Error('No wallet accounts found');
}
walletAddress = accounts[0];
console.log('Connected to wallet:', walletAddress);
// Step 1: Get challenge nonce
const challengeResponse = await fetch('/v1/auth/challenge', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
wallet: walletAddress,
purpose: 'api_key_generation',
namespace: namespace
})
});
if (!challengeResponse.ok) {
const errorData = await challengeResponse.json();
throw new Error(errorData.error || 'Failed to get challenge');
}
const challengeData = await challengeResponse.json();
const nonce = challengeData.nonce;
console.log('Received challenge nonce:', nonce);
// Step 2: Sign the nonce
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [nonce, walletAddress]
});
console.log('Signature obtained:', signature);
// Step 3: Get API key
const apiKeyResponse = await fetch('/v1/auth/api-key', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
wallet: walletAddress,
nonce: nonce,
signature: signature,
namespace: namespace
})
});
if (!apiKeyResponse.ok) {
const errorData = await apiKeyResponse.json();
throw new Error(errorData.error || 'Failed to get API key');
}
const apiKeyData = await apiKeyResponse.json();
console.log('API key received:', apiKeyData);
loading.style.display = 'none';
success.innerHTML = '✅ Authentication successful! Redirecting...';
success.style.display = 'block';
// Redirect to callback URL with credentials
const params = new URLSearchParams({
api_key: apiKeyData.api_key,
namespace: apiKeyData.namespace,
wallet: apiKeyData.wallet,
plan: apiKeyData.plan || 'free'
});
const redirectURL = callbackURL + '?' + params.toString();
console.log('Redirecting to:', redirectURL);
setTimeout(() => {
window.location.href = redirectURL;
}, 1500);
} catch (err) {
console.error('Authentication error:', err);
loading.style.display = 'none';
error.innerHTML = '❌ ' + err.message;
error.style.display = 'block';
btn.disabled = false;
}
}
// Auto-detect if wallet is already connected
window.addEventListener('load', async () => {
if (typeof window.ethereum !== 'undefined') {
try {
const accounts = await window.ethereum.request({ method: 'eth_accounts' });
if (accounts.length > 0) {
const btn = document.getElementById('connectBtn');
btn.innerHTML = '🔗 Continue with ' + accounts[0].slice(0, 6) + '...' + accounts[0].slice(-4);
}
} catch (err) {
console.log('Could not get accounts:', err);
}
}
});
</script>
</body>
</html>`, ns, callbackURL, ns)
fmt.Fprint(w, html)
}