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

This commit is contained in:
Kegsay 2017-05-30 17:51:40 +01:00 committed by GitHub
parent 65b66a6452
commit 50aacd4f3c
6 changed files with 106 additions and 17 deletions

View file

@ -16,7 +16,9 @@
package auth package auth
import ( import (
"crypto/rand"
"database/sql" "database/sql"
"encoding/base64"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
@ -27,6 +29,17 @@ import (
"github.com/matrix-org/util" "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 // 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 // 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. // 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 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 // extractAccessToken from a request, or return an error detailing what went wrong. The
// error message MUST be human-readable and comprehensible to the client. // error message MUST be human-readable and comprehensible to the client.
func extractAccessToken(req *http.Request) (string, error) { func extractAccessToken(req *http.Request) (string, error) {

View file

@ -16,6 +16,7 @@ package accounts
import ( import (
"database/sql" "database/sql"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"golang.org/x/crypto/bcrypt" "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. // 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) { func (d *Database) GetAccountByPassword(localpart, plaintextPassword string) (*authtypes.Account, error) {
hash, err := d.accounts.selectPasswordHash(localpart) hash, err := d.accounts.selectPasswordHash(localpart)
if err != nil { if err != nil {

View file

@ -86,7 +86,7 @@ func (s *devicesStatements) prepare(db *sql.DB, server gomatrixserverlib.ServerN
// Returns the device on success. // Returns the device on success.
func (s *devicesStatements) insertDevice(txn *sql.Tx, id, localpart, accessToken string) (dev *authtypes.Device, err error) { func (s *devicesStatements) insertDevice(txn *sql.Tx, id, localpart, accessToken string) (dev *authtypes.Device, err error) {
createdTimeMS := time.Now().UnixNano() / 1000000 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{ dev = &authtypes.Device{
ID: id, ID: id,
UserID: makeUserID(localpart, s.serverName), UserID: makeUserID(localpart, s.serverName),

View file

@ -16,12 +16,16 @@ package readers
import ( import (
"fmt" "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/config"
"github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util" "github.com/matrix-org/util"
"net/http"
) )
type loginFlows struct { type loginFlows struct {
@ -52,7 +56,7 @@ func passwordLogin() loginFlows {
} }
// Login implements GET and POST /login // 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 if req.Method == "GET" { // TODO: support other forms of login other than password, depending on config options
return util.JSONResponse{ return util.JSONResponse{
Code: 200, Code: 200,
@ -70,12 +74,41 @@ func Login(req *http.Request, cfg config.ClientAPI) util.JSONResponse {
JSON: jsonerror.BadJSON("'user' must be supplied."), 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{ return util.JSONResponse{
Code: 200, Code: 200,
JSON: loginResponse{ JSON: loginResponse{
UserID: makeUserID(r.User, cfg.ServerName), UserID: dev.UserID,
AccessToken: makeUserID(r.User, cfg.ServerName), // FIXME: token is the user ID for now AccessToken: dev.AccessToken,
HomeServer: cfg.ServerName, HomeServer: cfg.ServerName,
}, },
} }

View file

@ -82,14 +82,14 @@ func Setup(
) )
r0mux.Handle("/register", common.MakeAPI("register", func(req *http.Request) util.JSONResponse { 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 // Stub endpoints required by Riot
r0mux.Handle("/login", r0mux.Handle("/login",
common.MakeAPI("login", func(req *http.Request) util.JSONResponse { common.MakeAPI("login", func(req *http.Request) util.JSONResponse {
return readers.Login(req, cfg) return readers.Login(req, accountDB, deviceDB, cfg)
}), }),
) )

View file

@ -5,8 +5,10 @@ import (
"net/http" "net/http"
log "github.com/Sirupsen/logrus" 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/authtypes"
"github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" "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/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/gomatrixserverlib" "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 // 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 var r registerRequest
resErr := httputil.UnmarshalJSONRequest(req, &r) resErr := httputil.UnmarshalJSONRequest(req, &r)
if resErr != nil { if resErr != nil {
@ -131,7 +133,7 @@ func Register(req *http.Request, accountDB *accounts.Database) util.JSONResponse
switch r.Auth.Type { switch r.Auth.Type {
case authtypes.LoginTypeDummy: case authtypes.LoginTypeDummy:
// there is nothing to do // there is nothing to do
return completeRegistration(accountDB, r.Username, r.Password) return completeRegistration(accountDB, deviceDB, r.Username, r.Password)
default: default:
return util.JSONResponse{ return util.JSONResponse{
Code: 501, 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) acc, err := accountDB.CreateAccount(username, password)
if err != nil { if err != nil {
return util.JSONResponse{ return util.JSONResponse{
@ -148,15 +163,31 @@ func completeRegistration(accountDB *accounts.Database, username, password strin
JSON: jsonerror.Unknown("failed to create account: " + err.Error()), 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{ return util.JSONResponse{
Code: 200, Code: 200,
JSON: registerResponse{ JSON: registerResponse{
UserID: acc.UserID, UserID: dev.UserID,
AccessToken: acc.UserID, // FIXME AccessToken: dev.AccessToken,
HomeServer: acc.ServerName, HomeServer: acc.ServerName,
DeviceID: "kindauniquedeviceid", DeviceID: dev.ID,
}, },
} }
} }