Skip to content

Commit

Permalink
Merge pull request #623 from fairDataSociety/signatureLogin
Browse files Browse the repository at this point in the history
feat: add ability to login with signature, FIP 63
  • Loading branch information
asabya committed May 14, 2024
2 parents 9d2ec35 + a19abe4 commit a26be56
Show file tree
Hide file tree
Showing 16 changed files with 648 additions and 11 deletions.
6 changes: 6 additions & 0 deletions cmd/common/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ type UserLoginRequest struct {
Password string `json:"password,omitempty"`
}

// UserSignatureLoginRequest is the request body for user login with signature
type UserSignatureLoginRequest struct {
Signature string `json:"signature,omitempty"`
Password string `json:"password,omitempty"`
}

// PodRequest is the request body for pod creation
type PodRequest struct {
PodName string `json:"podName,omitempty"`
Expand Down
22 changes: 18 additions & 4 deletions cmd/dfs-cli/cmd/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,11 @@ const (
apiDocLoadJson = apiVersion + "/doc/loadjson"
apiDocIndexJson = apiVersion + "/doc/indexjson"

apiUserSignupV2 = apiVersionV2 + "/user/signup"
apiUserLoginV2 = apiVersionV2 + "/user/login"
apiUserPresentV2 = apiVersionV2 + "/user/present"
apiUserDeleteV2 = apiVersionV2 + "/user/delete"
apiUserSignupV2 = apiVersionV2 + "/user/signup"
apiUserLoginV2 = apiVersionV2 + "/user/login"
apiUserSignatureLogin = apiVersionV2 + "/user/login-with-signature"
apiUserPresentV2 = apiVersionV2 + "/user/present"
apiUserDeleteV2 = apiVersionV2 + "/user/delete"
)

func newPrompt() {
Expand Down Expand Up @@ -144,6 +145,7 @@ var userSuggestions = []prompt.Suggest{
{Text: "new", Description: "create a new user (v2)"},
{Text: "del", Description: "delete a existing user (v2)"},
{Text: "login", Description: "login to a existing user (v2)"},
{Text: "signatureLogin", Description: "login with signature"},
{Text: "logout", Description: "logout from a logged-in user"},
{Text: "present", Description: "is user present (v2)"},
{Text: "stat", Description: "shows information about a user"},
Expand Down Expand Up @@ -308,6 +310,17 @@ func executor(in string) {
currentPod = ""
currentDirectory = ""
currentPrompt = getCurrentPrompt()
case "signatureLogin":
if len(blocks) < 3 {
fmt.Println("invalid command. Missing \"signature\" argument")
return
}
signature := blocks[2]
fmt.Println("signatureLogin", signature)
signatureLogin(signature, apiUserSignatureLogin)
currentPod = ""
currentDirectory = ""
currentPrompt = getCurrentPrompt()
case "present":
if len(blocks) < 3 {
fmt.Println("invalid command. Missing \"userName\" argument")
Expand Down Expand Up @@ -1023,6 +1036,7 @@ func help() {
fmt.Println(" - user <new> (user-name) (mnemonic) - create a new user and login as that user")
fmt.Println(" - user <del> - deletes a logged-in user")
fmt.Println(" - user <login> (user-name) - login as a given user")
fmt.Println(" - user <signatureLogin> (signature) - login with signature")
fmt.Println(" - user <logout> - logout a logged-in user")
fmt.Println(" - user <present> (user-name) - returns true if the user is present, false otherwise")
fmt.Println(" - user <stat> - shows information about a user")
Expand Down
30 changes: 30 additions & 0 deletions cmd/dfs-cli/cmd/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,36 @@ func userLogin(userName, apiEndpoint string) {
fmt.Println(message)
}

func signatureLogin(signature, apiEndpoint string) {
password := getPassword()
loginUser := common.UserSignatureLoginRequest{
Signature: signature,
Password: password,
}
jsonData, err := json.Marshal(loginUser)
if err != nil {
fmt.Println("login user: error marshalling request")
return
}
data, err := fdfsAPI.postReq(http.MethodPost, apiEndpoint, jsonData)
if err != nil {
fmt.Println("login user: ", err)
return
}
var resp api.UserSignupResponse
err = json.Unmarshal(data, &resp)
if err != nil {
fmt.Println("create user: ", err)
return
}

currentUser = resp.Address
message := strings.ReplaceAll(string(data), "\n", "")
fdfsAPI.setAccessToken(resp.AccessToken)

fmt.Println(message)
}

func deleteUser(apiEndpoint string) {
password := getPassword()
delUser := common.UserSignupRequest{
Expand Down
2 changes: 1 addition & 1 deletion cmd/dfs/cmd/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func startDevServer() {
dfsApi := dfs.NewMockDfsAPI(mockClient, users, logger)
handler = api.NewMockHandler(dfsApi, logger, []string{"http://localhost:3000"})
defer handler.Close()
httpPort = ":9093"
httpPort = ":9090"
pprofPort = ":9091"
srv := startHttpService(logger)
fmt.Printf("Server running at:http://127.0.0.1%s\n", httpPort)
Expand Down
1 change: 1 addition & 0 deletions cmd/dfs/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ func startHttpService(logger logging.Logger) *http.Server {
baseRouterV2.Use(handler.LogMiddleware)
baseRouterV2.HandleFunc("/user/signup", handler.UserSignupV2Handler).Methods("POST")
baseRouterV2.HandleFunc("/user/login", handler.UserLoginV2Handler).Methods("POST")
baseRouterV2.HandleFunc("/user/login-with-signature", handler.UserLoginWithSignature).Methods("POST")
baseRouterV2.HandleFunc("/user/present", handler.UserPresentV2Handler).Methods("GET")
userRouterV2 := baseRouterV2.PathPrefix("/user/").Subrouter()
userRouterV2.Use(handler.LoginMiddleware)
Expand Down
37 changes: 37 additions & 0 deletions pkg/account/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,43 @@ func (a *Account) CreateUserAccount(mnemonic string) (string, []byte, error) {
return mnemonic, seed, nil
}

// GenerateUserAccountFromSignature create a new master account for a user.
func (a *Account) GenerateUserAccountFromSignature(signature, password string) (string, []byte, error) {
wal := newWallet(nil)
a.wallet = wal
acc, mnemonic, err := wal.GenerateWalletFromSignature(signature, password)
if err != nil {
return "", nil, err
}

hdw, err := hdwallet.NewFromMnemonic(mnemonic)
if err != nil { // skipcq: TCV-001
return "", nil, err
}

// store publicKey, private key and user
a.userAccount.privateKey, err = hdw.PrivateKey(acc)
if err != nil { // skipcq: TCV-001
return "", nil, err
}
a.userAccount.publicKey, err = hdw.PublicKey(acc)
if err != nil { // skipcq: TCV-001
return "", nil, err
}
addrBytes, err := crypto.NewEthereumAddress(a.userAccount.privateKey.PublicKey)
if err != nil { // skipcq: TCV-001
return "", nil, err
}
a.userAccount.address.SetBytes(addrBytes)

seed, err := hdwallet.NewSeedFromMnemonic(mnemonic)
if err != nil { // skipcq: TCV-001
return "", nil, err
}

return mnemonic, seed, nil
}

// LoadUserAccountFromSeed loads the user account given the bip39 seed
func (a *Account) LoadUserAccountFromSeed(seed []byte) error {
acc, err := a.wallet.CreateAccountFromSeed(rootPath, seed)
Expand Down
65 changes: 65 additions & 0 deletions pkg/account/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,22 @@ limitations under the License.
package account

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"

"github.com/ethereum/go-ethereum/accounts"
"github.com/ethersphere/bee/v2/pkg/crypto"
hdwallet "github.com/miguelmota/go-ethereum-hdwallet"
"github.com/tyler-smith/go-bip39"
)

const (
rootPath = "m/44'/60'/0'/0/0"
genericPath = "m/44'/60'/0'/0/"

MaxEntropyLength = 32
)

// Wallet is used to create root and pod accounts of user
Expand Down Expand Up @@ -122,3 +127,63 @@ func (*Wallet) IsValidMnemonic(mnemonic string) error {
}
return nil
}

// GenerateWalletFromSignature is used to create an account from a given signature
func (w *Wallet) GenerateWalletFromSignature(signature, password string) (accounts.Account, string, error) {
if signature == "" {
return accounts.Account{}, "", fmt.Errorf("signature is empty")
}
signatureBytes, err := hex.DecodeString(signature)
if err != nil {
return accounts.Account{}, "", err
}
wallet, acc, mnemonic, err := signatureToWallet(signatureBytes)
if err != nil { // skipcq: TCV-001
return accounts.Account{}, "", err
}
if password != "" {
pk, err := wallet.PrivateKey(acc)
if err != nil { // skipcq: TCV-001
return accounts.Account{}, "", err
}
signer := crypto.NewDefaultSigner(pk)
passBytes := sha256.Sum256([]byte(password))
signatureBytes, err = signer.Sign([]byte("0x" + hex.EncodeToString(passBytes[:])))
if err != nil {
return accounts.Account{}, "", err
}

wallet, acc, mnemonic, err = signatureToWallet(signatureBytes)
if err != nil { // skipcq: TCV-001
return accounts.Account{}, "", err
}
_ = wallet
}

seed, err := hdwallet.NewSeedFromMnemonic(mnemonic)
if err != nil { // skipcq: TCV-001
return accounts.Account{}, "", err
}
w.seed = seed
return acc, mnemonic, nil
}

func signatureToWallet(signatureBytes []byte) (*hdwallet.Wallet, accounts.Account, string, error) {
slicedSignature := signatureBytes[0:MaxEntropyLength]

mnemonic, err := bip39.NewMnemonic(slicedSignature)
if err != nil { // skipcq: TCV-001
return nil, accounts.Account{}, "", err
}
wallet, err := hdwallet.NewFromMnemonic(mnemonic)
if err != nil { // skipcq: TCV-001
return nil, accounts.Account{}, "", err
}

path := hdwallet.MustParseDerivationPath(rootPath)
acc, err := wallet.Derive(path, false)
if err != nil { // skipcq: TCV-001
return nil, accounts.Account{}, "", err
}
return wallet, acc, mnemonic, nil
}
18 changes: 18 additions & 0 deletions pkg/account/wallet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"os"
"testing"

"github.com/stretchr/testify/assert"

hdwallet "github.com/miguelmota/go-ethereum-hdwallet"

"github.com/tyler-smith/go-bip39"
Expand Down Expand Up @@ -38,3 +40,19 @@ func TestWallet(t *testing.T) {
t.Fatal("invalid mnemonic")
}
}

func TestSignatureToWallet(t *testing.T) {
signature := "b7f4346174a6ff79983bdb10348523de3a4bd2b4772b9f7217b997c6ca1f6abd3de015eab01818e459fad3c067e00969d9f02b808df027574da2f7fd50170a911c"
addrs := []string{"0x61E18Ac267f4d5af06D421DeA020818255678649", "0x13543e7BA5ff28AD8B203BB8e93b47D76ee2aE05"}
w := newWallet(nil)
acc, _, err := w.GenerateWalletFromSignature(signature, "")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, addrs[0], acc.Address.String())
acc, _, err = w.GenerateWalletFromSignature(signature, "111111111111")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, addrs[1], acc.Address.String())
}
79 changes: 76 additions & 3 deletions pkg/api/user_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ func (h *Handler) UserLoginV2Handler(w http.ResponseWriter, r *http.Request) {
jsonhttp.NotFound(w, &response{Message: "user login: " + err.Error()})
return
}
if err == u.ErrUserAlreadyLoggedIn ||
err == u.ErrInvalidUserName ||
err == u.ErrInvalidPassword {
if errors.Is(err, u.ErrUserAlreadyLoggedIn) ||
errors.Is(err, u.ErrInvalidUserName) ||
errors.Is(err, u.ErrInvalidPassword) {
h.logger.Errorf("user login: %v", err)
jsonhttp.BadRequest(w, &response{Message: "user login: " + err.Error()})
return
Expand All @@ -114,3 +114,76 @@ func (h *Handler) UserLoginV2Handler(w http.ResponseWriter, r *http.Request) {
AccessToken: loginResp.AccessToken,
})
}

// UserLoginWithSignature godoc
//
// @Summary Login User with signature
// @Description login user with signature described in https://github.com/fairDataSociety/FIPs/blob/master/text/0063-external-account-generator.md
// @ID user-login-signature
// @Tags user
// @Accept json
// @Produce json
// @Param user_request body common.UserSignatureLoginRequest true "signature and password"
// @Success 200 {object} UserLoginResponse
// @Failure 400 {object} response
// @Failure 500 {object} response
// @Header 200 {string} Set-Cookie "fairos-dfs session"
// @Router /v2/user/login-with-signature [post]
func (h *Handler) UserLoginWithSignature(w http.ResponseWriter, r *http.Request) {
contentType := r.Header.Get("Content-Type")
if contentType != jsonContentType {
h.logger.Errorf("user login: invalid request body type")
jsonhttp.BadRequest(w, &response{Message: "user login: invalid request body type"})
return
}

decoder := json.NewDecoder(r.Body)
var userReq common.UserSignatureLoginRequest
err := decoder.Decode(&userReq)
if err != nil {
h.logger.Errorf("user login: could not decode arguments")
jsonhttp.BadRequest(w, &response{Message: "user login: could not decode arguments"})
return
}

signature := userReq.Signature
password := userReq.Password
if signature == "" {
h.logger.Errorf("user login: \"signature\" argument missing")
jsonhttp.BadRequest(w, &response{Message: "user login: \"signature\" argument missing"})
return
}

// login user
loginResp, err := h.dfsAPI.LoginUserWithSignature(signature, password, "")
if err != nil {
if errors.Is(err, u.ErrUserNameNotFound) {
h.logger.Errorf("user login: %v", err)
jsonhttp.NotFound(w, &response{Message: "user login: " + err.Error()})
return
}
if errors.Is(err, u.ErrUserAlreadyLoggedIn) ||
errors.Is(err, u.ErrInvalidUserName) ||
errors.Is(err, u.ErrInvalidPassword) {
h.logger.Errorf("user login: %v", err)
jsonhttp.BadRequest(w, &response{Message: "user login: " + err.Error()})
return
}
h.logger.Errorf("user login: %v", err)
jsonhttp.InternalServerError(w, &response{Message: "user login: " + err.Error()})
return
}
err = cookie.SetSession(loginResp.UserInfo.GetSessionId(), w, h.cookieDomain)
if err != nil {
h.logger.Errorf("user login: %v", err)
jsonhttp.InternalServerError(w, &response{Message: "user login: " + err.Error()})
return
}
addr := loginResp.UserInfo.GetAccount().GetUserAccountInfo().GetAddress()
jsonhttp.OK(w, &UserSignupResponse{
Address: addr.Hex(),
PublicKey: loginResp.PublicKey,
Message: "user logged-in successfully",
AccessToken: loginResp.AccessToken,
})
}
5 changes: 5 additions & 0 deletions pkg/dfs/user_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ func (a *API) LoginUserV2(userName, passPhrase, sessionId string) (*user.LoginRe
return a.users.LoginUserV2(userName, passPhrase, a.client, a.tm, a.sm, sessionId)
}

// LoginUserWithSignature is a controller function which calls the users login with signature function.
func (a *API) LoginUserWithSignature(signature, passPhrase, sessionId string) (*user.LoginResponse, error) {
return a.users.LoginUserWithSignature(signature, passPhrase, a.client, a.tm, a.sm, sessionId)
}

// LoadLiteUser is a controller function which loads user from mnemonic and doesn't store any user info on chain
func (a *API) LoadLiteUser(userName, passPhrase, mnemonic, sessionId string) (string, string, *user.Info, error) {
return a.users.LoadLiteUser(userName, passPhrase, mnemonic, sessionId, a.tm, a.sm)
Expand Down
8 changes: 5 additions & 3 deletions pkg/pod/ls.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ func (p *Pod) ListPods() ([]string, []string, error) {
if err != nil { // skipcq: TCV-001
return nil, nil, err
}
err := p.storeUserPodsV2(podList)
if err != nil {
fmt.Println("error storing podsV2", err)
if len(podList.Pods) != 0 || len(podList.SharedPods) != 0 {
err := p.storeUserPodsV2(podList)
if err != nil {
fmt.Println("error storing podsV2", err)
}
}
}

Expand Down

0 comments on commit a26be56

Please sign in to comment.