@@ -54,56 +54,51 @@ type EnrollFactorResponse struct {
54
54
55
55
type ChallengeFactorParams struct {
56
56
Channel string `json:"channel"`
57
- WebAuthn * WebAuthnParams `json:"web_authn ,omitempty"`
57
+ WebAuthn * WebAuthnParams `json:"webauthn ,omitempty"`
58
58
}
59
59
60
60
type VerifyFactorParams struct {
61
61
ChallengeID uuid.UUID `json:"challenge_id"`
62
62
Code string `json:"code"`
63
- WebAuthn * WebAuthnParams `json:"web_authn ,omitempty"`
63
+ WebAuthn * WebAuthnParams `json:"webauthn ,omitempty"`
64
64
}
65
65
66
66
type ChallengeFactorResponse struct {
67
- ID uuid.UUID `json:"id"`
68
- Type string `json:"type"`
69
- ExpiresAt int64 `json:"expires_at,omitempty"`
70
- CredentialRequestOptions * wbnprotocol.CredentialAssertion `json:"credential_request_options,omitempty"`
71
- CredentialCreationOptions * wbnprotocol.CredentialCreation `json:"credential_creation_options,omitempty"`
67
+ ID uuid.UUID `json:"id"`
68
+ Type string `json:"type"`
69
+ ExpiresAt int64 `json:"expires_at,omitempty"`
70
+ WebAuthn * WebAuthnChallengeData `json:"webauthn,omitempty"`
72
71
}
73
72
74
- type UnenrollFactorResponse struct {
75
- ID uuid.UUID `json:"id"`
73
+ type WebAuthnChallengeData struct {
74
+ Type string `json:"type"` // "create" or "request"
75
+ CredentialOptions interface {} `json:"credential_options"`
76
76
}
77
77
78
78
type WebAuthnParams struct {
79
- RPID string `json:"rp_id,omitempty"`
80
- // Can encode multiple origins as comma separated values like: "origin1,origin2"
81
- RPOrigins string `json:"rp_origins,omitempty"`
82
- AssertionResponse json.RawMessage `json:"assertion_response,omitempty"`
83
- CreationResponse json.RawMessage `json:"creation_response,omitempty"`
79
+ RPID string `json:"rpId,omitempty"`
80
+ RPOrigins []string `json:"rpOrigins,omitempty"`
81
+ Type string `json:"type"` // "create" or "request"
82
+ CredentialResponse json.RawMessage `json:"credential_response"`
84
83
}
85
84
86
- func (w * WebAuthnParams ) GetRPOrigins () []string {
87
- if w .RPOrigins == "" {
88
- return nil
89
- }
90
- return strings .Split (w .RPOrigins , "," )
85
+ type UnenrollFactorResponse struct {
86
+ ID uuid.UUID `json:"id"`
91
87
}
92
88
93
89
func (w * WebAuthnParams ) ToConfig () (* webauthn.WebAuthn , error ) {
94
90
if w .RPID == "" {
95
91
return nil , fmt .Errorf ("webAuthn RP ID cannot be empty" )
96
92
}
97
93
98
- origins := w .GetRPOrigins ()
99
- if len (origins ) == 0 {
94
+ if len (w .RPOrigins ) == 0 {
100
95
return nil , fmt .Errorf ("webAuthn RP Origins cannot be empty" )
101
96
}
102
97
103
98
var validOrigins []string
104
99
var invalidOrigins []string
105
100
106
- for _ , origin := range origins {
101
+ for _ , origin := range w . RPOrigins {
107
102
parsedURL , err := url .Parse (origin )
108
103
if err != nil || (parsedURL .Scheme != "https" && ! (parsedURL .Scheme == "http" && parsedURL .Hostname () == "localhost" )) || parsedURL .Host == "" {
109
104
invalidOrigins = append (invalidOrigins , origin )
@@ -514,7 +509,18 @@ func (a *API) challengeWebAuthnFactor(w http.ResponseWriter, r *http.Request) er
514
509
var ws * models.WebAuthnSessionData
515
510
var challenge * models.Challenge
516
511
if factor .IsUnverified () {
517
- options , session , err := webAuthn .BeginRegistration (user )
512
+ // Get existing WebAuthn credentials to exclude duplicates
513
+ excludeList := []wbnprotocol.CredentialDescriptor {}
514
+ existingCredentials := user .WebAuthnCredentials ()
515
+ for _ , cred := range existingCredentials {
516
+ excludeList = append (excludeList , wbnprotocol.CredentialDescriptor {
517
+ Type : wbnprotocol .PublicKeyCredentialType ,
518
+ CredentialID : cred .ID ,
519
+ Transport : []wbnprotocol.AuthenticatorTransport {"usb" , "nfc" },
520
+ })
521
+ }
522
+
523
+ options , session , err := webAuthn .BeginRegistration (user , webauthn .WithExclusions (excludeList ))
518
524
if err != nil {
519
525
return apierrors .NewInternalServerError ("Failed to generate WebAuthn registration data" ).WithInternalError (err )
520
526
}
@@ -524,9 +530,12 @@ func (a *API) challengeWebAuthnFactor(w http.ResponseWriter, r *http.Request) er
524
530
challenge = ws .ToChallenge (factor .ID , ipAddress )
525
531
526
532
response = & ChallengeFactorResponse {
527
- CredentialCreationOptions : options ,
528
- Type : factor .FactorType ,
529
- ID : challenge .ID ,
533
+ Type : factor .FactorType ,
534
+ ID : challenge .ID ,
535
+ WebAuthn : & WebAuthnChallengeData {
536
+ Type : "create" ,
537
+ CredentialOptions : options ,
538
+ },
530
539
}
531
540
532
541
} else if factor .IsVerified () {
@@ -539,9 +548,12 @@ func (a *API) challengeWebAuthnFactor(w http.ResponseWriter, r *http.Request) er
539
548
}
540
549
challenge = ws .ToChallenge (factor .ID , ipAddress )
541
550
response = & ChallengeFactorResponse {
542
- CredentialRequestOptions : options ,
543
- Type : factor .FactorType ,
544
- ID : challenge .ID ,
551
+ Type : factor .FactorType ,
552
+ ID : challenge .ID ,
553
+ WebAuthn : & WebAuthnChallengeData {
554
+ Type : "request" ,
555
+ CredentialOptions : options ,
556
+ },
545
557
}
546
558
547
559
}
@@ -878,10 +890,10 @@ func (a *API) verifyWebAuthnFactor(w http.ResponseWriter, r *http.Request, param
878
890
switch {
879
891
case params .WebAuthn == nil :
880
892
return apierrors .NewBadRequestError (apierrors .ErrorCodeValidationFailed , "WebAuthn config required" )
881
- case factor . IsVerified () && params .WebAuthn .AssertionResponse == nil :
882
- return apierrors .NewBadRequestError (apierrors .ErrorCodeValidationFailed , "creation_response required to login " )
883
- case factor . IsUnverified () && params .WebAuthn .CreationResponse == nil :
884
- return apierrors .NewBadRequestError (apierrors .ErrorCodeValidationFailed , "assertion_response required to login " )
893
+ case params . WebAuthn . Type != "create" && params .WebAuthn .Type != "request" :
894
+ return apierrors .NewBadRequestError (apierrors .ErrorCodeValidationFailed , "WebAuthn type must be create or request " )
895
+ case params .WebAuthn .CredentialResponse == nil :
896
+ return apierrors .NewBadRequestError (apierrors .ErrorCodeValidationFailed , "credential_response required" )
885
897
default :
886
898
webAuthn , err = params .WebAuthn .ToConfig ()
887
899
if err != nil {
@@ -899,20 +911,21 @@ func (a *API) verifyWebAuthnFactor(w http.ResponseWriter, r *http.Request, param
899
911
return apierrors .NewInternalServerError ("Database error deleting challenge" ).WithInternalError (err )
900
912
}
901
913
902
- if factor .IsUnverified () {
903
- parsedResponse , err := wbnprotocol .ParseCredentialCreationResponseBody (bytes .NewReader (params .WebAuthn .CreationResponse ))
914
+ switch params .WebAuthn .Type {
915
+ case "create" :
916
+ parsedResponse , err := wbnprotocol .ParseCredentialCreationResponseBody (bytes .NewReader (params .WebAuthn .CredentialResponse ))
904
917
if err != nil {
905
- return apierrors .NewBadRequestError (apierrors .ErrorCodeValidationFailed , "Invalid credential_creation_response " )
918
+ return apierrors .NewBadRequestError (apierrors .ErrorCodeValidationFailed , "Invalid credential_response " )
906
919
}
907
920
credential , err = webAuthn .CreateCredential (user , webAuthnSession , parsedResponse )
908
921
if err != nil {
909
922
return err
910
923
}
911
924
912
- } else if factor . IsVerified () {
913
- parsedResponse , err := wbnprotocol .ParseCredentialRequestResponseBody (bytes .NewReader (params .WebAuthn .AssertionResponse ))
925
+ case "request" :
926
+ parsedResponse , err := wbnprotocol .ParseCredentialRequestResponseBody (bytes .NewReader (params .WebAuthn .CredentialResponse ))
914
927
if err != nil {
915
- return apierrors .NewBadRequestError (apierrors .ErrorCodeValidationFailed , "Invalid credential_request_response " )
928
+ return apierrors .NewBadRequestError (apierrors .ErrorCodeValidationFailed , "Invalid credential_response " )
916
929
}
917
930
credential , err = webAuthn .ValidateLogin (user , webAuthnSession , parsedResponse )
918
931
if err != nil {
0 commit comments