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/
|
||||
|
||||
media_store/
|
||||
|
||||
# Debug
|
||||
**/__debug_bin
|
||||
|
|
@ -11,4 +11,9 @@ const (
|
|||
LoginTypeRecaptcha = "m.login.recaptcha"
|
||||
LoginTypeApplicationService = "m.login.application_service"
|
||||
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.
|
||||
// If the final return value is non-nil, an error occurred and the cleanup function
|
||||
// is nil.
|
||||
func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.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)
|
||||
if err != nil {
|
||||
err := &util.JSONResponse{
|
||||
|
|
@ -55,17 +64,23 @@ func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.U
|
|||
}
|
||||
|
||||
var typ Type
|
||||
switch header.Type {
|
||||
case authtypes.LoginTypePassword:
|
||||
switch {
|
||||
case header.Type == authtypes.LoginTypePassword && !cfg.PasswordAuthenticationDisabled:
|
||||
typ = &LoginTypePassword{
|
||||
GetAccountByPassword: useraccountAPI.QueryAccountByPassword,
|
||||
Config: cfg,
|
||||
}
|
||||
case authtypes.LoginTypeToken:
|
||||
case header.Type == authtypes.LoginTypeToken:
|
||||
typ = &LoginTypeToken{
|
||||
UserAPI: userAPI,
|
||||
Config: cfg,
|
||||
}
|
||||
case header.Type == authtypes.LoginTypePublicKey && cfg.PublicKeyAuthentication.Enabled():
|
||||
typ = &LoginTypePublicKey{
|
||||
UserAPI: userRegisterAPI,
|
||||
UserInteractive: userInteractiveAuth,
|
||||
Config: cfg,
|
||||
}
|
||||
default:
|
||||
err := util.JSONResponse{
|
||||
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"},
|
||||
},
|
||||
}
|
||||
userInteractive := UserInteractive{
|
||||
Completed: []string{},
|
||||
Flows: []userInteractiveFlow{},
|
||||
Types: make(map[string]Type),
|
||||
Sessions: make(map[string][]string),
|
||||
}
|
||||
|
||||
for _, tst := range tsts {
|
||||
t.Run(tst.Name, func(t *testing.T) {
|
||||
var userAPI fakeUserInternalAPI
|
||||
|
|
@ -69,7 +76,7 @@ func TestLoginFromJSONReader(t *testing.T) {
|
|||
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 {
|
||||
t.Fatalf("LoginFromJSONReader failed: %+v", err)
|
||||
}
|
||||
|
|
@ -139,6 +146,13 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
|||
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 {
|
||||
t.Run(tst.Name, func(t *testing.T) {
|
||||
var userAPI fakeUserInternalAPI
|
||||
|
|
@ -147,7 +161,7 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
|||
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 {
|
||||
cleanup(ctx, nil)
|
||||
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
|
||||
|
|
@ -161,6 +175,7 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
|||
type fakeUserInternalAPI struct {
|
||||
UserInternalAPIForLogin
|
||||
uapi.UserAccountAPI
|
||||
uapi.UserRegisterAPI
|
||||
DeletedTokens []string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -107,3 +107,12 @@ func (t *LoginTypePassword) Login(ctx context.Context, req interface{}) (*Login,
|
|||
}
|
||||
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
|
||||
// Map of session ID to completed login types, will need to be extended in future
|
||||
Sessions map[string][]string
|
||||
Params map[string]interface{}
|
||||
}
|
||||
|
||||
func NewUserInteractive(userAccountAPI api.UserAccountAPI, cfg *config.ClientAPI) *UserInteractive {
|
||||
typePassword := &LoginTypePassword{
|
||||
GetAccountByPassword: userAccountAPI.QueryAccountByPassword,
|
||||
Config: cfg,
|
||||
}
|
||||
return &UserInteractive{
|
||||
func NewUserInteractive(
|
||||
userAccountAPI api.UserAccountAPI,
|
||||
userRegisterAPI api.UserRegisterAPI,
|
||||
cfg *config.ClientAPI,
|
||||
) *UserInteractive {
|
||||
userInteractive := UserInteractive{
|
||||
Completed: []string{},
|
||||
Flows: []userInteractiveFlow{
|
||||
{
|
||||
Stages: []string{typePassword.Name()},
|
||||
},
|
||||
},
|
||||
Types: map[string]Type{
|
||||
typePassword.Name(): typePassword,
|
||||
},
|
||||
Sessions: make(map[string][]string),
|
||||
Flows: []userInteractiveFlow{},
|
||||
Types: make(map[string]Type),
|
||||
Sessions: make(map[string][]string),
|
||||
Params: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -144,6 +159,10 @@ func (u *UserInteractive) AddCompletedStage(sessionID, authType string) {
|
|||
delete(u.Sessions, sessionID)
|
||||
}
|
||||
|
||||
func (u *UserInteractive) DeleteSession(sessionID string) {
|
||||
delete(u.Sessions, sessionID)
|
||||
}
|
||||
|
||||
type Challenge struct {
|
||||
Completed []string `json:"completed"`
|
||||
Flows []userInteractiveFlow `json:"flows"`
|
||||
|
|
@ -160,7 +179,7 @@ func (u *UserInteractive) Challenge(sessionID string) *util.JSONResponse {
|
|||
Completed: u.Completed,
|
||||
Flows: u.Flows,
|
||||
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.
|
||||
// `bodyBytes` is the HTTP request body which must contain an `auth` key.
|
||||
// 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
|
||||
|
||||
// "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 {
|
||||
api.UserAccountAPI
|
||||
api.UserRegisterAPI
|
||||
}
|
||||
|
||||
func (d *fakeAccountDatabase) PerformPasswordUpdate(ctx context.Context, req *api.PerformPasswordUpdateRequest, res *api.PerformPasswordUpdateResponse) error {
|
||||
|
|
@ -52,13 +53,14 @@ func setup() *UserInteractive {
|
|||
ServerName: serverName,
|
||||
},
|
||||
}
|
||||
return NewUserInteractive(&fakeAccountDatabase{}, cfg)
|
||||
accountApi := fakeAccountDatabase{}
|
||||
return NewUserInteractive(&accountApi, &accountApi, cfg)
|
||||
}
|
||||
|
||||
func TestUserInteractiveChallenge(t *testing.T) {
|
||||
uia := setup()
|
||||
// no auth key results in a challenge
|
||||
_, errRes := uia.Verify(ctx, []byte(`{}`), device)
|
||||
_, errRes := uia.Verify(ctx, []byte(`{}`))
|
||||
if errRes == nil {
|
||||
t.Fatalf("Verify succeeded with {} but expected failure")
|
||||
}
|
||||
|
|
@ -98,7 +100,7 @@ func TestUserInteractivePasswordLogin(t *testing.T) {
|
|||
}`),
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
_, errRes := uia.Verify(ctx, tc, device)
|
||||
_, errRes := uia.Verify(ctx, tc)
|
||||
if errRes != nil {
|
||||
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 {
|
||||
_, errRes := uia.Verify(ctx, tc.body, device)
|
||||
_, errRes := uia.Verify(ctx, tc.body)
|
||||
if errRes == nil {
|
||||
t.Errorf("Verify succeeded but expected failure for request: %s", string(tc.body))
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ func Deactivate(
|
|||
}
|
||||
}
|
||||
|
||||
login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes, deviceAPI)
|
||||
login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes)
|
||||
if errRes != nil {
|
||||
return *errRes
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ func DeleteDeviceById(
|
|||
sessionID = s
|
||||
}
|
||||
|
||||
login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes, device)
|
||||
login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes)
|
||||
if errRes != nil {
|
||||
switch data := errRes.JSON.(type) {
|
||||
case auth.Challenge:
|
||||
|
|
|
|||
|
|
@ -42,28 +42,42 @@ type flow struct {
|
|||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func passwordLogin() flows {
|
||||
f := flows{}
|
||||
func passwordLogin(f *flows) {
|
||||
s := flow{
|
||||
Type: "m.login.password",
|
||||
}
|
||||
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
|
||||
func Login(
|
||||
req *http.Request, userAPI userapi.UserInternalAPI,
|
||||
req *http.Request,
|
||||
userAPI userapi.UserInternalAPI,
|
||||
userInteractiveAuth *auth.UserInteractive,
|
||||
cfg *config.ClientAPI,
|
||||
) util.JSONResponse {
|
||||
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{
|
||||
Code: http.StatusOK,
|
||||
JSON: passwordLogin(),
|
||||
JSON: f,
|
||||
}
|
||||
} 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 {
|
||||
return *authErr
|
||||
}
|
||||
|
|
|
|||
|
|
@ -589,6 +589,10 @@ func Register(
|
|||
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"),
|
||||
}
|
||||
|
||||
case r.Auth.Type == authtypes.LoginTypePublicKey && cfg.PublicKeyAuthentication.Enabled():
|
||||
// Skip checks here. Will be validated later.
|
||||
|
||||
default:
|
||||
// Spec-compliant case (neither the access_token nor the login type are
|
||||
// specified, so it's a normal user registration)
|
||||
|
|
@ -607,7 +611,7 @@ func Register(
|
|||
"session_id": r.Auth.Session,
|
||||
}).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(
|
||||
|
|
@ -676,6 +680,7 @@ func handleGuestRegistration(
|
|||
// nolint: gocyclo
|
||||
func handleRegistrationFlow(
|
||||
req *http.Request,
|
||||
reqBody []byte,
|
||||
r registerRequest,
|
||||
sessionID string,
|
||||
cfg *config.ClientAPI,
|
||||
|
|
@ -736,6 +741,18 @@ func handleRegistrationFlow(
|
|||
// Add Dummy to the list of completed registration stages
|
||||
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 "":
|
||||
// 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
|
||||
|
|
|
|||
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)
|
||||
|
||||
rateLimits := httputil.NewRateLimits(&cfg.RateLimiting)
|
||||
userInteractiveAuth := auth.NewUserInteractive(userAPI, cfg)
|
||||
userInteractiveAuth := auth.NewUserInteractive(userAPI, userAPI, cfg)
|
||||
|
||||
unstableFeatures := map[string]bool{
|
||||
"org.matrix.e2e_cross_signing": true,
|
||||
|
|
@ -551,7 +551,7 @@ func Setup(
|
|||
if r := rateLimits.Limit(req); r != nil {
|
||||
return *r
|
||||
}
|
||||
return Login(req, userAPI, cfg)
|
||||
return Login(req, userAPI, userInteractiveAuth, cfg)
|
||||
}),
|
||||
).Methods(http.MethodGet, http.MethodPost, http.MethodOptions)
|
||||
|
||||
|
|
|
|||
|
|
@ -169,6 +169,16 @@ client_api:
|
|||
# whether registration is otherwise disabled.
|
||||
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.
|
||||
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/MFAshby/stdemuxerhook v1.0.0
|
||||
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/containerd/containerd v1.6.2 // indirect
|
||||
github.com/docker/docker v20.10.14+incompatible
|
||||
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/getsentry/sentry-go v0.13.0
|
||||
github.com/gologme/log v1.3.0
|
||||
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/gorilla/mux v1.8.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
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/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/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/gomatrix v0.0.0-20210324163249-be2af5ef2e16
|
||||
github.com/matrix-org/gomatrixserverlib v0.0.0-20220408160933-cf558306b56f
|
||||
github.com/matrix-org/pinecone v0.0.0-20220408153826-2999ea29ed48
|
||||
github.com/matrix-org/util v0.0.0-20200807132607-55161520e1d4
|
||||
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.go v1.14.0
|
||||
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"
|
||||
|
||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||
"github.com/matrix-org/dendrite/internal/mapsutil"
|
||||
"github.com/matrix-org/gomatrixserverlib"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
|
|
@ -280,6 +281,16 @@ func (config *Dendrite) Derive() error {
|
|||
config.Derived.Registration.Flows = append(config.Derived.Registration.Flows,
|
||||
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
|
||||
if err := loadAppServices(&config.AppServiceAPI, &config.Derived); err != nil {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package config
|
|||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||
)
|
||||
|
||||
type ClientAPI struct {
|
||||
|
|
@ -43,6 +45,12 @@ type ClientAPI struct {
|
|||
RateLimiting RateLimiting `yaml:"rate_limiting"`
|
||||
|
||||
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) {
|
||||
|
|
@ -127,3 +135,44 @@ func (r *RateLimiting) Defaults() {
|
|||
r.Threshold = 5
|
||||
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