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..90fb72775 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/auth/auth.go +++ b/src/github.com/matrix-org/dendrite/clientapi/auth/auth.go @@ -27,6 +27,8 @@ import ( "github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/jsonerror" + "github.com/matrix-org/dendrite/clientapi/userutil" + "github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/util" ) @@ -34,15 +36,83 @@ import ( // 32 bytes => 256 bits var tokenByteLength = 32 -// The length of generated device IDs -var deviceIDByteLength = 6 - // DeviceDatabase represents a device database. type DeviceDatabase interface { // Look up the device matching the given access token. GetDeviceByAccessToken(ctx context.Context, token string) (*authtypes.Device, error) } +// AccountDatabase represents a account database. +type AccountDatabase interface { + // Look up the account matching the given localpart. + GetAccountByLocalpart(ctx context.Context, localpart string) (*authtypes.Account, 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 AccountDatabase, deviceDB DeviceDatabase, + applicationServices []config.ApplicationService, +) (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 the 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 applicationServices { + if as.ASToken == token { + appService = &as + break + } + } + + if appService != nil { + userID := req.URL.Query().Get("user_id") + localpart, err := userutil.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) + + // Verify that account exists & appServiceID matches + if accountErr == nil && 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 +151,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..7683a427e 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,14 +16,18 @@ 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" ) +// The length of generated device IDs +var deviceIDByteLength = 6 + // Database represents a device database. type Database struct { db *sql.DB @@ -93,7 +97,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 +115,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..e53d90f07 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/routing/login.go +++ b/src/github.com/matrix-org/dendrite/clientapi/routing/login.go @@ -15,14 +15,15 @@ package routing import ( + "errors" "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/clientapi/userutil" "github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" @@ -82,24 +83,17 @@ 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"), - } - } + localpart, err := userutil.GetLocalpartFromUsername(r.User) + if err != nil { + // Check that domain matches this server. error should reflect a mismatch. + domain, domainErr := userutil.GetDomainFromUserID(r.User) - if domain != cfg.Matrix.ServerName { - return util.JSONResponse{ - Code: http.StatusBadRequest, - JSON: jsonerror.InvalidUsername("User ID not ours"), - } + if domainErr == nil && domain != cfg.Matrix.ServerName { + err = errors.New("User ID not ours") + } + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.InvalidUsername(err.Error()), } } 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 a746fa095..7ae72be1e 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go +++ b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go @@ -390,4 +390,10 @@ func Setup( }} }), ).Methods(http.MethodGet, http.MethodOptions) + + r0mux.Handle("/account/whoami", + common.MakeExternalAPI("whoami", func(req *http.Request) util.JSONResponse { + return Whoami(req, accountDB, deviceDB, &cfg) + }), + ).Methods(http.MethodGet, http.MethodOptions) } diff --git a/src/github.com/matrix-org/dendrite/clientapi/routing/whoami.go b/src/github.com/matrix-org/dendrite/clientapi/routing/whoami.go new file mode 100644 index 000000000..85695f53c --- /dev/null +++ b/src/github.com/matrix-org/dendrite/clientapi/routing/whoami.go @@ -0,0 +1,60 @@ +// 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 routing + +import ( + "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/jsonerror" + "github.com/matrix-org/dendrite/common/config" + "github.com/matrix-org/util" +) + +// whoamiResponse represents an response for a `whoami` request +type whoamiResponse struct { + UserID string `json:"user_id"` +} + +// Whoami implements `/account/whoami` which enables client to query owner of the HTTP request. +// https://matrix.org/docs/spec/client_server/r0.3.0.html#get-matrix-client-r0-account-whoami +func Whoami( + req *http.Request, accountDB *accounts.Database, deviceDB *devices.Database, + cfg *config.Dendrite, +) util.JSONResponse { + + if req.Method == http.MethodGet { + user, err := auth.VerifyUserFromRequest(req, accountDB, deviceDB, cfg.Derived.ApplicationServices) + + if err != nil { + return *err + } + util.GetLogger(req.Context()).WithField("user", user).Info("Processing whoami request") + + return util.JSONResponse{ + Code: http.StatusOK, + JSON: whoamiResponse{ + UserID: user, + }, + } + } + + return util.JSONResponse{ + Code: http.StatusMethodNotAllowed, + JSON: jsonerror.NotFound("Bad method"), + } +} diff --git a/src/github.com/matrix-org/dendrite/clientapi/userutil/userutil.go b/src/github.com/matrix-org/dendrite/clientapi/userutil/userutil.go new file mode 100644 index 000000000..d15b05d4a --- /dev/null +++ b/src/github.com/matrix-org/dendrite/clientapi/userutil/userutil.go @@ -0,0 +1,49 @@ +// 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 userutil + +import ( + "errors" + "strings" + + "github.com/matrix-org/gomatrixserverlib" +) + +// GetDomainFromUserID extracts the domain of server from userID. +// Returns error in case of invalid userID. +func GetDomainFromUserID(userID string, +) (gomatrixserverlib.ServerName, error) { + _, domain, err := gomatrixserverlib.SplitID('@', userID) + + return domain, err +} + +// GetLocalpartFromUsername extracts localpart from username. +// username 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, _, err := gomatrixserverlib.SplitID('@', userID) + + if err != nil { + return "", errors.New("Invalid username") + } + + localpart = lp + } + return localpart, nil +}