@@ -54,56 +54,51 @@ type EnrollFactorResponse struct {
5454
5555type ChallengeFactorParams struct {
5656 Channel string `json:"channel"`
57- WebAuthn * WebAuthnParams `json:"web_authn ,omitempty"`
57+ WebAuthn * WebAuthnParams `json:"webauthn ,omitempty"`
5858}
5959
6060type VerifyFactorParams struct {
6161 ChallengeID uuid.UUID `json:"challenge_id"`
6262 Code string `json:"code"`
63- WebAuthn * WebAuthnParams `json:"web_authn ,omitempty"`
63+ WebAuthn * WebAuthnParams `json:"webauthn ,omitempty"`
6464}
6565
6666type 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"`
7271}
7372
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"`
7676}
7777
7878type 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"`
8483}
8584
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"`
9187}
9288
9389func (w * WebAuthnParams ) ToConfig () (* webauthn.WebAuthn , error ) {
9490 if w .RPID == "" {
9591 return nil , fmt .Errorf ("webAuthn RP ID cannot be empty" )
9692 }
9793
98- origins := w .GetRPOrigins ()
99- if len (origins ) == 0 {
94+ if len (w .RPOrigins ) == 0 {
10095 return nil , fmt .Errorf ("webAuthn RP Origins cannot be empty" )
10196 }
10297
10398 var validOrigins []string
10499 var invalidOrigins []string
105100
106- for _ , origin := range origins {
101+ for _ , origin := range w . RPOrigins {
107102 parsedURL , err := url .Parse (origin )
108103 if err != nil || (parsedURL .Scheme != "https" && ! (parsedURL .Scheme == "http" && parsedURL .Hostname () == "localhost" )) || parsedURL .Host == "" {
109104 invalidOrigins = append (invalidOrigins , origin )
@@ -514,7 +509,18 @@ func (a *API) challengeWebAuthnFactor(w http.ResponseWriter, r *http.Request) er
514509 var ws * models.WebAuthnSessionData
515510 var challenge * models.Challenge
516511 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 ))
518524 if err != nil {
519525 return apierrors .NewInternalServerError ("Failed to generate WebAuthn registration data" ).WithInternalError (err )
520526 }
@@ -524,9 +530,12 @@ func (a *API) challengeWebAuthnFactor(w http.ResponseWriter, r *http.Request) er
524530 challenge = ws .ToChallenge (factor .ID , ipAddress )
525531
526532 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+ },
530539 }
531540
532541 } else if factor .IsVerified () {
@@ -539,9 +548,12 @@ func (a *API) challengeWebAuthnFactor(w http.ResponseWriter, r *http.Request) er
539548 }
540549 challenge = ws .ToChallenge (factor .ID , ipAddress )
541550 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+ },
545557 }
546558
547559 }
@@ -878,10 +890,10 @@ func (a *API) verifyWebAuthnFactor(w http.ResponseWriter, r *http.Request, param
878890 switch {
879891 case params .WebAuthn == nil :
880892 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" )
885897 default :
886898 webAuthn , err = params .WebAuthn .ToConfig ()
887899 if err != nil {
@@ -899,20 +911,21 @@ func (a *API) verifyWebAuthnFactor(w http.ResponseWriter, r *http.Request, param
899911 return apierrors .NewInternalServerError ("Database error deleting challenge" ).WithInternalError (err )
900912 }
901913
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 ))
904917 if err != nil {
905- return apierrors .NewBadRequestError (apierrors .ErrorCodeValidationFailed , "Invalid credential_creation_response " )
918+ return apierrors .NewBadRequestError (apierrors .ErrorCodeValidationFailed , "Invalid credential_response " )
906919 }
907920 credential , err = webAuthn .CreateCredential (user , webAuthnSession , parsedResponse )
908921 if err != nil {
909922 return err
910923 }
911924
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 ))
914927 if err != nil {
915- return apierrors .NewBadRequestError (apierrors .ErrorCodeValidationFailed , "Invalid credential_request_response " )
928+ return apierrors .NewBadRequestError (apierrors .ErrorCodeValidationFailed , "Invalid credential_response " )
916929 }
917930 credential , err = webAuthn .ValidateLogin (user , webAuthnSession , parsedResponse )
918931 if err != nil {
0 commit comments