Sign In With Solana (SIWS)

repository·main·Indexed 41 days ago

https://github.com/phantom/sign-in-with-solana

A standardized authentication protocol for Solana-based dapps that replaces manual message signing with a structured, one-click sign-in flow. This reference implementation (v1.0.0) provides guidelines for constructing signInInput objects, handling signInOutput, and integrating with the Solana Wallet Standard and Wallet Adapter.

Tokens
6.9K
Snippets
16
Records
22
Agent score
99%

What's inside SIWS

  1. What is Sign In With Solana (SIWS)?

    main

    Sign In With Solana (SIWS) is a standardized authentication protocol that allows applications to authenticate users and prove ownership of their Solana addresses. It replaces the traditional two-step connect + signMessage flow with a single signIn method.

    By shifting the responsibility of message construction from the dapp to the wallet, SIWS provides:

    • Consistent UX: Standardized message formats across different wallets.
    • Enhanced Security: Wallets can scrutinize message data, detect domain impersonation (domain binding), and raise red flags for suspicious activity.
    • Simplified Integration: Dapps provide a structured input object instead of raw strings.
  2. Understand the SIWS message format

    main

    The wallet constructs the message based on the provided signInInput.

    Minimal Message Template (if signInInput is empty):

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

    Maximal Message Template (if all fields are provided):

    ${domain} wants you to sign in with your Solana 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]}
    ...-
  3. Integrate Sign In With Solana into a Wallet

    main

    To integrate SIWS, a wallet must follow a specific lifecycle when receiving a signIn request from a dapp:

    1. Receive Request: The wallet receives the signIn request.
    2. Construct Message: The wallet builds a message string following the ABNF format using the provided SolanaSignInInput parameters.
    3. Parse & Validate: The wallet parses the constructed message to ensure it adheres to the SIWS ABNF specification. If parsing fails, the wallet must throw an RPC error and not show the prompt to the user.
    4. Verify Parameters: The wallet verifies that the parsed parameters (like domain, address, chainId, etc.) match expected values and fall within safe thresholds.
    5. Prompt User:
      • If verification succeeds, show the message statement (with advanced details hidden by default).
      • If verification fails, show the prompt but include the specific verification errors.
    6. Handle Response:
      • If the user accepts, the wallet signs the message and connects the user to the dapp.
      • If the user declines, the request is cancelled.
    7. Return Data: Upon acceptance, the wallet returns the constructed message, the message signature, and the public address of the connected account to the dapp.
  4. Generate Sign-In Input on the backend

    main

    The SolanaSignInInput object should be generated on your backend server. While all fields are optional and the object can be empty in some cases, providing specific parameters improves security and context.

    Key fields in SolanaSignInInput include:

    • domain: The host of the dapp.
    • statement: A human-readable description of the request.
    • version: The SIWS version (e.g., "1").
    • nonce: A unique string to prevent replay attacks.
    • chainId: The network identifier (e.g., "mainnet").
    • issuedAt: An ISO string representing the request time.
    • resources: An array of URIs the user is interacting with.
    import { SolanaSignInInput } from "@solana/wallet-standard-features";
    
    export const createSignInData = async (): Promise<SolanaSignInInput> => {
      const now: Date = new Date();
      const uri = window.location.href;
      const currentUrl = new URL(uri);
      const domain = currentUrl.host;
    
      const currentDateTime = now.toISOString();
    
      const signInData: SolanaSignInInput = {
        domain,
        statement:
          "Clicking Sign or Approve only means you have proved this wallet is owned by you. This request will not trigger any blockchain transaction or cost any gas fee.",
        version: "1",
        nonce: "oBbLoEldZs",
        chainId: "mainnet",
        issuedAt: currentDateTime,
        resources: ["https://example.com", "https://phantom.app/"],
      };
    
      return signInData;
    };
  5. Update dependencies for SIWS support

    main

    To use SIWS, ensure your package.json includes the following minimum versions for the Solana wallet ecosystem:

    "@solana/wallet-adapter-base": "0.9.23",
    "@solana/wallet-adapter-react": "0.15.34",
    "@solana/wallet-standard-features": "1.1.0",
    "@solana/wallet-standard-util": "1.1.0"
  6. Implement the `autoSignIn` callback for `WalletProvider`

    main

    To automate the sign-in flow, implement an autoSignIn callback and pass it to the autoConnect prop of your WalletProvider.

    Logic Flow:

    1. Check if the adapter supports the signIn feature. If not, return true (to allow legacy autoConnect).
    2. If supported, fetch the SolanaSignInInput from your backend.
    3. Call adapter.signIn(input) to trigger the wallet request.
    4. Send the input and output to your backend for verification.
    5. If verification succeeds, return false (indicating the user is now signed in and the auto-connect process is handled/interrupted by the sign-in flow). If it fails, throw an error.
    import { type SolanaSignInInput } from "@solana/wallet-standard-features";
    import { verifySignIn } from "@solana/wallet-standard-util";
    
    const autoSignIn = useCallback(async (adapter: Adapter) => {
      // If the signIn feature is not available, return true
      if (!("signIn" in adapter)) return true;
    
      // Fetch the signInInput from the backend
      const createResponse = await fetch("/backend/createSignInData");
    
      const input: SolanaSignInInput = await createResponse.json();
    
      // Send the signInInput to the wallet and trigger a sign-in request
      const output = await adapter.signIn(input);
    
      // Verify the sign-in output against the generated input server-side
      let strPayload = JSON.stringify({ input, output });
      const verifyResponse = await fetch("/backend/verifySIWS", {
        method: "POST",
        body: strPayload,
      });
      const success = await verifyResponse.json();
    
      // If verification fails, throw an error
      if (!success) throw new Error("Sign In verification failed!");
    
      return false;
    }, []);
    
    // Usage in component:
    <WalletProvider
        wallets={wallets}
        onError={onError}
        autoConnect={autoSignIn}
    >
  7. Set up the SIWS Reference Implementation locally

    main

    To run the Sign In With Solana (SIWS) reference implementation on your local machine, install the project dependencies and start the development server. The application will be available at http://localhost:3000.

    # Install dependencies
    npm install
    # or
    yarn install
    
    # Start the development server
    npm run start
    # or
    yarn start
  8. Integrate Sign In With Solana (SIWS) into a Dapp

    main

    SIWS is supported by the Solana Wallet Standard and Solana Wallet Adapter. The integration flow follows these steps:

    1. Feature Detection: Check if the selected wallet has the signIn feature enabled.
    2. Fallback: If signIn is not supported, use the legacy connect + signMessage mechanism.
    3. Input Generation: Construct a signInInput object (containing parameters like domain, nonce, chainId, etc.) on the backend.
    4. Request: Send the signInInput to the wallet via the signIn method.
    5. Response: Receive the signInOutput (constructed message, signature, and public address) from the wallet.
    6. Verification: Verify the returned message and signature against the original signInInput on the server-side.
    7. Completion: Upon successful verification, the user is authenticated.
  9. Use the AutoConnectProvider to manage automatic wallet connection

    main

    The AutoConnectProvider is a React Context Provider used to manage the autoConnect state, which determines whether a wallet should automatically connect to the dapp. It uses localStorage (via @solana/wallet-adapter-react) to persist the user's preference across sessions.

    Wrap your application (or the part of your app requiring wallet connection logic) with AutoConnectProvider to enable the use of the useAutoConnect hook.

    import { AutoConnectProvider } from './components/AutoConnectProvider';
    
    function App() {
      return (
        <AutoConnectProvider>
          <YourDappComponents />
        </AutoConnectProvider>
      );
    }
  10. Implement a Wallet-Standard Wrapper for `signIn`

    main

    To make the signIn feature compatible with the Wallet-Standard, you must implement a provider method and a wrapper. The wrapper should handle multiple inputs by iterating through them and returning an array of SolanaSignInOutput.

    import type {
      SolanaSignInInput,
      SolanaSignInOutput,
    } from "@solana/wallet-standard-features";
    
    export class PhantomWallet implements Wallet {
      #signIn: SolanaSignInMethod = async (...inputs) => {
        const outputs: SolanaSignInOutput[] = [];
        if (inputs.length > 1) {
          for (const input of inputs) {
            outputs.push(await this.#phantom.signIn(input));
          }
        } else {
          return [await this.#phantom.signIn(inputs[0])];
        }
        return outputs;
      };
    }
  11. Construct SIWS Messages using `createSignInMessageText`

    main

    Messages must be constructed following the ABNF format. The @solana/wallet-standard-utils package provides a createSignInMessageText method that implements the required algorithm.

    Important: While domain and address are not strictly mandatory in the SolanaSignInInput object, they are mandatory in the final constructed message. If they are missing from the input, the wallet must extract them from the requesting domain and address before construction.

    export function createSignInMessageText(input: SolanaSignInInput): string {
      let message = `${input.domain} wants you to sign in with your Solana account:\n`;
      message += `${input.address}`;
    
      if (input.statement) {
        message += `\n\n${input.statement}`;
      }
    
      const fields: string[] = [];
      if (input.uri) {
        fields.push(`URI: ${input.uri}`);
      }
      if (input.version) {
        fields.push(`Version: ${input.version}`);
      }
      if (input.chainId) {
        fields.push(`Chain ID: ${input.chainId}`);
      }
      if (input.nonce) {
        fields.push(`Nonce: ${input.nonce}`);
      }
      if (input.issuedAt) {
        fields.push(`Issued At: ${input.issuedAt}`);
      }
      if (input.expirationTime) {
        fields.push(`Expiration Time: ${input.expirationTime}`);
      }
      if (input.notBefore) {
        fields.push(`Not Before: ${input.notBefore}`);
      }
      if (input.requestId) {
        fields.push(`Request ID: ${input.requestId}`);
      }
      if (input.resources) {
        fields.push(`Resources:`);
        for (const resource of input.resources) {
          fields.push(`- ${resource}`);
        }
      }
      if (fields.length) {
        message += `\n\n${fields.join("\n")}`;
      }
    
      return message;
    }