Files
mcias/internal/grpcserver/webauthn.go
Kyle Isom 41d01edfb4 Migrate module path from kyle/ to mc/ org
All import paths updated from git.wntrmute.dev/kyle/mcias to
git.wntrmute.dev/mc/mcias to match the Gitea organization.
Includes main module and clients/go submodule.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:03:46 -07:00

93 lines
3.2 KiB
Go

// WebAuthn gRPC handlers for listing and removing WebAuthn credentials.
// These are admin-only operations that mirror the REST handlers in
// internal/server/handlers_webauthn.go.
package grpcserver
import (
"context"
"fmt"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
"git.wntrmute.dev/mc/mcias/internal/model"
)
// ListWebAuthnCredentials returns metadata for an account's WebAuthn credentials.
// Requires: admin JWT in metadata.
//
// Security: credential material (IDs, public keys) is never included in the
// response — only metadata (name, sign count, timestamps, etc.).
func (a *authServiceServer) ListWebAuthnCredentials(ctx context.Context, req *mciasv1.ListWebAuthnCredentialsRequest) (*mciasv1.ListWebAuthnCredentialsResponse, error) {
if err := a.s.requireAdmin(ctx); err != nil {
return nil, err
}
if req.AccountId == "" {
return nil, status.Error(codes.InvalidArgument, "account_id is required")
}
acct, err := a.s.db.GetAccountByUUID(req.AccountId)
if err != nil {
return nil, status.Error(codes.NotFound, "account not found")
}
creds, err := a.s.db.GetWebAuthnCredentials(acct.ID)
if err != nil {
a.s.logger.Error("list webauthn credentials", "error", err, "account_id", acct.ID)
return nil, status.Error(codes.Internal, "internal error")
}
resp := &mciasv1.ListWebAuthnCredentialsResponse{
Credentials: make([]*mciasv1.WebAuthnCredentialInfo, 0, len(creds)),
}
for _, c := range creds {
info := &mciasv1.WebAuthnCredentialInfo{
Id: c.ID,
Name: c.Name,
Aaguid: c.AAGUID,
SignCount: c.SignCount,
Discoverable: c.Discoverable,
Transports: c.Transports,
CreatedAt: timestamppb.New(c.CreatedAt),
}
if c.LastUsedAt != nil {
info.LastUsedAt = timestamppb.New(*c.LastUsedAt)
}
resp.Credentials = append(resp.Credentials, info)
}
return resp, nil
}
// RemoveWebAuthnCredential removes a specific WebAuthn credential.
// Requires: admin JWT in metadata.
func (a *authServiceServer) RemoveWebAuthnCredential(ctx context.Context, req *mciasv1.RemoveWebAuthnCredentialRequest) (*mciasv1.RemoveWebAuthnCredentialResponse, error) {
if err := a.s.requireAdmin(ctx); err != nil {
return nil, err
}
if req.AccountId == "" {
return nil, status.Error(codes.InvalidArgument, "account_id is required")
}
if req.CredentialId == 0 {
return nil, status.Error(codes.InvalidArgument, "credential_id is required")
}
acct, err := a.s.db.GetAccountByUUID(req.AccountId)
if err != nil {
return nil, status.Error(codes.NotFound, "account not found")
}
// DeleteWebAuthnCredentialAdmin bypasses ownership checks (admin operation).
if err := a.s.db.DeleteWebAuthnCredentialAdmin(req.CredentialId); err != nil {
a.s.logger.Error("delete webauthn credential", "error", err, "credential_id", req.CredentialId)
return nil, status.Error(codes.Internal, "internal error")
}
a.s.db.WriteAuditEvent(model.EventWebAuthnRemoved, nil, &acct.ID, peerIP(ctx), //nolint:errcheck
fmt.Sprintf(`{"credential_id":%d}`, req.CredentialId))
return &mciasv1.RemoveWebAuthnCredentialResponse{}, nil
}