Can login with 3pid and password using m.login.password

This commit is contained in:
Piotr Kozimor 2021-09-15 15:13:58 +02:00
parent f290e722c3
commit 433bc321ae
13 changed files with 336 additions and 52 deletions

View file

@ -26,6 +26,10 @@ import (
"github.com/matrix-org/util"
)
const (
email = "email"
)
type GetAccountByPassword func(ctx context.Context, localpart, password string) (*api.Account, error)
type PasswordRequest struct {
@ -59,9 +63,9 @@ func (t *LoginTypePassword) Login(ctx context.Context, req interface{}) (*Login,
if username != "" {
localpart, err = userutil.ParseUsernameParam(username, &t.Config.Matrix.ServerName)
} else {
if r.Medium == "email" {
if r.Medium == email {
if r.Address != "" {
localpart, err = t.AccountDB.GetLocalpartForThreePID(ctx, r.Address, "email")
localpart, err = t.AccountDB.GetLocalpartForThreePID(ctx, r.Address, email)
if localpart == "" {
return nil, &util.JSONResponse{
Code: http.StatusForbidden,

View file

@ -598,7 +598,7 @@ func Setup(
r0mux.Handle("/account/3pid",
httputil.MakeAuthAPI("account_3pid", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
return CheckAndSave3PIDAssociation(req, accountDB, device, cfg)
return CheckAndSave3PIDAssociation(req, accountDB, device, cfg, userAPI)
}),
).Methods(http.MethodPost, http.MethodOptions)
@ -610,10 +610,22 @@ func Setup(
r0mux.Handle("/{path:(?:account/3pid|register)}/email/requestToken",
httputil.MakeExternalAPI("account_3pid_request_token", func(req *http.Request) util.JSONResponse {
return RequestEmailToken(req, accountDB, cfg)
return RequestEmailToken(req, accountDB, userAPI, cfg)
}),
).Methods(http.MethodPost, http.MethodOptions)
r0mux.Handle("/account/password/email/requestToken",
httputil.MakeAuthAPI("account_password_request_token", userAPI, func(req *http.Request, dev *userapi.Device) util.JSONResponse {
return RequestAccountPasswordEmailToken(req, accountDB, userAPI, dev)
}),
).Methods(http.MethodPost, http.MethodOptions)
r0mux.Handle("/{path:(?:account/password|account/3pid|register)}/email/submitToken",
httputil.MakeExternalAPI("email_submit_token", func(req *http.Request) util.JSONResponse {
return SubmitToken(req, userAPI)
}),
).Methods(http.MethodGet, http.MethodOptions)
// Element logs get flooded unless this is handled
r0mux.Handle("/presence/{userID}/status",
httputil.MakeExternalAPI("presence", func(req *http.Request) util.JSONResponse {

View file

@ -15,14 +15,19 @@
package routing
import (
"context"
"net/http"
"net/url"
"strconv"
"github.com/gorilla/mux"
"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/clientapi/threepid"
"github.com/matrix-org/dendrite/setup/config"
"github.com/matrix-org/dendrite/userapi/api"
userapi "github.com/matrix-org/dendrite/userapi/api"
"github.com/matrix-org/dendrite/userapi/storage/accounts"
"github.com/matrix-org/gomatrixserverlib"
@ -30,7 +35,8 @@ import (
)
type reqTokenResponse struct {
SID string `json:"sid"`
SID string `json:"sid"`
SumbitURL string `json:"submit_url,omitempty"`
}
type threePIDsResponse struct {
@ -40,7 +46,7 @@ type threePIDsResponse struct {
// RequestEmailToken implements:
// POST /account/3pid/email/requestToken
// POST /register/email/requestToken
func RequestEmailToken(req *http.Request, accountDB accounts.Database, cfg *config.ClientAPI) util.JSONResponse {
func RequestEmailToken(req *http.Request, accountDB accounts.Database, userAPI userapi.UserInternalAPI, cfg *config.ClientAPI) util.JSONResponse {
var body threepid.EmailAssociationRequest
if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil {
return *reqErr
@ -66,27 +72,185 @@ func RequestEmailToken(req *http.Request, accountDB accounts.Database, cfg *conf
}
}
resp.SID, err = threepid.CreateSession(req.Context(), body, cfg)
if err == threepid.ErrNotTrusted {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.NotTrusted(body.IDServer),
if cfg.Derived.SendEmails {
createSessionResp := userapi.CreateSessionResponse{}
ctx := req.Context()
path := mux.Vars(req)["path"]
var sessionType userapi.ThreepidSessionType
switch path {
case "account/3pid":
sessionType = userapi.AccountThreepid
case "register":
sessionType = userapi.Register
}
} else if err != nil {
util.GetLogger(req.Context()).WithError(err).Error("threepid.CreateSession failed")
err = userAPI.CreateSession(ctx, &userapi.CreateSessionRequest{
ClientSecret: body.Secret,
NextLink: body.NextLink,
ThreePid: body.Email,
SendAttempt: body.SendAttempt,
SessionType: sessionType,
}, &createSessionResp)
if err != nil {
util.GetLogger(req.Context()).WithError(err).Error("userAPI.CreateSession failed")
return jsonerror.InternalServerError()
}
return util.JSONResponse{
Code: http.StatusOK,
JSON: createSessionResp,
}
} else {
resp.SID, err = threepid.CreateSession(req.Context(), body, cfg)
if err == threepid.ErrNotTrusted {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.NotTrusted(body.IDServer),
}
} else if err != nil {
util.GetLogger(req.Context()).WithError(err).Error("threepid.CreateSession failed")
return jsonerror.InternalServerError()
}
return util.JSONResponse{
Code: http.StatusOK,
JSON: resp,
}
}
}
func SubmitToken(req *http.Request, userAPI userapi.UserInternalAPI) util.JSONResponse {
ctx := req.Context()
validateSessionReq, matrixErr := parseSumbitTokenQuery(req.URL)
if matrixErr != nil {
util.GetLogger(req.Context()).WithError(matrixErr).Error("parseSumbitTokenQuery")
return jsonerror.InternalServerError()
}
res := userapi.ValidateSessionResponse{}
err := userAPI.ValidateSession(ctx, validateSessionReq, &res)
if err == userapi.ErrBadSession {
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound(err.Error()),
}
}
if res.NextLink != "" {
return util.JSONResponse{
Code: http.StatusFound,
Headers: map[string]string{
"Location": res.NextLink,
},
}
} else {
return util.JSONResponse{
Code: http.StatusOK,
}
}
}
func parseSumbitTokenQuery(u *url.URL) (*userapi.ValidateSessionRequest, *jsonerror.MatrixError) {
q := u.Query()
sid := q["sid"]
if sid == nil {
return nil, jsonerror.MissingParam("sid param missing")
}
if len(sid) != 1 {
return nil, jsonerror.InvalidParam("sid param malformed")
}
sidParsed, err := strconv.Atoi(sid[0])
if err != nil {
return nil, jsonerror.InvalidParam("sid is not an number")
}
clientSecret := q["client_secret"]
if clientSecret == nil {
return nil, jsonerror.MissingParam("client_secret param missing")
}
if len(clientSecret) != 1 {
return nil, jsonerror.InvalidParam("client_secret param malformed")
}
token := q["token"]
if token == nil {
return nil, jsonerror.MissingParam("token param missing")
}
if len(token) != 1 {
return nil, jsonerror.InvalidParam("token param malformed")
}
return &userapi.ValidateSessionRequest{
SessionOwnership: userapi.SessionOwnership{
Sid: int64(sidParsed),
ClientSecret: clientSecret[0],
},
Token: token[0]}, nil
}
// RequestAccountPasswordEmailToken implements:
// POST /account/password/email/requestToken
func RequestAccountPasswordEmailToken(req *http.Request, accountDB accounts.Database, userAPI userapi.UserInternalAPI, device *userapi.Device) util.JSONResponse {
var body threepid.EmailAssociationRequest
if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil {
return *reqErr
}
var resp reqTokenResponse
var err error
localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
if err != nil {
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
return jsonerror.InternalServerError()
}
ctx := req.Context()
associated, err := isThreePidAssociated(req.Context(), body.Email, localpart, accountDB)
if err != nil {
util.GetLogger(ctx).WithError(err).Error("isThreePidAssociated failed")
return jsonerror.InternalServerError()
}
if !associated {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.InvalidParam("threepid is not bound to this user"),
}
}
res := userapi.CreateSessionResponse{}
err = userAPI.CreateSession(
ctx,
&userapi.CreateSessionRequest{
ClientSecret: body.Secret,
NextLink: body.NextLink,
ThreePid: body.Email,
SendAttempt: body.SendAttempt,
SessionType: api.AccountPassword,
},
&res)
if err != nil {
util.GetLogger(ctx).WithError(err).Error("userapi.CreateSessionRequest failed")
return jsonerror.InternalServerError()
}
resp.SID = strconv.Itoa(int(res.Sid))
return util.JSONResponse{
Code: http.StatusOK,
JSON: resp,
}
}
func isThreePidAssociated(ctx context.Context, threepid, localpart string, db accounts.Database) (bool, error) {
threepids, err := db.GetThreePIDsForLocalpart(ctx, localpart)
if err != nil {
return false, err
}
for i := range threepids {
if threepid == threepids[i].Address && threepids[i].Medium == "email" {
return true, nil
}
}
return false, nil
}
// CheckAndSave3PIDAssociation implements POST /account/3pid
func CheckAndSave3PIDAssociation(
req *http.Request, accountDB accounts.Database, device *api.Device,
cfg *config.ClientAPI,
cfg *config.ClientAPI, userAPI userapi.UserInternalAPI,
) util.JSONResponse {
var body threepid.EmailAssociationCheckRequest
if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil {
@ -94,15 +258,42 @@ func CheckAndSave3PIDAssociation(
}
// Check if the association has been validated
verified, address, medium, err := threepid.CheckAssociation(req.Context(), body.Creds, cfg)
if err == threepid.ErrNotTrusted {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.NotTrusted(body.Creds.IDServer),
var verified bool
var err error
var address, medium string
if cfg.Derived.SendEmails {
var res userapi.IsSessionValidatedResponse
var sid int
sid, err = strconv.Atoi(body.Creds.SID)
if err != nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.InvalidParam("sid must be of type integer"),
}
}
err = userAPI.IsSessionValidated(req.Context(), &userapi.SessionOwnership{
Sid: int64(sid),
ClientSecret: body.Creds.Secret,
}, &res)
if err != nil {
util.GetLogger(req.Context()).WithError(err).Error("userAPI.IsSessionValidated failed")
return jsonerror.InternalServerError()
}
verified = res.Validated
address = res.ThreePid
medium = "email" // TODO handle msisdn as well
} else {
verified, address, medium, err = threepid.CheckAssociation(req.Context(), body.Creds, cfg)
if err == threepid.ErrNotTrusted {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.NotTrusted(body.Creds.IDServer),
}
} else if err != nil {
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAssociation failed")
return jsonerror.InternalServerError()
}
} else if err != nil {
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAssociation failed")
return jsonerror.InternalServerError()
}
if !verified {

View file

@ -112,6 +112,8 @@ type Derived struct {
ExclusiveApplicationServicesAliasRegexp *regexp.Regexp
// Note: An Exclusive Regex for room ID isn't necessary as we aren't blocking
// servers from creating RoomIDs in exclusive application service namespaces
// SendEmails is set to true when Email is enabled in User API.
SendEmails bool
}
type InternalAPIOptions struct {
@ -288,7 +290,7 @@ func (config *Dendrite) Derive() error {
if err := loadAppServices(&config.AppServiceAPI, &config.Derived); err != nil {
return err
}
config.Derived.SendEmails = config.UserAPI.Email.Enabled
return nil
}

View file

@ -514,7 +514,7 @@ func (u *testUserAPI) QueryAccessToken(ctx context.Context, req *userapi.QueryAc
func (u *testUserAPI) CreateSession(context.Context, *userapi.CreateSessionRequest, *userapi.CreateSessionResponse) error {
return nil
}
func (u *testUserAPI) ValidateSession(context.Context, *userapi.ValidateSessionRequest, struct{}) error {
func (u *testUserAPI) ValidateSession(context.Context, *userapi.ValidateSessionRequest, *userapi.ValidateSessionResponse) error {
return nil
}
func (u *testUserAPI) GetThreePidForSession(context.Context, *userapi.SessionOwnership, *userapi.GetThreePidForSessionResponse) error {

View file

@ -357,7 +357,7 @@ func (u *testUserAPI) QueryAccessToken(ctx context.Context, req *userapi.QueryAc
func (u *testUserAPI) CreateSession(context.Context, *userapi.CreateSessionRequest, *userapi.CreateSessionResponse) error {
return nil
}
func (u *testUserAPI) ValidateSession(context.Context, *userapi.ValidateSessionRequest, struct{}) error {
func (u *testUserAPI) ValidateSession(context.Context, *userapi.ValidateSessionRequest, *userapi.ValidateSessionResponse) error {
return nil
}
func (u *testUserAPI) GetThreePidForSession(context.Context, *userapi.SessionOwnership, *userapi.GetThreePidForSessionResponse) error {

View file

@ -556,3 +556,4 @@ Fails to upload self-signing key without master key
can fetch self-signing keys over federation
Changing master key notifies local users
Changing user-signing key notifies local users
Can login with 3pid and password using m.login.password

View file

@ -17,6 +17,7 @@ package api
import (
"context"
"encoding/json"
"errors"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/gomatrixserverlib"
@ -34,6 +35,8 @@ const (
Register
)
var ErrBadSession = errors.New("provided sid, client_secret and token does not point to valid session")
// UserInternalAPI is the internal API for information about users and devices.
type UserInternalAPI interface {
InputAccountData(ctx context.Context, req *InputAccountDataRequest, res *InputAccountDataResponse) error
@ -55,7 +58,7 @@ type UserInternalAPI interface {
QuerySearchProfiles(ctx context.Context, req *QuerySearchProfilesRequest, res *QuerySearchProfilesResponse) error
QueryOpenIDToken(ctx context.Context, req *QueryOpenIDTokenRequest, res *QueryOpenIDTokenResponse) error
CreateSession(context.Context, *CreateSessionRequest, *CreateSessionResponse) error
ValidateSession(context.Context, *ValidateSessionRequest, struct{}) error
ValidateSession(context.Context, *ValidateSessionRequest, *ValidateSessionResponse) error
GetThreePidForSession(context.Context, *SessionOwnership, *GetThreePidForSessionResponse) error
DeleteSession(context.Context, *SessionOwnership, struct{}) error
IsSessionValidated(context.Context, *SessionOwnership, *IsSessionValidatedResponse) error
@ -438,7 +441,7 @@ type CreateSessionRequest struct {
}
type CreateSessionResponse struct {
Sid int64
Sid int64 `json:"sid"`
}
type ValidateSessionRequest struct {
@ -446,6 +449,10 @@ type ValidateSessionRequest struct {
Token string
}
type ValidateSessionResponse struct {
NextLink string
}
type GetThreePidForSessionResponse struct {
ThreePid string
}
@ -466,6 +473,7 @@ type Session struct {
type IsSessionValidatedResponse struct {
Validated bool
ValidatedAt int
ThreePid string
}
type ThreepidSessionType int

View file

@ -121,7 +121,7 @@ func (t *UserInternalAPITrace) CreateSession(ctx context.Context, req *CreateSes
util.GetLogger(ctx).Infof("CreateSession req=%+v res=%+v", js(req), js(res))
return err
}
func (t *UserInternalAPITrace) ValidateSession(ctx context.Context, req *ValidateSessionRequest, res struct{}) error {
func (t *UserInternalAPITrace) ValidateSession(ctx context.Context, req *ValidateSessionRequest, res *ValidateSessionResponse) error {
err := t.Impl.ValidateSession(ctx, req, res)
util.GetLogger(ctx).Infof("ValidateSession req=%+v res=%+v", js(req), js(res))
return err

View file

@ -3,7 +3,6 @@ package internal
import (
"context"
"database/sql"
"errors"
"net/url"
"strconv"
"time"
@ -18,8 +17,6 @@ const (
tokenByteLength = 48
)
var ErrBadSession = errors.New("provided sid, client_secret and token does not point to valid session")
func (a *UserInternalAPI) CreateSession(ctx context.Context, req *api.CreateSessionRequest, res *api.CreateSessionResponse) error {
s, err := a.ThreePidDB.GetSessionByThreePidAndSecret(ctx, req.ThreePid, req.ClientSecret)
if err != nil {
@ -76,15 +73,20 @@ func (a *UserInternalAPI) CreateSession(ctx context.Context, req *api.CreateSess
}, req.SessionType)
}
func (a *UserInternalAPI) ValidateSession(ctx context.Context, req *api.ValidateSessionRequest, res struct{}) error {
func (a *UserInternalAPI) ValidateSession(ctx context.Context, req *api.ValidateSessionRequest, res *api.ValidateSessionResponse) error {
s, err := getSessionByOwnership(ctx, &req.SessionOwnership, a.ThreePidDB)
if err != nil {
return err
}
if s.Token != req.Token {
return ErrBadSession
return api.ErrBadSession
}
return a.ThreePidDB.ValidateSession(ctx, s.Sid, time.Now().Unix())
err = a.ThreePidDB.ValidateSession(ctx, s.Sid, time.Now().Unix())
if err != nil {
return err
}
res.NextLink = s.NextLink
return nil
}
func (a *UserInternalAPI) GetThreePidForSession(ctx context.Context, req *api.SessionOwnership, res *api.GetThreePidForSessionResponse) error {
@ -111,6 +113,7 @@ func (a *UserInternalAPI) IsSessionValidated(ctx context.Context, req *api.Sessi
}
res.Validated = s.Validated
res.ValidatedAt = int(s.ValidatedAt)
res.ThreePid = s.ThreePid
return nil
}
@ -118,12 +121,12 @@ func getSessionByOwnership(ctx context.Context, so *api.SessionOwnership, d thre
s, err := d.GetSession(ctx, so.Sid)
if err != nil {
if err == sql.ErrNoRows {
return nil, ErrBadSession
return nil, api.ErrBadSession
}
return nil, err
}
if s.ClientSecret != so.ClientSecret {
return nil, ErrBadSession
return nil, api.ErrBadSession
}
return s, err
}

View file

@ -242,7 +242,7 @@ func (h *httpUserInternalAPI) CreateSession(ctx context.Context, req *api.Create
return httputil.PostJSON(ctx, span, h.httpClient, apiURL, req, res)
}
func (h *httpUserInternalAPI) ValidateSession(ctx context.Context, req *api.ValidateSessionRequest, res struct{}) error {
func (h *httpUserInternalAPI) ValidateSession(ctx context.Context, req *api.ValidateSessionRequest, res *api.ValidateSessionResponse) error {
span, ctx := opentracing.StartSpanFromContext(ctx, "ValidateSession")
defer span.Finish()

View file

@ -2,8 +2,14 @@ package mail
import (
"bytes"
"crypto/tls"
"errors"
"fmt"
"io"
"io/ioutil"
"strings"
"sync"
"net/smtp"
"text/template"
"time"
@ -24,9 +30,15 @@ type Mailer interface {
// - https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-password-email-requesttoken
Send(*Mail, api.ThreepidSessionType) error
}
// SmtpMailer is safe for concurrent use. It will block if email sending is in progress as long as it uses single connection.
type SmtpMailer struct {
conf config.EmailConf
templates []*template.Template
auth smtp.Auth
cl *smtp.Client
// sendMutex guards ensures that MAIL, RCPT and DATA commands are not messed between mails.
sendMutex sync.Mutex
}
type Mail struct {
@ -61,23 +73,34 @@ func (m *SmtpMailer) send(mail *Mail, t *template.Template) error {
if err != nil {
return err
}
return smtp.SendMail(
m.conf.Smtp.Host,
smtp.PlainAuth(
"",
m.conf.Smtp.User,
m.conf.Smtp.Password,
m.conf.Smtp.Host,
),
m.conf.From,
[]string{
mail.To,
},
b.Bytes(),
)
if err = validateLine(mail.To); err != nil {
return err
}
// lock at the point when data are prepared and we are executing commands
m.sendMutex.Lock()
defer m.sendMutex.Unlock()
if err = m.cl.Mail(m.conf.From); err != nil {
return err
}
if err = m.cl.Rcpt(mail.To); err != nil {
return err
}
var w io.WriteCloser
w, err = m.cl.Data()
if err != nil {
return err
}
_, err = w.Write(b.Bytes())
if err != nil {
return err
}
return w.Close()
}
func NewMailer(c *config.UserAPI) (Mailer, error) {
if err := validateLine(c.Email.From); err != nil {
return nil, err
}
sessionTypes := api.ThreepidSessionTypes()
templates := make([]*template.Template, len(sessionTypes))
for _, t := range sessionTypes {
@ -92,9 +115,47 @@ func NewMailer(c *config.UserAPI) (Mailer, error) {
}
templates[t] = template
}
cl, err := smtp.Dial(c.Email.Smtp.Host)
if err != nil {
return nil, err
}
// defer c.Close() # TODO exit gracefully
if err = cl.Hello("localhost"); err != nil {
return nil, err
}
if ok, _ := cl.Extension("STARTTLS"); ok {
config := &tls.Config{ServerName: c.Email.Smtp.Host}
if err = cl.StartTLS(config); err != nil {
return nil, err
}
}
var auth smtp.Auth
if c.Email.Smtp.User != "" {
auth = smtp.PlainAuth(
"",
c.Email.Smtp.User,
c.Email.Smtp.Password,
c.Email.Smtp.Host,
)
if err = cl.Auth(auth); err != nil {
return nil, err
}
}
return &SmtpMailer{
conf: c.Email,
templates: templates,
auth: auth,
cl: cl,
sendMutex: sync.Mutex{},
}, nil
}
// validateLine checks to see if a line has CR or LF as per RFC 5321
func validateLine(line string) error {
if strings.ContainsAny(line, "\n\r") {
return errors.New("smtp: A line must not contain CR or LF")
}
return nil
}

View file

@ -252,6 +252,7 @@ func mustCreateSession(is *is.I, i *internal.UserInternalAPI) (resp *api.CreateS
}
func mustValidateSesson(is *is.I, i *internal.UserInternalAPI, secret, token string, sid int64) {
res := api.ValidateSessionResponse{}
err := i.ValidateSession(ctx, &api.ValidateSessionRequest{
SessionOwnership: api.SessionOwnership{
Sid: sid,
@ -259,7 +260,8 @@ func mustValidateSesson(is *is.I, i *internal.UserInternalAPI, secret, token str
},
Token: token,
},
struct{}{},
&res,
)
is.NoErr(err)
is.Equal(res.NextLink, testReq.NextLink)
}