Skip to content
This repository was archived by the owner on Nov 25, 2024. It is now read-only.

Commit c36e454

Browse files
tommiekegsayneilalexander
authored
Support for m.login.token (#2014)
* Add GOPATH to PATH in find-lint.sh. The user doesn't necessarily have it in PATH. * Refactor LoginTypePassword and Type to support m.login.token and m.login.sso. For login token: * m.login.token will require deleting the token after completeAuth has generated an access token, so a cleanup function is returned by Type.Login. * Allowing different login types will require parsing the /login body twice: first to extract the "type" and then the type-specific parsing. Thus, we will have to buffer the request JSON in /login, like UserInteractive already does. For SSO: * NewUserInteractive will have to also use GetAccountByLocalpart. It makes more sense to just pass a (narrowed-down) accountDB interface to it than adding more function pointers. Code quality: * Passing around (and down-casting) interface{} for login request types has drawbacks in terms of type-safety, and no inherent benefits. We always decode JSON anyway. Hence renaming to Type.LoginFromJSON. Code that directly uses LoginTypePassword with parsed data can still use Login. * Removed a TODO for SSO. This is already tracked in #1297. * httputil.UnmarshalJSON is useful because it returns a JSONResponse. This change is intended to have no functional changes. * Support login tokens in User API. This adds full lifecycle functions for login tokens: create, query, delete. * Support m.login.token in /login. * Fixes for PR review. * Set @matrix-org/dendrite-core as repository code owner * Return event NID from `StoreEvent`, match PSQL vs SQLite behaviour, tweak backfill persistence (#2071) Co-authored-by: kegsay <[email protected]> Co-authored-by: Neil Alexander <[email protected]>
1 parent 432c35a commit c36e454

28 files changed

+1244
-80
lines changed

build/scripts/find-lint.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ echo "Looking for lint..."
3333
# Capture exit code to ensure go.{mod,sum} is restored before exiting
3434
exit_code=0
3535

36-
golangci-lint run $args || exit_code=1
36+
PATH="$PATH:${GOPATH:-~/go}/bin" golangci-lint run $args || exit_code=1
3737

3838
# Restore go.{mod,sum}
3939
mv go.mod.bak go.mod && mv go.sum.bak go.sum

clientapi/auth/auth.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type DeviceDatabase interface {
4242
type AccountDatabase interface {
4343
// Look up the account matching the given localpart.
4444
GetAccountByLocalpart(ctx context.Context, localpart string) (*api.Account, error)
45+
GetAccountByPassword(ctx context.Context, localpart, password string) (*api.Account, error)
4546
}
4647

4748
// VerifyUserFromRequest authenticates the HTTP request,

clientapi/auth/authtypes/logintypes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ const (
1010
LoginTypeSharedSecret = "org.matrix.login.shared_secret"
1111
LoginTypeRecaptcha = "m.login.recaptcha"
1212
LoginTypeApplicationService = "m.login.application_service"
13+
LoginTypeToken = "m.login.token"
1314
)

clientapi/auth/login.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright 2021 The Matrix.org Foundation C.I.C.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package auth
16+
17+
import (
18+
"context"
19+
"encoding/json"
20+
"io"
21+
"io/ioutil"
22+
"net/http"
23+
24+
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
25+
"github.com/matrix-org/dendrite/clientapi/jsonerror"
26+
"github.com/matrix-org/dendrite/setup/config"
27+
uapi "github.com/matrix-org/dendrite/userapi/api"
28+
"github.com/matrix-org/util"
29+
)
30+
31+
// LoginFromJSONReader performs authentication given a login request body reader and
32+
// some context. It returns the basic login information and a cleanup function to be
33+
// called after authorization has completed, with the result of the authorization.
34+
// If the final return value is non-nil, an error occurred and the cleanup function
35+
// is nil.
36+
func LoginFromJSONReader(ctx context.Context, r io.Reader, accountDB AccountDatabase, userAPI UserInternalAPIForLogin, cfg *config.ClientAPI) (*Login, LoginCleanupFunc, *util.JSONResponse) {
37+
reqBytes, err := ioutil.ReadAll(r)
38+
if err != nil {
39+
err := &util.JSONResponse{
40+
Code: http.StatusBadRequest,
41+
JSON: jsonerror.BadJSON("Reading request body failed: " + err.Error()),
42+
}
43+
return nil, nil, err
44+
}
45+
46+
var header struct {
47+
Type string `json:"type"`
48+
}
49+
if err := json.Unmarshal(reqBytes, &header); err != nil {
50+
err := &util.JSONResponse{
51+
Code: http.StatusBadRequest,
52+
JSON: jsonerror.BadJSON("Reading request body failed: " + err.Error()),
53+
}
54+
return nil, nil, err
55+
}
56+
57+
var typ Type
58+
switch header.Type {
59+
case authtypes.LoginTypePassword:
60+
typ = &LoginTypePassword{
61+
GetAccountByPassword: accountDB.GetAccountByPassword,
62+
Config: cfg,
63+
}
64+
case authtypes.LoginTypeToken:
65+
typ = &LoginTypeToken{
66+
UserAPI: userAPI,
67+
Config: cfg,
68+
}
69+
default:
70+
err := util.JSONResponse{
71+
Code: http.StatusBadRequest,
72+
JSON: jsonerror.InvalidArgumentValue("unhandled login type: " + header.Type),
73+
}
74+
return nil, nil, &err
75+
}
76+
77+
return typ.LoginFromJSON(ctx, reqBytes)
78+
}
79+
80+
// UserInternalAPIForLogin contains the aspects of UserAPI required for logging in.
81+
type UserInternalAPIForLogin interface {
82+
uapi.LoginTokenInternalAPI
83+
}

clientapi/auth/login_test.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Copyright 2021 The Matrix.org Foundation C.I.C.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package auth
16+
17+
import (
18+
"context"
19+
"database/sql"
20+
"net/http"
21+
"reflect"
22+
"strings"
23+
"testing"
24+
25+
"github.com/matrix-org/dendrite/clientapi/jsonerror"
26+
"github.com/matrix-org/dendrite/setup/config"
27+
uapi "github.com/matrix-org/dendrite/userapi/api"
28+
"github.com/matrix-org/util"
29+
)
30+
31+
func TestLoginFromJSONReader(t *testing.T) {
32+
ctx := context.Background()
33+
34+
tsts := []struct {
35+
Name string
36+
Body string
37+
38+
WantUsername string
39+
WantDeviceID string
40+
WantDeletedTokens []string
41+
}{
42+
{
43+
Name: "passwordWorks",
44+
Body: `{
45+
"type": "m.login.password",
46+
"identifier": { "type": "m.id.user", "user": "alice" },
47+
"password": "herpassword",
48+
"device_id": "adevice"
49+
}`,
50+
WantUsername: "alice",
51+
WantDeviceID: "adevice",
52+
},
53+
{
54+
Name: "tokenWorks",
55+
Body: `{
56+
"type": "m.login.token",
57+
"token": "atoken",
58+
"device_id": "adevice"
59+
}`,
60+
WantUsername: "@auser:example.com",
61+
WantDeviceID: "adevice",
62+
WantDeletedTokens: []string{"atoken"},
63+
},
64+
}
65+
for _, tst := range tsts {
66+
t.Run(tst.Name, func(t *testing.T) {
67+
var accountDB fakeAccountDB
68+
var userAPI fakeUserInternalAPI
69+
cfg := &config.ClientAPI{
70+
Matrix: &config.Global{
71+
ServerName: serverName,
72+
},
73+
}
74+
login, cleanup, err := LoginFromJSONReader(ctx, strings.NewReader(tst.Body), &accountDB, &userAPI, cfg)
75+
if err != nil {
76+
t.Fatalf("LoginFromJSONReader failed: %+v", err)
77+
}
78+
cleanup(ctx, &util.JSONResponse{Code: http.StatusOK})
79+
80+
if login.Username() != tst.WantUsername {
81+
t.Errorf("Username: got %q, want %q", login.Username(), tst.WantUsername)
82+
}
83+
84+
if login.DeviceID == nil {
85+
if tst.WantDeviceID != "" {
86+
t.Errorf("DeviceID: got %v, want %q", login.DeviceID, tst.WantDeviceID)
87+
}
88+
} else {
89+
if *login.DeviceID != tst.WantDeviceID {
90+
t.Errorf("DeviceID: got %q, want %q", *login.DeviceID, tst.WantDeviceID)
91+
}
92+
}
93+
94+
if !reflect.DeepEqual(userAPI.DeletedTokens, tst.WantDeletedTokens) {
95+
t.Errorf("DeletedTokens: got %+v, want %+v", userAPI.DeletedTokens, tst.WantDeletedTokens)
96+
}
97+
})
98+
}
99+
}
100+
101+
func TestBadLoginFromJSONReader(t *testing.T) {
102+
ctx := context.Background()
103+
104+
tsts := []struct {
105+
Name string
106+
Body string
107+
108+
WantErrCode string
109+
}{
110+
{Name: "empty", WantErrCode: "M_BAD_JSON"},
111+
{
112+
Name: "badUnmarshal",
113+
Body: `badsyntaxJSON`,
114+
WantErrCode: "M_BAD_JSON",
115+
},
116+
{
117+
Name: "badPassword",
118+
Body: `{
119+
"type": "m.login.password",
120+
"identifier": { "type": "m.id.user", "user": "alice" },
121+
"password": "invalidpassword",
122+
"device_id": "adevice"
123+
}`,
124+
WantErrCode: "M_FORBIDDEN",
125+
},
126+
{
127+
Name: "badToken",
128+
Body: `{
129+
"type": "m.login.token",
130+
"token": "invalidtoken",
131+
"device_id": "adevice"
132+
}`,
133+
WantErrCode: "M_FORBIDDEN",
134+
},
135+
{
136+
Name: "badType",
137+
Body: `{
138+
"type": "m.login.invalid",
139+
"device_id": "adevice"
140+
}`,
141+
WantErrCode: "M_INVALID_ARGUMENT_VALUE",
142+
},
143+
}
144+
for _, tst := range tsts {
145+
t.Run(tst.Name, func(t *testing.T) {
146+
var accountDB fakeAccountDB
147+
var userAPI fakeUserInternalAPI
148+
cfg := &config.ClientAPI{
149+
Matrix: &config.Global{
150+
ServerName: serverName,
151+
},
152+
}
153+
_, cleanup, errRes := LoginFromJSONReader(ctx, strings.NewReader(tst.Body), &accountDB, &userAPI, cfg)
154+
if errRes == nil {
155+
cleanup(ctx, nil)
156+
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
157+
} else if merr, ok := errRes.JSON.(*jsonerror.MatrixError); ok && merr.ErrCode != tst.WantErrCode {
158+
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
159+
}
160+
})
161+
}
162+
}
163+
164+
type fakeAccountDB struct {
165+
AccountDatabase
166+
}
167+
168+
func (*fakeAccountDB) GetAccountByPassword(ctx context.Context, localpart, password string) (*uapi.Account, error) {
169+
if password == "invalidpassword" {
170+
return nil, sql.ErrNoRows
171+
}
172+
173+
return &uapi.Account{}, nil
174+
}
175+
176+
type fakeUserInternalAPI struct {
177+
UserInternalAPIForLogin
178+
179+
DeletedTokens []string
180+
}
181+
182+
func (ua *fakeUserInternalAPI) PerformLoginTokenDeletion(ctx context.Context, req *uapi.PerformLoginTokenDeletionRequest, res *uapi.PerformLoginTokenDeletionResponse) error {
183+
ua.DeletedTokens = append(ua.DeletedTokens, req.Token)
184+
return nil
185+
}
186+
187+
func (*fakeUserInternalAPI) QueryLoginToken(ctx context.Context, req *uapi.QueryLoginTokenRequest, res *uapi.QueryLoginTokenResponse) error {
188+
if req.Token == "invalidtoken" {
189+
return nil
190+
}
191+
192+
res.Data = &uapi.LoginTokenData{UserID: "@auser:example.com"}
193+
return nil
194+
}

clientapi/auth/login_token.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright 2021 The Matrix.org Foundation C.I.C.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package auth
16+
17+
import (
18+
"context"
19+
"net/http"
20+
21+
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
22+
"github.com/matrix-org/dendrite/clientapi/httputil"
23+
"github.com/matrix-org/dendrite/clientapi/jsonerror"
24+
"github.com/matrix-org/dendrite/setup/config"
25+
uapi "github.com/matrix-org/dendrite/userapi/api"
26+
"github.com/matrix-org/util"
27+
)
28+
29+
// LoginTypeToken describes how to authenticate with a login token.
30+
type LoginTypeToken struct {
31+
UserAPI uapi.LoginTokenInternalAPI
32+
Config *config.ClientAPI
33+
}
34+
35+
// Name implements Type.
36+
func (t *LoginTypeToken) Name() string {
37+
return authtypes.LoginTypeToken
38+
}
39+
40+
// LoginFromJSON implements Type. The cleanup function deletes the token from
41+
// the database on success.
42+
func (t *LoginTypeToken) LoginFromJSON(ctx context.Context, reqBytes []byte) (*Login, LoginCleanupFunc, *util.JSONResponse) {
43+
var r loginTokenRequest
44+
if err := httputil.UnmarshalJSON(reqBytes, &r); err != nil {
45+
return nil, nil, err
46+
}
47+
48+
var res uapi.QueryLoginTokenResponse
49+
if err := t.UserAPI.QueryLoginToken(ctx, &uapi.QueryLoginTokenRequest{Token: r.Token}, &res); err != nil {
50+
util.GetLogger(ctx).WithError(err).Error("UserAPI.QueryLoginToken failed")
51+
jsonErr := jsonerror.InternalServerError()
52+
return nil, nil, &jsonErr
53+
}
54+
if res.Data == nil {
55+
return nil, nil, &util.JSONResponse{
56+
Code: http.StatusForbidden,
57+
JSON: jsonerror.Forbidden("invalid login token"),
58+
}
59+
}
60+
61+
r.Login.Identifier.Type = "m.id.user"
62+
r.Login.Identifier.User = res.Data.UserID
63+
64+
cleanup := func(ctx context.Context, authRes *util.JSONResponse) {
65+
if authRes == nil {
66+
util.GetLogger(ctx).Error("No JSONResponse provided to LoginTokenType cleanup function")
67+
return
68+
}
69+
if authRes.Code == http.StatusOK {
70+
var res uapi.PerformLoginTokenDeletionResponse
71+
if err := t.UserAPI.PerformLoginTokenDeletion(ctx, &uapi.PerformLoginTokenDeletionRequest{Token: r.Token}, &res); err != nil {
72+
util.GetLogger(ctx).WithError(err).Error("UserAPI.PerformLoginTokenDeletion failed")
73+
}
74+
}
75+
}
76+
return &r.Login, cleanup, nil
77+
}
78+
79+
// loginTokenRequest struct to hold the possible parameters from an HTTP request.
80+
type loginTokenRequest struct {
81+
Login
82+
Token string `json:"token"`
83+
}

0 commit comments

Comments
 (0)