aboutsummaryrefslogtreecommitdiff
path: root/userapi
diff options
context:
space:
mode:
authorKegsay <kegan@matrix.org>2020-06-16 14:10:55 +0100
committerGitHub <noreply@github.com>2020-06-16 14:10:55 +0100
commit9c77022513f400db59409f5b55fc6223d38d6bb8 (patch)
tree52223755553ef4d7065747528e40c27a79a71dff /userapi
parent57b7fa3db801c27190bfd143cfebe98e3d76a6ae (diff)
Make userapi responsible for checking access tokens (#1133)
* Make userapi responsible for checking access tokens There's still plenty of dependencies on account/device DBs, but this is a start. This is a breaking change as it adds a required config value `listen.user_api`. * Cleanup * Review comments and test fix
Diffstat (limited to 'userapi')
-rw-r--r--userapi/api/api.go41
-rw-r--r--userapi/internal/api.go68
-rw-r--r--userapi/inthttp/client.go15
-rw-r--r--userapi/inthttp/server.go13
-rw-r--r--userapi/userapi.go12
-rw-r--r--userapi/userapi_test.go2
6 files changed, 144 insertions, 7 deletions
diff --git a/userapi/api/api.go b/userapi/api/api.go
index 8534fb17..57b5165a 100644
--- a/userapi/api/api.go
+++ b/userapi/api/api.go
@@ -19,6 +19,21 @@ import "context"
// UserInternalAPI is the internal API for information about users and devices.
type UserInternalAPI interface {
QueryProfile(ctx context.Context, req *QueryProfileRequest, res *QueryProfileResponse) error
+ QueryAccessToken(ctx context.Context, req *QueryAccessTokenRequest, res *QueryAccessTokenResponse) error
+}
+
+// QueryAccessTokenRequest is the request for QueryAccessToken
+type QueryAccessTokenRequest struct {
+ AccessToken string
+ // optional user ID, valid only if the token is an appservice.
+ // https://matrix.org/docs/spec/application_service/r0.1.2#using-sync-and-events
+ AppServiceUserID string
+}
+
+// QueryAccessTokenResponse is the response for QueryAccessToken
+type QueryAccessTokenResponse struct {
+ Device *Device
+ Err error // e.g ErrorForbidden
}
// QueryProfileRequest is the request for QueryProfile
@@ -29,10 +44,34 @@ type QueryProfileRequest struct {
// QueryProfileResponse is the response for QueryProfile
type QueryProfileResponse struct {
- // True if the user has been created. Querying for a profile does not create them.
+ // True if the user exists. Querying for a profile does not create them.
UserExists bool
// The current display name if set.
DisplayName string
// The current avatar URL if set.
AvatarURL string
}
+
+// Device represents a client's device (mobile, web, etc)
+type Device struct {
+ ID string
+ UserID string
+ // The access_token granted to this device.
+ // This uniquely identifies the device from all other devices and clients.
+ AccessToken string
+ // The unique ID of the session identified by the access token.
+ // Can be used as a secure substitution in places where data needs to be
+ // associated with access tokens.
+ SessionID int64
+ // TODO: display name, last used timestamp, keys, etc
+ DisplayName string
+}
+
+// ErrorForbidden is an error indicating that the supplied access token is forbidden
+type ErrorForbidden struct {
+ Message string
+}
+
+func (e *ErrorForbidden) Error() string {
+ return "Forbidden: " + e.Message
+}
diff --git a/userapi/internal/api.go b/userapi/internal/api.go
index 0144526c..1f0d5c94 100644
--- a/userapi/internal/api.go
+++ b/userapi/internal/api.go
@@ -19,8 +19,11 @@ import (
"database/sql"
"fmt"
+ "github.com/matrix-org/dendrite/appservice/types"
"github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
"github.com/matrix-org/dendrite/clientapi/auth/storage/devices"
+ "github.com/matrix-org/dendrite/clientapi/userutil"
+ "github.com/matrix-org/dendrite/internal/config"
"github.com/matrix-org/dendrite/userapi/api"
"github.com/matrix-org/gomatrixserverlib"
)
@@ -29,6 +32,8 @@ type UserInternalAPI struct {
AccountDB accounts.Database
DeviceDB devices.Database
ServerName gomatrixserverlib.ServerName
+ // AppServices is the list of all registered AS
+ AppServices []config.ApplicationService
}
func (a *UserInternalAPI) QueryProfile(ctx context.Context, req *api.QueryProfileRequest, res *api.QueryProfileResponse) error {
@@ -51,3 +56,66 @@ func (a *UserInternalAPI) QueryProfile(ctx context.Context, req *api.QueryProfil
res.DisplayName = prof.DisplayName
return nil
}
+
+func (a *UserInternalAPI) QueryAccessToken(ctx context.Context, req *api.QueryAccessTokenRequest, res *api.QueryAccessTokenResponse) error {
+ if req.AppServiceUserID != "" {
+ appServiceDevice, err := a.queryAppServiceToken(ctx, req.AccessToken, req.AppServiceUserID)
+ res.Device = appServiceDevice
+ res.Err = err
+ return nil
+ }
+ device, err := a.DeviceDB.GetDeviceByAccessToken(ctx, req.AccessToken)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return nil
+ }
+ return err
+ }
+ res.Device = device
+ return nil
+}
+
+// Return the appservice 'device' or nil if the token is not an appservice. Returns an error if there was a problem
+// creating a 'device'.
+func (a *UserInternalAPI) queryAppServiceToken(ctx context.Context, token, appServiceUserID string) (*api.Device, error) {
+ // Search for app service with given access_token
+ var appService *config.ApplicationService
+ for _, as := range a.AppServices {
+ if as.ASToken == token {
+ appService = &as
+ break
+ }
+ }
+ if appService == nil {
+ return nil, nil
+ }
+
+ // Create a dummy device for AS user
+ dev := api.Device{
+ // Use AS dummy device ID
+ ID: types.AppServiceDeviceID,
+ // AS dummy device has AS's token.
+ AccessToken: token,
+ }
+
+ localpart, err := userutil.ParseUsernameParam(appServiceUserID, &a.ServerName)
+ if err != nil {
+ return nil, err
+ }
+
+ if localpart != "" { // AS is masquerading as another user
+ // Verify that the user is registered
+ account, err := a.AccountDB.GetAccountByLocalpart(ctx, localpart)
+ // Verify that account exists & appServiceID matches
+ if err == nil && account.AppServiceID == appService.ID {
+ // Set the userID of dummy device
+ dev.UserID = appServiceUserID
+ return &dev, nil
+ }
+ return nil, &api.ErrorForbidden{Message: "appservice has not registered this user"}
+ }
+
+ // AS is not masquerading as any user, so use AS's sender_localpart
+ dev.UserID = appService.SenderLocalpart
+ return &dev, nil
+}
diff --git a/userapi/inthttp/client.go b/userapi/inthttp/client.go
index 90cc54a4..022243fa 100644
--- a/userapi/inthttp/client.go
+++ b/userapi/inthttp/client.go
@@ -26,7 +26,8 @@ import (
// HTTP paths for the internal HTTP APIs
const (
- QueryProfilePath = "/userapi/queryProfile"
+ QueryProfilePath = "/userapi/queryProfile"
+ QueryAccessTokenPath = "/userapi/queryAccessToken"
)
// NewUserAPIClient creates a UserInternalAPI implemented by talking to a HTTP POST API.
@@ -60,3 +61,15 @@ func (h *httpUserInternalAPI) QueryProfile(
apiURL := h.apiURL + QueryProfilePath
return httputil.PostJSON(ctx, span, h.httpClient, apiURL, request, response)
}
+
+func (h *httpUserInternalAPI) QueryAccessToken(
+ ctx context.Context,
+ request *api.QueryAccessTokenRequest,
+ response *api.QueryAccessTokenResponse,
+) error {
+ span, ctx := opentracing.StartSpanFromContext(ctx, "QueryAccessToken")
+ defer span.Finish()
+
+ apiURL := h.apiURL + QueryAccessTokenPath
+ return httputil.PostJSON(ctx, span, h.httpClient, apiURL, request, response)
+}
diff --git a/userapi/inthttp/server.go b/userapi/inthttp/server.go
index f3c17ccd..495b161c 100644
--- a/userapi/inthttp/server.go
+++ b/userapi/inthttp/server.go
@@ -38,4 +38,17 @@ func AddRoutes(internalAPIMux *mux.Router, s api.UserInternalAPI) {
return util.JSONResponse{Code: http.StatusOK, JSON: &response}
}),
)
+ internalAPIMux.Handle(QueryAccessTokenPath,
+ httputil.MakeInternalAPI("queryAccessToken", func(req *http.Request) util.JSONResponse {
+ request := api.QueryAccessTokenRequest{}
+ response := api.QueryAccessTokenResponse{}
+ if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
+ return util.MessageResponse(http.StatusBadRequest, err.Error())
+ }
+ if err := s.QueryAccessToken(req.Context(), &request, &response); err != nil {
+ return util.ErrorResponse(err)
+ }
+ return util.JSONResponse{Code: http.StatusOK, JSON: &response}
+ }),
+ )
}
diff --git a/userapi/userapi.go b/userapi/userapi.go
index 32f851cc..961d04fe 100644
--- a/userapi/userapi.go
+++ b/userapi/userapi.go
@@ -18,6 +18,7 @@ import (
"github.com/gorilla/mux"
"github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
"github.com/matrix-org/dendrite/clientapi/auth/storage/devices"
+ "github.com/matrix-org/dendrite/internal/config"
"github.com/matrix-org/dendrite/userapi/api"
"github.com/matrix-org/dendrite/userapi/internal"
"github.com/matrix-org/dendrite/userapi/inthttp"
@@ -32,10 +33,13 @@ func AddInternalRoutes(router *mux.Router, intAPI api.UserInternalAPI) {
// NewInternalAPI returns a concerete implementation of the internal API. Callers
// can call functions directly on the returned API or via an HTTP interface using AddInternalRoutes.
-func NewInternalAPI(accountDB accounts.Database, deviceDB devices.Database, serverName gomatrixserverlib.ServerName) api.UserInternalAPI {
+func NewInternalAPI(accountDB accounts.Database, deviceDB devices.Database,
+ serverName gomatrixserverlib.ServerName, appServices []config.ApplicationService) api.UserInternalAPI {
+
return &internal.UserInternalAPI{
- AccountDB: accountDB,
- DeviceDB: deviceDB,
- ServerName: serverName,
+ AccountDB: accountDB,
+ DeviceDB: deviceDB,
+ ServerName: serverName,
+ AppServices: appServices,
}
}
diff --git a/userapi/userapi_test.go b/userapi/userapi_test.go
index a4616363..509bdd7e 100644
--- a/userapi/userapi_test.go
+++ b/userapi/userapi_test.go
@@ -32,7 +32,7 @@ func MustMakeInternalAPI(t *testing.T) (api.UserInternalAPI, accounts.Database,
t.Fatalf("failed to create device DB: %s", err)
}
- return userapi.NewInternalAPI(accountDB, deviceDB, serverName), accountDB, deviceDB
+ return userapi.NewInternalAPI(accountDB, deviceDB, serverName, nil), accountDB, deviceDB
}
func TestQueryProfile(t *testing.T) {