Add auth fallback endpoint
Also adds support for the recaptcha auth type.
ADD
clientapi/routing/auth_fallback.go
+// Copyright 2019 Parminder Singh <parmsingh129@gmail.com>
+//
+// 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 = `
+<html>
+<head>
+<title>Authentication</title>
+<meta name='viewport' content='width=device-width, initial-scale=1,
+ user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'>
+<script src="https://www.google.com/recaptcha/api.js"
+ async defer></script>
+<script src="//code.jquery.com/jquery-1.11.2.min.js"></script>
+<script>
+function captchaDone() {
+ $('#registrationForm').submit();
+}
+</script>
+</head>
+<body>
+<form id="registrationForm" method="post" action="{{.myUrl}}">
+ <div>
+ <p>
+ Hello! We need to prevent computer programs and other automated
+ things from creating accounts on this server.
+ </p>
+ <p>
+ Please verify that you're not a robot.
+ </p>
+ <input type="hidden" name="session" value="{{.session}}" />
+ <div class="g-recaptcha"
+ data-sitekey="{{.siteKey}}"
+ data-callback="captchaDone">
+ </div>
+ <noscript>
+ <input type="submit" value="All Done" />
+ </noscript>
+ </div>
+ </div>
+</form>
+</body>
+</html>
+`
+
+// successTemplate is an HTML template presented to the user after successful
+// recaptcha completion
+const successTemplate = `
+<html>
+<head>
+<title>Success!</title>
+<meta name='viewport' content='width=device-width, initial-scale=1,
+ user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'>
+<script>
+if (window.onAuthDone) {
+ window.onAuthDone();
+} else if (window.opener && window.opener.postMessage) {
+ window.opener.postMessage("authDone", "*");
+}
+</script>
+</head>
+<body>
+ <div>
+ <p>Thank you!</p>
+ <p>You may now close this window and return to the application.</p>
+ </div>
+</body>
+</html>
+`
+
+// 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
+}

MODIFY
clientapi/routing/register.go
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{

MODIFY
clientapi/routing/routing.go
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

MODIFY
common/httpapi.go
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

Add auth fallback endpoint (#405)

Also adds support for the recaptcha auth type.