Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Appservice Login (2nd attempt) #3078

Merged
merged 29 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
56f6ad1
Replace "m.login.password" with authtypes constant
vijfhoek Jan 6, 2023
4899bdb
Add LoginTypeApplicationService to GET /login
vijfhoek Jan 6, 2023
29bf744
Move ValidateApplicationService to a separate package for reuse
vijfhoek Jan 7, 2023
20b0bfa
Implement POST /login for LoginTypeApplicationService
vijfhoek Jan 7, 2023
019a5d0
Make ValidateApplicationService work with UserIDs and move to internal
vijfhoek Jan 7, 2023
9bef287
Add tests for m.login.application_service
vijfhoek Jan 8, 2023
55bc0f6
Improve ValidateApplicationService test coverage
vijfhoek Jan 10, 2023
aafb13f
Remove dead, untested code from userIDIsWithinApplicationServiceNames…
vijfhoek Jan 10, 2023
d46267f
Simplify GET /login and add test for it
vijfhoek Jan 15, 2023
a5f4c5e
Fix typo "POSTGERS" in CONTRIBUTING.md
vijfhoek Jan 27, 2023
4cca740
Replace handcrafted genHTTPRequest with httptest.NewRequest
vijfhoek Jan 27, 2023
923f47c
Improve documentation and naming
vijfhoek Jan 27, 2023
25f2b51
Move UsernameMatchesExclusiveNamespaces back to register.go
vijfhoek Jan 27, 2023
4b98bbe
Convert TestValidationOfApplicationServices to be table-driven
vijfhoek Jan 27, 2023
fa42d71
Update TestValidationOfApplicationServices function name
vijfhoek Jan 27, 2023
1020f60
linting
kuhnchris May 7, 2023
843a303
:shrug:
kuhnchris May 7, 2023
814e2b8
Apparently that cfg structure was there on purpose.
kuhnchris May 7, 2023
bf05359
Clean up merge from main
kuhnchris Jun 18, 2023
27f2867
more cleanups
kuhnchris Jun 18, 2023
579eb2f
Make tests go brrrr.
kuhnchris Jun 30, 2023
721782f
Remove rouge ","
kuhnchris Jun 30, 2023
272f3cc
`gofmt -w` fixes it.
kuhnchris Jun 30, 2023
eb51b1d
Allow loginflows based on config
kuhnchris Jul 3, 2023
532cdd1
Make it more `go`
kuhnchris Jul 3, 2023
17a4ea7
Merge branch 'main' into appservice_login_2
kuhnchris Sep 10, 2023
083c729
Merge branch 'main' into appservice_login_2
kuhnchris Oct 19, 2023
f683c7d
Merge branch 'main' into appservice_login_2
S7evinK Nov 24, 2023
aa23baa
Inject a dummy AS to make the UT happy
S7evinK Nov 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions clientapi/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package auth

import (
"context"
"encoding/json"
"io"
"net/http"
Expand All @@ -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(
kuhnchris marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand Down
55 changes: 55 additions & 0 deletions clientapi/auth/login_application_service.go
Original file line number Diff line number Diff line change
@@ -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
}
129 changes: 121 additions & 8 deletions clientapi/auth/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ package auth
import (
"context"
"net/http"
"net/http/httptest"
"reflect"
"regexp"
"strings"
"testing"

Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}{
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion clientapi/auth/user_interactive.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
21 changes: 9 additions & 12 deletions clientapi/routing/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"net/http"

"github.com/matrix-org/dendrite/clientapi/auth"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/userutil"
"github.com/matrix-org/dendrite/setup/config"
userapi "github.com/matrix-org/dendrite/userapi/api"
Expand All @@ -40,28 +41,24 @@ type flow struct {
Type string `json:"type"`
}

func passwordLogin() flows {
f := flows{}
s := flow{
Type: "m.login.password",
}
f.Flows = append(f.Flows, s)
return f
}

// Login implements GET and POST /login
func Login(
req *http.Request, userAPI userapi.ClientUserAPI,
cfg *config.ClientAPI,
) util.JSONResponse {
if req.Method == http.MethodGet {
// TODO: support other forms of login other than password, depending on config options
// TODO: support other forms of login, depending on config options
return util.JSONResponse{
Code: http.StatusOK,
JSON: passwordLogin(),
JSON: flows{
Flows: []flow{
{Type: authtypes.LoginTypePassword},
{Type: authtypes.LoginTypeApplicationService},
kuhnchris marked this conversation as resolved.
Show resolved Hide resolved
},
},
}
} else if req.Method == http.MethodPost {
login, cleanup, authErr := auth.LoginFromJSONReader(req.Context(), req.Body, userAPI, userAPI, cfg)
login, cleanup, authErr := auth.LoginFromJSONReader(req, userAPI, userAPI, cfg)
if authErr != nil {
return *authErr
}
Expand Down
33 changes: 33 additions & 0 deletions clientapi/routing/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,39 @@ func TestLogin(t *testing.T) {

ctx := context.Background()

t.Run("Supported log-in flows are returned", func(t *testing.T) {
req := test.NewRequest(t, http.MethodGet, "/_matrix/client/v3/login")
rec := httptest.NewRecorder()
routers.Client.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("failed to get log-in flows: %s", rec.Body.String())
}

t.Logf("response: %s", rec.Body.String())
resp := flows{}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}

appServiceFound := false
passwordFound := false
for _, flow := range resp.Flows {
if flow.Type == "m.login.password" {
passwordFound = true
} else if flow.Type == "m.login.application_service" {
appServiceFound = true
} else {
t.Fatalf("got unknown login flow: %s", flow.Type)
}
}
if !appServiceFound {
t.Fatal("m.login.application_service missing from login flows")
}
if !passwordFound {
t.Fatal("m.login.password missing from login flows")
}
})

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := test.NewRequest(t, http.MethodPost, "/_matrix/client/v3/login", test.WithJSONBody(t, map[string]interface{}{
Expand Down
Loading
Loading