mirror of
https://github.com/matrix-org/dendrite.git
synced 2025-12-31 18:53:10 -06:00
Support m.login.token in /login.
This commit is contained in:
parent
53b5888bcc
commit
d124de75c3
|
|
@ -10,4 +10,5 @@ const (
|
||||||
LoginTypeSharedSecret = "org.matrix.login.shared_secret"
|
LoginTypeSharedSecret = "org.matrix.login.shared_secret"
|
||||||
LoginTypeRecaptcha = "m.login.recaptcha"
|
LoginTypeRecaptcha = "m.login.recaptcha"
|
||||||
LoginTypeApplicationService = "m.login.application_service"
|
LoginTypeApplicationService = "m.login.application_service"
|
||||||
|
LoginTypeToken = "m.login.token"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
83
clientapi/auth/login.go
Normal file
83
clientapi/auth/login.go
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
// Copyright 2021 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"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoginFromJSONReader performs authentication given a login request body reader and
|
||||||
|
// some context. It returns the basic login information and a cleanup function to be
|
||||||
|
// 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, accountDB AccountDatabase, userAPI UserInternalAPIForLogin, cfg *config.ClientAPI) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
||||||
|
reqBytes, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
err := &util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("Reading request body failed: " + err.Error()),
|
||||||
|
}
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var header struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(reqBytes, &header); err != nil {
|
||||||
|
err := &util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("Reading request body failed: " + err.Error()),
|
||||||
|
}
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var typ Type
|
||||||
|
switch header.Type {
|
||||||
|
case authtypes.LoginTypePassword:
|
||||||
|
typ = &LoginTypePassword{
|
||||||
|
GetAccountByPassword: accountDB.GetAccountByPassword,
|
||||||
|
Config: cfg,
|
||||||
|
}
|
||||||
|
case authtypes.LoginTypeToken:
|
||||||
|
typ = &LoginTypeToken{
|
||||||
|
UserAPI: userAPI,
|
||||||
|
Config: cfg,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err := util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.InvalidArgumentValue("unhandled login type: " + header.Type),
|
||||||
|
}
|
||||||
|
return nil, nil, &err
|
||||||
|
}
|
||||||
|
|
||||||
|
return typ.LoginFromJSON(ctx, reqBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserInternalAPIForLogin contains the aspects of UserAPI required for logging in.
|
||||||
|
type UserInternalAPIForLogin interface {
|
||||||
|
uapi.LoginTokenInternalAPI
|
||||||
|
}
|
||||||
131
clientapi/auth/login_test.go
Normal file
131
clientapi/auth/login_test.go
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
// Copyright 2021 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"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoginFromJSONReader(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
tsts := []struct {
|
||||||
|
Name string
|
||||||
|
Body string
|
||||||
|
|
||||||
|
WantErrCode string
|
||||||
|
WantUsername string
|
||||||
|
WantDeviceID string
|
||||||
|
WantDeletedTokens []string
|
||||||
|
}{
|
||||||
|
{Name: "empty", WantErrCode: "M_BAD_JSON"},
|
||||||
|
{
|
||||||
|
Name: "passwordWorks",
|
||||||
|
Body: `{
|
||||||
|
"type": "m.login.password",
|
||||||
|
"identifier": { "type": "m.id.user", "user": "alice" },
|
||||||
|
"password": "herpassword",
|
||||||
|
"device_id": "adevice"
|
||||||
|
}`,
|
||||||
|
WantUsername: "alice",
|
||||||
|
WantDeviceID: "adevice",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tokenWorks",
|
||||||
|
Body: `{
|
||||||
|
"type": "m.login.token",
|
||||||
|
"token": "atoken",
|
||||||
|
"device_id": "adevice"
|
||||||
|
}`,
|
||||||
|
WantUsername: "@auser:example.com",
|
||||||
|
WantDeviceID: "adevice",
|
||||||
|
WantDeletedTokens: []string{"atoken"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tst := range tsts {
|
||||||
|
t.Run(tst.Name, func(t *testing.T) {
|
||||||
|
var accountDB fakeAccountDB
|
||||||
|
var userAPI fakeUserInternalAPI
|
||||||
|
cfg := &config.ClientAPI{
|
||||||
|
Matrix: &config.Global{
|
||||||
|
ServerName: serverName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
login, cleanup, errRes := LoginFromJSONReader(ctx, strings.NewReader(tst.Body), &accountDB, &userAPI, cfg)
|
||||||
|
if tst.WantErrCode == "" {
|
||||||
|
if errRes != nil {
|
||||||
|
t.Fatalf("LoginFromJSONReader failed: %+v", errRes)
|
||||||
|
}
|
||||||
|
cleanup(ctx, nil)
|
||||||
|
} else {
|
||||||
|
if errRes == nil {
|
||||||
|
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
|
||||||
|
} else if merr, ok := errRes.JSON.(*jsonerror.MatrixError); ok && merr.ErrCode != tst.WantErrCode {
|
||||||
|
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if login.Username() != tst.WantUsername {
|
||||||
|
t.Errorf("Username: got %q, want %q", login.Username(), tst.WantUsername)
|
||||||
|
}
|
||||||
|
|
||||||
|
if login.DeviceID == nil {
|
||||||
|
if tst.WantDeviceID != "" {
|
||||||
|
t.Errorf("DeviceID: got %v, want %q", login.DeviceID, tst.WantDeviceID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if *login.DeviceID != tst.WantDeviceID {
|
||||||
|
t.Errorf("DeviceID: got %q, want %q", *login.DeviceID, tst.WantDeviceID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(userAPI.DeletedTokens, tst.WantDeletedTokens) {
|
||||||
|
t.Errorf("DeletedTokens: got %+v, want %+v", userAPI.DeletedTokens, tst.WantDeletedTokens)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeAccountDB struct {
|
||||||
|
AccountDatabase
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*fakeAccountDB) GetAccountByPassword(ctx context.Context, localpart, password string) (*uapi.Account, error) {
|
||||||
|
return &uapi.Account{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeUserInternalAPI struct {
|
||||||
|
UserInternalAPIForLogin
|
||||||
|
|
||||||
|
DeletedTokens []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ua *fakeUserInternalAPI) PerformLoginTokenDeletion(ctx context.Context, req *uapi.PerformLoginTokenDeletionRequest, res *uapi.PerformLoginTokenDeletionResponse) error {
|
||||||
|
ua.DeletedTokens = append(ua.DeletedTokens, req.Token)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*fakeUserInternalAPI) QueryLoginToken(ctx context.Context, req *uapi.QueryLoginTokenRequest, res *uapi.QueryLoginTokenResponse) error {
|
||||||
|
res.Data = &uapi.LoginTokenData{UserID: "@auser:example.com"}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
84
clientapi/auth/login_token.go
Normal file
84
clientapi/auth/login_token.go
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
// Copyright 2021 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"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"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/setup/config"
|
||||||
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoginTypeToken describes how to authenticate with a login token.
|
||||||
|
type LoginTypeToken struct {
|
||||||
|
UserAPI uapi.LoginTokenInternalAPI
|
||||||
|
Config *config.ClientAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name implements Type.
|
||||||
|
func (t *LoginTypeToken) Name() string {
|
||||||
|
return authtypes.LoginTypeToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginFromJSON implements Type. The cleanup function deletes the token from
|
||||||
|
// the database on success.
|
||||||
|
func (t *LoginTypeToken) LoginFromJSON(ctx context.Context, reqBytes []byte) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
||||||
|
var r loginTokenRequest
|
||||||
|
if err := httputil.UnmarshalJSON(reqBytes, &r); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.login(ctx, &r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loginTokenRequest struct to hold the possible parameters from an HTTP request.
|
||||||
|
type loginTokenRequest struct {
|
||||||
|
Login
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// login parses and validates the login token. It returns basic user information.
|
||||||
|
func (t *LoginTypeToken) login(ctx context.Context, r *loginTokenRequest) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
||||||
|
var res uapi.QueryLoginTokenResponse
|
||||||
|
if err := t.UserAPI.QueryLoginToken(ctx, &uapi.QueryLoginTokenRequest{Token: r.Token}, &res); err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("UserAPI.QueryLoginToken failed")
|
||||||
|
jsonErr := jsonerror.InternalServerError()
|
||||||
|
return nil, nil, &jsonErr
|
||||||
|
}
|
||||||
|
if res.Data == nil {
|
||||||
|
return nil, nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: jsonerror.Forbidden("invalid login token"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Login.Identifier.Type = "m.id.user"
|
||||||
|
r.Login.Identifier.User = res.Data.UserID
|
||||||
|
|
||||||
|
cleanup := func(ctx context.Context, authRes *util.JSONResponse) {
|
||||||
|
if authRes == nil || authRes.Code == http.StatusOK {
|
||||||
|
var res uapi.PerformLoginTokenDeletionResponse
|
||||||
|
if err := t.UserAPI.PerformLoginTokenDeletion(ctx, &uapi.PerformLoginTokenDeletionRequest{Token: r.Token}, &res); err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("UserAPI.PerformLoginTokenDeletion failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &r.Login, cleanup, nil
|
||||||
|
}
|
||||||
|
|
@ -63,11 +63,8 @@ type LoginIdentifier struct {
|
||||||
|
|
||||||
// Login represents the shared fields used in all forms of login/sudo endpoints.
|
// Login represents the shared fields used in all forms of login/sudo endpoints.
|
||||||
type Login struct {
|
type Login struct {
|
||||||
Type string `json:"type"`
|
LoginIdentifier // Flat fields deprecated in favour of `identifier`.
|
||||||
Identifier LoginIdentifier `json:"identifier"`
|
Identifier LoginIdentifier `json:"identifier"`
|
||||||
User string `json:"user"` // deprecated in favour of identifier
|
|
||||||
Medium string `json:"medium"` // deprecated in favour of identifier
|
|
||||||
Address string `json:"address"` // deprecated in favour of identifier
|
|
||||||
|
|
||||||
// Both DeviceID and InitialDisplayName can be omitted, or empty strings ("")
|
// Both DeviceID and InitialDisplayName can be omitted, or empty strings ("")
|
||||||
// Thus a pointer is needed to differentiate between the two
|
// Thus a pointer is needed to differentiate between the two
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,13 @@ package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/clientapi/userutil"
|
"github.com/matrix-org/dendrite/clientapi/userutil"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/dendrite/userapi/storage/accounts"
|
"github.com/matrix-org/dendrite/userapi/storage/accounts"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
|
|
@ -55,7 +54,7 @@ func passwordLogin() flows {
|
||||||
|
|
||||||
// Login implements GET and POST /login
|
// Login implements GET and POST /login
|
||||||
func Login(
|
func Login(
|
||||||
req *http.Request, accountDB accounts.Database, userAPI userapi.UserInternalAPI,
|
req *http.Request, accountDB accounts.Database, userAPI uapi.UserInternalAPI,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
if req.Method == http.MethodGet {
|
if req.Method == http.MethodGet {
|
||||||
|
|
@ -65,18 +64,7 @@ func Login(
|
||||||
JSON: passwordLogin(),
|
JSON: passwordLogin(),
|
||||||
}
|
}
|
||||||
} else if req.Method == http.MethodPost {
|
} else if req.Method == http.MethodPost {
|
||||||
typePassword := auth.LoginTypePassword{
|
login, cleanup, authErr := auth.LoginFromJSONReader(req.Context(), req.Body, accountDB, userAPI, cfg)
|
||||||
GetAccountByPassword: accountDB.GetAccountByPassword,
|
|
||||||
Config: cfg,
|
|
||||||
}
|
|
||||||
body, err := ioutil.ReadAll(req.Body)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.BadJSON("Reading request body failed: " + err.Error()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
login, cleanup, authErr := typePassword.LoginFromJSON(req.Context(), body)
|
|
||||||
if authErr != nil {
|
if authErr != nil {
|
||||||
return *authErr
|
return *authErr
|
||||||
}
|
}
|
||||||
|
|
@ -92,7 +80,7 @@ func Login(
|
||||||
}
|
}
|
||||||
|
|
||||||
func completeAuth(
|
func completeAuth(
|
||||||
ctx context.Context, serverName gomatrixserverlib.ServerName, userAPI userapi.UserInternalAPI, login *auth.Login,
|
ctx context.Context, serverName gomatrixserverlib.ServerName, userAPI uapi.UserInternalAPI, login *auth.Login,
|
||||||
ipAddr, userAgent string,
|
ipAddr, userAgent string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
token, err := auth.GenerateAccessToken()
|
token, err := auth.GenerateAccessToken()
|
||||||
|
|
@ -107,8 +95,8 @@ func completeAuth(
|
||||||
return jsonerror.InternalServerError()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
var performRes userapi.PerformDeviceCreationResponse
|
var performRes uapi.PerformDeviceCreationResponse
|
||||||
err = userAPI.PerformDeviceCreation(ctx, &userapi.PerformDeviceCreationRequest{
|
err = userAPI.PerformDeviceCreation(ctx, &uapi.PerformDeviceCreationRequest{
|
||||||
DeviceDisplayName: login.InitialDisplayName,
|
DeviceDisplayName: login.InitialDisplayName,
|
||||||
DeviceID: login.DeviceID,
|
DeviceID: login.DeviceID,
|
||||||
AccessToken: token,
|
AccessToken: token,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue