mirror of
https://github.com/matrix-org/dendrite
synced 2024-12-13 23:02:46 +00:00
Add auth fallback endpoint (#405)
Also adds support for the recaptcha auth type.
This commit is contained in:
parent
28f60bec4e
commit
d21a2fb152
4 changed files with 249 additions and 14 deletions
210
clientapi/routing/auth_fallback.go
Normal file
210
clientapi/routing/auth_fallback.go
Normal file
|
@ -0,0 +1,210 @@
|
|||
// 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
|
||||
}
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue