From 50aacd4f3c0225e78bd3be773b29bbbdcc48072e Mon Sep 17 00:00:00 2001 From: Kegsay Date: Tue, 30 May 2017 17:51:40 +0100 Subject: [PATCH] Hook up registration/login APIs and implement access token generation (#122) --- .../dendrite/clientapi/auth/auth.go | 24 ++++++++++ .../auth/storage/accounts/storage.go | 3 +- .../auth/storage/devices/devices_table.go | 2 +- .../dendrite/clientapi/readers/login.go | 43 +++++++++++++++-- .../dendrite/clientapi/routing/routing.go | 4 +- .../dendrite/clientapi/writers/register.go | 47 +++++++++++++++---- 6 files changed, 106 insertions(+), 17 deletions(-) diff --git a/src/github.com/matrix-org/dendrite/clientapi/auth/auth.go b/src/github.com/matrix-org/dendrite/clientapi/auth/auth.go index 80eed2b3..9f350b4b 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/auth/auth.go +++ b/src/github.com/matrix-org/dendrite/clientapi/auth/auth.go @@ -16,7 +16,9 @@ package auth import ( + "crypto/rand" "database/sql" + "encoding/base64" "fmt" "net/http" "strings" @@ -27,6 +29,17 @@ import ( "github.com/matrix-org/util" ) +// UnknownDeviceID is the default device id if one is not specified. +// This deviates from Synapse which generates a new device ID if one is not specified. +// It's preferable to not amass a huge list of valid access tokens for an account, +// so limiting it to 1 unknown device for now limits the number of valid tokens. +// Clients should be giving us device IDs. +var UnknownDeviceID = "unknown-device" + +// OWASP recommends at least 128 bits of entropy for tokens: https://www.owasp.org/index.php/Insufficient_Session-ID_Length +// 32 bytes => 256 bits +var tokenByteLength = 32 + // VerifyAccessToken verifies that an access token was supplied in the given HTTP request // and returns the device it corresponds to. Returns resErr (an error response which can be // sent to the client) if the token is invalid or there was a problem querying the database. @@ -56,6 +69,17 @@ func VerifyAccessToken(req *http.Request, deviceDB *devices.Database) (device *a return } +// GenerateAccessToken creates a new access token. Returns an error if failed to generate +// random bytes. +func GenerateAccessToken() (string, error) { + b := make([]byte, tokenByteLength) + if _, err := rand.Read(b); err != nil { + return "", err + } + // url-safe no padding + return base64.RawURLEncoding.EncodeToString(b), nil +} + // extractAccessToken from a request, or return an error detailing what went wrong. The // error message MUST be human-readable and comprehensible to the client. func extractAccessToken(req *http.Request) (string, error) { diff --git a/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts/storage.go b/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts/storage.go index 5dbd4943..1f1499bb 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts/storage.go +++ b/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts/storage.go @@ -16,6 +16,7 @@ package accounts import ( "database/sql" + "github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/gomatrixserverlib" "golang.org/x/crypto/bcrypt" @@ -44,7 +45,7 @@ func NewDatabase(dataSourceName string, serverName gomatrixserverlib.ServerName) } // GetAccountByPassword returns the account associated with the given localpart and password. -// Returns sql.ErrNoRows if no account exists which matches the given credentials. +// Returns sql.ErrNoRows if no account exists which matches the given localpart. func (d *Database) GetAccountByPassword(localpart, plaintextPassword string) (*authtypes.Account, error) { hash, err := d.accounts.selectPasswordHash(localpart) if err != nil { diff --git a/src/github.com/matrix-org/dendrite/clientapi/auth/storage/devices/devices_table.go b/src/github.com/matrix-org/dendrite/clientapi/auth/storage/devices/devices_table.go index 690ea464..9d6fade7 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/auth/storage/devices/devices_table.go +++ b/src/github.com/matrix-org/dendrite/clientapi/auth/storage/devices/devices_table.go @@ -86,7 +86,7 @@ func (s *devicesStatements) prepare(db *sql.DB, server gomatrixserverlib.ServerN // Returns the device on success. func (s *devicesStatements) insertDevice(txn *sql.Tx, id, localpart, accessToken string) (dev *authtypes.Device, err error) { createdTimeMS := time.Now().UnixNano() / 1000000 - if _, err = s.insertDeviceStmt.Exec(id, localpart, accessToken, createdTimeMS); err == nil { + if _, err = txn.Stmt(s.insertDeviceStmt).Exec(id, localpart, accessToken, createdTimeMS); err == nil { dev = &authtypes.Device{ ID: id, UserID: makeUserID(localpart, s.serverName), diff --git a/src/github.com/matrix-org/dendrite/clientapi/readers/login.go b/src/github.com/matrix-org/dendrite/clientapi/readers/login.go index 75df276a..50ee0daa 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/readers/login.go +++ b/src/github.com/matrix-org/dendrite/clientapi/readers/login.go @@ -16,12 +16,16 @@ package readers import ( "fmt" + "net/http" + + "github.com/matrix-org/dendrite/clientapi/auth" + "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" + "github.com/matrix-org/dendrite/clientapi/auth/storage/devices" "github.com/matrix-org/dendrite/clientapi/config" "github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" - "net/http" ) type loginFlows struct { @@ -52,7 +56,7 @@ func passwordLogin() loginFlows { } // Login implements GET and POST /login -func Login(req *http.Request, cfg config.ClientAPI) util.JSONResponse { +func Login(req *http.Request, accountDB *accounts.Database, deviceDB *devices.Database, cfg config.ClientAPI) util.JSONResponse { if req.Method == "GET" { // TODO: support other forms of login other than password, depending on config options return util.JSONResponse{ Code: 200, @@ -70,12 +74,41 @@ func Login(req *http.Request, cfg config.ClientAPI) util.JSONResponse { JSON: jsonerror.BadJSON("'user' must be supplied."), } } - // TODO: Check username and password properly + + util.GetLogger(req.Context()).WithField("user", r.User).Info("Processing login request") + + acc, err := accountDB.GetAccountByPassword(r.User, 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. + return util.JSONResponse{ + Code: 403, + JSON: jsonerror.BadJSON("username or password was incorrect, or the account does not exist"), + } + } + + token, err := auth.GenerateAccessToken() + if err != nil { + return util.JSONResponse{ + Code: 500, + JSON: jsonerror.Unknown("Failed to generate access token"), + } + } + + // TODO: Use the device ID in the request + dev, err := deviceDB.CreateDevice(acc.Localpart, auth.UnknownDeviceID, token) + if err != nil { + return util.JSONResponse{ + Code: 500, + JSON: jsonerror.Unknown("failed to create device: " + err.Error()), + } + } + return util.JSONResponse{ Code: 200, JSON: loginResponse{ - UserID: makeUserID(r.User, cfg.ServerName), - AccessToken: makeUserID(r.User, cfg.ServerName), // FIXME: token is the user ID for now + UserID: dev.UserID, + AccessToken: dev.AccessToken, HomeServer: cfg.ServerName, }, } diff --git a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go index d104f645..66f26a70 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go +++ b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go @@ -82,14 +82,14 @@ func Setup( ) r0mux.Handle("/register", common.MakeAPI("register", func(req *http.Request) util.JSONResponse { - return writers.Register(req, accountDB) + return writers.Register(req, accountDB, deviceDB) })) // Stub endpoints required by Riot r0mux.Handle("/login", common.MakeAPI("login", func(req *http.Request) util.JSONResponse { - return readers.Login(req, cfg) + return readers.Login(req, accountDB, deviceDB, cfg) }), ) diff --git a/src/github.com/matrix-org/dendrite/clientapi/writers/register.go b/src/github.com/matrix-org/dendrite/clientapi/writers/register.go index 5191a719..da03126b 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/writers/register.go +++ b/src/github.com/matrix-org/dendrite/clientapi/writers/register.go @@ -5,8 +5,10 @@ import ( "net/http" log "github.com/Sirupsen/logrus" + "github.com/matrix-org/dendrite/clientapi/auth" "github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" + "github.com/matrix-org/dendrite/clientapi/auth/storage/devices" "github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/gomatrixserverlib" @@ -90,7 +92,7 @@ func (r *registerRequest) Validate() *util.JSONResponse { } // Register processes a /register request. http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register -func Register(req *http.Request, accountDB *accounts.Database) util.JSONResponse { +func Register(req *http.Request, accountDB *accounts.Database, deviceDB *devices.Database) util.JSONResponse { var r registerRequest resErr := httputil.UnmarshalJSONRequest(req, &r) if resErr != nil { @@ -131,7 +133,7 @@ func Register(req *http.Request, accountDB *accounts.Database) util.JSONResponse switch r.Auth.Type { case authtypes.LoginTypeDummy: // there is nothing to do - return completeRegistration(accountDB, r.Username, r.Password) + return completeRegistration(accountDB, deviceDB, r.Username, r.Password) default: return util.JSONResponse{ Code: 501, @@ -140,7 +142,20 @@ func Register(req *http.Request, accountDB *accounts.Database) util.JSONResponse } } -func completeRegistration(accountDB *accounts.Database, username, password string) util.JSONResponse { +func completeRegistration(accountDB *accounts.Database, deviceDB *devices.Database, username, password string) util.JSONResponse { + if username == "" { + return util.JSONResponse{ + Code: 400, + JSON: jsonerror.BadJSON("missing username"), + } + } + if password == "" { + return util.JSONResponse{ + Code: 400, + JSON: jsonerror.BadJSON("missing password"), + } + } + acc, err := accountDB.CreateAccount(username, password) if err != nil { return util.JSONResponse{ @@ -148,15 +163,31 @@ func completeRegistration(accountDB *accounts.Database, username, password strin JSON: jsonerror.Unknown("failed to create account: " + err.Error()), } } - // TODO: Make and store a proper access_token - // TODO: Store the client's device information? + + token, err := auth.GenerateAccessToken() + if err != nil { + return util.JSONResponse{ + Code: 500, + JSON: jsonerror.Unknown("Failed to generate access token"), + } + } + + // // TODO: Use the device ID in the request. + dev, err := deviceDB.CreateDevice(username, auth.UnknownDeviceID, token) + if err != nil { + return util.JSONResponse{ + Code: 500, + JSON: jsonerror.Unknown("failed to create device: " + err.Error()), + } + } + return util.JSONResponse{ Code: 200, JSON: registerResponse{ - UserID: acc.UserID, - AccessToken: acc.UserID, // FIXME + UserID: dev.UserID, + AccessToken: dev.AccessToken, HomeServer: acc.ServerName, - DeviceID: "kindauniquedeviceid", + DeviceID: dev.ID, }, } }