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 f44b6eea7..31c097d65 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/auth/auth.go +++ b/src/github.com/matrix-org/dendrite/clientapi/auth/auth.go @@ -25,8 +25,12 @@ import ( "strings" "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/dendrite/common" + "github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/util" ) @@ -43,6 +47,77 @@ type DeviceDatabase interface { GetDeviceByAccessToken(ctx context.Context, token string) (*authtypes.Device, error) } +// VerifyUserFromRequest authenticates the HTTP request, +// on success returns UserID of the requester. +// Finds local user or an application service user. +// On failure returns an JSON error response which can be sent to the client. +func VerifyUserFromRequest( + req *http.Request, accountDB *accounts.Database, deviceDB *devices.Database, + cfg *config.Dendrite, +) (string, *util.JSONResponse) { + // Try to find local user from device database + dev, devErr := VerifyAccessToken(req, deviceDB) + + if devErr == nil { + return dev.UserID, nil + } + + // Try to find Application Service user + token, err := extractAccessToken(req) + + if err != nil { + return "", &util.JSONResponse{ + Code: http.StatusUnauthorized, + JSON: jsonerror.MissingToken(err.Error()), + } + } + + // Search for app service with given access_token + var appService *config.ApplicationService + for _, as := range cfg.Derived.ApplicationServices { + if as.ASToken == token { + appService = &as + break + } + } + + if appService != nil { + userID := req.URL.Query().Get("user_id") + localpart, err := common.GetLocalpartFromUsername(userID) + + if err != nil { + return "", &util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.InvalidUsername(err.Error()), + } + } + + // Verify that the user is registered + account, accountErr := accountDB.GetAccountByLocalpart(req.Context(), localpart) + + if accountErr != nil { + return "", &util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("Application service has not registered this user"), + } + } + + if account.AppServiceID == appService.ID { + return userID, nil + } + + return "", &util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("Application service has not registered this user"), + } + } + + return "", &util.JSONResponse{ + Code: http.StatusUnauthorized, + JSON: jsonerror.UnknownToken("Unrecognized access token"), + } +} + // 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. @@ -81,18 +156,6 @@ func GenerateAccessToken() (string, error) { return base64.RawURLEncoding.EncodeToString(b), nil } -// GenerateDeviceID creates a new device id. Returns an error if failed to generate -// random bytes. -func GenerateDeviceID() (string, error) { - b := make([]byte, deviceIDByteLength) - _, err := rand.Read(b) - if 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 571482739..4164ce45f 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 @@ -358,3 +358,11 @@ func (d *Database) CheckAccountAvailability(ctx context.Context, localpart strin } return false, err } + +// GetAccountByLocalpart returns the account associated with the given localpart. +// This function assumes the request is authenticated or the account data is used only internally. +// Returns sql.ErrNoRows if no account exists which matches the given localpart. +func (d *Database) GetAccountByLocalpart(ctx context.Context, localpart string, +) (*authtypes.Account, error) { + return d.accounts.selectAccountByLocalpart(ctx, localpart) +} diff --git a/src/github.com/matrix-org/dendrite/clientapi/auth/storage/devices/storage.go b/src/github.com/matrix-org/dendrite/clientapi/auth/storage/devices/storage.go index 6ac475a66..017a28bda 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/auth/storage/devices/storage.go +++ b/src/github.com/matrix-org/dendrite/clientapi/auth/storage/devices/storage.go @@ -16,9 +16,10 @@ package devices import ( "context" + "crypto/rand" "database/sql" + "encoding/base64" - "github.com/matrix-org/dendrite/clientapi/auth" "github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/common" "github.com/matrix-org/gomatrixserverlib" @@ -93,7 +94,7 @@ func (d *Database) CreateDevice( // We cap this at going round 5 times to ensure we don't spin forever var newDeviceID string for i := 1; i <= 5; i++ { - newDeviceID, returnErr = auth.GenerateDeviceID() + newDeviceID, returnErr = GenerateDeviceID() if returnErr != nil { return } @@ -111,6 +112,18 @@ func (d *Database) CreateDevice( return } +// generateDeviceID creates a new device id. Returns an error if failed to generate +// random bytes. +func generateDeviceID() (string, error) { + b := make([]byte, deviceIDByteLength) + _, err := rand.Read(b) + if err != nil { + return "", err + } + // url-safe no padding + return base64.RawURLEncoding.EncodeToString(b), nil +} + // UpdateDevice updates the given device with the display name. // Returns SQL error if there are problems and nil on success. func (d *Database) UpdateDevice( diff --git a/src/github.com/matrix-org/dendrite/clientapi/routing/login.go b/src/github.com/matrix-org/dendrite/clientapi/routing/login.go index e0a4e6327..3f35bff7e 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/routing/login.go +++ b/src/github.com/matrix-org/dendrite/clientapi/routing/login.go @@ -16,13 +16,13 @@ package routing import ( "net/http" - "strings" "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/httputil" "github.com/matrix-org/dendrite/clientapi/jsonerror" + "github.com/matrix-org/dendrite/common" "github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" @@ -82,24 +82,11 @@ func Login( util.GetLogger(req.Context()).WithField("user", r.User).Info("Processing login request") - // r.User can either be a user ID or just the localpart... or other things maybe. - localpart := r.User - if strings.HasPrefix(r.User, "@") { - var domain gomatrixserverlib.ServerName - var err error - localpart, domain, err = gomatrixserverlib.SplitID('@', r.User) - if err != nil { - return util.JSONResponse{ - Code: http.StatusBadRequest, - JSON: jsonerror.InvalidUsername("Invalid username"), - } - } - - if domain != cfg.Matrix.ServerName { - return util.JSONResponse{ - Code: http.StatusBadRequest, - JSON: jsonerror.InvalidUsername("User ID not ours"), - } + localpart, err = common.GetLocalpartFromUsername(r.User) + if err != nil { + return &util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.InvalidUsername(err.Error()), } } diff --git a/src/github.com/matrix-org/dendrite/common/username_utils.go b/src/github.com/matrix-org/dendrite/common/username_utils.go new file mode 100644 index 000000000..05ddc673e --- /dev/null +++ b/src/github.com/matrix-org/dendrite/common/username_utils.go @@ -0,0 +1,57 @@ +// Copyright 2018 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package common + +import ( + "errors" + "strings" + + "github.com/matrix-org/gomatrixserverlib" +) + +// GetLocalpartDomainFromUserID extracts localpart & domain of server from userID. +// Returns error in case of invalid username. +func GetLocalpartDomainFromUserID(userID string, +) (string, gomatrixserverlib.ServerName, error) { + localpart, domain, err := gomatrixserverlib.SplitID('@', userID) + + if err != nil { + return localpart, domain, err + } + + return localpart, domain, nil +} + +// GetLocalpartFromUsername extracts localpart from userID +// userID can either be a user ID or just the localpart. +// Returns error in case of invalid username. +func GetLocalpartFromUsername(userID string, +) (string, error) { + localpart := userID + + if strings.HasPrefix(userID, "@") { + lp, domain, err := GetLocalpartDomainFromUserid(userID) + + if err != nil { + return "", errors.New("Invalid username") + } + + if domain != cfg.Matrix.ServerName { + return "", errors.New("User ID not ours") + } + localpart = lp + } + return localpart, nil +}