Hook up registration/login APIs and implement access token generation (#122)
parent
65b66a6452
commit
50aacd4f3c
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue