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:
Kegsay 2020-07-10 00:39:44 +01:00 committed by GitHub
parent 9cc52f47f3
commit abf26c12f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 594 additions and 127 deletions

View file

@ -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
}