authentricity/internal/webui/webauthn.go

237 lines
5.2 KiB
Go

package webui
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/google/uuid"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwe"
"go.e43.eu/authentricity/internal/models"
"go.e43.eu/authentricity/internal/store"
)
type WebAuthnUser struct {
*models.UserRecord
}
func (u WebAuthnUser) WebAuthnID() []byte {
return u.UUID[:]
}
func (u WebAuthnUser) WebAuthnName() string {
switch {
case u.UserName != "":
return u.UserName
case u.EmailAddress != "":
return u.EmailAddress
default:
return u.UUID.String()
}
}
func (u WebAuthnUser) WebAuthnDisplayName() string {
switch {
case u.RealName != "":
return u.RealName
case u.UserName != "":
return u.UserName
case u.EmailAddress != "":
return u.EmailAddress
default:
return u.UUID.String()
}
}
func (u WebAuthnUser) WebAuthnCredentials() []webauthn.Credential {
if u.Privileged == nil {
return nil
}
creds := make([]webauthn.Credential, len(u.Privileged.PublicKeyCredentials))
for i, c := range u.Privileged.PublicKeyCredentials {
creds[i] = webauthn.Credential{
ID: c.Credential,
PublicKey: c.PublicKey,
Flags: webauthn.CredentialFlags{
UserPresent: c.UserPresent,
UserVerified: c.UserVerified,
},
}
}
return creds
}
func (u WebAuthnUser) WebAuthnCredentialDescriptors() []protocol.CredentialDescriptor {
if u.Privileged == nil {
return nil
}
creds := make([]protocol.CredentialDescriptor, len(u.Privileged.PublicKeyCredentials))
for i, c := range u.Privileged.PublicKeyCredentials {
creds[i] = protocol.CredentialDescriptor{
Type: protocol.PublicKeyCredentialType,
CredentialID: protocol.URLEncodedBase64(c.Credential),
}
}
return creds
}
func (u WebAuthnUser) WebAuthnIcon() string {
return ""
}
func (s *Service) webAuthnMarshalSession(sd *webauthn.SessionData) (string, error) {
data, err := json.Marshal(sd)
if err != nil {
return "", err
}
data, err = jwe.Encrypt(data,
jwe.WithKey(jwa.DIRECT, s.webAuthnKey),
jwe.WithContentEncryption(jwa.A128GCM))
return string(data), err
}
func (s *Service) webAuthnUnmarshalSession(session string) (*webauthn.SessionData, error) {
body, err := jwe.Decrypt([]byte(session),
jwe.WithKey(jwa.DIRECT, s.webAuthnKey))
if err != nil {
return nil, fmt.Errorf("Decrypting WebAuthn session: %v", err)
}
sess := new(webauthn.SessionData)
err = json.Unmarshal(body, sess)
return sess, err
}
type WebAuthnRegistrationRequest struct {
Request string
Session string
}
type WebAuthnRegistrationResponse struct {
Response string
Session string
}
func (s *Service) webAuthnRegister(user *models.UserRecord) (WebAuthnRegistrationRequest, error) {
waUser := WebAuthnUser{user}
waReq, waSess, err := s.wa.BeginRegistration(waUser,
webauthn.WithExclusions(waUser.WebAuthnCredentialDescriptors()),
webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementRequired))
if err != nil {
return WebAuthnRegistrationRequest{}, err
}
req, err := json.Marshal(waReq)
if err != nil {
return WebAuthnRegistrationRequest{}, err
}
sess, err := s.webAuthnMarshalSession(waSess)
if err != nil {
return WebAuthnRegistrationRequest{}, err
}
return WebAuthnRegistrationRequest{
Request: string(req),
Session: sess,
}, nil
}
func (s *Service) webAuthnCreateCredential(user *models.UserRecord, regResp WebAuthnRegistrationResponse) (*webauthn.Credential, error) {
waUser := WebAuthnUser{user}
sess, err := s.webAuthnUnmarshalSession(regResp.Session)
if err != nil {
return nil, err
}
pcc, err := protocol.ParseCredentialCreationResponseBody(bytes.NewReader([]byte(regResp.Response)))
if err != nil {
return nil, err
}
cred, err := s.wa.CreateCredential(waUser, *sess, pcc)
if err != nil {
return nil, err
}
return cred, nil
}
type WebAuthnDiscoverRequest struct {
Request string
Session string
}
type WebAuthnDiscoverResponse struct {
Response string
Session string
}
func (s *Service) webAuthnBeginDiscover() (WebAuthnDiscoverRequest, error) {
waReq, waSess, err := s.wa.BeginDiscoverableLogin(webauthn.WithUserVerification(protocol.VerificationRequired))
req, err := json.Marshal(waReq)
if err != nil {
return WebAuthnDiscoverRequest{}, err
}
sess, err := s.webAuthnMarshalSession(waSess)
if err != nil {
return WebAuthnDiscoverRequest{}, err
}
return WebAuthnDiscoverRequest{
Request: string(req),
Session: sess,
}, nil
}
func (s *Service) webauthnCompleteDiscover(
ctx context.Context,
resp WebAuthnDiscoverResponse,
) (*models.UserRecord, error) {
sess, err := s.webAuthnUnmarshalSession(resp.Session)
if err != nil {
return nil, err
}
pcr, err := protocol.ParseCredentialRequestResponseBody(bytes.NewReader([]byte(resp.Response)))
if err != nil {
return nil, err
}
if len(pcr.Response.UserHandle) != 16 {
return nil, errors.New("Invalid user handle")
}
userID, err := uuid.FromBytes(pcr.Response.UserHandle)
if err != nil {
return nil, err
}
user, _, err := store.GetUser(ctx, s.store, userID)
if err != nil {
return nil, err
}
_, err = s.wa.ValidateDiscoverableLogin(func(_, _ []byte) (webauthn.User, error) {
return WebAuthnUser{user}, nil
}, *sess, pcr)
if err != nil {
return nil, err
}
return user, nil
}