package hydra import ( "encoding/base64" "encoding/gob" "encoding/json" "net/http" "forge.cadoles.com/wpetit/hydra-webauthn/internal/config" "forge.cadoles.com/wpetit/hydra-webauthn/internal/hydra" "forge.cadoles.com/wpetit/hydra-webauthn/internal/route/common" "forge.cadoles.com/wpetit/hydra-webauthn/internal/storage" "forge.cadoles.com/wpetit/hydra-webauthn/internal/webauthn" "github.com/getsentry/sentry-go" "github.com/go-webauthn/webauthn/protocol" "github.com/gorilla/csrf" "github.com/pkg/errors" "gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/middleware/container" "gitlab.com/wpetit/goweb/service/session" "gitlab.com/wpetit/goweb/service/template" ) func init() { gob.Register(&webauthn.SessionData{}) } func serveLoginPage(w http.ResponseWriter, r *http.Request) { ctn := container.Must(r.Context()) hydr := hydra.Must(ctn) challenge, err := hydr.LoginChallenge(r) if err != nil { if errors.Is(err, hydra.ErrChallengeNotFound) { err := common.RenderErrorPage( w, r, http.StatusBadRequest, "Requête invalide", "Certaines informations requises afin de réaliser votre requête sont absentes.", ) if err != nil { panic(errors.Wrapf(err, "could not render '%s' page", r.URL.Path)) } return } panic(errors.Wrap(err, "could not retrieve login challenge")) } res, err := hydr.LoginRequest(challenge) if err != nil { panic(errors.Wrap(err, "could not retrieve hydra login response")) } if res.Skip { accept := &hydra.AcceptLoginRequest{ Subject: res.Subject, Context: map[string]interface{}{ "username": res.Subject, }, } res, err := hydr.AcceptLoginRequest(challenge, accept) if err != nil { panic(errors.Wrap(err, "could not retrieve hydra accept response")) } http.Redirect(w, r, res.RedirectTo, http.StatusSeeOther) return } tmpl := template.Must(ctn) data := common.ExtendTemplateData(w, r, template.Data{ csrf.TemplateTag: csrf.TemplateField(r), "LoginChallenge": challenge, "ClientName": res.Client.ClientName, "ClientURI": res.Client.ClientURI, "Username": "", "RememberMe": "", "CredentialAssertion": nil, }) if err := tmpl.RenderPage(w, "login.html.tmpl", data); err != nil { panic(errors.Wrapf(err, "could not render '%s' page", r.URL.Path)) } } func handleLoginForm(w http.ResponseWriter, r *http.Request) { ctx := r.Context() ctn := container.Must(ctx) tmpl := template.Must(ctn) hydr := hydra.Must(ctn) strg := storage.Must(ctn) conf := config.Must(ctn) wbth := webauthn.Must(ctn) sesn := session.Must(ctn) if err := r.ParseForm(); err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } challenge := r.Form.Get("challenge") if challenge == "" { err := common.RenderErrorPage( w, r, http.StatusBadRequest, "Requête invalide", "Certaines informations requises sont manquantes pour pouvoir réaliser votre requête.", ) if err != nil { panic(errors.Wrapf(err, "could not render '%s' page", r.URL.Path)) } return } res, err := hydr.LoginRequest(challenge) if err != nil { panic(errors.Wrap(err, "could not retrieve hydra login response")) } username := r.Form.Get("username") rememberMe := r.Form.Get("rememberme") == "on" renderPage := func(assertionRequest *protocol.CredentialAssertion) { data := common.ExtendTemplateData(w, r, template.Data{ csrf.TemplateTag: csrf.TemplateField(r), "LoginChallenge": challenge, "Username": username, "RememberMe": rememberMe, "AssertionRequest": assertionRequest, "ClientName": res.Client.ClientName, "ClientURI": res.Client.ClientURI, }) if err := tmpl.RenderPage(w, "login.html.tmpl", data); err != nil { panic(errors.Wrapf(err, "could not render '%s' page", r.URL.Path)) } } sess, err := sesn.Get(w, r) if err != nil { panic(errors.Wrap(err, "could not retrieve session")) } renderFlashError := func(message string) { sess.AddFlash(session.FlashError, message) if err := sess.Save(w, r); err != nil { panic(errors.Wrap(err, "could not save session")) } renderPage(nil) } user, err := strg.User().FindUserByUsername(ctx, username) if err != nil { if errors.Is(err, storage.ErrNotFound) { renderFlashError("Impossible de trouver un compte associé à ce nom d'utilisateur.") return } panic(errors.Wrap(err, "could not find user")) } if !r.Form.Has("assertion") { if len(user.WebAuthnCredentials()) == 0 { renderFlashError("Aucune clé n'est encore associée à ce compte. Contactez votre administrateur.") return } assertionRequest, webAuthnSessionData, err := wbth.BeginLogin(user) if err != nil { panic(errors.Wrap(err, "could not begin webauthn login")) } sess.Set("webauthn", webAuthnSessionData) if err := sess.Save(w, r); err != nil { panic(errors.Wrap(err, "could not save session")) } renderPage(assertionRequest) return } webAuthnSessionData, ok := sess.Get("webauthn").(*webauthn.SessionData) if !ok { panic(errors.New("could not retrieve webauthn session data")) } base64Assertion := r.Form.Get("assertion") if base64Assertion == "" { renderFlashError("Authentification échouée.") return } rawAssertion, err := base64.RawURLEncoding.DecodeString(base64Assertion) if err != nil { logger.Error(ctx, "could not parse base64 encoded assertion", logger.E(errors.WithStack(err))) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } var car protocol.CredentialAssertionResponse if err := json.Unmarshal(rawAssertion, &car); err != nil { logger.Error(ctx, "could not parse credential assertion response", logger.E(errors.WithStack(err))) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } parsedResponse, err := car.Parse() if err != nil { logger.Error(ctx, "could not parse credential assertion response", logger.E(errors.WithStack(err))) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if _, err := wbth.ValidateLogin(user, *webAuthnSessionData, parsedResponse); err != nil { logger.Error(ctx, "could not authenticate user", logger.E(errors.WithStack(err))) renderFlashError("Authentification échouée.") return } rememberFor := conf.Session.DefaultDuration if rememberMe { rememberFor = conf.Session.RememberMeDuration } accept := &hydra.AcceptLoginRequest{ Subject: user.Username, Remember: rememberMe, RememberFor: rememberFor, Context: map[string]any{ "userAttributes": user.Attributes, }, } loginRes, err := hydr.AcceptLoginRequest(challenge, accept) if err != nil { sentry.CaptureException(err) logger.Error(ctx, "could not retrieve hydra accept response", logger.E(err)) err := common.RenderErrorPage( w, r, http.StatusBadRequest, "Lien invalide", "Le lien de connexion utilisé est invalide ou a expiré.", ) if err != nil { panic(errors.Wrapf(err, "could not render '%s' page", r.URL.Path)) } return } http.Redirect(w, r, loginRes.RedirectTo, http.StatusSeeOther) }