diff --git a/clientapi/routing/auth_fallback.go b/clientapi/routing/auth_fallback.go new file mode 100644 index 00000000..cd4530d1 --- /dev/null +++ b/clientapi/routing/auth_fallback.go @@ -0,0 +1,210 @@ +// Copyright 2019 Parminder Singh +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package routing + +import ( + "html/template" + "net/http" + + "github.com/matrix-org/dendrite/clientapi/auth/authtypes" + "github.com/matrix-org/dendrite/clientapi/httputil" + "github.com/matrix-org/dendrite/clientapi/jsonerror" + "github.com/matrix-org/dendrite/common/config" + "github.com/matrix-org/util" +) + +// recaptchaTemplate is an HTML webpage template for recaptcha auth +const recaptchaTemplate = ` + + +Authentication + + + + + + +
+
+

+ Hello! We need to prevent computer programs and other automated + things from creating accounts on this server. +

+

+ Please verify that you're not a robot. +

+ +
+
+ +
+ +
+ + +` + +// successTemplate is an HTML template presented to the user after successful +// recaptcha completion +const successTemplate = ` + + +Success! + + + + +
+

Thank you!

+

You may now close this window and return to the application.

+
+ + +` + +// serveTemplate fills template data and serves it using http.ResponseWriter +func serveTemplate(w http.ResponseWriter, templateHTML string, data map[string]string) { + t := template.Must(template.New("response").Parse(templateHTML)) + if err := t.Execute(w, data); err != nil { + panic(err) + } +} + +// AuthFallback implements GET and POST /auth/{authType}/fallback/web?session={sessionID} +func AuthFallback( + w http.ResponseWriter, req *http.Request, authType string, + cfg config.Dendrite, +) *util.JSONResponse { + sessionID := req.URL.Query().Get("session") + + if sessionID == "" { + return writeHTTPMessage(w, req, + "Session ID not provided", + http.StatusBadRequest, + ) + } + + serveRecaptcha := func() { + data := map[string]string{ + "myUrl": req.URL.String(), + "session": sessionID, + "siteKey": cfg.Matrix.RecaptchaPublicKey, + } + serveTemplate(w, recaptchaTemplate, data) + } + + serveSuccess := func() { + data := map[string]string{} + serveTemplate(w, successTemplate, data) + } + + if req.Method == http.MethodGet { + // Handle Recaptcha + if authType == authtypes.LoginTypeRecaptcha { + if err := checkRecaptchaEnabled(&cfg, w, req); err != nil { + return err + } + + serveRecaptcha() + return nil + } + return &util.JSONResponse{ + Code: http.StatusNotFound, + JSON: jsonerror.NotFound("Unknown auth stage type"), + } + } else if req.Method == http.MethodPost { + // Handle Recaptcha + if authType == authtypes.LoginTypeRecaptcha { + if err := checkRecaptchaEnabled(&cfg, w, req); err != nil { + return err + } + + clientIP := req.RemoteAddr + err := req.ParseForm() + if err != nil { + res := httputil.LogThenError(req, err) + return &res + } + + response := req.Form.Get("g-recaptcha-response") + if err := validateRecaptcha(&cfg, response, clientIP); err != nil { + util.GetLogger(req.Context()).Error(err) + return err + } + + // Success. Add recaptcha as a completed login flow + AddCompletedSessionStage(sessionID, authtypes.LoginTypeRecaptcha) + + serveSuccess() + return nil + } + + return &util.JSONResponse{ + Code: http.StatusNotFound, + JSON: jsonerror.NotFound("Unknown auth stage type"), + } + } + return &util.JSONResponse{ + Code: http.StatusMethodNotAllowed, + JSON: jsonerror.NotFound("Bad method"), + } +} + +// checkRecaptchaEnabled creates an error response if recaptcha is not usable on homeserver. +func checkRecaptchaEnabled( + cfg *config.Dendrite, + w http.ResponseWriter, + req *http.Request, +) *util.JSONResponse { + if !cfg.Matrix.RecaptchaEnabled { + return writeHTTPMessage(w, req, + "Recaptcha login is disabled on this Homeserver", + http.StatusBadRequest, + ) + } + return nil +} + +// writeHTTPMessage writes the given header and message to the HTTP response writer. +// Returns an error JSONResponse obtained through httputil.LogThenError if the writing failed, otherwise nil. +func writeHTTPMessage( + w http.ResponseWriter, req *http.Request, + message string, header int, +) *util.JSONResponse { + w.WriteHeader(header) + _, err := w.Write([]byte(message)) + if err != nil { + res := httputil.LogThenError(req, err) + return &res + } + return nil +} diff --git a/clientapi/routing/register.go b/clientapi/routing/register.go index c5a3d301..0af40758 100644 --- a/clientapi/routing/register.go +++ b/clientapi/routing/register.go @@ -83,23 +83,22 @@ func (d sessionsDict) GetCompletedStages(sessionID string) []authtypes.LoginType return make([]authtypes.LoginType, 0) } -// AddCompletedStage records that a session has completed an auth stage. -func (d *sessionsDict) AddCompletedStage(sessionID string, stage authtypes.LoginType) { - // Return if the stage is already present - for _, completedStage := range d.GetCompletedStages(sessionID) { - if completedStage == stage { - return - } - } - d.sessions[sessionID] = append(d.GetCompletedStages(sessionID), stage) -} - func newSessionsDict() *sessionsDict { return &sessionsDict{ sessions: make(map[string][]authtypes.LoginType), } } +// AddCompletedSessionStage records that a session has completed an auth stage. +func AddCompletedSessionStage(sessionID string, stage authtypes.LoginType) { + for _, completedStage := range sessions.GetCompletedStages(sessionID) { + if completedStage == stage { + return + } + } + sessions.sessions[sessionID] = append(sessions.GetCompletedStages(sessionID), stage) +} + var ( // TODO: Remove old sessions. Need to do so on a session-specific timeout. // sessions stores the completed flow stages for all sessions. Referenced using their sessionID. @@ -530,7 +529,7 @@ func handleRegistrationFlow( } // Add Recaptcha to the list of completed registration stages - sessions.AddCompletedStage(sessionID, authtypes.LoginTypeRecaptcha) + AddCompletedSessionStage(sessionID, authtypes.LoginTypeRecaptcha) case authtypes.LoginTypeSharedSecret: // Check shared secret against config @@ -543,7 +542,7 @@ func handleRegistrationFlow( } // Add SharedSecret to the list of completed registration stages - sessions.AddCompletedStage(sessionID, authtypes.LoginTypeSharedSecret) + AddCompletedSessionStage(sessionID, authtypes.LoginTypeSharedSecret) case "": // Extract the access token from the request, if there's one to extract @@ -573,7 +572,7 @@ func handleRegistrationFlow( case authtypes.LoginTypeDummy: // there is nothing to do // Add Dummy to the list of completed registration stages - sessions.AddCompletedStage(sessionID, authtypes.LoginTypeDummy) + AddCompletedSessionStage(sessionID, authtypes.LoginTypeDummy) default: return util.JSONResponse{ diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index d36ed695..d4b323a2 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -245,6 +245,13 @@ func Setup( }), ).Methods(http.MethodGet, http.MethodPost, http.MethodOptions) + r0mux.Handle("/auth/{authType}/fallback/web", + common.MakeHTMLAPI("auth_fallback", func(w http.ResponseWriter, req *http.Request) *util.JSONResponse { + vars := mux.Vars(req) + return AuthFallback(w, req, vars["authType"], cfg) + }), + ).Methods(http.MethodGet, http.MethodPost, http.MethodOptions) + r0mux.Handle("/pushrules/", common.MakeExternalAPI("push_rules", func(req *http.Request) util.JSONResponse { // TODO: Implement push rules API diff --git a/common/httpapi.go b/common/httpapi.go index 99e15830..bf634ff4 100644 --- a/common/httpapi.go +++ b/common/httpapi.go @@ -10,6 +10,7 @@ import ( "github.com/matrix-org/util" opentracing "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) @@ -43,6 +44,24 @@ func MakeExternalAPI(metricsName string, f func(*http.Request) util.JSONResponse return http.HandlerFunc(withSpan) } +// MakeHTMLAPI adds Span metrics to the HTML Handler function +// This is used to serve HTML alongside JSON error messages +func MakeHTMLAPI(metricsName string, f func(http.ResponseWriter, *http.Request) *util.JSONResponse) http.Handler { + withSpan := func(w http.ResponseWriter, req *http.Request) { + span := opentracing.StartSpan(metricsName) + defer span.Finish() + req = req.WithContext(opentracing.ContextWithSpan(req.Context(), span)) + if err := f(w, req); err != nil { + h := util.MakeJSONAPI(util.NewJSONRequestHandler(func(req *http.Request) util.JSONResponse { + return *err + })) + h.ServeHTTP(w, req) + } + } + + return prometheus.InstrumentHandler(metricsName, http.HandlerFunc(withSpan)) +} + // MakeInternalAPI turns a util.JSONRequestHandler function into an http.Handler. // This is used for APIs that are internal to dendrite. // If we are passed a tracing context in the request headers then we use that