diff --git a/go.mod b/go.mod index 105c7c6..6c8a268 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.4 require ( github.com/emersion/go-imap/v2 v2.0.0-beta.7 github.com/getkin/kin-openapi v0.133.0 + github.com/go-webauthn/webauthn v0.15.0 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/go-github/v74 v74.0.0 github.com/jackc/pgx/v5 v5.8.0 @@ -15,9 +16,14 @@ require ( require ( github.com/emersion/go-message v0.18.1 // indirect github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-webauthn/x v0.1.26 // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/google/go-tpm v0.9.6 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect @@ -28,7 +34,10 @@ require ( github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/crypto v0.43.0 // indirect golang.org/x/sync v0.17.0 // indirect - golang.org/x/text v0.29.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f79ab04..d4ed2f4 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/emersion/go-message v0.18.1 h1:tfTxIoXFSFRwWaZsgnqS1DSZuGpYGzSmCZD8SK github.com/emersion/go-message v0.18.1/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA= github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY= github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= @@ -15,6 +17,12 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-webauthn/webauthn v0.15.0 h1:LR1vPv62E0/6+sTenX35QrCmpMCzLeVAcnXeH4MrbJY= +github.com/go-webauthn/webauthn v0.15.0/go.mod h1:hcAOhVChPRG7oqG7Xj6XKN1mb+8eXTGP/B7zBLzkX5A= +github.com/go-webauthn/x v0.1.26 h1:eNzreFKnwNLDFoywGh9FA8YOMebBWTUNlNSdolQRebs= +github.com/go-webauthn/x v0.1.26/go.mod h1:jmf/phPV6oIsF6hmdVre+ovHkxjDOmNH0t6fekWUxvg= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -24,6 +32,10 @@ github.com/google/go-github/v74 v74.0.0 h1:yZcddTUn8DPbj11GxnMrNiAnXH14gNs559AsU github.com/google/go-github/v74 v74.0.0/go.mod h1:ubn/YdyftV80VPSI26nSJvaEsTOnsjrxG3o9kJhcyak= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA= +github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -63,9 +75,15 @@ github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0 github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -85,6 +103,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -93,8 +113,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/test.go b/test.go new file mode 100644 index 0000000..c96c924 --- /dev/null +++ b/test.go @@ -0,0 +1,255 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" +) + +// Example_passkeysRegisterAndLogin demonstrates handling Passkey registration and Logins. This uses the higher level APIs to +// perform all of the various requirements. The Crude and Abstract examples are purely domain logic and will often +// describe aspects that should be considered during their implementation if they are important; these aspects +// are not strictly concerns related to the library as there are too many logical implementations to count. +func main() { + config := &webauthn.Config{ + RPDisplayName: "Go WebAuthn", + RPID: "app.awesome-go-webauthn.com", + RPOrigins: []string{"https://app.awesome-go-webauthn.com"}, + } + + w, err := webauthn.New(config) + if err != nil { + // Crude example of error handling. + panic(err) + } + + mux := http.NewServeMux() + + // Register the handlers. The second component describes the action (i.e. register/login), the final component + // describes the step (i.e. start/finish). + mux.HandleFunc("/webauthn/register/start", handlerExamplePasskeyCreateChallenge(w)) + mux.HandleFunc("/webauthn/register/finish", handlerExamplePasskeyValidateCreateChallengeResponse(w)) + mux.HandleFunc("/webauthn/login/start", handlerExamplePasskeyLoginChallenge(w)) + mux.HandleFunc("/webauthn/login/finish", handlerExamplePasskeyLoginChallengeResponse(w)) + + // Crude example that assumes the app is handled exclusively by a proxy which handles TLS termination. You will + // have to adjust this depending on the context to ensure TLS is used on port 443 or the relevant config options + // are adjusted. + server := &http.Server{ + Addr: ":8080", + Handler: mux, + ReadTimeout: 5 * time.Second, + ReadHeaderTimeout: 2 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 120 * time.Second, + MaxHeaderBytes: 1 << 20, + } + + if err = server.ListenAndServe(); err != nil { + panic(err) + } +} + +var sessionExamplePasskey *webauthn.SessionData + +func saveSessionExamplePasskey(s *webauthn.SessionData) { + sessionExamplePasskey = s +} + +func loadSessionExamplePasskey() (*webauthn.SessionData, error) { + if sessionExamplePasskey == nil { + return nil, fmt.Errorf("no session found") + } + + return sessionExamplePasskey, nil +} + +func loadUserExamplePasskey(rawID []byte, userHandle []byte) (user webauthn.User, err error) { + // Crude / Abstract example of retrieving the user for the rawID/userHandle value. + return LoadUserByHandle(userHandle) +} + +func handlerExamplePasskeyCreateChallenge(w *webauthn.WebAuthn) func(rw http.ResponseWriter, r *http.Request) { + return func(rw http.ResponseWriter, r *http.Request) { + // Crude / Abstract example of retrieving the user this registration will belong to. The user must be logged in + // for this step unless you plan to register the user and the credential at the same time i.e. usernameless. + // The user should have a unique and stable value returned from WebAuthnID that can be used to retrieve the + // account details for the user. + user, err := LoadUser() + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + + return + } + + var ( + creation *protocol.CredentialCreation + s *webauthn.SessionData + ) + + opts := []webauthn.RegistrationOption{ + webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementRequired), + webauthn.WithExclusions(webauthn.Credentials(user.WebAuthnCredentials()).CredentialDescriptors()), + webauthn.WithExtensions(map[string]any{"credProps": true}), + } + + if creation, s, err = w.BeginMediatedRegistration(user, protocol.MediationDefault, opts...); err != nil { + rw.WriteHeader(http.StatusInternalServerError) + + return + } + + // Crude example saving the session data securely to be loaded in the finish step of the register action. This + // should be stored in such a way that the user and user agent has no access to it. For example using an opaque + // session cookie. + saveSessionExamplePasskey(s) + + encoder := json.NewEncoder(rw) + + if err = encoder.Encode(creation); err != nil { + rw.WriteHeader(http.StatusInternalServerError) + + return + } + + rw.Header().Set("Content-Type", "application/json; charset=utf-8") + rw.WriteHeader(http.StatusOK) + } +} + +func handlerExamplePasskeyValidateCreateChallengeResponse(w *webauthn.WebAuthn) func(rw http.ResponseWriter, r *http.Request) { + return func(rw http.ResponseWriter, r *http.Request) { + // Crude / Abstract example of retrieving the user this registration will belong to. The user must be logged in + // for this step unless you plan to register the user and the credential at the same time i.e. usernameless. + // The user should have a unique and stable value returned from WebAuthnID that can be used to retrieve the + // account details for the user. + user, err := LoadUser() + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + + return + } + + // Crude example loading the session data securely from the start step for the register action. This should be + // loaded from a place the user and user agent has no access to it. For example using an opaque session cookie. + s, err := loadSessionExamplePasskey() + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + + return + } + + credential, err := w.FinishRegistration(user, *s, r) + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + + return + } + + // Crude / Abstract example of adding the credential to the list of credentials for the user. This is critical + // for performing future logins. + user.credentials = append(user.credentials, *credential) + + // Crude / Abstract example of saving the updated user. This is critical for performing future logins. + if err = SaveUser(user); err != nil { + rw.WriteHeader(http.StatusInternalServerError) + + return + } + + rw.WriteHeader(http.StatusOK) + } +} + +func handlerExamplePasskeyLoginChallenge(w *webauthn.WebAuthn) func(rw http.ResponseWriter, r *http.Request) { + return func(rw http.ResponseWriter, r *http.Request) { + assertion, s, err := w.BeginDiscoverableMediatedLogin(protocol.MediationDefault) + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + + return + } + + // Crude example saving the session data securely to be loaded in the finish step of the login action. This + // should be stored in such a way that the user and user agent has no access to it. For example using an opaque + // session cookie. + saveSessionExamplePasskey(s) + + encoder := json.NewEncoder(rw) + + if err = encoder.Encode(assertion); err != nil { + rw.WriteHeader(http.StatusInternalServerError) + + return + } + + rw.Header().Set("Content-Type", "application/json; charset=utf-8") + rw.WriteHeader(http.StatusOK) + } +} + +func handlerExamplePasskeyLoginChallengeResponse(w *webauthn.WebAuthn) func(rw http.ResponseWriter, r *http.Request) { + return func(rw http.ResponseWriter, r *http.Request) { + // Crude example loading the session data securely from the start step for the login action. This should be + // loaded from a place the user and user agent has no access to it. For example using an opaque session cookie. + s, err := loadSessionExamplePasskey() + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + + return + } + + validatedUser, validatedCredential, err := w.FinishPasskeyLogin(loadUserExamplePasskey, *s, r) + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + + return + } + + // This type assertion is necessary to perform the necessary updates. + user, ok := validatedUser.(*defaultUser) + if !ok { + rw.WriteHeader(http.StatusInternalServerError) + + return + } + + var found bool + + // Modify the matching credential in the user struct which is critical for proper future validations as the + // metadata for this credential has been updated. No type assertion is required here since the LoadUser function + // returns the concrete implementation, you may have to adjust this if you return the abstract implementation + // instead. + for i, credential := range user.credentials { + if bytes.Equal(validatedCredential.ID, credential.ID) { + user.credentials[i] = *validatedCredential + + // Crude / Abstract example of saving the user with their updated credentials. This is critical for + // proper future validations. + if err = SaveUser(user); err != nil { + rw.WriteHeader(http.StatusInternalServerError) + + return + } + + found = true + + break + } + } + + // Should error if we can't update the credentials for the user. + if !found { + rw.WriteHeader(http.StatusInternalServerError) + + return + } + + rw.WriteHeader(http.StatusOK) + } +}