Hook up registration/login APIs and implement access token generation
MODIFY
src/github.com/matrix-org/dendrite/clientapi/auth/auth.go
src/github.com/matrix-org/dendrite/clientapi/auth/auth.go
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) {

MODIFY
src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts/storage.go
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 {

MODIFY
src/github.com/matrix-org/dendrite/clientapi/auth/storage/devices/devices_table.go
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),

MODIFY
src/github.com/matrix-org/dendrite/clientapi/readers/login.go
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,
},
}

MODIFY
src/github.com/matrix-org/dendrite/clientapi/routing/routing.go
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)
}),
)

MODIFY
src/github.com/matrix-org/dendrite/clientapi/writers/register.go
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,
},
}
}

Hook up registration/login APIs and implement access token generation (#122)