mirror of
https://github.com/matrix-org/dendrite.git
synced 2026-01-09 15:13:12 -06:00
Add login_publickey tests, and register_publickey tests. Fixed a bug in login, and a bug in register
This commit is contained in:
parent
94799f1fd1
commit
c905b1c09d
|
|
@ -30,10 +30,10 @@ import (
|
||||||
|
|
||||||
type LoginPublicKeyHandler interface {
|
type LoginPublicKeyHandler interface {
|
||||||
AccountExists(ctx context.Context) (string, *jsonerror.MatrixError)
|
AccountExists(ctx context.Context) (string, *jsonerror.MatrixError)
|
||||||
IsValidUserIdForRegistration(userId string) bool
|
|
||||||
CreateLogin() *Login
|
CreateLogin() *Login
|
||||||
GetSession() string
|
GetSession() string
|
||||||
GetType() string
|
GetType() string
|
||||||
|
IsValidUserId(userId string) bool
|
||||||
ValidateLoginResponse() (bool, *jsonerror.MatrixError)
|
ValidateLoginResponse() (bool, *jsonerror.MatrixError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,10 @@ func (pk LoginPublicKeyEthereum) AccountExists(ctx context.Context) (string, *js
|
||||||
return "", jsonerror.Forbidden("the address is incorrect, or the account does not exist.")
|
return "", jsonerror.Forbidden("the address is incorrect, or the account does not exist.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !pk.IsValidUserId(localPart) {
|
||||||
|
return "", jsonerror.InvalidUsername("the username is not valid.")
|
||||||
|
}
|
||||||
|
|
||||||
res := userapi.QueryAccountAvailabilityResponse{}
|
res := userapi.QueryAccountAvailabilityResponse{}
|
||||||
if err := pk.userAPI.QueryAccountAvailability(ctx, &userapi.QueryAccountAvailabilityRequest{
|
if err := pk.userAPI.QueryAccountAvailability(ctx, &userapi.QueryAccountAvailabilityRequest{
|
||||||
Localpart: localPart,
|
Localpart: localPart,
|
||||||
|
|
@ -80,7 +84,7 @@ func (pk LoginPublicKeyEthereum) AccountExists(ctx context.Context) (string, *js
|
||||||
return "", jsonerror.Unknown("failed to check availability: " + err.Error())
|
return "", jsonerror.Unknown("failed to check availability: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.Available {
|
if localPart == "" || res.Available {
|
||||||
return "", jsonerror.Forbidden("the address is incorrect, account does not exist")
|
return "", jsonerror.Forbidden("the address is incorrect, account does not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,7 +93,7 @@ func (pk LoginPublicKeyEthereum) AccountExists(ctx context.Context) (string, *js
|
||||||
|
|
||||||
var validChainAgnosticIdRegex = regexp.MustCompile("^eip155=3a[0-9]+=3a0x[0-9a-fA-F]+$")
|
var validChainAgnosticIdRegex = regexp.MustCompile("^eip155=3a[0-9]+=3a0x[0-9a-fA-F]+$")
|
||||||
|
|
||||||
func (pk LoginPublicKeyEthereum) IsValidUserIdForRegistration(userId string) bool {
|
func (pk LoginPublicKeyEthereum) IsValidUserId(userId string) bool {
|
||||||
// Verify that the user ID is a valid one according to spec.
|
// Verify that the user ID is a valid one according to spec.
|
||||||
// https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-10.md
|
// https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-10.md
|
||||||
|
|
||||||
|
|
@ -100,9 +104,9 @@ func (pk LoginPublicKeyEthereum) IsValidUserIdForRegistration(userId string) boo
|
||||||
|
|
||||||
isValid := validChainAgnosticIdRegex.MatchString(userId)
|
isValid := validChainAgnosticIdRegex.MatchString(userId)
|
||||||
|
|
||||||
// In addition, double check that the user ID for registration
|
// In addition, double check that the user ID
|
||||||
// matches the authentication data in the request.
|
// matches the authentication data in the request.
|
||||||
return isValid && userId == pk.UserId
|
return isValid && strings.ToLower(userId) == pk.UserId
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pk LoginPublicKeyEthereum) ValidateLoginResponse() (bool, *jsonerror.MatrixError) {
|
func (pk LoginPublicKeyEthereum) ValidateLoginResponse() (bool, *jsonerror.MatrixError) {
|
||||||
|
|
|
||||||
472
clientapi/auth/login_publickey_ethereum_test.go
Normal file
472
clientapi/auth/login_publickey_ethereum_test.go
Normal file
|
|
@ -0,0 +1,472 @@
|
||||||
|
// Copyright 2022 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"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
|
"github.com/matrix-org/dendrite/internal/mapsutil"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/dendrite/test"
|
||||||
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type loginContext struct {
|
||||||
|
config *config.ClientAPI
|
||||||
|
userInteractive *UserInteractive
|
||||||
|
}
|
||||||
|
|
||||||
|
func createLoginContext(t *testing.T) *loginContext {
|
||||||
|
chainIds := []int{4}
|
||||||
|
|
||||||
|
cfg := &config.ClientAPI{
|
||||||
|
Matrix: &config.Global{
|
||||||
|
ServerName: test.TestServerName,
|
||||||
|
},
|
||||||
|
Derived: &config.Derived{},
|
||||||
|
PasswordAuthenticationDisabled: true,
|
||||||
|
PublicKeyAuthentication: config.PublicKeyAuthentication{
|
||||||
|
Ethereum: config.EthereumAuthConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Version: 1,
|
||||||
|
ChainIDs: chainIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pkFlows := cfg.PublicKeyAuthentication.GetPublicKeyRegistrationFlows()
|
||||||
|
cfg.Derived.Registration.Flows = append(cfg.Derived.Registration.Flows, pkFlows...)
|
||||||
|
pkParams := cfg.PublicKeyAuthentication.GetPublicKeyRegistrationParams()
|
||||||
|
cfg.Derived.Registration.Params = mapsutil.MapsUnion(cfg.Derived.Registration.Params, pkParams)
|
||||||
|
|
||||||
|
var userAPI fakePublicKeyUserApi
|
||||||
|
var loginApi uapi.UserLoginAPI
|
||||||
|
|
||||||
|
userInteractive := NewUserInteractive(
|
||||||
|
loginApi,
|
||||||
|
&userAPI,
|
||||||
|
cfg)
|
||||||
|
|
||||||
|
return &loginContext{
|
||||||
|
config: cfg,
|
||||||
|
userInteractive: userInteractive,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakePublicKeyUserApi struct {
|
||||||
|
UserInternalAPIForLogin
|
||||||
|
uapi.UserLoginAPI
|
||||||
|
uapi.ClientUserAPI
|
||||||
|
DeletedTokens []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ua *fakePublicKeyUserApi) QueryAccountAvailability(ctx context.Context, req *uapi.QueryAccountAvailabilityRequest, res *uapi.QueryAccountAvailabilityResponse) error {
|
||||||
|
if req.Localpart == "does_not_exist" {
|
||||||
|
res.Available = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Available = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ua *fakePublicKeyUserApi) QueryAccountByPassword(ctx context.Context, req *uapi.QueryAccountByPasswordRequest, res *uapi.QueryAccountByPasswordResponse) error {
|
||||||
|
if req.PlaintextPassword == "invalidpassword" {
|
||||||
|
res.Account = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
res.Exists = true
|
||||||
|
res.Account = &uapi.Account{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ua *fakePublicKeyUserApi) PerformLoginTokenDeletion(ctx context.Context, req *uapi.PerformLoginTokenDeletionRequest, res *uapi.PerformLoginTokenDeletionResponse) error {
|
||||||
|
ua.DeletedTokens = append(ua.DeletedTokens, req.Token)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ua *fakePublicKeyUserApi) PerformLoginTokenCreation(ctx context.Context, req *uapi.PerformLoginTokenCreationRequest, res *uapi.PerformLoginTokenCreationResponse) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*fakePublicKeyUserApi) QueryLoginToken(ctx context.Context, req *uapi.QueryLoginTokenRequest, res *uapi.QueryLoginTokenResponse) error {
|
||||||
|
if req.Token == "invalidtoken" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Data = &uapi.LoginTokenData{UserID: "@auser:example.com"}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func publicKeyTestSession(
|
||||||
|
ctx *context.Context,
|
||||||
|
cfg *config.ClientAPI,
|
||||||
|
userInteractive *UserInteractive,
|
||||||
|
userAPI *fakePublicKeyUserApi,
|
||||||
|
|
||||||
|
) string {
|
||||||
|
emptyAuth := struct {
|
||||||
|
Body string
|
||||||
|
}{
|
||||||
|
Body: `{
|
||||||
|
"type": "m.login.publickey"
|
||||||
|
}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, cleanup, err := LoginFromJSONReader(
|
||||||
|
*ctx,
|
||||||
|
strings.NewReader(emptyAuth.Body),
|
||||||
|
userAPI,
|
||||||
|
userAPI,
|
||||||
|
userAPI,
|
||||||
|
userInteractive,
|
||||||
|
cfg)
|
||||||
|
|
||||||
|
if cleanup != nil {
|
||||||
|
cleanup(*ctx, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
json := err.JSON.(Challenge)
|
||||||
|
return json.Session
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoginPublicKeyEthereum(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
var userAPI fakePublicKeyUserApi
|
||||||
|
ctx := context.Background()
|
||||||
|
loginContext := createLoginContext(t)
|
||||||
|
wallet, _ := test.CreateTestAccount()
|
||||||
|
message, _ := test.CreateEip4361TestMessage(wallet.PublicAddress)
|
||||||
|
signature, _ := test.SignMessage(message.String(), wallet.PrivateKey)
|
||||||
|
sessionId := publicKeyTestSession(
|
||||||
|
&ctx,
|
||||||
|
loginContext.config,
|
||||||
|
loginContext.userInteractive,
|
||||||
|
&userAPI,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Escape \t and \n. Work around for marshalling and unmarshalling message.
|
||||||
|
msgStr := test.FromEip4361MessageToString(message)
|
||||||
|
body := fmt.Sprintf(`{
|
||||||
|
"type": "m.login.publickey",
|
||||||
|
"auth": {
|
||||||
|
"type": "m.login.publickey.ethereum",
|
||||||
|
"session": "%v",
|
||||||
|
"user_id": "%v",
|
||||||
|
"message": "%v",
|
||||||
|
"signature": "%v"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
sessionId,
|
||||||
|
wallet.Eip155UserId,
|
||||||
|
msgStr,
|
||||||
|
signature,
|
||||||
|
)
|
||||||
|
test := struct {
|
||||||
|
Body string
|
||||||
|
}{
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
login, cleanup, err := LoginFromJSONReader(
|
||||||
|
ctx,
|
||||||
|
strings.NewReader(test.Body),
|
||||||
|
&userAPI,
|
||||||
|
&userAPI,
|
||||||
|
&userAPI,
|
||||||
|
loginContext.userInteractive,
|
||||||
|
loginContext.config)
|
||||||
|
|
||||||
|
if cleanup != nil {
|
||||||
|
cleanup(ctx, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asserts
|
||||||
|
assert := assert.New(t)
|
||||||
|
assert.Nilf(err, "err actual: %v, expected: nil", err)
|
||||||
|
assert.NotNil(login, "login: actual: nil, expected: not nil")
|
||||||
|
assert.Truef(
|
||||||
|
login.Identifier.Type == "m.id.decentralizedid",
|
||||||
|
"login.Identifier.Type actual: %v, expected: %v", login.Identifier.Type, "m.id.decentralizedid")
|
||||||
|
walletAddress := strings.ToLower(wallet.Eip155UserId)
|
||||||
|
assert.Truef(
|
||||||
|
login.Identifier.User == walletAddress,
|
||||||
|
"login.Identifier.User actual: %v, expected: %v", login.Identifier.User, walletAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoginPublicKeyEthereumMissingSignature(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
var userAPI fakePublicKeyUserApi
|
||||||
|
ctx := context.Background()
|
||||||
|
loginContext := createLoginContext(t)
|
||||||
|
wallet, _ := test.CreateTestAccount()
|
||||||
|
message, _ := test.CreateEip4361TestMessage(wallet.PublicAddress)
|
||||||
|
sessionId := publicKeyTestSession(
|
||||||
|
&ctx,
|
||||||
|
loginContext.config,
|
||||||
|
loginContext.userInteractive,
|
||||||
|
&userAPI,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Escape \t and \n. Work around for marshalling and unmarshalling message.
|
||||||
|
msgStr := test.FromEip4361MessageToString(message)
|
||||||
|
body := fmt.Sprintf(`{
|
||||||
|
"type": "m.login.publickey",
|
||||||
|
"auth": {
|
||||||
|
"type": "m.login.publickey.ethereum",
|
||||||
|
"session": "%v",
|
||||||
|
"user_id": "%v",
|
||||||
|
"message": "%v"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
sessionId,
|
||||||
|
wallet.Eip155UserId,
|
||||||
|
msgStr,
|
||||||
|
)
|
||||||
|
test := struct {
|
||||||
|
Body string
|
||||||
|
}{
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
_, cleanup, err := LoginFromJSONReader(
|
||||||
|
ctx,
|
||||||
|
strings.NewReader(test.Body),
|
||||||
|
&userAPI,
|
||||||
|
&userAPI,
|
||||||
|
&userAPI,
|
||||||
|
loginContext.userInteractive,
|
||||||
|
loginContext.config)
|
||||||
|
|
||||||
|
if cleanup != nil {
|
||||||
|
cleanup(ctx, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asserts
|
||||||
|
assert := assert.New(t)
|
||||||
|
assert.Truef(
|
||||||
|
err.Code == http.StatusUnauthorized,
|
||||||
|
"err.Code actual: %v, expected: %v", err.Code, http.StatusUnauthorized)
|
||||||
|
json := err.JSON.(*jsonerror.MatrixError)
|
||||||
|
expectedErr := jsonerror.InvalidSignature("")
|
||||||
|
assert.Truef(
|
||||||
|
json.ErrCode == expectedErr.ErrCode,
|
||||||
|
"err.JSON.ErrCode actual: %v, expected: %v", json.ErrCode, expectedErr.ErrCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoginPublicKeyEthereumEmptyMessage(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
var userAPI fakePublicKeyUserApi
|
||||||
|
ctx := context.Background()
|
||||||
|
loginContext := createLoginContext(t)
|
||||||
|
wallet, _ := test.CreateTestAccount()
|
||||||
|
sessionId := publicKeyTestSession(
|
||||||
|
&ctx,
|
||||||
|
loginContext.config,
|
||||||
|
loginContext.userInteractive,
|
||||||
|
&userAPI,
|
||||||
|
)
|
||||||
|
|
||||||
|
body := fmt.Sprintf(`{
|
||||||
|
"type": "m.login.publickey",
|
||||||
|
"auth": {
|
||||||
|
"type": "m.login.publickey.ethereum",
|
||||||
|
"session": "%v",
|
||||||
|
"user_id": "%v"
|
||||||
|
}
|
||||||
|
}`, sessionId, wallet.Eip155UserId)
|
||||||
|
test := struct {
|
||||||
|
Body string
|
||||||
|
}{
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
_, cleanup, err := LoginFromJSONReader(
|
||||||
|
ctx,
|
||||||
|
strings.NewReader(test.Body),
|
||||||
|
&userAPI,
|
||||||
|
&userAPI,
|
||||||
|
&userAPI,
|
||||||
|
loginContext.userInteractive,
|
||||||
|
loginContext.config)
|
||||||
|
|
||||||
|
if cleanup != nil {
|
||||||
|
cleanup(ctx, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asserts
|
||||||
|
assert := assert.New(t)
|
||||||
|
assert.Truef(
|
||||||
|
err.Code == http.StatusUnauthorized,
|
||||||
|
"err.Code actual: %v, expected: %v", err.Code, http.StatusUnauthorized)
|
||||||
|
json := err.JSON.(*jsonerror.MatrixError)
|
||||||
|
expectedErr := jsonerror.InvalidParam("")
|
||||||
|
assert.Truef(
|
||||||
|
json.ErrCode == expectedErr.ErrCode,
|
||||||
|
"err.JSON.ErrCode actual: %v, expected: %v", json.ErrCode, expectedErr.ErrCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoginPublicKeyEthereumWrongUserId(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
var userAPI fakePublicKeyUserApi
|
||||||
|
ctx := context.Background()
|
||||||
|
loginContext := createLoginContext(t)
|
||||||
|
wallet, _ := test.CreateTestAccount()
|
||||||
|
sessionId := publicKeyTestSession(
|
||||||
|
&ctx,
|
||||||
|
loginContext.config,
|
||||||
|
loginContext.userInteractive,
|
||||||
|
&userAPI,
|
||||||
|
)
|
||||||
|
|
||||||
|
body := fmt.Sprintf(`{
|
||||||
|
"type": "m.login.publickey",
|
||||||
|
"auth": {
|
||||||
|
"type": "m.login.publickey.ethereum",
|
||||||
|
"session": "%v",
|
||||||
|
"user_id": "%v"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
sessionId,
|
||||||
|
wallet.PublicAddress)
|
||||||
|
test := struct {
|
||||||
|
Body string
|
||||||
|
}{
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
_, cleanup, err := LoginFromJSONReader(
|
||||||
|
ctx,
|
||||||
|
strings.NewReader(test.Body),
|
||||||
|
&userAPI,
|
||||||
|
&userAPI,
|
||||||
|
&userAPI,
|
||||||
|
loginContext.userInteractive,
|
||||||
|
loginContext.config)
|
||||||
|
|
||||||
|
if cleanup != nil {
|
||||||
|
cleanup(ctx, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asserts
|
||||||
|
assert := assert.New(t)
|
||||||
|
assert.Truef(
|
||||||
|
err.Code == http.StatusForbidden,
|
||||||
|
"err.Code actual: %v, expected: %v", err.Code, http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoginPublicKeyEthereumMissingUserId(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
var userAPI fakePublicKeyUserApi
|
||||||
|
ctx := context.Background()
|
||||||
|
loginContext := createLoginContext(t)
|
||||||
|
sessionId := publicKeyTestSession(
|
||||||
|
&ctx,
|
||||||
|
loginContext.config,
|
||||||
|
loginContext.userInteractive,
|
||||||
|
&userAPI,
|
||||||
|
)
|
||||||
|
|
||||||
|
body := fmt.Sprintf(`{
|
||||||
|
"type": "m.login.publickey",
|
||||||
|
"auth": {
|
||||||
|
"type": "m.login.publickey.ethereum",
|
||||||
|
"session": "%v"
|
||||||
|
}
|
||||||
|
}`, sessionId)
|
||||||
|
test := struct {
|
||||||
|
Body string
|
||||||
|
}{
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
_, cleanup, err := LoginFromJSONReader(
|
||||||
|
ctx,
|
||||||
|
strings.NewReader(test.Body),
|
||||||
|
&userAPI,
|
||||||
|
&userAPI,
|
||||||
|
&userAPI,
|
||||||
|
loginContext.userInteractive,
|
||||||
|
loginContext.config)
|
||||||
|
|
||||||
|
if cleanup != nil {
|
||||||
|
cleanup(ctx, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asserts
|
||||||
|
assert := assert.New(t)
|
||||||
|
assert.Truef(
|
||||||
|
err.Code == http.StatusForbidden,
|
||||||
|
"err.Code actual: %v, expected: %v", err.Code, http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoginPublicKeyEthereumAccountNotAvailable(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
var userAPI fakePublicKeyUserApi
|
||||||
|
ctx := context.Background()
|
||||||
|
loginContext := createLoginContext(t)
|
||||||
|
sessionId := publicKeyTestSession(
|
||||||
|
&ctx,
|
||||||
|
loginContext.config,
|
||||||
|
loginContext.userInteractive,
|
||||||
|
&userAPI,
|
||||||
|
)
|
||||||
|
|
||||||
|
body := fmt.Sprintf(`{
|
||||||
|
"type": "m.login.publickey",
|
||||||
|
"auth": {
|
||||||
|
"type": "m.login.publickey.ethereum",
|
||||||
|
"session": "%v",
|
||||||
|
"user_id": "does_not_exist"
|
||||||
|
}
|
||||||
|
}`, sessionId)
|
||||||
|
test := struct {
|
||||||
|
Body string
|
||||||
|
}{
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
_, cleanup, err := LoginFromJSONReader(
|
||||||
|
ctx,
|
||||||
|
strings.NewReader(test.Body),
|
||||||
|
&userAPI,
|
||||||
|
&userAPI,
|
||||||
|
&userAPI,
|
||||||
|
loginContext.userInteractive,
|
||||||
|
loginContext.config)
|
||||||
|
|
||||||
|
if cleanup != nil {
|
||||||
|
cleanup(ctx, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asserts
|
||||||
|
assert := assert.New(t)
|
||||||
|
assert.Truef(
|
||||||
|
err.Code == http.StatusForbidden,
|
||||||
|
"err.Code actual: %v, expected: %v", err.Code, http.StatusForbidden)
|
||||||
|
}
|
||||||
161
clientapi/auth/login_publickey_test.go
Normal file
161
clientapi/auth/login_publickey_test.go
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
// Copyright 2022 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"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoginPublicKeyNewSession(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
var userAPI fakePublicKeyUserApi
|
||||||
|
ctx := context.Background()
|
||||||
|
loginContext := createLoginContext(t)
|
||||||
|
|
||||||
|
test := struct {
|
||||||
|
Body string
|
||||||
|
}{
|
||||||
|
Body: `{ "type": "m.login.publickey" }`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
login, cleanup, err := LoginFromJSONReader(
|
||||||
|
ctx,
|
||||||
|
strings.NewReader(test.Body),
|
||||||
|
&userAPI,
|
||||||
|
&userAPI,
|
||||||
|
&userAPI,
|
||||||
|
loginContext.userInteractive,
|
||||||
|
loginContext.config)
|
||||||
|
|
||||||
|
if cleanup != nil {
|
||||||
|
cleanup(ctx, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asserts
|
||||||
|
assert := assert.New(t)
|
||||||
|
assert.NotNilf(
|
||||||
|
err,
|
||||||
|
"err actual: not nil returned %+v, expected: nil", login)
|
||||||
|
assert.Truef(
|
||||||
|
err.Code == http.StatusUnauthorized,
|
||||||
|
"err.Code actual: %v, expected: %v", err.Code, http.StatusUnauthorized)
|
||||||
|
challenge := err.JSON.(Challenge)
|
||||||
|
assert.NotEmptyf(challenge.Session, "challenge.Session")
|
||||||
|
assert.NotEmptyf(challenge.Completed, "challenge.Completed")
|
||||||
|
assert.Truef(
|
||||||
|
authtypes.LoginTypePublicKeyEthereum == challenge.Flows[0].Stages[0],
|
||||||
|
"challenge.Flows[0].Stages[0] actual: %v, expected: %v", challenge.Flows[0].Stages[0], authtypes.LoginTypePublicKeyEthereum)
|
||||||
|
params := challenge.Params[authtypes.LoginTypePublicKeyEthereum]
|
||||||
|
assert.NotEmptyf(
|
||||||
|
params,
|
||||||
|
"challenge.Params[\"%v\"] actual %v, expected %v",
|
||||||
|
authtypes.LoginTypePublicKeyEthereum,
|
||||||
|
params,
|
||||||
|
"[object]")
|
||||||
|
ethParams := params.(config.EthereumAuthParams)
|
||||||
|
assert.NotEmptyf(ethParams.ChainIDs, "ChainIDs actual: empty, expected not empty")
|
||||||
|
assert.NotEmptyf(ethParams.Nonce, "Nonce actual: \"\", expected: not empty")
|
||||||
|
assert.NotEmptyf(ethParams.Version, "Version actual: \"\", expected: not empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoginPublicKeyInvalidSessionId(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
var userAPI fakePublicKeyUserApi
|
||||||
|
ctx := context.Background()
|
||||||
|
loginContext := createLoginContext(t)
|
||||||
|
|
||||||
|
test := struct {
|
||||||
|
Body string
|
||||||
|
}{
|
||||||
|
Body: `{
|
||||||
|
"type": "m.login.publickey",
|
||||||
|
"auth": {
|
||||||
|
"type": "m.login.publickey.ethereum",
|
||||||
|
"session": "invalid_session_id"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
_, cleanup, err := LoginFromJSONReader(
|
||||||
|
ctx,
|
||||||
|
strings.NewReader(test.Body),
|
||||||
|
&userAPI,
|
||||||
|
&userAPI,
|
||||||
|
&userAPI,
|
||||||
|
loginContext.userInteractive,
|
||||||
|
loginContext.config)
|
||||||
|
|
||||||
|
if cleanup != nil {
|
||||||
|
cleanup(ctx, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asserts
|
||||||
|
assert := assert.New(t)
|
||||||
|
assert.Truef(
|
||||||
|
err.Code == http.StatusUnauthorized,
|
||||||
|
"err.Code actual %v, expected %v", err.Code, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoginPublicKeyInvalidAuthType(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
var userAPI fakePublicKeyUserApi
|
||||||
|
ctx := context.Background()
|
||||||
|
loginContext := createLoginContext(t)
|
||||||
|
|
||||||
|
test := struct {
|
||||||
|
Body string
|
||||||
|
}{
|
||||||
|
Body: `{
|
||||||
|
"type": "m.login.publickey",
|
||||||
|
"auth": {
|
||||||
|
"type": "m.login.publickey.someAlgo"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
_, cleanup, err := LoginFromJSONReader(
|
||||||
|
ctx,
|
||||||
|
strings.NewReader(test.Body),
|
||||||
|
&userAPI,
|
||||||
|
&userAPI,
|
||||||
|
&userAPI,
|
||||||
|
loginContext.userInteractive,
|
||||||
|
loginContext.config)
|
||||||
|
|
||||||
|
if cleanup != nil {
|
||||||
|
cleanup(ctx, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asserts
|
||||||
|
assert := assert.New(t)
|
||||||
|
assert.NotNil(err, "Expected an err response.actual: nil")
|
||||||
|
assert.Truef(
|
||||||
|
err.Code == http.StatusUnauthorized,
|
||||||
|
"err.Code actual %v, expected %v", err.Code, http.StatusUnauthorized)
|
||||||
|
_, ok := err.JSON.(Challenge)
|
||||||
|
assert.False(
|
||||||
|
ok,
|
||||||
|
"should not return a Challenge response")
|
||||||
|
}
|
||||||
|
|
@ -173,13 +173,13 @@ type Challenge struct {
|
||||||
func (u *UserInteractive) Challenge(sessionID string) *util.JSONResponse {
|
func (u *UserInteractive) Challenge(sessionID string) *util.JSONResponse {
|
||||||
paramsCopy := mapsutil.MapCopy(u.Params)
|
paramsCopy := mapsutil.MapCopy(u.Params)
|
||||||
for key, element := range paramsCopy {
|
for key, element := range paramsCopy {
|
||||||
p := getAuthParams(element)
|
p := GetAuthParams(element)
|
||||||
if p != nil {
|
if p != nil {
|
||||||
// If an auth flow has params,
|
// If an auth flow has params,
|
||||||
// send it as part of the challenge.
|
// send it as part of the challenge.
|
||||||
paramsCopy[key] = p
|
paramsCopy[key] = p
|
||||||
|
|
||||||
// If an auth flow generated a nonce, track it as well.
|
// If an auth flow generated a nonce, add it to the session.
|
||||||
nonce := getAuthParamNonce(p)
|
nonce := getAuthParamNonce(p)
|
||||||
if nonce != "" {
|
if nonce != "" {
|
||||||
u.Sessions[sessionID] = append(u.Sessions[sessionID], nonce)
|
u.Sessions[sessionID] = append(u.Sessions[sessionID], nonce)
|
||||||
|
|
@ -280,7 +280,7 @@ func (u *UserInteractive) Verify(ctx context.Context, bodyBytes []byte, device *
|
||||||
return login, nil
|
return login, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAuthParams(params interface{}) interface{} {
|
func GetAuthParams(params interface{}) interface{} {
|
||||||
v, ok := params.(config.AuthParams)
|
v, ok := params.(config.AuthParams)
|
||||||
if ok {
|
if ok {
|
||||||
p := v.GetParams()
|
p := v.GetParams()
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import (
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"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/internal/mapsutil"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -247,7 +248,7 @@ type authDict struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#user-interactive-authentication-api
|
// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#user-interactive-authentication-api
|
||||||
type userInteractiveResponse struct {
|
type UserInteractiveResponse struct {
|
||||||
Flows []authtypes.Flow `json:"flows"`
|
Flows []authtypes.Flow `json:"flows"`
|
||||||
Completed []authtypes.LoginType `json:"completed"`
|
Completed []authtypes.LoginType `json:"completed"`
|
||||||
Params map[string]interface{} `json:"params"`
|
Params map[string]interface{} `json:"params"`
|
||||||
|
|
@ -260,9 +261,18 @@ func newUserInteractiveResponse(
|
||||||
sessionID string,
|
sessionID string,
|
||||||
fs []authtypes.Flow,
|
fs []authtypes.Flow,
|
||||||
params map[string]interface{},
|
params map[string]interface{},
|
||||||
) userInteractiveResponse {
|
) UserInteractiveResponse {
|
||||||
return userInteractiveResponse{
|
paramsCopy := mapsutil.MapCopy(params)
|
||||||
fs, sessions.getCompletedStages(sessionID), params, sessionID,
|
for key, element := range paramsCopy {
|
||||||
|
p := auth.GetAuthParams(element)
|
||||||
|
if p != nil {
|
||||||
|
// If an auth flow has params, make a new copy
|
||||||
|
// and send it as part of the response.
|
||||||
|
paramsCopy[key] = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return UserInteractiveResponse{
|
||||||
|
fs, sessions.getCompletedStages(sessionID), paramsCopy, sessionID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ func handlePublicKeyRegistration(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isValidUserId := authHandler.IsValidUserIdForRegistration(r.Username)
|
isValidUserId := authHandler.IsValidUserId(r.Username)
|
||||||
if !isValidUserId {
|
if !isValidUserId {
|
||||||
return false, "", &util.JSONResponse{
|
return false, "", &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
|
|
|
||||||
386
clientapi/routing/register_publickey_test.go
Normal file
386
clientapi/routing/register_publickey_test.go
Normal file
|
|
@ -0,0 +1,386 @@
|
||||||
|
// Copyright 2022 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 routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
"github.com/matrix-org/dendrite/internal/mapsutil"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/dendrite/test"
|
||||||
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testCaip10UserId = "eip155=3a1=3a0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb"
|
||||||
|
|
||||||
|
type registerContext struct {
|
||||||
|
config *config.ClientAPI
|
||||||
|
userInteractive *auth.UserInteractive
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRegisterContext(t *testing.T) *registerContext {
|
||||||
|
chainIds := []int{4}
|
||||||
|
|
||||||
|
cfg := &config.ClientAPI{
|
||||||
|
Matrix: &config.Global{
|
||||||
|
ServerName: test.TestServerName,
|
||||||
|
},
|
||||||
|
Derived: &config.Derived{},
|
||||||
|
PasswordAuthenticationDisabled: true,
|
||||||
|
PublicKeyAuthentication: config.PublicKeyAuthentication{
|
||||||
|
Ethereum: config.EthereumAuthConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Version: 1,
|
||||||
|
ChainIDs: chainIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pkFlows := cfg.PublicKeyAuthentication.GetPublicKeyRegistrationFlows()
|
||||||
|
cfg.Derived.Registration.Flows = append(cfg.Derived.Registration.Flows, pkFlows...)
|
||||||
|
pkParams := cfg.PublicKeyAuthentication.GetPublicKeyRegistrationParams()
|
||||||
|
cfg.Derived.Registration.Params = mapsutil.MapsUnion(cfg.Derived.Registration.Params, pkParams)
|
||||||
|
|
||||||
|
var userAPI fakePublicKeyUserApi
|
||||||
|
var loginApi uapi.UserLoginAPI
|
||||||
|
|
||||||
|
userInteractive := auth.NewUserInteractive(
|
||||||
|
loginApi,
|
||||||
|
&userAPI,
|
||||||
|
cfg)
|
||||||
|
|
||||||
|
return ®isterContext{
|
||||||
|
config: cfg,
|
||||||
|
userInteractive: userInteractive,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeHttpRequest struct {
|
||||||
|
request *http.Request
|
||||||
|
body []byte
|
||||||
|
registerRequest registerRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFakeHttpRequest(body string) *fakeHttpRequest {
|
||||||
|
var r registerRequest
|
||||||
|
req, _ := http.NewRequest(http.MethodPost, "", strings.NewReader(body))
|
||||||
|
reqBody := []byte(body)
|
||||||
|
json.Unmarshal([]byte(body), &r)
|
||||||
|
|
||||||
|
return &fakeHttpRequest{
|
||||||
|
request: req,
|
||||||
|
body: reqBody,
|
||||||
|
registerRequest: r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakePublicKeyUserApi struct {
|
||||||
|
auth.UserInternalAPIForLogin
|
||||||
|
uapi.UserLoginAPI
|
||||||
|
uapi.ClientUserAPI
|
||||||
|
DeletedTokens []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ua *fakePublicKeyUserApi) QueryAccountAvailability(ctx context.Context, req *uapi.QueryAccountAvailabilityRequest, res *uapi.QueryAccountAvailabilityResponse) error {
|
||||||
|
if req.Localpart == "does_not_exist" {
|
||||||
|
res.Available = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Available = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ua *fakePublicKeyUserApi) QueryAccountByPassword(ctx context.Context, req *uapi.QueryAccountByPasswordRequest, res *uapi.QueryAccountByPasswordResponse) error {
|
||||||
|
if req.PlaintextPassword == "invalidpassword" {
|
||||||
|
res.Account = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
res.Exists = true
|
||||||
|
res.Account = &uapi.Account{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ua *fakePublicKeyUserApi) PerformDeviceCreation(
|
||||||
|
ctx context.Context,
|
||||||
|
req *uapi.PerformDeviceCreationRequest,
|
||||||
|
res *uapi.PerformDeviceCreationResponse) error {
|
||||||
|
res.DeviceCreated = true
|
||||||
|
res.Device = &api.Device{
|
||||||
|
ID: "device_id",
|
||||||
|
UserID: req.Localpart,
|
||||||
|
AccessToken: req.AccessToken,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ua *fakePublicKeyUserApi) PerformAccountCreation(
|
||||||
|
ctx context.Context,
|
||||||
|
req *uapi.PerformAccountCreationRequest,
|
||||||
|
res *uapi.PerformAccountCreationResponse) error {
|
||||||
|
res.AccountCreated = true
|
||||||
|
res.Account = &api.Account{
|
||||||
|
AppServiceID: req.AppServiceID,
|
||||||
|
Localpart: req.Localpart,
|
||||||
|
ServerName: test.TestServerName,
|
||||||
|
UserID: fmt.Sprintf("@%s:%s", req.Localpart, test.TestServerName),
|
||||||
|
AccountType: req.AccountType,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ua *fakePublicKeyUserApi) PerformLoginTokenDeletion(ctx context.Context, req *uapi.PerformLoginTokenDeletionRequest, res *uapi.PerformLoginTokenDeletionResponse) error {
|
||||||
|
ua.DeletedTokens = append(ua.DeletedTokens, req.Token)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ua *fakePublicKeyUserApi) PerformLoginTokenCreation(ctx context.Context, req *uapi.PerformLoginTokenCreationRequest, res *uapi.PerformLoginTokenCreationResponse) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*fakePublicKeyUserApi) QueryLoginToken(ctx context.Context, req *uapi.QueryLoginTokenRequest, res *uapi.QueryLoginTokenResponse) error {
|
||||||
|
if req.Token == "invalidtoken" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Data = &uapi.LoginTokenData{UserID: "@auser:example.com"}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRegistrationSession(
|
||||||
|
t *testing.T,
|
||||||
|
userId string,
|
||||||
|
cfg *config.ClientAPI,
|
||||||
|
userInteractive *auth.UserInteractive,
|
||||||
|
userAPI *fakePublicKeyUserApi,
|
||||||
|
) string {
|
||||||
|
body := fmt.Sprintf(`{
|
||||||
|
"auth": {
|
||||||
|
"type": "m.login.publickey",
|
||||||
|
"username": "%v"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
userId)
|
||||||
|
|
||||||
|
test := struct {
|
||||||
|
Body string
|
||||||
|
}{
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeReq := createFakeHttpRequest(test.Body)
|
||||||
|
sessionID := util.RandomString(sessionIDLength)
|
||||||
|
registerContext := createRegisterContext(t)
|
||||||
|
|
||||||
|
// Test
|
||||||
|
response := handleRegistrationFlow(
|
||||||
|
fakeReq.request,
|
||||||
|
fakeReq.body,
|
||||||
|
fakeReq.registerRequest,
|
||||||
|
sessionID,
|
||||||
|
registerContext.config,
|
||||||
|
userAPI,
|
||||||
|
"",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
json := response.JSON.(UserInteractiveResponse)
|
||||||
|
return json.Session
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterEthereum(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
var userAPI fakePublicKeyUserApi
|
||||||
|
wallet, _ := test.CreateTestAccount()
|
||||||
|
message, _ := test.CreateEip4361TestMessage(wallet.PublicAddress)
|
||||||
|
signature, _ := test.SignMessage(message.String(), wallet.PrivateKey)
|
||||||
|
registerContext := createRegisterContext(t)
|
||||||
|
sessionId := newRegistrationSession(
|
||||||
|
t,
|
||||||
|
wallet.Eip155UserId,
|
||||||
|
registerContext.config,
|
||||||
|
registerContext.userInteractive,
|
||||||
|
&userAPI,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Escape \t and \n. Work around for marshalling and unmarshalling message.
|
||||||
|
msgStr := test.FromEip4361MessageToString(message)
|
||||||
|
body := fmt.Sprintf(`{
|
||||||
|
"username": "%v",
|
||||||
|
"auth": {
|
||||||
|
"type": "m.login.publickey",
|
||||||
|
"session": "%v",
|
||||||
|
"public_key_response": {
|
||||||
|
"type": "m.login.publickey.ethereum",
|
||||||
|
"session": "%v",
|
||||||
|
"user_id": "%v",
|
||||||
|
"message": "%v",
|
||||||
|
"signature": "%v"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
wallet.Eip155UserId,
|
||||||
|
sessionId,
|
||||||
|
sessionId,
|
||||||
|
wallet.Eip155UserId,
|
||||||
|
msgStr,
|
||||||
|
signature,
|
||||||
|
)
|
||||||
|
test := struct {
|
||||||
|
Body string
|
||||||
|
}{
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeReq := createFakeHttpRequest(test.Body)
|
||||||
|
|
||||||
|
// Test
|
||||||
|
response := handleRegistrationFlow(
|
||||||
|
fakeReq.request,
|
||||||
|
fakeReq.body,
|
||||||
|
fakeReq.registerRequest,
|
||||||
|
sessionId,
|
||||||
|
registerContext.config,
|
||||||
|
&userAPI,
|
||||||
|
"",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Asserts
|
||||||
|
assert := assert.New(t)
|
||||||
|
assert.NotNil(response, "response actual: nil, expected: not nil")
|
||||||
|
registerRes := response.JSON.(registerResponse)
|
||||||
|
assert.Truef(
|
||||||
|
registerRes.UserID == wallet.Eip155UserId,
|
||||||
|
"registerRes.UserID actual: %v, expected: %v", registerRes.UserID, wallet.Eip155UserId)
|
||||||
|
assert.NotEmptyf(
|
||||||
|
registerRes.AccessToken,
|
||||||
|
"registerRes.AccessToken actual: empty, expected: not empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRegistrationSession(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
var userAPI fakePublicKeyUserApi
|
||||||
|
|
||||||
|
body := fmt.Sprintf(`{
|
||||||
|
"auth": {
|
||||||
|
"type": "m.login.publickey",
|
||||||
|
"username": "%v"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
testCaip10UserId)
|
||||||
|
|
||||||
|
test := struct {
|
||||||
|
Body string
|
||||||
|
}{
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeReq := createFakeHttpRequest(test.Body)
|
||||||
|
sessionID := util.RandomString(sessionIDLength)
|
||||||
|
registerContext := createRegisterContext(t)
|
||||||
|
|
||||||
|
// Test
|
||||||
|
response := handleRegistrationFlow(
|
||||||
|
fakeReq.request,
|
||||||
|
fakeReq.body,
|
||||||
|
fakeReq.registerRequest,
|
||||||
|
sessionID,
|
||||||
|
registerContext.config,
|
||||||
|
&userAPI,
|
||||||
|
"",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Asserts
|
||||||
|
assert := assert.New(t)
|
||||||
|
assert.NotNilf(response, "response not nil")
|
||||||
|
assert.Truef(
|
||||||
|
response.Code == http.StatusUnauthorized,
|
||||||
|
"response.Code actual %v, expected %v", response.Code, http.StatusUnauthorized)
|
||||||
|
json := response.JSON.(UserInteractiveResponse)
|
||||||
|
assert.NotEmptyf(json.Session, "response.Session")
|
||||||
|
assert.NotEmptyf(json.Completed, "response.Completed")
|
||||||
|
assert.Truef(
|
||||||
|
json.Completed[0] == authtypes.LoginStagePublicKeyNewRegistration,
|
||||||
|
"response.Completed[0] actual %v, expected %v", json.Completed[0], authtypes.LoginStagePublicKeyNewRegistration)
|
||||||
|
assert.Truef(
|
||||||
|
authtypes.LoginTypePublicKeyEthereum == json.Flows[0].Stages[0],
|
||||||
|
"response.Flows[0].Stages[0] actual: %v, expected: %v", json.Flows[0].Stages[0], authtypes.LoginTypePublicKeyEthereum)
|
||||||
|
|
||||||
|
params := json.Params[authtypes.LoginTypePublicKeyEthereum]
|
||||||
|
assert.NotEmptyf(
|
||||||
|
params,
|
||||||
|
"response.Params[\"%v\"] actual %v, expected %v",
|
||||||
|
authtypes.LoginTypePublicKeyEthereum,
|
||||||
|
params,
|
||||||
|
"[object]")
|
||||||
|
ethParams := params.(config.EthereumAuthParams)
|
||||||
|
assert.NotEmptyf(ethParams.ChainIDs, "ChainIDs actual: empty, expected not empty")
|
||||||
|
assert.NotEmptyf(ethParams.Nonce, "Nonce actual: \"\", expected: not empty")
|
||||||
|
assert.NotEmptyf(ethParams.Version, "Version actual: \"\", expected: not empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistrationUnimplementedAlgo(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
var userAPI fakePublicKeyUserApi
|
||||||
|
body := fmt.Sprintf(`{
|
||||||
|
"auth": {
|
||||||
|
"type": "m.login.publickey.someAlgo",
|
||||||
|
"username": "%v"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
testCaip10UserId)
|
||||||
|
|
||||||
|
test := struct {
|
||||||
|
Body string
|
||||||
|
}{
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeReq := createFakeHttpRequest(test.Body)
|
||||||
|
sessionID := util.RandomString(sessionIDLength)
|
||||||
|
registerContext := createRegisterContext(t)
|
||||||
|
|
||||||
|
// Test
|
||||||
|
response := handleRegistrationFlow(
|
||||||
|
fakeReq.request,
|
||||||
|
fakeReq.body,
|
||||||
|
fakeReq.registerRequest,
|
||||||
|
sessionID,
|
||||||
|
registerContext.config,
|
||||||
|
&userAPI,
|
||||||
|
"",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Asserts
|
||||||
|
assert := assert.New(t)
|
||||||
|
assert.NotNilf(response, "response not nil")
|
||||||
|
assert.Truef(
|
||||||
|
response.Code == http.StatusNotImplemented,
|
||||||
|
"response.Code actual %v, expected %v", response.Code, http.StatusNotImplemented)
|
||||||
|
}
|
||||||
2
go.mod
2
go.mod
|
|
@ -59,7 +59,7 @@ require (
|
||||||
nhooyr.io/websocket v1.8.7
|
nhooyr.io/websocket v1.8.7
|
||||||
)
|
)
|
||||||
|
|
||||||
require github.com/ethereum/go-ethereum v1.10.15 // indirect
|
require github.com/ethereum/go-ethereum v1.10.15
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ type ClientAPI struct {
|
||||||
PasswordAuthenticationDisabled bool `yaml:"password_authentication_disabled"`
|
PasswordAuthenticationDisabled bool `yaml:"password_authentication_disabled"`
|
||||||
|
|
||||||
// Public key authentication
|
// Public key authentication
|
||||||
PublicKeyAuthentication publicKeyAuthentication `yaml:"public_key_authentication"`
|
PublicKeyAuthentication PublicKeyAuthentication `yaml:"public_key_authentication"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClientAPI) Defaults(generate bool) {
|
func (c *ClientAPI) Defaults(generate bool) {
|
||||||
|
|
|
||||||
|
|
@ -32,21 +32,21 @@ func (p EthereumAuthParams) GetNonce() string {
|
||||||
return p.Nonce
|
return p.Nonce
|
||||||
}
|
}
|
||||||
|
|
||||||
type ethereumAuthConfig struct {
|
type EthereumAuthConfig struct {
|
||||||
Enabled bool `yaml:"enabled"`
|
Enabled bool `yaml:"enabled"`
|
||||||
Version uint `yaml:"version"`
|
Version uint `yaml:"version"`
|
||||||
ChainIDs []int `yaml:"chain_ids"`
|
ChainIDs []int `yaml:"chain_ids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type publicKeyAuthentication struct {
|
type PublicKeyAuthentication struct {
|
||||||
Ethereum ethereumAuthConfig `yaml:"ethereum"`
|
Ethereum EthereumAuthConfig `yaml:"ethereum"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pk *publicKeyAuthentication) Enabled() bool {
|
func (pk *PublicKeyAuthentication) Enabled() bool {
|
||||||
return pk.Ethereum.Enabled
|
return pk.Ethereum.Enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pk *publicKeyAuthentication) GetPublicKeyRegistrationFlows() []authtypes.Flow {
|
func (pk *PublicKeyAuthentication) GetPublicKeyRegistrationFlows() []authtypes.Flow {
|
||||||
var flows []authtypes.Flow
|
var flows []authtypes.Flow
|
||||||
if pk.Ethereum.Enabled {
|
if pk.Ethereum.Enabled {
|
||||||
flows = append(flows, authtypes.Flow{Stages: []authtypes.LoginType{authtypes.LoginTypePublicKeyEthereum}})
|
flows = append(flows, authtypes.Flow{Stages: []authtypes.LoginType{authtypes.LoginTypePublicKeyEthereum}})
|
||||||
|
|
@ -55,7 +55,7 @@ func (pk *publicKeyAuthentication) GetPublicKeyRegistrationFlows() []authtypes.F
|
||||||
return flows
|
return flows
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pk *publicKeyAuthentication) GetPublicKeyRegistrationParams() map[string]interface{} {
|
func (pk *PublicKeyAuthentication) GetPublicKeyRegistrationParams() map[string]interface{} {
|
||||||
params := make(map[string]interface{})
|
params := make(map[string]interface{})
|
||||||
if pk.Ethereum.Enabled {
|
if pk.Ethereum.Enabled {
|
||||||
p := EthereumAuthParams{
|
p := EthereumAuthParams{
|
||||||
|
|
|
||||||
108
test/publickey_utils.go
Normal file
108
test/publickey_utils.go
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
// Copyright 2022 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 test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/spruceid/siwe-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
const EthereumTestNetworkId = 4 // Rinkeby test network ID
|
||||||
|
const TestServerName = "localhost"
|
||||||
|
|
||||||
|
type EthereumTestWallet struct {
|
||||||
|
Eip155UserId string
|
||||||
|
PublicAddress string
|
||||||
|
PrivateKey *ecdsa.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://goethereumbook.org/wallet-generate/
|
||||||
|
func CreateTestAccount() (*EthereumTestWallet, error) {
|
||||||
|
// Create a new public / private key pair.
|
||||||
|
privateKey, err := crypto.GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the public key
|
||||||
|
publicKey := privateKey.Public()
|
||||||
|
|
||||||
|
// Transform public key to the Ethereum address
|
||||||
|
publicKeyEcdsa, ok := publicKey.(*ecdsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("error casting public key to ECDSA")
|
||||||
|
}
|
||||||
|
|
||||||
|
address := crypto.PubkeyToAddress(*publicKeyEcdsa).Hex()
|
||||||
|
eip155UserId := fmt.Sprintf("eip155=3a%d=3a%s", EthereumTestNetworkId, address)
|
||||||
|
|
||||||
|
return &EthereumTestWallet{
|
||||||
|
PublicAddress: address,
|
||||||
|
PrivateKey: privateKey,
|
||||||
|
Eip155UserId: eip155UserId,
|
||||||
|
},
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateEip4361TestMessage(
|
||||||
|
publicAddress string,
|
||||||
|
) (*siwe.Message, error) {
|
||||||
|
options := make(map[string]interface{})
|
||||||
|
options["chainId"] = 4 // Rinkeby test network
|
||||||
|
options["statement"] = "This is a test statement"
|
||||||
|
message, err := siwe.InitMessage(
|
||||||
|
TestServerName,
|
||||||
|
publicAddress,
|
||||||
|
"https://localhost/login",
|
||||||
|
siwe.GenerateNonce(),
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return message, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromEip4361MessageToString(message *siwe.Message) string {
|
||||||
|
// Escape the formatting characters to
|
||||||
|
// prevent unmarshal exceptions.
|
||||||
|
str := strings.ReplaceAll(message.String(), "\n", "\\n")
|
||||||
|
str = strings.ReplaceAll(str, "\t", "\\t")
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://goethereumbook.org/signature-generate/
|
||||||
|
func SignMessage(message string, privateKey *ecdsa.PrivateKey) (string, error) {
|
||||||
|
msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(message), message)
|
||||||
|
data := []byte(msg)
|
||||||
|
hash := crypto.Keccak256Hash(data)
|
||||||
|
|
||||||
|
signature, err := crypto.Sign(hash.Bytes(), privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/ethereum/go-ethereum/blob/55599ee95d4151a2502465e0afc7c47bd1acba77/internal/ethapi/api.go#L442
|
||||||
|
signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
|
||||||
|
return hexutil.Encode(signature), nil
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue