mirror of
https://github.com/matrix-org/dendrite.git
synced 2026-01-01 03:03:10 -06:00
implement MSC 3782
This commit is contained in:
parent
0eb5bd1e13
commit
390d557df8
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -66,3 +66,6 @@ test/wasm/node_modules
|
||||||
complement/
|
complement/
|
||||||
|
|
||||||
media_store/
|
media_store/
|
||||||
|
|
||||||
|
# Debug
|
||||||
|
**/__debug_bin
|
||||||
|
|
@ -11,4 +11,9 @@ const (
|
||||||
LoginTypeRecaptcha = "m.login.recaptcha"
|
LoginTypeRecaptcha = "m.login.recaptcha"
|
||||||
LoginTypeApplicationService = "m.login.application_service"
|
LoginTypeApplicationService = "m.login.application_service"
|
||||||
LoginTypeToken = "m.login.token"
|
LoginTypeToken = "m.login.token"
|
||||||
|
LoginTypePublicKey = "m.login.publickey"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LoginTypePublicKeyEthereum = "m.login.publickey.ethereum"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,16 @@ import (
|
||||||
// called after authorization has completed, with the result of the authorization.
|
// 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
|
// If the final return value is non-nil, an error occurred and the cleanup function
|
||||||
// is nil.
|
// is nil.
|
||||||
func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.UserAccountAPI, userAPI UserInternalAPIForLogin, cfg *config.ClientAPI) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
func LoginFromJSONReader(
|
||||||
|
ctx context.Context,
|
||||||
|
r io.Reader,
|
||||||
|
useraccountAPI uapi.UserAccountAPI,
|
||||||
|
userAPI UserInternalAPIForLogin,
|
||||||
|
userRegisterAPI uapi.UserRegisterAPI,
|
||||||
|
userInteractiveAuth *UserInteractive,
|
||||||
|
cfg *config.ClientAPI,
|
||||||
|
) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
||||||
|
|
||||||
reqBytes, err := ioutil.ReadAll(r)
|
reqBytes, err := ioutil.ReadAll(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := &util.JSONResponse{
|
err := &util.JSONResponse{
|
||||||
|
|
@ -55,17 +64,23 @@ func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.U
|
||||||
}
|
}
|
||||||
|
|
||||||
var typ Type
|
var typ Type
|
||||||
switch header.Type {
|
switch {
|
||||||
case authtypes.LoginTypePassword:
|
case header.Type == authtypes.LoginTypePassword && !cfg.PasswordAuthenticationDisabled:
|
||||||
typ = &LoginTypePassword{
|
typ = &LoginTypePassword{
|
||||||
GetAccountByPassword: useraccountAPI.QueryAccountByPassword,
|
GetAccountByPassword: useraccountAPI.QueryAccountByPassword,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
case authtypes.LoginTypeToken:
|
case header.Type == authtypes.LoginTypeToken:
|
||||||
typ = &LoginTypeToken{
|
typ = &LoginTypeToken{
|
||||||
UserAPI: userAPI,
|
UserAPI: userAPI,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
|
case header.Type == authtypes.LoginTypePublicKey && cfg.PublicKeyAuthentication.Enabled():
|
||||||
|
typ = &LoginTypePublicKey{
|
||||||
|
UserAPI: userRegisterAPI,
|
||||||
|
UserInteractive: userInteractiveAuth,
|
||||||
|
Config: cfg,
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
err := util.JSONResponse{
|
err := util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
|
||||||
149
clientapi/auth/login_publickey.go
Normal file
149
clientapi/auth/login_publickey.go
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
// 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"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
|
"github.com/matrix-org/dendrite/internal/mapsutil"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
userApi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoginPublicKeyHandler interface {
|
||||||
|
AccountExists(ctx context.Context) (string, *jsonerror.MatrixError)
|
||||||
|
CreateLogin() *Login
|
||||||
|
GetSession() string
|
||||||
|
GetType() string
|
||||||
|
ValidateLoginResponse() (bool, *jsonerror.MatrixError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginTypePublicKey implements https://matrix.org/docs/spec/client_server/..... (to be spec'ed)
|
||||||
|
type LoginTypePublicKey struct {
|
||||||
|
UserAPI userApi.UserRegisterAPI
|
||||||
|
UserInteractive *UserInteractive
|
||||||
|
Config *config.ClientAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *LoginTypePublicKey) Name() string {
|
||||||
|
return authtypes.LoginTypePublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *LoginTypePublicKey) AddFlows(userInteractive *UserInteractive) {
|
||||||
|
if t.Config.PublicKeyAuthentication.Ethereum.Enabled {
|
||||||
|
userInteractive.Flows = append(userInteractive.Flows, userInteractiveFlow{
|
||||||
|
Stages: []string{
|
||||||
|
authtypes.LoginTypePublicKeyEthereum,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
params := t.Config.PublicKeyAuthentication.GetPublicKeyRegistrationParams()
|
||||||
|
userInteractive.Params = mapsutil.MapsUnion(userInteractive.Params, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Config.PublicKeyAuthentication.Enabled() {
|
||||||
|
userInteractive.Types[t.Name()] = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginFromJSON implements Type.
|
||||||
|
func (t *LoginTypePublicKey) LoginFromJSON(ctx context.Context, reqBytes []byte) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
||||||
|
// "A client should first make a request with no auth parameter. The homeserver returns an HTTP 401 response, with a JSON body"
|
||||||
|
// https://matrix.org/docs/spec/client_server/r0.6.1#user-interactive-api-in-the-rest-api
|
||||||
|
authBytes := gjson.GetBytes(reqBytes, "auth")
|
||||||
|
if !authBytes.Exists() {
|
||||||
|
return nil, nil, t.UserInteractive.NewSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
var authHandler LoginPublicKeyHandler
|
||||||
|
authType := gjson.GetBytes(reqBytes, "auth.type").String()
|
||||||
|
|
||||||
|
switch authType {
|
||||||
|
case authtypes.LoginTypePublicKeyEthereum:
|
||||||
|
pkEthHandler, err := CreatePublicKeyEthereumHandler(
|
||||||
|
[]byte(authBytes.Raw),
|
||||||
|
t.UserAPI,
|
||||||
|
t.Config,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
authHandler = *pkEthHandler
|
||||||
|
default:
|
||||||
|
return nil, nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: jsonerror.InvalidParam("auth.type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.continueLoginFlow(ctx, authHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *LoginTypePublicKey) continueLoginFlow(ctx context.Context, authHandler LoginPublicKeyHandler) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
||||||
|
loginOK := false
|
||||||
|
sessionID := authHandler.GetSession()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if loginOK {
|
||||||
|
t.UserInteractive.AddCompletedStage(sessionID, authHandler.GetType())
|
||||||
|
} else {
|
||||||
|
t.UserInteractive.DeleteSession(sessionID)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _, ok := t.UserInteractive.Sessions[sessionID]; !ok {
|
||||||
|
return nil, nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: jsonerror.Unknown("the session ID is missing or unknown."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localPart, err := authHandler.AccountExists(ctx)
|
||||||
|
// user account does not exist or there is an error.
|
||||||
|
if localPart == "" || err != nil {
|
||||||
|
return nil, nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// user account exists
|
||||||
|
isValidated, err := authHandler.ValidateLoginResponse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isValidated {
|
||||||
|
loginOK = true
|
||||||
|
login := authHandler.CreateLogin()
|
||||||
|
return login, func(context.Context, *util.JSONResponse) {}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: jsonerror.Unknown("authentication failed, or the account does not exist."),
|
||||||
|
}
|
||||||
|
}
|
||||||
233
clientapi/auth/login_publickey_ethereum.go
Normal file
233
clientapi/auth/login_publickey_ethereum.go
Normal file
|
|
@ -0,0 +1,233 @@
|
||||||
|
// 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"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/accounts"
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/userutil"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
userApi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoginPublicKeyEthereum struct {
|
||||||
|
// Todo: See https://...
|
||||||
|
Type string `json:"type"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Session string `json:"session"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
HashFields publicKeyEthereumHashFields `json:"hashFields"`
|
||||||
|
HashFieldsRaw string // Raw base64 encoded string of MessageFields for hash verification
|
||||||
|
|
||||||
|
userAPI userApi.UserRegisterAPI
|
||||||
|
config *config.ClientAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
type publicKeyEthereumHashFields struct {
|
||||||
|
// Todo: See https://...
|
||||||
|
Domain string `json:"domain"` // home server domain
|
||||||
|
Address string `json:"address"` // Ethereum address. 0x...
|
||||||
|
Nonce string `json:"nonce"` // session ID
|
||||||
|
Version string `json:"version"` // version of the Matrix public key spec that the client is complying with
|
||||||
|
ChainId string `json:"chainId"` // blockchain network ID.
|
||||||
|
}
|
||||||
|
|
||||||
|
type publicKeyEthereumRequiredFields struct {
|
||||||
|
From string // Sender
|
||||||
|
To string // Recipient
|
||||||
|
Hash string // Hash of JSON representation of the message fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreatePublicKeyEthereumHandler(
|
||||||
|
reqBytes []byte,
|
||||||
|
userAPI userApi.UserRegisterAPI,
|
||||||
|
config *config.ClientAPI,
|
||||||
|
) (*LoginPublicKeyEthereum, *jsonerror.MatrixError) {
|
||||||
|
var pk LoginPublicKeyEthereum
|
||||||
|
if err := json.Unmarshal(reqBytes, &pk); err != nil {
|
||||||
|
return nil, jsonerror.BadJSON("auth")
|
||||||
|
}
|
||||||
|
|
||||||
|
hashFields := gjson.GetBytes(reqBytes, "hashFields")
|
||||||
|
if !hashFields.Exists() {
|
||||||
|
return nil, jsonerror.BadJSON("auth.hashFields")
|
||||||
|
}
|
||||||
|
|
||||||
|
pk.config = config
|
||||||
|
pk.userAPI = userAPI
|
||||||
|
// Save raw bytes for hash verification later.
|
||||||
|
pk.HashFieldsRaw = hashFields.Raw
|
||||||
|
// Case-insensitive
|
||||||
|
pk.Address = strings.ToLower(pk.Address)
|
||||||
|
|
||||||
|
return &pk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk LoginPublicKeyEthereum) GetSession() string {
|
||||||
|
return pk.Session
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk LoginPublicKeyEthereum) GetType() string {
|
||||||
|
return pk.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk LoginPublicKeyEthereum) AccountExists(ctx context.Context) (string, *jsonerror.MatrixError) {
|
||||||
|
localPart, err := userutil.ParseUsernameParam(pk.Address, &pk.config.Matrix.ServerName)
|
||||||
|
if err != nil {
|
||||||
|
// userId does not exist
|
||||||
|
return "", jsonerror.Forbidden("the address is incorrect, or the account does not exist.")
|
||||||
|
}
|
||||||
|
|
||||||
|
res := userApi.QueryAccountAvailabilityResponse{}
|
||||||
|
if err := pk.userAPI.QueryAccountAvailability(ctx, &userApi.QueryAccountAvailabilityRequest{
|
||||||
|
Localpart: localPart,
|
||||||
|
}, &res); err != nil {
|
||||||
|
return "", jsonerror.Unknown("failed to check availability: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Available {
|
||||||
|
return "", jsonerror.Forbidden("the address is incorrect, account does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
return localPart, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk LoginPublicKeyEthereum) ValidateLoginResponse() (bool, *jsonerror.MatrixError) {
|
||||||
|
// Check signature to verify message was not tempered
|
||||||
|
isVerified := verifySignature(pk.Address, []byte(pk.Message), pk.Signature)
|
||||||
|
if !isVerified {
|
||||||
|
return false, jsonerror.InvalidSignature("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the required message fields for validation
|
||||||
|
requiredFields, err := extractRequiredMessageFields(pk.Message)
|
||||||
|
if err != nil {
|
||||||
|
return false, jsonerror.MissingParam("message does not contain domain, address, or hash")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the hash is valid for the message fields.
|
||||||
|
if !verifyHash(pk.HashFieldsRaw, requiredFields.Hash) {
|
||||||
|
return false, jsonerror.InvalidParam("error verifying message hash")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal the hashFields for further validation
|
||||||
|
var authData publicKeyEthereumHashFields
|
||||||
|
if err := json.Unmarshal([]byte(pk.HashFieldsRaw), &authData); err != nil {
|
||||||
|
return false, jsonerror.BadJSON("auth.hashFields")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error if the message is not from the expected public address
|
||||||
|
if pk.Address != requiredFields.From || requiredFields.From != pk.HashFields.Address {
|
||||||
|
return false, jsonerror.InvalidParam("address")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error if the message is not for the home server
|
||||||
|
if requiredFields.To != pk.HashFields.Domain {
|
||||||
|
return false, jsonerror.InvalidParam("domain")
|
||||||
|
}
|
||||||
|
|
||||||
|
// No errors.
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk LoginPublicKeyEthereum) CreateLogin() *Login {
|
||||||
|
identifier := LoginIdentifier{
|
||||||
|
Type: "m.id.user",
|
||||||
|
User: pk.Address,
|
||||||
|
}
|
||||||
|
login := Login{
|
||||||
|
Identifier: identifier,
|
||||||
|
}
|
||||||
|
return &login
|
||||||
|
}
|
||||||
|
|
||||||
|
// The required fields in the signed message are:
|
||||||
|
// 1. Domain -- home server. First non-whitespace characters in the first line.
|
||||||
|
// 2. Address -- public address of the user. Starts with 0x... in the second line on its own.
|
||||||
|
// 3. Hash -- Base64-encoded hash string of the metadata that represents the message.
|
||||||
|
// The rest of the fields are informational, and will be used in the future.
|
||||||
|
var regexpAuthority = regexp.MustCompile(`^\S+`)
|
||||||
|
var regexpAddress = regexp.MustCompile(`\n(?P<address>0x\w+)\n`)
|
||||||
|
var regexpHash = regexp.MustCompile(`\nHash: (?P<hash>.*)\n`)
|
||||||
|
|
||||||
|
func extractRequiredMessageFields(message string) (*publicKeyEthereumRequiredFields, error) {
|
||||||
|
var requiredFields publicKeyEthereumRequiredFields
|
||||||
|
/*
|
||||||
|
service.org wants you to sign in with your account:
|
||||||
|
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
|
||||||
|
|
||||||
|
I accept the ServiceOrg Terms of Service: https://service.org/tos
|
||||||
|
|
||||||
|
Hash: yfSIwarByPfKFxeYSCWN3XoIgNgeEFJffbwFA+JxYbA=
|
||||||
|
*/
|
||||||
|
|
||||||
|
requiredFields.To = regexpAuthority.FindString(message)
|
||||||
|
|
||||||
|
from := regexpAddress.FindStringSubmatch(message)
|
||||||
|
if len(from) == 2 {
|
||||||
|
requiredFields.From = from[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := regexpHash.FindStringSubmatch(message)
|
||||||
|
if len(hash) == 2 {
|
||||||
|
requiredFields.Hash = hash[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(requiredFields.To) == 0 || len(requiredFields.From) == 0 || len(requiredFields.Hash) == 0 {
|
||||||
|
return nil, errors.New("required message fields are missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make these fields case-insensitive
|
||||||
|
requiredFields.From = strings.ToLower(requiredFields.From)
|
||||||
|
requiredFields.To = strings.ToLower(requiredFields.To)
|
||||||
|
|
||||||
|
return &requiredFields, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifySignature(from string, message []byte, signature string) bool {
|
||||||
|
decodedSig := hexutil.MustDecode(signature)
|
||||||
|
|
||||||
|
message = accounts.TextHash(message)
|
||||||
|
// Issue: https://stackoverflow.com/questions/49085737/geth-ecrecover-invalid-signature-recovery-id
|
||||||
|
// Fix: https://gist.github.com/dcb9/385631846097e1f59e3cba3b1d42f3ed#file-eth_sign_verify-go
|
||||||
|
decodedSig[crypto.RecoveryIDOffset] -= 27 // Transform yellow paper V from 27/28 to 0/1
|
||||||
|
|
||||||
|
recovered, err := crypto.SigToPub(message, decodedSig)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
recoveredAddr := crypto.PubkeyToAddress(*recovered)
|
||||||
|
|
||||||
|
addressStr := strings.ToLower(recoveredAddr.Hex())
|
||||||
|
return from == addressStr
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyHash(rawStr string, expectedHash string) bool {
|
||||||
|
hash := crypto.Keccak256([]byte(rawStr))
|
||||||
|
hashStr := base64.StdEncoding.EncodeToString(hash)
|
||||||
|
return expectedHash == hashStr
|
||||||
|
}
|
||||||
|
|
@ -61,6 +61,13 @@ func TestLoginFromJSONReader(t *testing.T) {
|
||||||
WantDeletedTokens: []string{"atoken"},
|
WantDeletedTokens: []string{"atoken"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
userInteractive := UserInteractive{
|
||||||
|
Completed: []string{},
|
||||||
|
Flows: []userInteractiveFlow{},
|
||||||
|
Types: make(map[string]Type),
|
||||||
|
Sessions: make(map[string][]string),
|
||||||
|
}
|
||||||
|
|
||||||
for _, tst := range tsts {
|
for _, tst := range tsts {
|
||||||
t.Run(tst.Name, func(t *testing.T) {
|
t.Run(tst.Name, func(t *testing.T) {
|
||||||
var userAPI fakeUserInternalAPI
|
var userAPI fakeUserInternalAPI
|
||||||
|
|
@ -69,7 +76,7 @@ func TestLoginFromJSONReader(t *testing.T) {
|
||||||
ServerName: serverName,
|
ServerName: serverName,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
login, cleanup, err := LoginFromJSONReader(ctx, strings.NewReader(tst.Body), &userAPI, &userAPI, cfg)
|
login, cleanup, err := LoginFromJSONReader(ctx, strings.NewReader(tst.Body), &userAPI, &userAPI, &userAPI, &userInteractive, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("LoginFromJSONReader failed: %+v", err)
|
t.Fatalf("LoginFromJSONReader failed: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -139,6 +146,13 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
||||||
WantErrCode: "M_INVALID_ARGUMENT_VALUE",
|
WantErrCode: "M_INVALID_ARGUMENT_VALUE",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
userInteractive := UserInteractive{
|
||||||
|
Completed: []string{},
|
||||||
|
Flows: []userInteractiveFlow{},
|
||||||
|
Types: make(map[string]Type),
|
||||||
|
Sessions: make(map[string][]string),
|
||||||
|
}
|
||||||
|
|
||||||
for _, tst := range tsts {
|
for _, tst := range tsts {
|
||||||
t.Run(tst.Name, func(t *testing.T) {
|
t.Run(tst.Name, func(t *testing.T) {
|
||||||
var userAPI fakeUserInternalAPI
|
var userAPI fakeUserInternalAPI
|
||||||
|
|
@ -147,7 +161,7 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
||||||
ServerName: serverName,
|
ServerName: serverName,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
_, cleanup, errRes := LoginFromJSONReader(ctx, strings.NewReader(tst.Body), &userAPI, &userAPI, cfg)
|
_, cleanup, errRes := LoginFromJSONReader(ctx, strings.NewReader(tst.Body), &userAPI, &userAPI, &userAPI, &userInteractive, cfg)
|
||||||
if errRes == nil {
|
if errRes == nil {
|
||||||
cleanup(ctx, nil)
|
cleanup(ctx, nil)
|
||||||
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
|
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
|
||||||
|
|
@ -161,6 +175,7 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
||||||
type fakeUserInternalAPI struct {
|
type fakeUserInternalAPI struct {
|
||||||
UserInternalAPIForLogin
|
UserInternalAPIForLogin
|
||||||
uapi.UserAccountAPI
|
uapi.UserAccountAPI
|
||||||
|
uapi.UserRegisterAPI
|
||||||
DeletedTokens []string
|
DeletedTokens []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,3 +107,12 @@ func (t *LoginTypePassword) Login(ctx context.Context, req interface{}) (*Login,
|
||||||
}
|
}
|
||||||
return &r.Login, nil
|
return &r.Login, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *LoginTypePassword) AddFLows(userInteractive *UserInteractive) {
|
||||||
|
flow := userInteractiveFlow{
|
||||||
|
Stages: []string{t.Name()},
|
||||||
|
}
|
||||||
|
|
||||||
|
userInteractive.Flows = append(userInteractive.Flows, flow)
|
||||||
|
userInteractive.Types[t.Name()] = t
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,25 +108,40 @@ type UserInteractive struct {
|
||||||
Types map[string]Type
|
Types map[string]Type
|
||||||
// Map of session ID to completed login types, will need to be extended in future
|
// Map of session ID to completed login types, will need to be extended in future
|
||||||
Sessions map[string][]string
|
Sessions map[string][]string
|
||||||
|
Params map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserInteractive(userAccountAPI api.UserAccountAPI, cfg *config.ClientAPI) *UserInteractive {
|
func NewUserInteractive(
|
||||||
typePassword := &LoginTypePassword{
|
userAccountAPI api.UserAccountAPI,
|
||||||
GetAccountByPassword: userAccountAPI.QueryAccountByPassword,
|
userRegisterAPI api.UserRegisterAPI,
|
||||||
Config: cfg,
|
cfg *config.ClientAPI,
|
||||||
}
|
) *UserInteractive {
|
||||||
return &UserInteractive{
|
userInteractive := UserInteractive{
|
||||||
Completed: []string{},
|
Completed: []string{},
|
||||||
Flows: []userInteractiveFlow{
|
Flows: []userInteractiveFlow{},
|
||||||
{
|
Types: make(map[string]Type),
|
||||||
Stages: []string{typePassword.Name()},
|
Sessions: make(map[string][]string),
|
||||||
},
|
Params: make(map[string]interface{}),
|
||||||
},
|
|
||||||
Types: map[string]Type{
|
|
||||||
typePassword.Name(): typePassword,
|
|
||||||
},
|
|
||||||
Sessions: make(map[string][]string),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !cfg.PasswordAuthenticationDisabled {
|
||||||
|
typePassword := &LoginTypePassword{
|
||||||
|
GetAccountByPassword: userAccountAPI.QueryAccountByPassword,
|
||||||
|
Config: cfg,
|
||||||
|
}
|
||||||
|
typePassword.AddFLows(&userInteractive)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.PublicKeyAuthentication.Enabled() {
|
||||||
|
typePublicKey := &LoginTypePublicKey{
|
||||||
|
userRegisterAPI,
|
||||||
|
&userInteractive,
|
||||||
|
cfg,
|
||||||
|
}
|
||||||
|
typePublicKey.AddFlows(&userInteractive)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &userInteractive
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UserInteractive) IsSingleStageFlow(authType string) bool {
|
func (u *UserInteractive) IsSingleStageFlow(authType string) bool {
|
||||||
|
|
@ -144,6 +159,10 @@ func (u *UserInteractive) AddCompletedStage(sessionID, authType string) {
|
||||||
delete(u.Sessions, sessionID)
|
delete(u.Sessions, sessionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *UserInteractive) DeleteSession(sessionID string) {
|
||||||
|
delete(u.Sessions, sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
type Challenge struct {
|
type Challenge struct {
|
||||||
Completed []string `json:"completed"`
|
Completed []string `json:"completed"`
|
||||||
Flows []userInteractiveFlow `json:"flows"`
|
Flows []userInteractiveFlow `json:"flows"`
|
||||||
|
|
@ -160,7 +179,7 @@ func (u *UserInteractive) Challenge(sessionID string) *util.JSONResponse {
|
||||||
Completed: u.Completed,
|
Completed: u.Completed,
|
||||||
Flows: u.Flows,
|
Flows: u.Flows,
|
||||||
Session: sessionID,
|
Session: sessionID,
|
||||||
Params: make(map[string]interface{}),
|
Params: u.Params,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -204,7 +223,7 @@ func (u *UserInteractive) ResponseWithChallenge(sessionID string, response inter
|
||||||
// Verify returns an error/challenge response to send to the client, or nil if the user is authenticated.
|
// Verify returns an error/challenge response to send to the client, or nil if the user is authenticated.
|
||||||
// `bodyBytes` is the HTTP request body which must contain an `auth` key.
|
// `bodyBytes` is the HTTP request body which must contain an `auth` key.
|
||||||
// Returns the login that was verified for additional checks if required.
|
// Returns the login that was verified for additional checks if required.
|
||||||
func (u *UserInteractive) Verify(ctx context.Context, bodyBytes []byte, device *api.Device) (*Login, *util.JSONResponse) {
|
func (u *UserInteractive) Verify(ctx context.Context, bodyBytes []byte) (*Login, *util.JSONResponse) {
|
||||||
// TODO: rate limit
|
// TODO: rate limit
|
||||||
|
|
||||||
// "A client should first make a request with no auth parameter. The homeserver returns an HTTP 401 response, with a JSON body"
|
// "A client should first make a request with no auth parameter. The homeserver returns an HTTP 401 response, with a JSON body"
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ var (
|
||||||
|
|
||||||
type fakeAccountDatabase struct {
|
type fakeAccountDatabase struct {
|
||||||
api.UserAccountAPI
|
api.UserAccountAPI
|
||||||
|
api.UserRegisterAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *fakeAccountDatabase) PerformPasswordUpdate(ctx context.Context, req *api.PerformPasswordUpdateRequest, res *api.PerformPasswordUpdateResponse) error {
|
func (d *fakeAccountDatabase) PerformPasswordUpdate(ctx context.Context, req *api.PerformPasswordUpdateRequest, res *api.PerformPasswordUpdateResponse) error {
|
||||||
|
|
@ -52,13 +53,14 @@ func setup() *UserInteractive {
|
||||||
ServerName: serverName,
|
ServerName: serverName,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return NewUserInteractive(&fakeAccountDatabase{}, cfg)
|
accountApi := fakeAccountDatabase{}
|
||||||
|
return NewUserInteractive(&accountApi, &accountApi, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUserInteractiveChallenge(t *testing.T) {
|
func TestUserInteractiveChallenge(t *testing.T) {
|
||||||
uia := setup()
|
uia := setup()
|
||||||
// no auth key results in a challenge
|
// no auth key results in a challenge
|
||||||
_, errRes := uia.Verify(ctx, []byte(`{}`), device)
|
_, errRes := uia.Verify(ctx, []byte(`{}`))
|
||||||
if errRes == nil {
|
if errRes == nil {
|
||||||
t.Fatalf("Verify succeeded with {} but expected failure")
|
t.Fatalf("Verify succeeded with {} but expected failure")
|
||||||
}
|
}
|
||||||
|
|
@ -98,7 +100,7 @@ func TestUserInteractivePasswordLogin(t *testing.T) {
|
||||||
}`),
|
}`),
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
_, errRes := uia.Verify(ctx, tc, device)
|
_, errRes := uia.Verify(ctx, tc)
|
||||||
if errRes != nil {
|
if errRes != nil {
|
||||||
t.Errorf("Verify failed but expected success for request: %s - got %+v", string(tc), errRes)
|
t.Errorf("Verify failed but expected success for request: %s - got %+v", string(tc), errRes)
|
||||||
}
|
}
|
||||||
|
|
@ -179,7 +181,7 @@ func TestUserInteractivePasswordBadLogin(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
_, errRes := uia.Verify(ctx, tc.body, device)
|
_, errRes := uia.Verify(ctx, tc.body)
|
||||||
if errRes == nil {
|
if errRes == nil {
|
||||||
t.Errorf("Verify succeeded but expected failure for request: %s", string(tc.body))
|
t.Errorf("Verify succeeded but expected failure for request: %s", string(tc.body))
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ func Deactivate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes, deviceAPI)
|
login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes)
|
||||||
if errRes != nil {
|
if errRes != nil {
|
||||||
return *errRes
|
return *errRes
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,7 @@ func DeleteDeviceById(
|
||||||
sessionID = s
|
sessionID = s
|
||||||
}
|
}
|
||||||
|
|
||||||
login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes, device)
|
login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes)
|
||||||
if errRes != nil {
|
if errRes != nil {
|
||||||
switch data := errRes.JSON.(type) {
|
switch data := errRes.JSON.(type) {
|
||||||
case auth.Challenge:
|
case auth.Challenge:
|
||||||
|
|
|
||||||
|
|
@ -42,28 +42,42 @@ type flow struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func passwordLogin() flows {
|
func passwordLogin(f *flows) {
|
||||||
f := flows{}
|
|
||||||
s := flow{
|
s := flow{
|
||||||
Type: "m.login.password",
|
Type: "m.login.password",
|
||||||
}
|
}
|
||||||
f.Flows = append(f.Flows, s)
|
f.Flows = append(f.Flows, s)
|
||||||
return f
|
}
|
||||||
|
|
||||||
|
func publicKeyLogin(f *flows) {
|
||||||
|
loginFlow := flow{
|
||||||
|
Type: "m.login.publickey",
|
||||||
|
}
|
||||||
|
f.Flows = append(f.Flows, loginFlow)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login implements GET and POST /login
|
// Login implements GET and POST /login
|
||||||
func Login(
|
func Login(
|
||||||
req *http.Request, userAPI userapi.UserInternalAPI,
|
req *http.Request,
|
||||||
|
userAPI userapi.UserInternalAPI,
|
||||||
|
userInteractiveAuth *auth.UserInteractive,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
if req.Method == http.MethodGet {
|
if req.Method == http.MethodGet {
|
||||||
// TODO: support other forms of login other than password, depending on config options
|
f := flows{}
|
||||||
|
if !cfg.PasswordAuthenticationDisabled {
|
||||||
|
passwordLogin(&f)
|
||||||
|
}
|
||||||
|
if cfg.PublicKeyAuthentication.Enabled() {
|
||||||
|
publicKeyLogin(&f)
|
||||||
|
}
|
||||||
|
// TODO: support other forms of login depending on config options
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: passwordLogin(),
|
JSON: f,
|
||||||
}
|
}
|
||||||
} else if req.Method == http.MethodPost {
|
} else if req.Method == http.MethodPost {
|
||||||
login, cleanup, authErr := auth.LoginFromJSONReader(req.Context(), req.Body, userAPI, userAPI, cfg)
|
login, cleanup, authErr := auth.LoginFromJSONReader(req.Context(), req.Body, userAPI, userAPI, userAPI, userInteractiveAuth, cfg)
|
||||||
if authErr != nil {
|
if authErr != nil {
|
||||||
return *authErr
|
return *authErr
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -589,6 +589,10 @@ func Register(
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.MissingArgument("A known registration type (e.g. m.login.application_service) must be specified if an access_token is provided"),
|
JSON: jsonerror.MissingArgument("A known registration type (e.g. m.login.application_service) must be specified if an access_token is provided"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case r.Auth.Type == authtypes.LoginTypePublicKey && cfg.PublicKeyAuthentication.Enabled():
|
||||||
|
// Skip checks here. Will be validated later.
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Spec-compliant case (neither the access_token nor the login type are
|
// Spec-compliant case (neither the access_token nor the login type are
|
||||||
// specified, so it's a normal user registration)
|
// specified, so it's a normal user registration)
|
||||||
|
|
@ -607,7 +611,7 @@ func Register(
|
||||||
"session_id": r.Auth.Session,
|
"session_id": r.Auth.Session,
|
||||||
}).Info("Processing registration request")
|
}).Info("Processing registration request")
|
||||||
|
|
||||||
return handleRegistrationFlow(req, r, sessionID, cfg, userAPI, accessToken, accessTokenErr)
|
return handleRegistrationFlow(req, reqBody, r, sessionID, cfg, userAPI, accessToken, accessTokenErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGuestRegistration(
|
func handleGuestRegistration(
|
||||||
|
|
@ -676,6 +680,7 @@ func handleGuestRegistration(
|
||||||
// nolint: gocyclo
|
// nolint: gocyclo
|
||||||
func handleRegistrationFlow(
|
func handleRegistrationFlow(
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
|
reqBody []byte,
|
||||||
r registerRequest,
|
r registerRequest,
|
||||||
sessionID string,
|
sessionID string,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
|
|
@ -736,6 +741,18 @@ func handleRegistrationFlow(
|
||||||
// Add Dummy to the list of completed registration stages
|
// Add Dummy to the list of completed registration stages
|
||||||
sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypeDummy)
|
sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypeDummy)
|
||||||
|
|
||||||
|
case authtypes.LoginTypePublicKey:
|
||||||
|
isCompleted, authType, err := handlePublicKeyRegistration(cfg, reqBody, userAPI)
|
||||||
|
if err != nil {
|
||||||
|
return *err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isCompleted {
|
||||||
|
sessions.addCompletedSessionStage(sessionID, authType)
|
||||||
|
} else {
|
||||||
|
newPublicKeyAuthSession(&r)
|
||||||
|
}
|
||||||
|
|
||||||
case "":
|
case "":
|
||||||
// An empty auth type means that we want to fetch the available
|
// An empty auth type means that we want to fetch the available
|
||||||
// flows. It can also mean that we want to register as an appservice
|
// flows. It can also mean that we want to register as an appservice
|
||||||
|
|
|
||||||
80
clientapi/routing/register_publickey.go
Normal file
80
clientapi/routing/register_publickey.go
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
// 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 (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newPublicKeyAuthSession(request *registerRequest) {
|
||||||
|
// Public key auth does not use password. But the registration flow
|
||||||
|
// requires setting a password in order to create the account.
|
||||||
|
// Create a random password to satisfy the requirement.
|
||||||
|
request.Password = util.RandomString(sessionIDLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePublicKeyRegistration(
|
||||||
|
cfg *config.ClientAPI,
|
||||||
|
reqBytes []byte,
|
||||||
|
userAPI userapi.UserRegisterAPI,
|
||||||
|
) (bool, authtypes.LoginType, *util.JSONResponse) {
|
||||||
|
if !cfg.PublicKeyAuthentication.Enabled() {
|
||||||
|
return false, "", &util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: jsonerror.Forbidden("public key account registration is disabled"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var authHandler auth.LoginPublicKeyHandler
|
||||||
|
authType := gjson.GetBytes(reqBytes, "auth.public_key_response.type").String()
|
||||||
|
|
||||||
|
switch authType {
|
||||||
|
case authtypes.LoginTypePublicKeyEthereum:
|
||||||
|
authBytes := gjson.GetBytes(reqBytes, "auth.public_key_response")
|
||||||
|
pkEthHandler, err := auth.CreatePublicKeyEthereumHandler(
|
||||||
|
[]byte(authBytes.Raw),
|
||||||
|
userAPI,
|
||||||
|
cfg,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
authHandler = pkEthHandler
|
||||||
|
default:
|
||||||
|
// No response. Client is asking for a new registration session
|
||||||
|
return false, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
isCompleted, jerr := authHandler.ValidateLoginResponse()
|
||||||
|
if jerr != nil {
|
||||||
|
return false, "", &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: jerr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isCompleted, authtypes.LoginType(authHandler.GetType()), nil
|
||||||
|
}
|
||||||
|
|
@ -64,7 +64,7 @@ func Setup(
|
||||||
prometheus.MustRegister(amtRegUsers, sendEventDuration)
|
prometheus.MustRegister(amtRegUsers, sendEventDuration)
|
||||||
|
|
||||||
rateLimits := httputil.NewRateLimits(&cfg.RateLimiting)
|
rateLimits := httputil.NewRateLimits(&cfg.RateLimiting)
|
||||||
userInteractiveAuth := auth.NewUserInteractive(userAPI, cfg)
|
userInteractiveAuth := auth.NewUserInteractive(userAPI, userAPI, cfg)
|
||||||
|
|
||||||
unstableFeatures := map[string]bool{
|
unstableFeatures := map[string]bool{
|
||||||
"org.matrix.e2e_cross_signing": true,
|
"org.matrix.e2e_cross_signing": true,
|
||||||
|
|
@ -551,7 +551,7 @@ func Setup(
|
||||||
if r := rateLimits.Limit(req); r != nil {
|
if r := rateLimits.Limit(req); r != nil {
|
||||||
return *r
|
return *r
|
||||||
}
|
}
|
||||||
return Login(req, userAPI, cfg)
|
return Login(req, userAPI, userInteractiveAuth, cfg)
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodGet, http.MethodPost, http.MethodOptions)
|
).Methods(http.MethodGet, http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,16 @@ client_api:
|
||||||
# whether registration is otherwise disabled.
|
# whether registration is otherwise disabled.
|
||||||
registration_shared_secret: ""
|
registration_shared_secret: ""
|
||||||
|
|
||||||
|
# Disable password authentication.
|
||||||
|
password_authentication_disabled: false
|
||||||
|
|
||||||
|
# public key authentication
|
||||||
|
public_key_authentication:
|
||||||
|
ethereum:
|
||||||
|
enabled: false
|
||||||
|
version: 1
|
||||||
|
chain_ids: [] // https://chainlist.org/
|
||||||
|
|
||||||
# Whether to require reCAPTCHA for registration.
|
# Whether to require reCAPTCHA for registration.
|
||||||
enable_registration_captcha: false
|
enable_registration_captcha: false
|
||||||
|
|
||||||
|
|
|
||||||
11
go.mod
11
go.mod
|
|
@ -11,29 +11,36 @@ require (
|
||||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect
|
github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect
|
||||||
github.com/MFAshby/stdemuxerhook v1.0.0
|
github.com/MFAshby/stdemuxerhook v1.0.0
|
||||||
github.com/Masterminds/semver/v3 v3.1.1
|
github.com/Masterminds/semver/v3 v3.1.1
|
||||||
|
github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect
|
||||||
github.com/codeclysm/extract v2.2.0+incompatible
|
github.com/codeclysm/extract v2.2.0+incompatible
|
||||||
github.com/containerd/containerd v1.6.2 // indirect
|
github.com/containerd/containerd v1.6.2 // indirect
|
||||||
github.com/docker/docker v20.10.14+incompatible
|
github.com/docker/docker v20.10.14+incompatible
|
||||||
github.com/docker/go-connections v0.4.0
|
github.com/docker/go-connections v0.4.0
|
||||||
|
github.com/ethereum/go-ethereum v1.10.17
|
||||||
github.com/frankban/quicktest v1.14.3 // indirect
|
github.com/frankban/quicktest v1.14.3 // indirect
|
||||||
github.com/getsentry/sentry-go v0.13.0
|
github.com/getsentry/sentry-go v0.13.0
|
||||||
github.com/gologme/log v1.3.0
|
github.com/gologme/log v1.3.0
|
||||||
github.com/google/go-cmp v0.5.7
|
github.com/google/go-cmp v0.5.7
|
||||||
|
github.com/google/gopacket v1.1.18 // indirect
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/h2non/filetype v1.1.3 // indirect
|
github.com/h2non/filetype v1.1.3 // indirect
|
||||||
github.com/hashicorp/golang-lru v0.5.4
|
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d
|
||||||
github.com/juju/testing v0.0.0-20220203020004-a0ff61f03494 // indirect
|
github.com/juju/testing v0.0.0-20220203020004-a0ff61f03494 // indirect
|
||||||
github.com/lib/pq v1.10.5
|
github.com/lib/pq v1.10.5
|
||||||
|
github.com/libp2p/go-libp2p v0.13.0
|
||||||
|
github.com/libp2p/go-libp2p-core v0.8.3 // indirect
|
||||||
|
github.com/libp2p/go-libp2p-gostream v0.3.1
|
||||||
|
github.com/libp2p/go-libp2p-http v0.2.0
|
||||||
github.com/matrix-org/dugong v0.0.0-20210921133753-66e6b1c67e2e
|
github.com/matrix-org/dugong v0.0.0-20210921133753-66e6b1c67e2e
|
||||||
|
github.com/matrix-org/go-http-js-libp2p v0.0.0-20200518170932-783164aeeda4
|
||||||
github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91
|
github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91
|
||||||
github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16
|
github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16
|
||||||
github.com/matrix-org/gomatrixserverlib v0.0.0-20220408160933-cf558306b56f
|
github.com/matrix-org/gomatrixserverlib v0.0.0-20220408160933-cf558306b56f
|
||||||
github.com/matrix-org/pinecone v0.0.0-20220408153826-2999ea29ed48
|
github.com/matrix-org/pinecone v0.0.0-20220408153826-2999ea29ed48
|
||||||
github.com/matrix-org/util v0.0.0-20200807132607-55161520e1d4
|
github.com/matrix-org/util v0.0.0-20200807132607-55161520e1d4
|
||||||
github.com/mattn/go-sqlite3 v1.14.10
|
github.com/mattn/go-sqlite3 v1.14.10
|
||||||
github.com/miekg/dns v1.1.31 // indirect
|
|
||||||
github.com/nats-io/nats-server/v2 v2.7.4-0.20220309205833-773636c1c5bb
|
github.com/nats-io/nats-server/v2 v2.7.4-0.20220309205833-773636c1c5bb
|
||||||
github.com/nats-io/nats.go v1.14.0
|
github.com/nats-io/nats.go v1.14.0
|
||||||
github.com/neilalexander/utp v0.1.1-0.20210727203401-54ae7b1cd5f9
|
github.com/neilalexander/utp v0.1.1-0.20210727203401-54ae7b1cd5f9
|
||||||
|
|
|
||||||
29
internal/mapsutil/maps.go
Normal file
29
internal/mapsutil/maps.go
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
// 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 mapsutil
|
||||||
|
|
||||||
|
// Union two maps together with "b" overriding the values of "a"
|
||||||
|
// if the keys collide.
|
||||||
|
func MapsUnion(a map[string]interface{}, b map[string]interface{}) map[string]interface{} {
|
||||||
|
c := make(map[string]interface{})
|
||||||
|
for k, v := range a {
|
||||||
|
c[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range b {
|
||||||
|
c[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
"github.com/matrix-org/dendrite/internal/mapsutil"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/crypto/ed25519"
|
"golang.org/x/crypto/ed25519"
|
||||||
|
|
@ -280,6 +281,16 @@ func (config *Dendrite) Derive() error {
|
||||||
config.Derived.Registration.Flows = append(config.Derived.Registration.Flows,
|
config.Derived.Registration.Flows = append(config.Derived.Registration.Flows,
|
||||||
authtypes.Flow{Stages: []authtypes.LoginType{authtypes.LoginTypeDummy}})
|
authtypes.Flow{Stages: []authtypes.LoginType{authtypes.LoginTypeDummy}})
|
||||||
}
|
}
|
||||||
|
if config.ClientAPI.PublicKeyAuthentication.Enabled() {
|
||||||
|
pkFlows := config.ClientAPI.PublicKeyAuthentication.GetPublicKeyRegistrationFlows()
|
||||||
|
if pkFlows != nil {
|
||||||
|
config.Derived.Registration.Flows = append(config.Derived.Registration.Flows, pkFlows...)
|
||||||
|
}
|
||||||
|
pkParams := config.ClientAPI.PublicKeyAuthentication.GetPublicKeyRegistrationParams()
|
||||||
|
if pkParams != nil {
|
||||||
|
config.Derived.Registration.Params = mapsutil.MapsUnion(config.Derived.Registration.Params, pkParams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load application service configuration files
|
// Load application service configuration files
|
||||||
if err := loadAppServices(&config.AppServiceAPI, &config.Derived); err != nil {
|
if err := loadAppServices(&config.AppServiceAPI, &config.Derived); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package config
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClientAPI struct {
|
type ClientAPI struct {
|
||||||
|
|
@ -43,6 +45,12 @@ type ClientAPI struct {
|
||||||
RateLimiting RateLimiting `yaml:"rate_limiting"`
|
RateLimiting RateLimiting `yaml:"rate_limiting"`
|
||||||
|
|
||||||
MSCs *MSCs `yaml:"mscs"`
|
MSCs *MSCs `yaml:"mscs"`
|
||||||
|
|
||||||
|
// Disable password authentication.
|
||||||
|
PasswordAuthenticationDisabled bool `yaml:"password_authentication_disabled"`
|
||||||
|
|
||||||
|
// Public key authentication
|
||||||
|
PublicKeyAuthentication publicKeyAuthentication `yaml:"public_key_authentication"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClientAPI) Defaults(generate bool) {
|
func (c *ClientAPI) Defaults(generate bool) {
|
||||||
|
|
@ -127,3 +135,44 @@ func (r *RateLimiting) Defaults() {
|
||||||
r.Threshold = 5
|
r.Threshold = 5
|
||||||
r.CooloffMS = 500
|
r.CooloffMS = 500
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ethereumAuthParams struct {
|
||||||
|
Version uint32 `json:"version"`
|
||||||
|
ChainIDs []string `json:"chain_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ethereumAuthConfig struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
Version uint32 `yaml:"version"`
|
||||||
|
ChainIDs []string `yaml:"chain_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type publicKeyAuthentication struct {
|
||||||
|
Ethereum ethereumAuthConfig `yaml:"ethereum"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *publicKeyAuthentication) Enabled() bool {
|
||||||
|
return pk.Ethereum.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *publicKeyAuthentication) GetPublicKeyRegistrationFlows() []authtypes.Flow {
|
||||||
|
var flows []authtypes.Flow
|
||||||
|
if pk.Ethereum.Enabled {
|
||||||
|
flows = append(flows, authtypes.Flow{Stages: []authtypes.LoginType{authtypes.LoginTypePublicKeyEthereum}})
|
||||||
|
}
|
||||||
|
|
||||||
|
return flows
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *publicKeyAuthentication) GetPublicKeyRegistrationParams() map[string]interface{} {
|
||||||
|
params := make(map[string]interface{})
|
||||||
|
if pk.Ethereum.Enabled {
|
||||||
|
p := ethereumAuthParams{
|
||||||
|
Version: pk.Ethereum.Version,
|
||||||
|
ChainIDs: pk.Ethereum.ChainIDs,
|
||||||
|
}
|
||||||
|
params[authtypes.LoginTypePublicKeyEthereum] = p
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue