mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
266 lines
7.3 KiB
Go
266 lines
7.3 KiB
Go
// Package handlers provides HTTP handlers for the highway server
|
|
package handlers
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
// CustomClaims defines custom JWT claims for vault operations
|
|
type CustomClaims struct {
|
|
UserID string `json:"user_id"`
|
|
Permissions []string `json:"permissions"`
|
|
jwt.RegisteredClaims
|
|
}
|
|
|
|
// LoginRequest represents the login request payload
|
|
type LoginRequest struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
// LoginResponse represents the login response payload
|
|
type LoginResponse struct {
|
|
Token string `json:"token"`
|
|
}
|
|
|
|
// LoginHandler generates JWT tokens for authentication
|
|
// This now supports both traditional login and OIDC-based authentication
|
|
func LoginHandler(jwtSecret []byte) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
// Check if this is an OIDC callback
|
|
code := c.QueryParam("code")
|
|
if code != "" {
|
|
return handleOIDCCallback(c, code, jwtSecret)
|
|
}
|
|
|
|
// Check if user has WebAuthn credentials
|
|
authHeader := c.Request().Header.Get("X-WebAuthn-Assertion")
|
|
if authHeader != "" {
|
|
return handleWebAuthnLogin(c, authHeader, jwtSecret)
|
|
}
|
|
|
|
// Traditional login flow
|
|
var req LoginRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid JSON payload"})
|
|
}
|
|
|
|
// Simple authentication - in production, validate against a proper user database
|
|
if req.Username == "" || req.Password == "" {
|
|
return c.JSON(
|
|
http.StatusBadRequest,
|
|
map[string]string{"error": "Username and password are required"},
|
|
)
|
|
}
|
|
|
|
// For demo purposes, accept any non-empty credentials
|
|
// In production, verify credentials against database/directory
|
|
if req.Username == "vault-user" && req.Password == "vault-pass" {
|
|
// Create custom claims
|
|
claims := &CustomClaims{
|
|
UserID: req.Username,
|
|
Permissions: []string{
|
|
"vault:generate",
|
|
"vault:sign",
|
|
"vault:verify",
|
|
"vault:export",
|
|
"vault:import",
|
|
"vault:refresh",
|
|
},
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), // 24 hours
|
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
|
Issuer: "highway-vault",
|
|
Subject: req.Username,
|
|
},
|
|
}
|
|
|
|
// Create token with claims
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
|
|
// Sign token with secret
|
|
tokenString, err := token.SignedString(jwtSecret)
|
|
if err != nil {
|
|
return c.JSON(
|
|
http.StatusInternalServerError,
|
|
map[string]string{"error": "Failed to generate token"},
|
|
)
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, LoginResponse{Token: tokenString})
|
|
}
|
|
|
|
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid credentials"})
|
|
}
|
|
}
|
|
|
|
// handleOIDCCallback processes OIDC authorization code callback
|
|
func handleOIDCCallback(c echo.Context, code string, jwtSecret []byte) error {
|
|
// Exchange code for tokens using OIDC token endpoint
|
|
tokenReq := &OIDCTokenRequest{
|
|
GrantType: "authorization_code",
|
|
Code: code,
|
|
RedirectURI: c.QueryParam("redirect_uri"),
|
|
ClientID: c.QueryParam("client_id"),
|
|
}
|
|
|
|
// Call internal OIDC token handler
|
|
// In production, this would make an HTTP call to the OIDC provider
|
|
c.Set("oidc_token_request", tokenReq)
|
|
if err := handleAuthorizationCodeGrant(c, tokenReq); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get the OIDC session from context
|
|
session, ok := c.Get("oidc_session").(*OIDCSession)
|
|
if !ok {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{
|
|
"error": "Failed to establish OIDC session",
|
|
})
|
|
}
|
|
|
|
// Create JWT token from OIDC session
|
|
claims := &CustomClaims{
|
|
UserID: session.UserDID,
|
|
Permissions: []string{
|
|
"vault:generate",
|
|
"vault:sign",
|
|
"vault:verify",
|
|
"vault:export",
|
|
"vault:import",
|
|
"vault:refresh",
|
|
},
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ExpiresAt: jwt.NewNumericDate(session.ExpiresAt),
|
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
|
Issuer: "highway-vault-oidc",
|
|
Subject: session.UserDID,
|
|
},
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
tokenString, err := token.SignedString(jwtSecret)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{
|
|
"error": "Failed to generate token",
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, LoginResponse{Token: tokenString})
|
|
}
|
|
|
|
// handleWebAuthnLogin processes WebAuthn-based login
|
|
func handleWebAuthnLogin(c echo.Context, assertion string, jwtSecret []byte) error {
|
|
// Verify WebAuthn assertion
|
|
// This would integrate with the WebAuthn handlers
|
|
|
|
// For now, extract username from assertion (simplified)
|
|
username := c.Request().Header.Get("X-WebAuthn-Username")
|
|
if username == "" {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{
|
|
"error": "WebAuthn username required",
|
|
})
|
|
}
|
|
|
|
// Verify the assertion matches a stored credential
|
|
webAuthnStore.mu.RLock()
|
|
credentials, exists := webAuthnStore.credentials[username]
|
|
webAuthnStore.mu.RUnlock()
|
|
|
|
if !exists || len(credentials) == 0 {
|
|
return c.JSON(http.StatusUnauthorized, map[string]string{
|
|
"error": "No WebAuthn credentials found for user",
|
|
})
|
|
}
|
|
|
|
// Create JWT token for WebAuthn authenticated user
|
|
claims := &CustomClaims{
|
|
UserID: username,
|
|
Permissions: []string{
|
|
"vault:generate",
|
|
"vault:sign",
|
|
"vault:verify",
|
|
"vault:export",
|
|
"vault:import",
|
|
"vault:refresh",
|
|
},
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)),
|
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
|
Issuer: "highway-vault-webauthn",
|
|
Subject: username,
|
|
},
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
tokenString, err := token.SignedString(jwtSecret)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{
|
|
"error": "Failed to generate token",
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, LoginResponse{Token: tokenString})
|
|
}
|
|
|
|
// OIDCLoginHandler initiates OIDC login flow
|
|
func OIDCLoginHandler() echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
// Build authorization URL
|
|
authURL := buildOIDCAuthorizationURL(
|
|
c.QueryParam("client_id"),
|
|
c.QueryParam("redirect_uri"),
|
|
c.QueryParam("scope"),
|
|
c.QueryParam("state"),
|
|
)
|
|
|
|
// Redirect to OIDC authorization endpoint
|
|
return c.Redirect(http.StatusFound, authURL)
|
|
}
|
|
}
|
|
|
|
// buildOIDCAuthorizationURL constructs the OIDC authorization URL
|
|
func buildOIDCAuthorizationURL(clientID, redirectURI, scope, state string) string {
|
|
// Default values if not provided
|
|
if clientID == "" {
|
|
clientID = "highway-vault-client"
|
|
}
|
|
if redirectURI == "" {
|
|
redirectURI = "http://localhost:8080/auth/callback"
|
|
}
|
|
if scope == "" {
|
|
scope = "openid profile did vault"
|
|
}
|
|
if state == "" {
|
|
state = generateState()
|
|
}
|
|
|
|
// Build authorization URL
|
|
return fmt.Sprintf(
|
|
"https://localhost:8080/oidc/authorize?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&state=%s",
|
|
clientID,
|
|
redirectURI,
|
|
scope,
|
|
state,
|
|
)
|
|
}
|
|
|
|
// generateState generates a random state parameter for OIDC
|
|
func generateState() string {
|
|
bytes := make([]byte, 16)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "default-state"
|
|
}
|
|
return base64.RawURLEncoding.EncodeToString(bytes)
|
|
}
|