Appservice Login (2nd attempt) (#3078)
Rebase of #2936 as @vijfhoek wrote he got no time to work on this, and I kind of needed it for my experiments. I checked the tests, and it is working with my example code (i.e. impersonating, registering, creating channel, invite people, write messages). I'm not a huge `go` pro, and still learning, but I tried to fix and/or integrate the changes as best as possible with the current `main` branch changes. If there is anything left, let me know and I'll try to figure it out. Signed-off-by: `Kuhn Christopher <kuhnchris+git@kuhnchris.eu>` --------- Signed-off-by: Sijmen <me@sijman.nl> Signed-off-by: Sijmen Schoon <me@sijman.nl> Co-authored-by: Sijmen Schoon <me@sijman.nl> Co-authored-by: Sijmen Schoon <me@vijf.life> Co-authored-by: Till <2353100+S7evinK@users.noreply.github.com>
This commit is contained in:
parent
b8f91485b4
commit
4f943771fa
11 changed files with 530 additions and 35 deletions
|
|
@ -15,7 +15,6 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
|
@ -32,8 +31,13 @@ import (
|
|||
// called after authorization has completed, with the result of the authorization.
|
||||
// If the final return value is non-nil, an error occurred and the cleanup function
|
||||
// is nil.
|
||||
func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.UserLoginAPI, userAPI UserInternalAPIForLogin, cfg *config.ClientAPI) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
||||
reqBytes, err := io.ReadAll(r)
|
||||
func LoginFromJSONReader(
|
||||
req *http.Request,
|
||||
useraccountAPI uapi.UserLoginAPI,
|
||||
userAPI UserInternalAPIForLogin,
|
||||
cfg *config.ClientAPI,
|
||||
) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
||||
reqBytes, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
err := &util.JSONResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
|
|
@ -65,6 +69,20 @@ func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.U
|
|||
UserAPI: userAPI,
|
||||
Config: cfg,
|
||||
}
|
||||
case authtypes.LoginTypeApplicationService:
|
||||
token, err := ExtractAccessToken(req)
|
||||
if err != nil {
|
||||
err := &util.JSONResponse{
|
||||
Code: http.StatusForbidden,
|
||||
JSON: spec.MissingToken(err.Error()),
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
typ = &LoginTypeApplicationService{
|
||||
Config: cfg,
|
||||
Token: token,
|
||||
}
|
||||
default:
|
||||
err := util.JSONResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
|
|
@ -73,7 +91,7 @@ func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.U
|
|||
return nil, nil, &err
|
||||
}
|
||||
|
||||
return typ.LoginFromJSON(ctx, reqBytes)
|
||||
return typ.LoginFromJSON(req.Context(), reqBytes)
|
||||
}
|
||||
|
||||
// UserInternalAPIForLogin contains the aspects of UserAPI required for logging in.
|
||||
|
|
|
|||
55
clientapi/auth/login_application_service.go
Normal file
55
clientapi/auth/login_application_service.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||
"github.com/matrix-org/dendrite/internal"
|
||||
"github.com/matrix-org/dendrite/setup/config"
|
||||
"github.com/matrix-org/util"
|
||||
)
|
||||
|
||||
// LoginTypeApplicationService describes how to authenticate as an
|
||||
// application service
|
||||
type LoginTypeApplicationService struct {
|
||||
Config *config.ClientAPI
|
||||
Token string
|
||||
}
|
||||
|
||||
// Name implements Type
|
||||
func (t *LoginTypeApplicationService) Name() string {
|
||||
return authtypes.LoginTypeApplicationService
|
||||
}
|
||||
|
||||
// LoginFromJSON implements Type
|
||||
func (t *LoginTypeApplicationService) LoginFromJSON(
|
||||
ctx context.Context, reqBytes []byte,
|
||||
) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
||||
var r Login
|
||||
if err := httputil.UnmarshalJSON(reqBytes, &r); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
_, err := internal.ValidateApplicationServiceRequest(t.Config, r.Identifier.User, t.Token)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
cleanup := func(ctx context.Context, j *util.JSONResponse) {}
|
||||
return &r, cleanup, nil
|
||||
}
|
||||
|
|
@ -17,7 +17,9 @@ package auth
|
|||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
|
@ -33,8 +35,9 @@ func TestLoginFromJSONReader(t *testing.T) {
|
|||
ctx := context.Background()
|
||||
|
||||
tsts := []struct {
|
||||
Name string
|
||||
Body string
|
||||
Name string
|
||||
Body string
|
||||
Token string
|
||||
|
||||
WantUsername string
|
||||
WantDeviceID string
|
||||
|
|
@ -62,6 +65,30 @@ func TestLoginFromJSONReader(t *testing.T) {
|
|||
WantDeviceID: "adevice",
|
||||
WantDeletedTokens: []string{"atoken"},
|
||||
},
|
||||
{
|
||||
Name: "appServiceWorksUserID",
|
||||
Body: `{
|
||||
"type": "m.login.application_service",
|
||||
"identifier": { "type": "m.id.user", "user": "@alice:example.com" },
|
||||
"device_id": "adevice"
|
||||
}`,
|
||||
Token: "astoken",
|
||||
|
||||
WantUsername: "@alice:example.com",
|
||||
WantDeviceID: "adevice",
|
||||
},
|
||||
{
|
||||
Name: "appServiceWorksLocalpart",
|
||||
Body: `{
|
||||
"type": "m.login.application_service",
|
||||
"identifier": { "type": "m.id.user", "user": "alice" },
|
||||
"device_id": "adevice"
|
||||
}`,
|
||||
Token: "astoken",
|
||||
|
||||
WantUsername: "alice",
|
||||
WantDeviceID: "adevice",
|
||||
},
|
||||
}
|
||||
for _, tst := range tsts {
|
||||
t.Run(tst.Name, func(t *testing.T) {
|
||||
|
|
@ -72,11 +99,35 @@ func TestLoginFromJSONReader(t *testing.T) {
|
|||
ServerName: serverName,
|
||||
},
|
||||
},
|
||||
Derived: &config.Derived{
|
||||
ApplicationServices: []config.ApplicationService{
|
||||
{
|
||||
ID: "anapplicationservice",
|
||||
ASToken: "astoken",
|
||||
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
||||
"users": {
|
||||
{
|
||||
Exclusive: true,
|
||||
Regex: "@alice:example.com",
|
||||
RegexpObject: regexp.MustCompile("@alice:example.com"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
login, cleanup, err := LoginFromJSONReader(ctx, strings.NewReader(tst.Body), &userAPI, &userAPI, cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("LoginFromJSONReader failed: %+v", err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tst.Body))
|
||||
if tst.Token != "" {
|
||||
req.Header.Add("Authorization", "Bearer "+tst.Token)
|
||||
}
|
||||
|
||||
login, cleanup, jsonErr := LoginFromJSONReader(req, &userAPI, &userAPI, cfg)
|
||||
if jsonErr != nil {
|
||||
t.Fatalf("LoginFromJSONReader failed: %+v", jsonErr)
|
||||
}
|
||||
|
||||
cleanup(ctx, &util.JSONResponse{Code: http.StatusOK})
|
||||
|
||||
if login.Username() != tst.WantUsername {
|
||||
|
|
@ -104,8 +155,9 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
|||
ctx := context.Background()
|
||||
|
||||
tsts := []struct {
|
||||
Name string
|
||||
Body string
|
||||
Name string
|
||||
Body string
|
||||
Token string
|
||||
|
||||
WantErrCode spec.MatrixErrorCode
|
||||
}{
|
||||
|
|
@ -142,6 +194,45 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
|||
}`,
|
||||
WantErrCode: spec.ErrorInvalidParam,
|
||||
},
|
||||
{
|
||||
Name: "noASToken",
|
||||
Body: `{
|
||||
"type": "m.login.application_service",
|
||||
"identifier": { "type": "m.id.user", "user": "@alice:example.com" },
|
||||
"device_id": "adevice"
|
||||
}`,
|
||||
WantErrCode: "M_MISSING_TOKEN",
|
||||
},
|
||||
{
|
||||
Name: "badASToken",
|
||||
Token: "badastoken",
|
||||
Body: `{
|
||||
"type": "m.login.application_service",
|
||||
"identifier": { "type": "m.id.user", "user": "@alice:example.com" },
|
||||
"device_id": "adevice"
|
||||
}`,
|
||||
WantErrCode: "M_UNKNOWN_TOKEN",
|
||||
},
|
||||
{
|
||||
Name: "badASNamespace",
|
||||
Token: "astoken",
|
||||
Body: `{
|
||||
"type": "m.login.application_service",
|
||||
"identifier": { "type": "m.id.user", "user": "@bob:example.com" },
|
||||
"device_id": "adevice"
|
||||
}`,
|
||||
WantErrCode: "M_EXCLUSIVE",
|
||||
},
|
||||
{
|
||||
Name: "badASUserID",
|
||||
Token: "astoken",
|
||||
Body: `{
|
||||
"type": "m.login.application_service",
|
||||
"identifier": { "type": "m.id.user", "user": "@alice:wrong.example.com" },
|
||||
"device_id": "adevice"
|
||||
}`,
|
||||
WantErrCode: "M_INVALID_USERNAME",
|
||||
},
|
||||
}
|
||||
for _, tst := range tsts {
|
||||
t.Run(tst.Name, func(t *testing.T) {
|
||||
|
|
@ -152,8 +243,30 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
|||
ServerName: serverName,
|
||||
},
|
||||
},
|
||||
Derived: &config.Derived{
|
||||
ApplicationServices: []config.ApplicationService{
|
||||
{
|
||||
ID: "anapplicationservice",
|
||||
ASToken: "astoken",
|
||||
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
||||
"users": {
|
||||
{
|
||||
Exclusive: true,
|
||||
Regex: "@alice:example.com",
|
||||
RegexpObject: regexp.MustCompile("@alice:example.com"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
_, cleanup, errRes := LoginFromJSONReader(ctx, strings.NewReader(tst.Body), &userAPI, &userAPI, cfg)
|
||||
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tst.Body))
|
||||
if tst.Token != "" {
|
||||
req.Header.Add("Authorization", "Bearer "+tst.Token)
|
||||
}
|
||||
|
||||
_, cleanup, errRes := LoginFromJSONReader(req, &userAPI, &userAPI, cfg)
|
||||
if errRes == nil {
|
||||
cleanup(ctx, nil)
|
||||
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ type LoginCleanupFunc func(context.Context, *util.JSONResponse)
|
|||
// https://matrix.org/docs/spec/client_server/r0.6.1#identifier-types
|
||||
type LoginIdentifier struct {
|
||||
Type string `json:"type"`
|
||||
// when type = m.id.user
|
||||
// when type = m.id.user or m.id.application_service
|
||||
User string `json:"user"`
|
||||
// when type = m.id.thirdparty
|
||||
Medium string `json:"medium"`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue