mirror of
https://github.com/hoernschen/dendrite.git
synced 2025-07-31 13:22:46 +00:00
Add User-Interactive Authentication (#1193)
* Add User-Interactive Authentication And use it when deleting a device. With tests. * Make remaining sytest pass * Linting * 403 not 401 on wrong user/pass
This commit is contained in:
parent
9cc52f47f3
commit
abf26c12f1
7 changed files with 594 additions and 127 deletions
|
@ -17,8 +17,10 @@ package routing
|
|||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||
"github.com/matrix-org/dendrite/userapi/api"
|
||||
"github.com/matrix-org/dendrite/userapi/storage/devices"
|
||||
|
@ -72,7 +74,8 @@ func GetDeviceByID(
|
|||
return util.JSONResponse{
|
||||
Code: http.StatusOK,
|
||||
JSON: deviceJSON{
|
||||
DeviceID: dev.ID,
|
||||
DeviceID: dev.ID,
|
||||
DisplayName: dev.DisplayName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -99,7 +102,8 @@ func GetDevicesByLocalpart(
|
|||
|
||||
for _, dev := range deviceList {
|
||||
res.Devices = append(res.Devices, deviceJSON{
|
||||
DeviceID: dev.ID,
|
||||
DeviceID: dev.ID,
|
||||
DisplayName: dev.DisplayName,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -161,20 +165,40 @@ func UpdateDeviceByID(
|
|||
|
||||
// DeleteDeviceById handles DELETE requests to /devices/{deviceId}
|
||||
func DeleteDeviceById(
|
||||
req *http.Request, deviceDB devices.Database, device *api.Device,
|
||||
req *http.Request, userInteractiveAuth *auth.UserInteractive, deviceDB devices.Database, device *api.Device,
|
||||
deviceID string,
|
||||
) util.JSONResponse {
|
||||
ctx := req.Context()
|
||||
defer req.Body.Close() // nolint:errcheck
|
||||
bodyBytes, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return util.JSONResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
JSON: jsonerror.BadJSON("The request body could not be read: " + err.Error()),
|
||||
}
|
||||
}
|
||||
login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes, device)
|
||||
if errRes != nil {
|
||||
return *errRes
|
||||
}
|
||||
|
||||
localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
|
||||
if err != nil {
|
||||
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
||||
util.GetLogger(ctx).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
||||
return jsonerror.InternalServerError()
|
||||
}
|
||||
ctx := req.Context()
|
||||
|
||||
defer req.Body.Close() // nolint: errcheck
|
||||
// make sure that the access token being used matches the login creds used for user interactive auth, else
|
||||
// 1 compromised access token could be used to logout all devices.
|
||||
if login.Username() != localpart && login.Username() != device.UserID {
|
||||
return util.JSONResponse{
|
||||
Code: 403,
|
||||
JSON: jsonerror.Forbidden("Cannot delete another user's device"),
|
||||
}
|
||||
}
|
||||
|
||||
if err := deviceDB.RemoveDevice(ctx, deviceID, localpart); err != nil {
|
||||
util.GetLogger(req.Context()).WithError(err).Error("deviceDB.RemoveDevice failed")
|
||||
util.GetLogger(ctx).WithError(err).Error("deviceDB.RemoveDevice failed")
|
||||
return jsonerror.InternalServerError()
|
||||
}
|
||||
|
||||
|
|
|
@ -15,46 +15,20 @@
|
|||
package routing
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||
"github.com/matrix-org/dendrite/clientapi/userutil"
|
||||
"github.com/matrix-org/dendrite/internal/config"
|
||||
"github.com/matrix-org/dendrite/userapi/api"
|
||||
"github.com/matrix-org/dendrite/userapi/storage/accounts"
|
||||
"github.com/matrix-org/dendrite/userapi/storage/devices"
|
||||
"github.com/matrix-org/gomatrixserverlib"
|
||||
"github.com/matrix-org/util"
|
||||
)
|
||||
|
||||
type loginFlows struct {
|
||||
Flows []flow `json:"flows"`
|
||||
}
|
||||
|
||||
type flow struct {
|
||||
Type string `json:"type"`
|
||||
Stages []string `json:"stages"`
|
||||
}
|
||||
|
||||
type loginIdentifier struct {
|
||||
Type string `json:"type"`
|
||||
User string `json:"user"`
|
||||
}
|
||||
|
||||
type passwordRequest struct {
|
||||
Identifier loginIdentifier `json:"identifier"`
|
||||
User string `json:"user"` // deprecated in favour of identifier
|
||||
Password string `json:"password"`
|
||||
// Both DeviceID and InitialDisplayName can be omitted, or empty strings ("")
|
||||
// Thus a pointer is needed to differentiate between the two
|
||||
InitialDisplayName *string `json:"initial_device_display_name"`
|
||||
DeviceID *string `json:"device_id"`
|
||||
}
|
||||
|
||||
type loginResponse struct {
|
||||
UserID string `json:"user_id"`
|
||||
AccessToken string `json:"access_token"`
|
||||
|
@ -62,9 +36,21 @@ type loginResponse struct {
|
|||
DeviceID string `json:"device_id"`
|
||||
}
|
||||
|
||||
func passwordLogin() loginFlows {
|
||||
f := loginFlows{}
|
||||
s := flow{"m.login.password", []string{"m.login.password"}}
|
||||
type flows struct {
|
||||
Flows []flow `json:"flows"`
|
||||
}
|
||||
|
||||
type flow struct {
|
||||
Type string `json:"type"`
|
||||
Stages []string `json:"stages"`
|
||||
}
|
||||
|
||||
func passwordLogin() flows {
|
||||
f := flows{}
|
||||
s := flow{
|
||||
Type: "m.login.password",
|
||||
Stages: []string{"m.login.password"},
|
||||
}
|
||||
f.Flows = append(f.Flows, s)
|
||||
return f
|
||||
}
|
||||
|
@ -74,69 +60,28 @@ func Login(
|
|||
req *http.Request, accountDB accounts.Database, deviceDB devices.Database,
|
||||
cfg *config.Dendrite,
|
||||
) util.JSONResponse {
|
||||
if req.Method == http.MethodGet { // TODO: support other forms of login other than password, depending on config options
|
||||
if req.Method == http.MethodGet {
|
||||
// TODO: support other forms of login other than password, depending on config options
|
||||
return util.JSONResponse{
|
||||
Code: http.StatusOK,
|
||||
JSON: passwordLogin(),
|
||||
}
|
||||
} else if req.Method == http.MethodPost {
|
||||
var r passwordRequest
|
||||
var acc *api.Account
|
||||
var errJSON *util.JSONResponse
|
||||
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
||||
typePassword := auth.LoginTypePassword{
|
||||
GetAccountByPassword: accountDB.GetAccountByPassword,
|
||||
Config: cfg,
|
||||
}
|
||||
r := typePassword.Request()
|
||||
resErr := httputil.UnmarshalJSONRequest(req, r)
|
||||
if resErr != nil {
|
||||
return *resErr
|
||||
}
|
||||
switch r.Identifier.Type {
|
||||
case "m.id.user":
|
||||
if r.Identifier.User == "" {
|
||||
return util.JSONResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
JSON: jsonerror.BadJSON("'user' must be supplied."),
|
||||
}
|
||||
}
|
||||
acc, errJSON = r.processUsernamePasswordLoginRequest(req, accountDB, cfg, r.Identifier.User)
|
||||
if errJSON != nil {
|
||||
return *errJSON
|
||||
}
|
||||
default:
|
||||
// TODO: The below behaviour is deprecated but without it Riot iOS won't log in
|
||||
if r.User != "" {
|
||||
acc, errJSON = r.processUsernamePasswordLoginRequest(req, accountDB, cfg, r.User)
|
||||
if errJSON != nil {
|
||||
return *errJSON
|
||||
}
|
||||
} else {
|
||||
return util.JSONResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
JSON: jsonerror.BadJSON("login identifier '" + r.Identifier.Type + "' not supported"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
token, err := auth.GenerateAccessToken()
|
||||
if err != nil {
|
||||
util.GetLogger(req.Context()).WithError(err).Error("auth.GenerateAccessToken failed")
|
||||
return jsonerror.InternalServerError()
|
||||
}
|
||||
|
||||
dev, err := getDevice(req.Context(), r, deviceDB, acc, token)
|
||||
if err != nil {
|
||||
return util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: jsonerror.Unknown("failed to create device: " + err.Error()),
|
||||
}
|
||||
}
|
||||
|
||||
return util.JSONResponse{
|
||||
Code: http.StatusOK,
|
||||
JSON: loginResponse{
|
||||
UserID: dev.UserID,
|
||||
AccessToken: dev.AccessToken,
|
||||
HomeServer: cfg.Matrix.ServerName,
|
||||
DeviceID: dev.ID,
|
||||
},
|
||||
login, authErr := typePassword.Login(req.Context(), r)
|
||||
if authErr != nil {
|
||||
return *authErr
|
||||
}
|
||||
// make a device/access token
|
||||
return completeAuth(req.Context(), cfg.Matrix.ServerName, deviceDB, login)
|
||||
}
|
||||
return util.JSONResponse{
|
||||
Code: http.StatusMethodNotAllowed,
|
||||
|
@ -144,45 +89,38 @@ func Login(
|
|||
}
|
||||
}
|
||||
|
||||
// getDevice returns a new or existing device
|
||||
func getDevice(
|
||||
ctx context.Context,
|
||||
r passwordRequest,
|
||||
deviceDB devices.Database,
|
||||
acc *api.Account,
|
||||
token string,
|
||||
) (dev *api.Device, err error) {
|
||||
dev, err = deviceDB.CreateDevice(
|
||||
ctx, acc.Localpart, r.DeviceID, token, r.InitialDisplayName,
|
||||
func completeAuth(
|
||||
ctx context.Context, serverName gomatrixserverlib.ServerName, deviceDB devices.Database, login *auth.Login,
|
||||
) util.JSONResponse {
|
||||
token, err := auth.GenerateAccessToken()
|
||||
if err != nil {
|
||||
util.GetLogger(ctx).WithError(err).Error("auth.GenerateAccessToken failed")
|
||||
return jsonerror.InternalServerError()
|
||||
}
|
||||
|
||||
localpart, err := userutil.ParseUsernameParam(login.Username(), &serverName)
|
||||
if err != nil {
|
||||
util.GetLogger(ctx).WithError(err).Error("auth.ParseUsernameParam failed")
|
||||
return jsonerror.InternalServerError()
|
||||
}
|
||||
|
||||
dev, err := deviceDB.CreateDevice(
|
||||
ctx, localpart, login.DeviceID, token, login.InitialDisplayName,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func (r *passwordRequest) processUsernamePasswordLoginRequest(
|
||||
req *http.Request, accountDB accounts.Database,
|
||||
cfg *config.Dendrite, username string,
|
||||
) (acc *api.Account, errJSON *util.JSONResponse) {
|
||||
util.GetLogger(req.Context()).WithField("user", username).Info("Processing login request")
|
||||
|
||||
localpart, err := userutil.ParseUsernameParam(username, &cfg.Matrix.ServerName)
|
||||
if err != nil {
|
||||
errJSON = &util.JSONResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
JSON: jsonerror.InvalidUsername(err.Error()),
|
||||
return util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: jsonerror.Unknown("failed to create device: " + err.Error()),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
acc, err = accountDB.GetAccountByPassword(req.Context(), localpart, r.Password)
|
||||
if err != nil {
|
||||
// Technically we could tell them if the user does not exist by checking if err == sql.ErrNoRows
|
||||
// but that would leak the existence of the user.
|
||||
errJSON = &util.JSONResponse{
|
||||
Code: http.StatusForbidden,
|
||||
JSON: jsonerror.Forbidden("username or password was incorrect, or the account does not exist"),
|
||||
}
|
||||
return
|
||||
return util.JSONResponse{
|
||||
Code: http.StatusOK,
|
||||
JSON: loginResponse{
|
||||
UserID: dev.UserID,
|
||||
AccessToken: dev.AccessToken,
|
||||
HomeServer: serverName,
|
||||
DeviceID: dev.ID,
|
||||
},
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||
"github.com/matrix-org/dendrite/clientapi/api"
|
||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||
"github.com/matrix-org/dendrite/clientapi/producers"
|
||||
currentstateAPI "github.com/matrix-org/dendrite/currentstateserver/api"
|
||||
|
@ -63,6 +64,7 @@ func Setup(
|
|||
stateAPI currentstateAPI.CurrentStateInternalAPI,
|
||||
extRoomsProvider api.ExtraPublicRoomsProvider,
|
||||
) {
|
||||
userInteractiveAuth := auth.NewUserInteractive(accountDB.GetAccountByPassword, cfg)
|
||||
|
||||
publicAPIMux.Handle("/client/versions",
|
||||
httputil.MakeExternalAPI("versions", func(req *http.Request) util.JSONResponse {
|
||||
|
@ -629,7 +631,7 @@ func Setup(
|
|||
if err != nil {
|
||||
return util.ErrorResponse(err)
|
||||
}
|
||||
return DeleteDeviceById(req, deviceDB, device, vars["deviceID"])
|
||||
return DeleteDeviceById(req, userInteractiveAuth, deviceDB, device, vars["deviceID"])
|
||||
}),
|
||||
).Methods(http.MethodDelete, http.MethodOptions)
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue