mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
769 lines
22 KiB
Go
769 lines
22 KiB
Go
package server
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"os"
|
|
"slices"
|
|
"time"
|
|
|
|
"cosmossdk.io/log"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/sonr-io/sonr/types/webauthn"
|
|
"github.com/sonr-io/sonr/types/webauthn/webauthncbor"
|
|
)
|
|
|
|
var logger = log.NewLogger(os.Stderr)
|
|
|
|
// HandleIndex handles the index route
|
|
func HandleIndex(c echo.Context) error {
|
|
return c.String(http.StatusOK, "Sonr Auth Server")
|
|
}
|
|
|
|
// HandleHealth handles the health route
|
|
func HandleHealth(c echo.Context) error {
|
|
return c.String(http.StatusOK, "OK")
|
|
}
|
|
|
|
// HandleLogin handles the basic login route
|
|
func HandleLogin(c echo.Context) error {
|
|
return c.String(http.StatusOK, "Login endpoint")
|
|
}
|
|
|
|
// HandleWebAuthnLogin serves the WebAuthn login HTML page
|
|
func HandleWebAuthnLogin(c echo.Context) error {
|
|
// Support both username and identifier parameters
|
|
username := c.QueryParam("username")
|
|
if username == "" {
|
|
username = c.QueryParam("identifier")
|
|
}
|
|
if username == "" {
|
|
return c.String(http.StatusBadRequest, "Username or identifier parameter required")
|
|
}
|
|
|
|
// Check if user exists
|
|
service := NewWebAuthnCredentialService()
|
|
credentials, err := service.GetByUsername(username)
|
|
if err != nil || len(credentials) == 0 {
|
|
return c.String(
|
|
http.StatusNotFound,
|
|
fmt.Sprintf("No WebAuthn credentials found for user: %s", username),
|
|
)
|
|
}
|
|
|
|
// Render the WebAuthn login page
|
|
tmpl := template.Must(template.New("webauthn-login").Parse(webAuthnLoginHTML))
|
|
return tmpl.Execute(c.Response().Writer, map[string]any{
|
|
"Username": username,
|
|
"RPID": "localhost",
|
|
"RPName": "Sonr Identity Platform",
|
|
})
|
|
}
|
|
|
|
// HandleBeginLogin starts the WebAuthn authentication ceremony
|
|
func HandleBeginLogin(c echo.Context) error {
|
|
var username string
|
|
|
|
// Handle both GET and POST requests
|
|
if c.Request().Method == "POST" {
|
|
// For POST requests, try to get username from body
|
|
var body map[string]string
|
|
if err := c.Bind(&body); err == nil {
|
|
username = body["username"]
|
|
}
|
|
}
|
|
|
|
// Fall back to query param for both GET and POST
|
|
if username == "" {
|
|
username = c.QueryParam("username")
|
|
}
|
|
|
|
// Also check for identifier parameter
|
|
if username == "" {
|
|
username = c.QueryParam("identifier")
|
|
}
|
|
|
|
if username == "" {
|
|
return c.JSON(
|
|
http.StatusBadRequest,
|
|
map[string]string{"error": "Username or identifier parameter required"},
|
|
)
|
|
}
|
|
|
|
logger.Info("Starting WebAuthn authentication", "username", username)
|
|
|
|
// Check if user exists and get their credentials
|
|
service := NewWebAuthnCredentialService()
|
|
credentials, err := service.GetByUsername(username)
|
|
if err != nil || len(credentials) == 0 {
|
|
return c.JSON(
|
|
http.StatusNotFound,
|
|
map[string]string{
|
|
"error": fmt.Sprintf("No WebAuthn credentials found for user: %s", username),
|
|
},
|
|
)
|
|
}
|
|
|
|
// Generate challenge
|
|
challenge, err := generateChallenge()
|
|
if err != nil {
|
|
logger.Error("Failed to generate challenge", "error", err)
|
|
return c.JSON(
|
|
http.StatusInternalServerError,
|
|
map[string]string{"error": "Failed to generate challenge"},
|
|
)
|
|
}
|
|
|
|
// Create authentication options
|
|
allowCredentials := make([]map[string]any, len(credentials))
|
|
for i, cred := range credentials {
|
|
allowCredentials[i] = map[string]any{
|
|
"type": "public-key",
|
|
"id": cred.CredentialID,
|
|
}
|
|
}
|
|
|
|
options := map[string]any{
|
|
"challenge": challenge,
|
|
"timeout": 60000,
|
|
"rpId": "localhost",
|
|
"allowCredentials": allowCredentials,
|
|
"userVerification": "preferred", // Changed from required to preferred for broader compatibility
|
|
}
|
|
|
|
// Store challenge in session
|
|
if authServer != nil {
|
|
if authServer.sessionStore == nil {
|
|
authServer.sessionStore = make(map[string]string)
|
|
}
|
|
authServer.sessionStore[username] = challenge
|
|
}
|
|
|
|
logger.Info(
|
|
"Sending authentication options",
|
|
"username",
|
|
username,
|
|
"challenge",
|
|
challenge,
|
|
"credentialCount",
|
|
len(credentials),
|
|
)
|
|
return c.JSON(http.StatusOK, options)
|
|
}
|
|
|
|
// HandleFinishLogin completes the WebAuthn authentication ceremony
|
|
func HandleFinishLogin(c echo.Context) error {
|
|
username := c.QueryParam("username")
|
|
if username == "" {
|
|
return c.JSON(
|
|
http.StatusBadRequest,
|
|
map[string]string{"error": "Username parameter required"},
|
|
)
|
|
}
|
|
|
|
// Parse authentication response from client
|
|
var authResponse map[string]any
|
|
if err := c.Bind(&authResponse); err != nil {
|
|
logger.Error("Failed to parse authentication response", "error", err)
|
|
return c.JSON(
|
|
http.StatusBadRequest,
|
|
map[string]string{"error": "Invalid authentication response"},
|
|
)
|
|
}
|
|
|
|
logger.Info("Received authentication response", "username", username)
|
|
|
|
// Get stored challenge
|
|
var storedChallenge string
|
|
if authServer != nil && authServer.sessionStore != nil {
|
|
storedChallenge = authServer.sessionStore[username]
|
|
}
|
|
|
|
if storedChallenge == "" {
|
|
logger.Error("No stored challenge found", "username", username)
|
|
return c.JSON(
|
|
http.StatusBadRequest,
|
|
map[string]string{"error": "No challenge found for user"},
|
|
)
|
|
}
|
|
|
|
// Extract credential data from the response
|
|
credentialID, ok := authResponse["id"].(string)
|
|
if !ok {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid credential ID"})
|
|
}
|
|
|
|
response, ok := authResponse["response"].(map[string]any)
|
|
if !ok {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid response object"})
|
|
}
|
|
|
|
clientDataJSON, ok := response["clientDataJSON"].(string)
|
|
if !ok {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid client data JSON"})
|
|
}
|
|
|
|
// Verify client data and challenge for authentication
|
|
if err := verifyClientDataForAuthentication(clientDataJSON, storedChallenge); err != nil {
|
|
logger.Error("Client data verification failed for authentication", "error", err)
|
|
return c.JSON(
|
|
http.StatusBadRequest,
|
|
map[string]string{"error": "Authentication verification failed"},
|
|
)
|
|
}
|
|
|
|
// Verify the credential exists for this user
|
|
service := NewWebAuthnCredentialService()
|
|
credential, err := service.GetByCredentialID(credentialID)
|
|
if err != nil {
|
|
logger.Error("Credential not found", "error", err, "credentialID", credentialID)
|
|
return c.JSON(
|
|
http.StatusNotFound,
|
|
map[string]string{"error": "Credential not found"},
|
|
)
|
|
}
|
|
|
|
if credential.Username != username {
|
|
logger.Error(
|
|
"Credential belongs to different user",
|
|
"credentialUser",
|
|
credential.Username,
|
|
"requestedUser",
|
|
username,
|
|
)
|
|
return c.JSON(
|
|
http.StatusUnauthorized,
|
|
map[string]string{"error": "Credential does not belong to this user"},
|
|
)
|
|
}
|
|
|
|
// Clean up session
|
|
if authServer != nil && authServer.sessionStore != nil {
|
|
delete(authServer.sessionStore, username)
|
|
}
|
|
|
|
// Signal completion to CLI
|
|
if authServer != nil && authServer.registrationDone != nil {
|
|
select {
|
|
case authServer.registrationDone <- nil:
|
|
logger.Info("Authentication completion signaled to CLI", "username", username)
|
|
default:
|
|
logger.Warn(
|
|
"Failed to signal authentication completion - channel full",
|
|
"username",
|
|
username,
|
|
)
|
|
}
|
|
}
|
|
|
|
logger.Info(
|
|
"WebAuthn authentication completed successfully",
|
|
"username",
|
|
username,
|
|
"credentialID",
|
|
credentialID,
|
|
)
|
|
return c.JSON(http.StatusOK, map[string]any{
|
|
"success": true,
|
|
"message": "Authentication completed successfully",
|
|
"credentialId": credentialID,
|
|
})
|
|
}
|
|
|
|
// HandleWebAuthnRegister serves the WebAuthn registration HTML page
|
|
func HandleWebAuthnRegister(c echo.Context) error {
|
|
// Support both username and identifier parameters
|
|
username := c.QueryParam("username")
|
|
if username == "" {
|
|
username = c.QueryParam("identifier")
|
|
}
|
|
if username == "" {
|
|
return c.String(http.StatusBadRequest, "Username or identifier parameter required")
|
|
}
|
|
|
|
// Render the WebAuthn registration page
|
|
tmpl := template.Must(template.New("webauthn-register").Parse(webAuthnRegistrationHTML))
|
|
return tmpl.Execute(c.Response().Writer, map[string]any{
|
|
"Username": username,
|
|
"RPID": "localhost",
|
|
"RPName": "Sonr Identity Platform",
|
|
})
|
|
}
|
|
|
|
// HandleBeginRegister starts the WebAuthn registration ceremony
|
|
func HandleBeginRegister(c echo.Context) error {
|
|
var username string
|
|
|
|
// Handle both GET and POST requests
|
|
if c.Request().Method == "POST" {
|
|
// For POST requests, try to get username from body
|
|
var body map[string]string
|
|
if err := c.Bind(&body); err == nil {
|
|
username = body["username"]
|
|
}
|
|
}
|
|
|
|
// Fall back to query param for both GET and POST
|
|
if username == "" {
|
|
username = c.QueryParam("username")
|
|
}
|
|
|
|
// Also check for identifier parameter
|
|
if username == "" {
|
|
username = c.QueryParam("identifier")
|
|
}
|
|
|
|
if username == "" {
|
|
return c.JSON(
|
|
http.StatusBadRequest,
|
|
map[string]string{"error": "Username or identifier parameter required"},
|
|
)
|
|
}
|
|
|
|
logger.Info("Starting WebAuthn registration", "username", username)
|
|
|
|
// Generate challenge
|
|
challenge, err := generateChallenge()
|
|
if err != nil {
|
|
logger.Error("Failed to generate challenge", "error", err)
|
|
return c.JSON(
|
|
http.StatusInternalServerError,
|
|
map[string]string{"error": "Failed to generate challenge"},
|
|
)
|
|
}
|
|
|
|
// Create registration options
|
|
options := map[string]any{
|
|
"challenge": challenge,
|
|
"rp": map[string]string{
|
|
"id": "localhost",
|
|
"name": "Sonr Identity Platform",
|
|
},
|
|
"user": map[string]any{
|
|
"id": base64.URLEncoding.EncodeToString([]byte(username)),
|
|
"name": username,
|
|
"displayName": username,
|
|
},
|
|
"pubKeyCredParams": []map[string]any{
|
|
{
|
|
"type": "public-key",
|
|
"alg": -7, // ES256 algorithm (most common)
|
|
},
|
|
{
|
|
"type": "public-key",
|
|
"alg": -257, // RS256 algorithm
|
|
},
|
|
{
|
|
"type": "public-key",
|
|
"alg": -8, // EdDSA algorithm
|
|
},
|
|
},
|
|
"authenticatorSelection": map[string]any{
|
|
// Remove authenticatorAttachment to allow both platform and cross-platform authenticators
|
|
// "authenticatorAttachment": "platform", // Commented out to allow QR codes
|
|
"userVerification": "preferred", // Changed from required to preferred for broader compatibility
|
|
"residentKey": "preferred",
|
|
"requireResidentKey": false, // Allow non-resident keys for broader compatibility
|
|
},
|
|
"timeout": 60000,
|
|
"attestation": "none", // Changed from direct to none for broader compatibility
|
|
}
|
|
|
|
// Store challenge in session (in production, use proper session store)
|
|
if authServer != nil {
|
|
if authServer.sessionStore == nil {
|
|
authServer.sessionStore = make(map[string]string)
|
|
}
|
|
authServer.sessionStore[username] = challenge
|
|
}
|
|
|
|
logger.Info("Sending registration options", "username", username, "challenge", challenge)
|
|
return c.JSON(http.StatusOK, options)
|
|
}
|
|
|
|
// HandleFinishRegister completes the WebAuthn registration ceremony
|
|
func HandleFinishRegister(c echo.Context) error {
|
|
username := c.QueryParam("username")
|
|
if username == "" {
|
|
return c.JSON(
|
|
http.StatusBadRequest,
|
|
map[string]string{"error": "Username parameter required"},
|
|
)
|
|
}
|
|
|
|
// Parse registration response from client
|
|
var regResponse map[string]any
|
|
if err := c.Bind(®Response); err != nil {
|
|
logger.Error("Failed to parse registration response", "error", err)
|
|
return c.JSON(
|
|
http.StatusBadRequest,
|
|
map[string]string{"error": "Invalid registration response"},
|
|
)
|
|
}
|
|
|
|
logger.Info("Received registration response", "username", username)
|
|
|
|
// Get stored challenge
|
|
var storedChallenge string
|
|
if authServer != nil && authServer.sessionStore != nil {
|
|
storedChallenge = authServer.sessionStore[username]
|
|
}
|
|
|
|
if storedChallenge == "" {
|
|
logger.Error("No stored challenge found", "username", username)
|
|
return c.JSON(
|
|
http.StatusBadRequest,
|
|
map[string]string{"error": "No challenge found for user"},
|
|
)
|
|
}
|
|
|
|
// Extract credential data from the response
|
|
credentialID, ok := regResponse["id"].(string)
|
|
if !ok {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid credential ID"})
|
|
}
|
|
|
|
rawID, ok := regResponse["rawId"].(string)
|
|
if !ok {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid raw ID"})
|
|
}
|
|
|
|
response, ok := regResponse["response"].(map[string]any)
|
|
if !ok {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid response object"})
|
|
}
|
|
|
|
clientDataJSON, ok := response["clientDataJSON"].(string)
|
|
if !ok {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid client data JSON"})
|
|
}
|
|
|
|
attestationObject, ok := response["attestationObject"].(string)
|
|
if !ok {
|
|
return c.JSON(
|
|
http.StatusBadRequest,
|
|
map[string]string{"error": "Invalid attestation object"},
|
|
)
|
|
}
|
|
|
|
// Verify client data and challenge
|
|
if err := verifyClientData(clientDataJSON, storedChallenge); err != nil {
|
|
logger.Error("Client data verification failed", "error", err)
|
|
return c.JSON(
|
|
http.StatusBadRequest,
|
|
map[string]string{"error": "Client data verification failed"},
|
|
)
|
|
}
|
|
|
|
// Create WebAuthn credential record
|
|
webAuthnCredential := &WebAuthnCredential{
|
|
CredentialID: credentialID,
|
|
RawID: rawID,
|
|
ClientDataJSON: clientDataJSON,
|
|
AttestationObject: attestationObject,
|
|
Username: username,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
// Process the registration and store in database
|
|
if err := processWebAuthnRegistration(webAuthnCredential); err != nil {
|
|
logger.Error("Failed to process WebAuthn registration", "error", err)
|
|
return c.JSON(
|
|
http.StatusInternalServerError,
|
|
map[string]string{"error": "Registration processing failed"},
|
|
)
|
|
}
|
|
|
|
// Store WebAuthn credential in database
|
|
if err := storeWebAuthnCredential(webAuthnCredential); err != nil {
|
|
logger.Error("Failed to store WebAuthn credential in database", "error", err)
|
|
// Don't fail the registration if database storage fails
|
|
logger.Warn("Continuing registration despite database storage failure")
|
|
}
|
|
|
|
// Clean up session
|
|
if authServer != nil && authServer.sessionStore != nil {
|
|
delete(authServer.sessionStore, username)
|
|
}
|
|
|
|
// Send credential data to CLI if channel is available
|
|
if authServer != nil && authServer.credentialData != nil {
|
|
select {
|
|
case authServer.credentialData <- webAuthnCredential:
|
|
logger.Info(
|
|
"WebAuthn credential data sent to CLI",
|
|
"username",
|
|
username,
|
|
"credentialID",
|
|
credentialID,
|
|
)
|
|
default:
|
|
logger.Warn("Failed to send credential data - channel full", "username", username)
|
|
}
|
|
}
|
|
|
|
// Signal completion to CLI
|
|
if authServer != nil && authServer.registrationDone != nil {
|
|
select {
|
|
case authServer.registrationDone <- nil:
|
|
logger.Info("Registration completion signaled to CLI", "username", username)
|
|
default:
|
|
logger.Warn(
|
|
"Failed to signal registration completion - channel full",
|
|
"username",
|
|
username,
|
|
)
|
|
}
|
|
}
|
|
|
|
logger.Info(
|
|
"WebAuthn registration completed successfully",
|
|
"username",
|
|
username,
|
|
"credentialID",
|
|
credentialID,
|
|
)
|
|
return c.JSON(http.StatusOK, map[string]any{
|
|
"success": true,
|
|
"message": "Registration completed successfully",
|
|
"credentialId": credentialID,
|
|
})
|
|
}
|
|
|
|
// generateChallenge generates a cryptographically secure challenge
|
|
func generateChallenge() (string, error) {
|
|
bytes := make([]byte, 32)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", err
|
|
}
|
|
return base64.RawURLEncoding.EncodeToString(bytes), nil
|
|
}
|
|
|
|
// verifyClientData verifies the client data JSON and challenge using centralized WebAuthn validation
|
|
func verifyClientData(clientDataJSON, expectedChallenge string) error {
|
|
// Parse client data using the centralized WebAuthn protocol parser
|
|
clientData, err := webauthn.ValidateClientDataJSONFormat(clientDataJSON)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse client data: %w", err)
|
|
}
|
|
|
|
// Verify challenge
|
|
if clientData.Challenge != expectedChallenge {
|
|
return fmt.Errorf("challenge mismatch")
|
|
}
|
|
|
|
// Verify type
|
|
if clientData.Type != "webauthn.create" {
|
|
return fmt.Errorf("invalid client data type: %s", clientData.Type)
|
|
}
|
|
|
|
// Verify origin (adjust for your domain)
|
|
expectedOrigin := "http://localhost"
|
|
if clientData.Origin != expectedOrigin &&
|
|
!containsString(
|
|
clientData.Origin,
|
|
[]string{
|
|
"http://localhost:8080",
|
|
"http://localhost:8081",
|
|
"http://localhost:8082",
|
|
"http://localhost:8083",
|
|
"http://localhost:8084",
|
|
},
|
|
) {
|
|
return fmt.Errorf("invalid origin: %s", clientData.Origin)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// verifyClientDataForAuthentication verifies the client data JSON and challenge for authentication
|
|
func verifyClientDataForAuthentication(clientDataJSON, expectedChallenge string) error {
|
|
// Parse client data using the centralized WebAuthn protocol parser
|
|
clientData, err := webauthn.ValidateClientDataJSONFormat(clientDataJSON)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse client data: %w", err)
|
|
}
|
|
|
|
// Verify challenge
|
|
if clientData.Challenge != expectedChallenge {
|
|
return fmt.Errorf("challenge mismatch")
|
|
}
|
|
|
|
// Verify type for authentication (webauthn.get instead of webauthn.create)
|
|
if clientData.Type != "webauthn.get" {
|
|
return fmt.Errorf("invalid client data type for authentication: %s", clientData.Type)
|
|
}
|
|
|
|
// Verify origin (adjust for your domain)
|
|
expectedOrigin := "http://localhost"
|
|
if clientData.Origin != expectedOrigin &&
|
|
!containsString(
|
|
clientData.Origin,
|
|
[]string{
|
|
"http://localhost:8080",
|
|
"http://localhost:8081",
|
|
"http://localhost:8082",
|
|
"http://localhost:8083",
|
|
"http://localhost:8084",
|
|
"http://localhost:8085",
|
|
"http://localhost:8086",
|
|
"http://localhost:8087",
|
|
"http://localhost:8088",
|
|
"http://localhost:8089",
|
|
},
|
|
) {
|
|
return fmt.Errorf("invalid origin: %s", clientData.Origin)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// containsString checks if a string is in a slice
|
|
func containsString(str string, slice []string) bool {
|
|
return slices.Contains(slice, str)
|
|
}
|
|
|
|
// processWebAuthnRegistration processes the WebAuthn registration and extracts required fields
|
|
func processWebAuthnRegistration(credential *WebAuthnCredential) error {
|
|
logger.Info(
|
|
"Processing WebAuthn registration",
|
|
"username",
|
|
credential.Username,
|
|
"credentialID",
|
|
credential.CredentialID,
|
|
)
|
|
|
|
// Extract origin from client data JSON
|
|
origin, err := extractOriginFromClientData(credential.ClientDataJSON)
|
|
if err != nil {
|
|
logger.Error("Failed to extract origin from client data", "error", err)
|
|
return fmt.Errorf("failed to extract origin: %w", err)
|
|
}
|
|
credential.Origin = origin
|
|
logger.Info("Extracted origin from client data", "origin", origin)
|
|
|
|
// Extract public key and algorithm from attestation object
|
|
publicKey, algorithm, err := extractPublicKeyFromAttestation(credential.AttestationObject)
|
|
if err != nil {
|
|
logger.Error("Failed to extract public key from attestation", "error", err)
|
|
return fmt.Errorf("failed to extract public key: %w", err)
|
|
}
|
|
credential.PublicKey = publicKey
|
|
credential.Algorithm = algorithm
|
|
logger.Info("Extracted public key from attestation",
|
|
"algorithm", algorithm,
|
|
"publicKeyLength", len(publicKey))
|
|
|
|
logger.Info(
|
|
"WebAuthn credential data collected - ready for blockchain transaction",
|
|
"credentialID",
|
|
credential.CredentialID,
|
|
"username",
|
|
credential.Username,
|
|
"origin",
|
|
credential.Origin,
|
|
"algorithm",
|
|
credential.Algorithm,
|
|
)
|
|
return nil
|
|
}
|
|
|
|
// extractOriginFromClientData extracts the origin from client data JSON using centralized WebAuthn parsing
|
|
func extractOriginFromClientData(clientDataJSON string) (string, error) {
|
|
// Use the centralized WebAuthn client data parser
|
|
clientData, err := webauthn.ValidateClientDataJSONFormat(clientDataJSON)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to parse client data: %w", err)
|
|
}
|
|
|
|
if clientData.Origin == "" {
|
|
return "", fmt.Errorf("origin not found in client data JSON")
|
|
}
|
|
|
|
return clientData.Origin, nil
|
|
}
|
|
|
|
// extractPublicKeyFromAttestation extracts public key and algorithm from attestation object using centralized WebAuthn parsing
|
|
func extractPublicKeyFromAttestation(attestationObject string) ([]byte, int32, error) {
|
|
// Use the centralized WebAuthn attestation validation first
|
|
if err := webauthn.ValidateAttestationObjectFormat(attestationObject); err != nil {
|
|
return nil, 0, fmt.Errorf("invalid attestation object format: %w", err)
|
|
}
|
|
|
|
// Decode the attestation object
|
|
attestationBytes, err := base64.RawURLEncoding.DecodeString(attestationObject)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to decode attestation object: %w", err)
|
|
}
|
|
|
|
// Parse the attestation object using the centralized WebAuthn CBOR parsing
|
|
var attestationObj webauthn.AttestationObject
|
|
if err := webauthncbor.Unmarshal(attestationBytes, &attestationObj); err != nil {
|
|
return nil, 0, fmt.Errorf("failed to unmarshal attestation object: %w", err)
|
|
}
|
|
|
|
// Unmarshal the authenticator data
|
|
if err := attestationObj.AuthData.Unmarshal(attestationObj.RawAuthData); err != nil {
|
|
return nil, 0, fmt.Errorf("failed to unmarshal authenticator data: %w", err)
|
|
}
|
|
|
|
// Extract the attested credential data
|
|
if !attestationObj.AuthData.Flags.HasAttestedCredentialData() {
|
|
return nil, 0, fmt.Errorf("attestation object missing attested credential data")
|
|
}
|
|
|
|
publicKey := attestationObj.AuthData.AttData.CredentialPublicKey
|
|
if len(publicKey) == 0 {
|
|
return nil, 0, fmt.Errorf("no public key found in attested credential data")
|
|
}
|
|
|
|
// Assume ES256 algorithm for now. In the future, this could be extracted
|
|
// from the COSE key format in the public key bytes
|
|
algorithm := int32(-7) // ES256
|
|
|
|
return publicKey, algorithm, nil
|
|
}
|
|
|
|
// WebAuthnCredential represents a WebAuthn credential for processing
|
|
type WebAuthnCredential struct {
|
|
CredentialID string
|
|
RawID string
|
|
ClientDataJSON string
|
|
AttestationObject string
|
|
Username string
|
|
CreatedAt time.Time
|
|
// Extracted fields
|
|
Origin string
|
|
PublicKey []byte
|
|
Algorithm int32
|
|
}
|
|
|
|
// storeWebAuthnCredential stores the WebAuthn credential in the database
|
|
func storeWebAuthnCredential(credential *WebAuthnCredential) error {
|
|
// Initialize database if not already done
|
|
if db == nil {
|
|
if err := InitDB(); err != nil {
|
|
return fmt.Errorf("failed to initialize database: %w", err)
|
|
}
|
|
}
|
|
|
|
// Convert WebAuthn credential to database model
|
|
storedCredential := &StoredWebAuthnCredential{
|
|
CredentialID: credential.CredentialID,
|
|
RawID: credential.RawID,
|
|
ClientDataJSON: credential.ClientDataJSON,
|
|
AttestationObject: credential.AttestationObject,
|
|
Username: credential.Username,
|
|
Origin: "localhost", // Default for CLI registration
|
|
RPID: "localhost",
|
|
Algorithm: -7, // ES256 algorithm by default
|
|
}
|
|
|
|
// Store using service
|
|
service := NewWebAuthnCredentialService()
|
|
return service.Store(storedCredential)
|
|
}
|