Sign In With Ethereum

AppKit provides a simple solution for integrating with “Sign In With Ethereum” (SIWE), a new form of authentication that enables users to control their digital identity with their Ethereum account. SIWE is a standard also known as EIP-4361.

One-Click Auth

One-Click Auth represents a key advancement within WalletConnect v2, streamlining the user authentication process in AppKit by enabling them to seamlessly connect with a wallet and sign a SIWE message with just one click.

Connecting a wallet, proving control of an address with an off-chain signature, authorizing specific actions. These are the kinds of authorizations that can be encoded as “ReCaps”. ReCaps are permissions for a specific website or dapp that can be compactly encoded as a long string in the message you sign and translated by any wallet into a straight-forward one-sentence summary. WalletConnect uses permissions expressed as ReCaps to enable a One-Click Authentication.

Pre-requisites

For 1-CA + SIWE to function properly, a backend for communication is required. This backend will be used to generate a nonce, verify messages and handle sessions. More info here

This feature is compatible only with EVM blockchains. Therefore, including non-EVM blockchains will result in the internal disabling of the 1-CA + SIWE mechanism.

If you are willing to give support just to EVM blockchains you should disabled Solana support from AppKit by calling ReownAppKitModalNetworks.removeSupportedNetworks('solana'); right before your ReownAppKitModal() definition.

Configure your SIWEConfig object

final _siweConfig = SIWEConfig(
  getNonce: () async {
    // The getNonce method functions as a safeguard
    // against spoofing, akin to a CSRF token.

    return await yourApi.getNonce();
  },
  getMessageParams: () async {
    // Parameters to create the SIWE message internally.
    // More info in https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-222.method

    return SIWEMessageArgs(
      domain: 'yourdomain.com',
      uri: 'https://yourdomain.com/login',
      statement: 'Please sign with your account',
      methods: ['personal_sign', 'eth_sendTransaction'],
    );
  },
  createMessage: (SIWECreateMessageArgs args) {
    // Method for generating an EIP-4361-compatible message.
    // You can use our provided formatMessage() method or implement your own

    return SIWEUtils.formatMessage(args);
  },
  verifyMessage: (SIWEVerifyMessageArgs args) async {
    // This function ensures the message is valid,
    // has not been tampered with, and has been appropriately
    // signed by the wallet address.

    try {
      final isValidMessage = await yourApi.verifyMessage(args.toJson());
      return isValidMessage;
    } catch (error) {
      // error validating message
      return false;
    }
  },
  getSession: () async {
    // Called after verifyMessage() succeeds
    // The backend session should store the associated address and chainId
    // and return it via the `getSession` method.

    try {
      final session = await yourApi.getSession();
      return SIWESession(address: session.address, chains: [session.chainId]);
    } catch (error) {
      // error getting session
      rethrow;
    }
  },
  onSignIn: (SIWESession session) {
    // Called after getSession() succeeds
  },
  signOut: () async {
    // Called when wallet disconnects if `signOutOnDisconnect == true` and/or when
    // `signOutOnAccountChange == true` and/or
    // `signOutOnNetworkChange == true`
    try {
      final success = await yourApi.signOut();
      return success;
    } catch (error) {
      // error signing out
      return false;
    }
  },
  onSignOut: () {
    // Called after signOut() succeeds
  },
  // enabled: true, // OPTIONAL. Enables One-Click Auth + SIWE logic, if `false`, regular session proposal will be used. (default `true`)
  // signOutOnDisconnect: true, // OPTIONAL (default `true`)
  // signOutOnAccountChange: true, // OPTIONAL (default `true`)
  // signOutOnNetworkChange: true, // OPTIONAL (default `true`)
);

Initialize ReownAppKitModal with your siweConfig

Add the siwe configuration in ReownAppKitModal initialization

final _appKitModal = ReownAppKitModal(
  context: context,
  projectId: '{YOUR_PROJECT_ID}',
  metadata: const PairingMetadata(
    name: 'Example App',
    description: 'Example app description',
    url: 'https://example.com/',
    icons: ['https://example.com/logo.png'],
    redirect: Redirect(
      native: 'exampleapp://',
      universal: 'https://reown.com/exampleapp',
    ),
  ),
  siweConfig: SIWEConfig(...),
);

SIWEConfig reference

class SIWEConfig {
  final Future<String> Function() getNonce;
  final Future<SIWEMessageArgs> Function() getMessageParams;
  final String Function(SIWECreateMessageArgs args) createMessage;
  final Future<bool> Function(SIWEVerifyMessageArgs args) verifyMessage;
  final Future<SIWESession?> Function() getSession;
  final Future<bool> Function() signOut;
  // Callback when user signs in
  final Function(SIWESession session)? onSignIn;
  // Callback when user signs out
  final VoidCallback? onSignOut;
  // Defaults to true
  final bool enabled;
  // In milliseconds, defaults to 5 minutes
  final int nonceRefetchIntervalMs;
  // In milliseconds, defaults to 5 minutes
  final int sessionRefetchIntervalMs;
  // Defaults to true
  final bool signOutOnDisconnect;
  // Defaults to true
  final bool signOutOnAccountChange;
  // Defaults to true
  final bool signOutOnNetworkChange;
  //

  SIWEConfig({
    required this.getNonce,
    required this.getMessageParams,
    required this.createMessage,
    required this.verifyMessage,
    required this.getSession,
    required this.signOut,
    this.onSignIn,
    this.onSignOut,
    this.enabled = true,
    this.signOutOnDisconnect = true,
    this.signOutOnAccountChange = true,
    this.signOutOnNetworkChange = true,
    this.nonceRefetchIntervalMs = 300000,
    this.sessionRefetchIntervalMs = 300000,
  });
}

Not configuring siweConfig object has the same effect as setting false on siweConfig.enable parameter.

Exported functions

generateNonce

Simple method to generate a timestamp-based nonce

SIWEUtils.generateNonce();

formatMessage

Creates EIP-4361 message based on input arguments.

SIWEUtils.formatMessage(args);

verifySignature

Verify a SIWE signature. Internally it calls your backend verification method.

await SIWEUtils.verifySignature(
  address,
  message,
  signature,
  chainId,
  projectId
);

getChainIdFromMessage

Get the chain ID from the SIWE message.

SIWEUtils.getChainIdFromMessage(message);

getAddressFromMessage

Get the address from the SIWE message.

SIWEUtils.getAddressFromMessage(message);

Basic usage example

This basic configuration is enough to try the feature out without a backend

final _appKitModal = ReownAppKitModal(
  context: context,
  projectId: '{YOUR_PROJECT_ID}',
  metadata: const PairingMetadata(
    name: 'Example App',
    description: 'Example app description',
    url: 'https://example.com/',
    icons: ['https://example.com/logo.png'],
    redirect: Redirect(
      native: 'exampleapp://',
      universal: 'https://reown.com/exampleapp',
    ),
  ),
  siweConfig: SIWEConfig(
    getNonce: () async {
      return SIWEUtils.generateNonce();
    },
    getMessageParams: () async {
      return SIWEMessageArgs(
        domain: Uri.parse(_appKitModal.appKit!.metadata.url).authority,
        uri: _appKitModal.appKit!.metadata.url,
        statement: '{Your custom message here}',
        methods: MethodsConstants.allMethods,
      );
    },
    createMessage: (SIWECreateMessageArgs args) {
      return SIWEUtils.formatMessage(args);
    },
    verifyMessage: (SIWEVerifyMessageArgs args) async {
      final chainId = SIWEUtils.getChainIdFromMessage(args.message);
      final address = SIWEUtils.getAddressFromMessage(args.message);
      final cacaoSignature = args.cacao != null
          ? args.cacao!.s
          : CacaoSignature(
              t: CacaoSignature.EIP191,
              s: args.signature,
            );
      return await SIWEUtils.verifySignature(
        address,
        args.message,
        cacaoSignature,
        chainId,
        DartDefines.projectId,
      );
    },
    getSession: () async {
      final chainId = _appKitModal.selectedChain?.chainId ?? '1';
      final namespace = ReownAppKitModalNetworks.getNamespaceForChainId(
        chainId,
      );
      final address = _appKitModal.session!.getAddress(namespace)!;
      return SIWESession(address: address, chains: [chainId]);
    },
    signOut: () async {
      return true;
    },
  ),
);