Skip to content
On this page

Authorization (Client-side)

In certain situations, you may need to request that a user signs in before they can use your application. To facilitate this authentication process in Pando Proto, you can make use of the following methods. Both of these methods will prompt the user to use their wallet to continue with the authentication process.

Request to authenticate

For Javascript/Typescript developers, we provide a @foxone/mixin-passport package to help you request the Mixin Messenger's OAuth or EIP-4361 authentication (MVM-based).

Here is a code snippet to show how to use the package to request the authentications.

js
async function login() {
  // ...
  const data = await passport.auth({
    origin: "Your App",
    authMethods: ["metamask", "walletconnect", "mixin", "fennec"],
    clientId: "Your Client ID",
    scope: "PROFILE:READ ASSETS:READ",
    pkce: true,
    mvmAuthType: "SignedMessage",
    // only works when mvmAuthType is "SignedMessage"
    hooks: {
      beforeSignMessage: async () => {
        // sign a message 
        return {
          statement: "You'll login to your_app by the signature",
          expirationTime: new Date(
            new Date().getTime() + 1000 * 60 * 3
          ).toISOString(),
        };
      },
      afterSignMessage: async ({ signedMsg, sign }) => {
        // use the signedMsg and sign to login to your backend
        const resp = await loginEIP4361(signedMsg, sign);
        return resp.access_token;
      },
    }
  });
  // save data.token and data.channel for later use
}

INFO

If using the SignedMessage, you must generate your own access token by the credentials provided. Once generated, assign the access token to data.token.

You can use the package passport-go to verify the provided credentials, and return a self-signed access token

In some cases, you may want to use the Mixin Network's keystore to generate a access token locally, without a backend server. In this case, you can use the mvmAuthType option to set it to MixinToken, and ignore the hooks option. The code snippet below shows how to use the package to request the authentications.

js
async function login() {
  // ...
  const data = await passport.auth({
    origin: "Your App",
    authMethods: ["metamask", "walletconnect", "mixin", "fennec"],
    clientId: "Your Client ID",
    scope: "PROFILE:READ ASSETS:READ",
    pkce: true,
    mvmAuthType: "MixinToken"
  });
  // save data.token and data.channel for later use
}

INFO

By using MixinToken, a Mixin Network access token based on the provided credentials will be assigned to data.token.

Verify the authentication

For Golang developers, we provide a golang package passport-go to help you verify the Mixin Messenger's OAuth or EIP-4361 authentication (MVM-based).

The code snippet below shows how to use the package to verify the authentications.

go
import 	"github.com/pandodao/passport-go/auth"

// ...
// Create an authorizer with your client ID and the trust domain list
  authorizer := auth.New([]string{
    "Your Client ID",
  }, []string{
    "pando.im",
  })
// ...

func Login(ctx context.Context, loginMethod, mixinToken, sign, signedMsg) error {
  switch loginMethod {
  case "mixin_token":
    // Mixin OAuth flow
    {
      authUser, err := authorizer.Authorize(ctx, &auth.AuthorizationParams{
        Method:     auth.AuthMethodMixinToken,
        MixinToken: mixinToken,
      })
      if err != nil {
        render.Error(w, http.StatusUnauthorized, err)
        return
      }
      // deal with authUser, sign a JWT token, save to db, etc.
      return
    }
  case "mvm":
    // EIP-4361 flow (MVM-based)
    {
      authUser, err := authorizer.Authorize(ctx, &auth.AuthorizationParams{
        Method:           auth.AuthMethodMvm,
        MvmSignature:     sign,
        MvmSignedMessage: signedMsg,
      })
      if err != nil {
        render.Error(w, http.StatusUnauthorized, err)
        return
      }
      // deal with authUser, sign a JWT token, save to db, etc.
      return
    }
  case "email":
  case "phone":
    // ... other login methods
  default:

Technical Specifications

EIP-4361: Sign-In with Ethereum

This approach works for most ethereum compaitable wallets (e.g. Metamask, TrustWallet, Coinbase Wallet, etc.). It is based on the EIP-4361 standard. The wallet will sign a message with the private key of the user's account. The message contains the information that the relying party (e.g. Pando Proto) needs to verify the authentication.

Before signing, the message is prefixed with \x19Ethereum Signed Message:\n<length of message> as defined in EIP-191.

Here is a template of the full message is presented below for readability and ease of understanding. Field descriptions are provided in the following:

bash
${domain} wants you to sign in with your Ethereum account:
${address}

${statement}

URI: ${uri}
Version: ${version}
Chain ID: ${chain-id}
Nonce: ${nonce}
Issued At: ${issued-at}
Expiration Time: ${expiration-time}
Not Before: ${not-before}
Request ID: ${request-id}
Resources:
- ${resources[0]}
- ${resources[1]}
...
- ${resources[n]}

in which,

  • domain is the RFC 3986 authority that is requesting the signing.
  • address is the Ethereum address performing the signing conformant to capitalization encoded checksum specified in EIP-55 where applicable.
  • statement (optional) is a human-readable ASCII assertion that the user will sign, and it must not contain '\n' (the byte 0x0a).
  • uri is an RFC 3986 URI referring to the resource that is the subject of the signing (as in the subject of a claim).
  • version is the current version of the message, which MUST be 1 for this specification.
  • chain-id is the EIP-155 Chain ID to which the session is bound, and the network where Contract Accounts MUST be resolved.
  • nonce is a randomized token typically chosen by the relying party and used to prevent replay attacks, at least 8 alphanumeric characters.
  • issued-at is the ISO 8601 datetime string of the current time.
  • expiration-time (optional) is the ISO 8601 datetime string that, if present, indicates when the signed authentication message is no longer valid.
  • not-before (optional) is the ISO 8601 datetime string that, if present, indicates when the signed authentication message will become valid.
  • request-id (optional) is an system-specific identifier that may be used to uniquely refer to the sign-in request.
  • resources (optional) is a list of information or references to information the user wishes to have resolved as part of authentication by the relying party. They are expressed as RFC 3986 URIs separated by "\n- " where \n is the byte 0x0a.

For ABNF formal definition, please refer to EIP-4361.

Example

bash
login.xyz wants you to sign in with your Ethereum account:
0x5d9de0318BeF0c3B81C46aeC31450Ffa54aa6906
Sign-In With Ethereum Example Statement
URI: https://login.xyz
Version: 1
Chain ID: 1
Nonce: risxcddc
Issued At: 2023-02-16T09:48:07.667Z
Expiration Time: 2023-02-18T09:48:07.665Z
Resources:
- ipfs://...../
- https://example.com/res.json

Sign the message

To sign the message, you need to use the private key of the Ethereum account. For most wallets (e.g. Metamask, TrustWallet), you can not access the private key directly. You need to use the wallet's API to invoke the signing process.

The pesudo code is presented below:

js
data = executeTemplate(tpl, {
  "domain": "login.xyz",
  "address": "0x5d9de.....06",
  "statement": "Sign-In With Ethereum Example Statement",
  // ...
});

// The wallet will prompt the user to sign the message
signature = sign(data);

You can refer to the wallet's API documentation to implement the signing process.

Use our SDKs

To simplify the procedure, we offer an npm package @foxone/mixin-passport for generating and signing the message, as well as a golang module passport-go for parsing and validating the signature.

Here is the example of using them:

js
const data = await passport.auth({
  clientId: "YOUR_CLIENT_ID",
  authMethods: ["metamask", "walletconnect", "mixin", "fennec", "onekey"],
  scope: "PROFILE:READ",
  origin: "app.pando.im",
  pkce: true,
  // mvmAuthType supports "SignedMessage" or "MixinToken". Default is "SignedMessage"
  // - "SignedMessage": will sign a message with the private key of the user's account
  // - "MixinToken": will use the Mixin Network's authentication flow, which will generate a token and return it directly
  mvmAuthType: "SignedMessage",
  // if mvmAuthType is "SignedMessage", you need to provide the following hooks
  hooks: {
    beforeSignMessage: async () => {
      // put the fields you want to sign here
      return { statement: "This is statement" };
    },
    afterSignMessage: async ({ message, signature }) => {
      // send the message and signature to wherever you want
      // e.g. POST /auth to your server, or invoke a smart contract's method
      const resp = await api.post("/auth", { message, signature });
      return resp.access_token
    },
  },
});

In the handler of /auth, you can use the passport-go to parse and validate the signature:

go
import (
	eip4361 "github.com/fox-one/passport-go/eip4361"
	"github.com/fox-one/passport-go/mvm"
)

type LoginPayload struct {
	Signature     string `json:"signature"`
	Message string `json:"message"`
}

// ...
func handler(w http.ResponseWriter, r *http.Request) error {
	ctx := r.Context()
	body := &LoginPayload{}
	if err := param.Binding(r, body); err != nil {
		return err
	}

	if body.Signature == "" {
		return err
	}

	message, err := eip4361.Parse(body.Message)
	if err != nil {
		return err
	}

	if err := message.Validate(time.Now()); err != nil {
		return err
	}

	if err := eip4361.Verify(message, body.Signature); err != nil {
		return err
	}

	// get the public key from the message, and use it to login
	token, err := Login(ctx, message.Address)
	if err != nil {
		return err
	}
	// ...
}

func Login(ctx context.Context, pubkey string) (string, error) {
  addr := common.HexToAddress(pubkey)
  mvmUser, err := mvm.GetBridgeUser(ctx, addr)
  if err != nil {
    return "", err
  }

  // save the user here
  // ...

  // generate an access token for the user
  token := jwt.NewWithClaims( 
    // .... 
  )
  return token, nil
}

Prevent EIP4361 from logining by mixin token

In most cases, we should not allow the user to login with mixin token. To prevent this, we can check if the userID has already associated with a valid address.

go
  contractAddr, err := mvm.GetUserContract(ctx, profile.UserID)
  if err != nil {
    fmt.Printf("err mvm.GetUserContract: %v\n", err)
    return nil, err
  }

  // if contractAddr is not 0x000..00, it means the user has already registered a mvm account
  emptyAddr := common.Address{}
  if contractAddr != emptyAddr {
    return nil, core.ErrBadMvmLoginMethod
  }

Use a Mixin OAuth token

This approach works for Mixin Messenger only. It is a bit different from the EIP-4361 approach. Instead of signing a message, you will be redirected to the Mixin Messenger app to complete the authentication process.

The Mixin OAuth token works like the signed token mentioned above. Please refer to Mixin OAuth for more details.