forked from authentricity/authentricity
237 lines
5.2 KiB
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
|
|
}
|