|
@@ -10,11 +10,18 @@
|
|
* [Maximal Message Template](#maximal-message-template)
|
|
* [Maximal Message Template](#maximal-message-template)
|
|
* [Dapp Integration](#dapp-integration)
|
|
* [Dapp Integration](#dapp-integration)
|
|
* [Overview](#overview)
|
|
* [Overview](#overview)
|
|
- * [Integration Steps](#integration-steps)
|
|
|
|
-* [Wallet Integration Guide](#wallet-integration-guide)
|
|
|
|
|
|
+ * [Dependencies](#dependencies)
|
|
|
|
+ * [Sign-In Input Generation](#sign-in-input-generation-backend)
|
|
|
|
+ * [Sign-In Output Verification](#sign-in-output-verification-backend)
|
|
|
|
+ * [Context Provider](#context-provider-frontend)
|
|
|
|
+ * [Wallet Provider](#wallet-provider-frontend)
|
|
|
|
+* [Wallet Integration](#wallet-integration-guide)
|
|
* [Overview](#overview-1)
|
|
* [Overview](#overview-1)
|
|
-* [Best Practices](#best-practices)
|
|
|
|
-* [Dependencies](#dependencies)
|
|
|
|
|
|
+ * [Dependencies](#dependencies-1)
|
|
|
|
+ * [Wallet-Standard Wrapper and Provider Method](#wallet-standard-wrapper-and-provider-method)
|
|
|
|
+ * [Message Construction](#message-construction)
|
|
|
|
+ * [Message Parsing](#message-parsing)
|
|
|
|
+ * [Message Verification](#message-verification)
|
|
* [Full Feature Demo](#full-feature-demo)
|
|
* [Full Feature Demo](#full-feature-demo)
|
|
* [Reference Implementation](#reference-implementation)
|
|
* [Reference Implementation](#reference-implementation)
|
|
|
|
|
|
@@ -43,14 +50,14 @@ Additionally, SIWS standardises the message format, which enables wallets to scr
|
|
### Sign-In Input Fields
|
|
### Sign-In Input Fields
|
|
To make a sign-in request, dapps do not need to construct a message themselves, unlike EIP-4361 or legacy Solana messages. Rather, dapps construct the `signInInput` object containing a set of standard message parameters. All these fields are optional strings. Dapps can optionally send a minimal (empty) `signInInput` object to the wallet.
|
|
To make a sign-in request, dapps do not need to construct a message themselves, unlike EIP-4361 or legacy Solana messages. Rather, dapps construct the `signInInput` object containing a set of standard message parameters. All these fields are optional strings. Dapps can optionally send a minimal (empty) `signInInput` object to the wallet.
|
|
|
|
|
|
-- `domain`: Optional EIP-4361 domain requesting the sign-in. The domain includes the subdomain, top-level domain and the port number if present. If not provided, the wallet must determine the domain to include in the message.
|
|
|
|
|
|
+- `domain`: Optional EIP-4361 domain requesting the sign-in. If not provided, the wallet must determine the domain to include in the message.
|
|
- `address`: Optional Solana address performing the sign-in. The address is case-sensitive. If not provided, the wallet must determine the Address to include in the message.
|
|
- `address`: Optional Solana address performing the sign-in. The address is case-sensitive. If not provided, the wallet must determine the Address to include in the message.
|
|
- `statement`: Optional EIP-4361 Statement. The statement is a human readable string and should not have new-line characters (`\n`). If not provided, the wallet must not include Statement in the message.
|
|
- `statement`: Optional EIP-4361 Statement. The statement is a human readable string and should not have new-line characters (`\n`). If not provided, the wallet must not include Statement in the message.
|
|
- `uri`: Optional EIP-4361 URI. The URL that is requesting the sign-in. If not provided, the wallet must not include URI in the message.
|
|
- `uri`: Optional EIP-4361 URI. The URL that is requesting the sign-in. If not provided, the wallet must not include URI in the message.
|
|
- `version`: Optional EIP-4361 version, hardcoded to `1` if provided in the current spec. If not provided, the wallet must not include Version in the message.
|
|
- `version`: Optional EIP-4361 version, hardcoded to `1` if provided in the current spec. If not provided, the wallet must not include Version in the message.
|
|
- `chainId`: Optional EIP-4361 Chain ID. The chainId can be one of the following: `mainnet`, `testnet`, `devnet`, `localnet`, `solana:mainnet`, `solana:testnet`, `solana:devnet`. If not provided, the wallet must not include Chain ID in the message.
|
|
- `chainId`: Optional EIP-4361 Chain ID. The chainId can be one of the following: `mainnet`, `testnet`, `devnet`, `localnet`, `solana:mainnet`, `solana:testnet`, `solana:devnet`. If not provided, the wallet must not include Chain ID in the message.
|
|
- `nonce`: Optional EIP-4361 Nonce. It should be an alphanumeric string containing a minimum of 8 characters. If not provided, the wallet must not include Nonce in the message.
|
|
- `nonce`: Optional EIP-4361 Nonce. It should be an alphanumeric string containing a minimum of 8 characters. If not provided, the wallet must not include Nonce in the message.
|
|
-- `issuedAt`: Optional ISO 8601 datetime string. This represents the time at which the sign-in request was issued to the wallet. If not provided, the wallet must not include Issued At in the message.
|
|
|
|
|
|
+- `issuedAt`: Optional ISO 8601 datetime string. This represents the time at which the sign-in request was issued to the wallet. Note: For Phantom, issuedAt has a threshold and it should be within +- 10 minutes from the timestamp at which verification is taking place. If not provided, the wallet must not include Issued At in the message.
|
|
- `expirationTime`: Optional ISO 8601 datetime string. This represents the time at which the sign-in request should expire. If not provided, the wallet must not include Expiration Time in the message.
|
|
- `expirationTime`: Optional ISO 8601 datetime string. This represents the time at which the sign-in request should expire. If not provided, the wallet must not include Expiration Time in the message.
|
|
- `notBefore`: Optional ISO 8601 datetime string. This represents the time at which the sign-in request becomes valid. If not provided, the wallet must not include Not Before in the message.
|
|
- `notBefore`: Optional ISO 8601 datetime string. This represents the time at which the sign-in request becomes valid. If not provided, the wallet must not include Not Before in the message.
|
|
- `requestId`: Optional EIP-4361 Request ID. In addition to using `nonce` to avoid replay attacks, dapps can also choose to include a unique signature in the `requestId` . Once the wallet returns the signed message, dapps can then verify this signature against the state to add an additional, strong layer of security. If not provided, the wallet must not include Request ID in the message.
|
|
- `requestId`: Optional EIP-4361 Request ID. In addition to using `nonce` to avoid replay attacks, dapps can also choose to include a unique signature in the `requestId` . Once the wallet returns the signed message, dapps can then verify this signature against the state to add an additional, strong layer of security. If not provided, the wallet must not include Request ID in the message.
|
|
@@ -130,6 +137,7 @@ Resources:
|
|
```
|
|
```
|
|
|
|
|
|
## Dapp Integration
|
|
## Dapp Integration
|
|
|
|
+SIWS comes with first-class support in both the Solana Wallet Standard and Solana Wallet Adapter libraries. If your dapp makes use of the Solana Wallet Adapter, migration is easy.
|
|
### Overview
|
|
### Overview
|
|
1. User chooses one of the standard wallets (wallet standard compatible wallets)
|
|
1. User chooses one of the standard wallets (wallet standard compatible wallets)
|
|
2. Dapp checks if the given wallet has the signIn feature enabled
|
|
2. Dapp checks if the given wallet has the signIn feature enabled
|
|
@@ -140,53 +148,148 @@ Resources:
|
|
7. The Dapp verifies the returned message and the signature against the signInInput provided to the Wallet. This verification happens server-side
|
|
7. The Dapp verifies the returned message and the signature against the signInInput provided to the Wallet. This verification happens server-side
|
|
8. On successful verification, the authentication process is completed and the user is connected and authenticated to the Dapp
|
|
8. On successful verification, the authentication process is completed and the user is connected and authenticated to the Dapp
|
|
|
|
|
|
-### Integration Steps
|
|
|
|
-SIWS comes with first-class support in both the Solana Wallet Standard and Solana Wallet Adapter libraries. If your dapp makes use of the Solana Wallet Adapter, migration is easy:
|
|
|
|
|
|
+### Dependencies
|
|
|
|
+The first step is to update the necessary dependencies. Add/update these in your `package.json`:
|
|
|
|
|
|
-1. Update the necessary [dependencies](./dependencies)
|
|
|
|
-2. Add this snippet in your `ContextProvider` :
|
|
|
|
-
|
|
|
|
- ```tsx
|
|
|
|
- import { type SolanaSignInInput } from '@solana/wallet-standard-features';
|
|
|
|
- import { verifySignIn } from '@solana/wallet-standard-util';
|
|
|
|
-
|
|
|
|
- const autoSignIn = useCallback(async (adapter: Adapter) => {
|
|
|
|
- if (!('signIn' in adapter)) return true;
|
|
|
|
-
|
|
|
|
- // For demo purposes only: The signInInput should be generated server-side.
|
|
|
|
- const input: SolanaSignInInput = {
|
|
|
|
- statement: "Welcome to Drip!"
|
|
|
|
- };
|
|
|
|
- const output = await adapter.signIn(input);
|
|
|
|
-
|
|
|
|
- // For demo purposes only: The sign-in verification should happen server-side.
|
|
|
|
- if (!verifySignIn(input, output)) throw new Error('Sign In verification failed!');
|
|
|
|
-
|
|
|
|
- return false;
|
|
|
|
- }, []);
|
|
|
|
- ```
|
|
|
|
-
|
|
|
|
- This callback function determines whether a user should be auto-connected (returns `true`) or prompted to sign-in (returns `false`). It does this by:
|
|
|
|
|
|
+```json
|
|
|
|
+"@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",
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+The following configs will be important while testing as some packages like `@solana/wallet-adapter-material-ui` and `@solana/wallet-adapter-react-ui` use older versions of the react and base packages and may cause conflicts.
|
|
|
|
+
|
|
|
|
+```json
|
|
|
|
+"resolutions": {
|
|
|
|
+ "@solana/wallet-adapter-react": "0.15.34",
|
|
|
|
+ "@solana/wallet-adapter-base": "0.9.23"
|
|
|
|
+
|
|
|
|
+},
|
|
|
|
+"overrides": {
|
|
|
|
+ "@solana/wallet-adapter-react": "0.15.34",
|
|
|
|
+ "@solana/wallet-adapter-base": "0.9.23"
|
|
|
|
+}
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+### Sign-In Input Generation (Backend)
|
|
|
|
+The Sign-In input object should be generated server-side. In most cases, the object can be empty and thus the input generation step can be skipped. Create the following endpoint on your backend server:
|
|
|
|
+
|
|
|
|
+```tsx
|
|
|
|
+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;
|
|
|
|
+
|
|
|
|
+ // Convert the Date object to a string
|
|
|
|
+ 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;
|
|
|
|
+ // signInData can be kept empty as well as all the fields are optional
|
|
|
|
+};
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+### Sign-In Output Verification (Backend)
|
|
|
|
+Legacy message verification on Solana is tedious and dapps have to verify using the `tweetnacl` library as follows:
|
|
|
|
+
|
|
|
|
+```tsx
|
|
|
|
+const verified = nacl
|
|
|
|
+ .sign
|
|
|
|
+ .detached
|
|
|
|
+ .verify(
|
|
|
|
+ new TextEncoder().encode(message),
|
|
|
|
+ bs58.decode(signature),
|
|
|
|
+ bs58.decode(public_key)
|
|
|
|
+ )
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+Sign-In With Solana brings about a dev-ex improvement with a helper method implementation for message and signature verification: [`verifySignIn`](https://github.com/solana-labs/wallet-standard/blob/master/packages/core/util/src/signIn.ts#L8) method of the `@solana/wallet-standard-util` package. Under-the-hood, the `verifySignIn` method:
|
|
|
|
+1. parses and deconstructs the `signedMessage` field of the `output` (ie, the constructed message returned by the wallet)
|
|
|
|
+2. checks the extracted fields against the fields in the `input`
|
|
|
|
+3. re-constructs the message according to the ABNF Message Format
|
|
|
|
+4. verifies the message signature
|
|
|
|
+
|
|
|
|
+With all the complexity abstracted away, make a simple backend endpoint calling the `verifySignIn` method as follows:
|
|
|
|
+
|
|
|
|
+```tsx
|
|
|
|
+import type { SolanaSignInInput, SolanaSignInOutput } from '@solana/wallet-standard-features';
|
|
|
|
+import { verifySignIn } from '@solana/wallet-standard-util';
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+export function verifySIWS(input: SolanaSignInInput, output: SolanaSignInOutput): boolean {
|
|
|
|
+ return verifySignIn(input, output);
|
|
|
|
+}
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+### Context Provider (Frontend)
|
|
|
|
+Add the following `autoSignIn` callback method to your `ContextProvider`:
|
|
|
|
|
|
- - Checking if the user’s wallet supports the `signIn` feature.
|
|
|
|
- - If `signIn` is not available, this callback returns `true`
|
|
|
|
- - If `signIn` is available, checking if:
|
|
|
|
- - The `SolanaSignInInput` object is prepared
|
|
|
|
- - The input is passed into the `signIn` method
|
|
|
|
- - The output is verified using the `verifySignIn` [method](https://github.com/solana-labs/wallet-standard/blob/alpha/packages/core/util/src/verify.ts)
|
|
|
|
- - If all of the above, the callback returns `false` and the user is prompted to sign-in
|
|
|
|
|
|
+```tsx
|
|
|
|
+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
|
|
|
|
+ const verifyResponse = await fetch('/backend/verifySIWS', {
|
|
|
|
+ method: 'POST',
|
|
|
|
+ body: JSON.stringify({input, output}),
|
|
|
|
+ });
|
|
|
|
+ const success = await verifyResponse.json();
|
|
|
|
+
|
|
|
|
+ // If verification fails, throw an error
|
|
|
|
+ if (!success) throw new Error('Sign In verification failed!');
|
|
|
|
+
|
|
|
|
+ return false;
|
|
|
|
+}, []);
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+This callback function determines whether a user should be auto-connected (returns `true`) or prompted to sign-in (returns `false`).
|
|
|
|
+
|
|
|
|
+- Check if the user’s wallet supports the `signIn` feature.
|
|
|
|
+ - If `signIn` is not available, this callback returns `true`
|
|
|
|
+- If `signIn` is available:
|
|
|
|
+ - Create the `SolanaSignInInput` object server-side
|
|
|
|
+ - The input is passed into the `signIn` method and the sign-in request is triggered
|
|
|
|
+ - The output is verified using the `verifySignIn` [method](https://github.com/solana-labs/wallet-standard/blob/master/packages/core/util/src/signIn.ts#L8) server-side
|
|
|
|
+ - If all of the above, the callback returns `false` and the user is prompted to sign-in
|
|
|
|
|
|
-3. Pass `autoSignIn` to `WalletProvider` like so:
|
|
|
|
|
|
+### Wallet Provider (Frontend)
|
|
|
|
+Pass `autoSignIn` callback to the `autoConnect` attribute of `WalletProvider`:
|
|
|
|
|
|
```tsx
|
|
```tsx
|
|
<WalletProvider
|
|
<WalletProvider
|
|
- wallets={wallets}
|
|
|
|
- onError={onError}
|
|
|
|
- autoConnect={autoSignIn}
|
|
|
|
|
|
+ wallets={wallets}
|
|
|
|
+ onError={onError}
|
|
|
|
+ autoConnect={autoSignIn}
|
|
>
|
|
>
|
|
```
|
|
```
|
|
|
|
|
|
-## Wallet Integration Guide
|
|
|
|
|
|
+When the connecting wallet does not support the `signIn` feature, `autoSignIn` returns `true` and thus the wallet goes on to `autoConnect` to the dapp, rather than `signIn`.
|
|
|
|
+
|
|
|
|
+## Wallet Integration
|
|
### Overview
|
|
### Overview
|
|
1. The Wallet receieves the signIn request from the dapp
|
|
1. The Wallet receieves the signIn request from the dapp
|
|
2. The Wallet constructs a message in the ABNF format using the message parameter strings
|
|
2. The Wallet constructs a message in the ABNF format using the message parameter strings
|
|
@@ -199,32 +302,158 @@ SIWS comes with first-class support in both the Solana Wallet Standard and Solan
|
|
9. In case the user accepts the request, the wallet connects the user to the Dapp and signs the constructed message
|
|
9. In case the user accepts the request, the wallet connects the user to the Dapp and signs the constructed message
|
|
10. The Wallet returns the constructed message, the message signature and the public address of the connected account back to the Dapp
|
|
10. The Wallet returns the constructed message, the message signature and the public address of the connected account back to the Dapp
|
|
|
|
|
|
-## Best Practices
|
|
|
|
-
|
|
|
|
-- In a production application, both `SolanaSignInInput` generation and `SolanaSignInOutput` verification **should happen** **server-side**. Client-side verification should not be relied upon due to its inherent security flaws. Developers can make use of the asynchronous nature of the `autoSignIn` callback to make a request to the server to obtain SIWS params, and make another request to the server to perform verification of the SIWS input and output.
|
|
|
|
-- When generating `SolanaSignInInput`, developers should include as many fields as possible. Each field adds an extra layer of security to protect users against various forms of attacks. Examples of these fields include `nonce`, `chainId`, `issuedAt`, `expirationTime`, and `requestId`.
|
|
|
|
-
|
|
|
|
-## Dependencies
|
|
|
|
-These dependencies need to be added to your `package.json`:
|
|
|
|
-
|
|
|
|
|
|
+### Dependencies
|
|
|
|
+Wallets will need to upgrade the following packages:
|
|
```json
|
|
```json
|
|
-"@solana/wallet-adapter-base": "0.9.23",
|
|
|
|
-"@solana/wallet-adapter-react": "0.15.34",
|
|
|
|
"@solana/wallet-standard-features": "1.1.0",
|
|
"@solana/wallet-standard-features": "1.1.0",
|
|
"@solana/wallet-standard-util": "1.1.0",
|
|
"@solana/wallet-standard-util": "1.1.0",
|
|
```
|
|
```
|
|
|
|
|
|
-The following configs will be important while testing as some packages like `@solana/wallet-adapter-material-ui` and `@solana/wallet-adapter-react-ui` use older versions of the react and base packages and may cause conflicts.
|
|
|
|
|
|
+### Wallet-Standard Wrapper and Provider Method
|
|
|
|
+First step will be to implement a provider method and a wrapper for the `signIn` provider method to make the feature Wallet-Standard compatible. Here's a [sample implementation](https://github.com/solana-labs/wallet-standard/blob/9d17ab038fb4c39fa08378571de40ea5ad593d46/packages/wallets/ghost/src/wallet.ts#L288) for the Wallet-Standard wrapper:
|
|
|
|
|
|
-```json
|
|
|
|
-"resolutions": {
|
|
|
|
- "@solana/wallet-adapter-react": "0.15.34",
|
|
|
|
- "@solana/wallet-adapter-base": "0.9.23"
|
|
|
|
-
|
|
|
|
-},
|
|
|
|
-"overrides": {
|
|
|
|
- "@solana/wallet-adapter-react": "0.15.34",
|
|
|
|
- "@solana/wallet-adapter-base": "0.9.23"
|
|
|
|
|
|
+```tsx
|
|
|
|
+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;
|
|
|
|
+ };
|
|
|
|
+}
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+### Message Construction
|
|
|
|
+Once we have the provider method and the Wallet-Standard wrapper, we can start with the message construction. The message should be constructed following the [ABNF Message format](#abnf-message-format). The construction follows the same algorithm implemented in the [`createSignInMessageText` method](https://github.com/solana-labs/wallet-standard/blob/9d17ab038fb4c39fa08378571de40ea5ad593d46/packages/core/util/src/signIn.ts#L121) of the `@solana/wallet-standard-utils` package.
|
|
|
|
+
|
|
|
|
+One thing to note is that although the `domain` and the `address` are not mandatory fields for the `signInInput`, they are mandatory for the constucted message. If these fields are not present in the input, they need to be extracted by the wallet using the requesting domain and address.
|
|
|
|
+
|
|
|
|
+```tsx
|
|
|
|
+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;
|
|
|
|
+}
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+### Message Parsing
|
|
|
|
+Message parsing can be made easy and manageable using an ABNF Parser Generator like apg-js: (!https://github.com/ldthomas/apg-js).
|
|
|
|
+
|
|
|
|
+Wallets can create BNF grammar files and the `apg-js` packages recursively generates parsers for the constructed message.
|
|
|
|
+
|
|
|
|
+The grammar for SIWS ABNF messages follows [this](#abnf-message-format) format.
|
|
|
|
+
|
|
|
|
+### Message Verification
|
|
|
|
+Once successfully parsed, individual message fields should be verified against predefined thresholds and conditions. Here is a sample implementation of the verification method:
|
|
|
|
+
|
|
|
|
+```tsx
|
|
|
|
+export function verify(data: SolanaSignInInput, opts: VerificationOptions) {
|
|
|
|
+ const { expectedAddress, expectedURL, expectedChainId, issuedAtThreshold } = opts;
|
|
|
|
+ const errors: VerificationErrorType[] = [];
|
|
|
|
+ const now = Date.now();
|
|
|
|
+
|
|
|
|
+ // verify if parsed address is same as the expected address
|
|
|
|
+ if (data.address !== expectedAddress) {
|
|
|
|
+ errors.push(VerificationErrorType.ADDRESS_MISMATCH);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // verify if parsed domain is same as the expected domain
|
|
|
|
+ if (data.domain !== expectedURL.host) {
|
|
|
|
+ errors.push(VerificationErrorType.DOMAIN_MISMATCH);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // verify if parsed uri is same as the expected uri
|
|
|
|
+ if (data.uri && data.uri.origin !== expectedURL.origin) {
|
|
|
|
+ errors.push(VerificationErrorType.URI_MISMATCH);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // verify if parsed chainId is same as the expected chainId
|
|
|
|
+ if (data.chainId && data.chainId !== expectedChainId) {
|
|
|
|
+ errors.push(VerificationErrorType.CHAIN_ID_MISMATCH);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // verify if parsed issuedAt is within +- issuedAtThreshold of the current timestamp
|
|
|
|
+ // NOTE: Phantom's issuedAtThreshold is 10 minutes
|
|
|
|
+ if (data.issuedAt) {
|
|
|
|
+ const iat = data.issuedAt.getTime();
|
|
|
|
+ if (Math.abs(iat - now) > issuedAtThreshold) {
|
|
|
|
+ if (iat < now) {
|
|
|
|
+ errors.push(VerificationErrorType.ISSUED_TOO_FAR_IN_THE_PAST);
|
|
|
|
+ } else {
|
|
|
|
+ errors.push(VerificationErrorType.ISSUED_TOO_FAR_IN_THE_FUTURE);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // verify if parsed expirationTime is:
|
|
|
|
+ // 1. after the current timestamp
|
|
|
|
+ // 2. after the parsed issuedAt
|
|
|
|
+ // 3. after the parsed notBefore
|
|
|
|
+ if (data.expirationTime) {
|
|
|
|
+ const exp = data.expirationTime.getTime();
|
|
|
|
+ if (exp <= now) {
|
|
|
|
+ errors.push(VerificationErrorType.EXPIRED);
|
|
|
|
+ }
|
|
|
|
+ if (data.issuedAt && exp < data.issuedAt.getTime()) {
|
|
|
|
+ errors.push(VerificationErrorType.EXPIRES_BEFORE_ISSUANCE);
|
|
|
|
+ }
|
|
|
|
+ // Not Before
|
|
|
|
+ if (data.notBefore) {
|
|
|
|
+ const nbf = data.notBefore.getTime();
|
|
|
|
+ if (nbf > exp) {
|
|
|
|
+ errors.push(VerificationErrorType.VALID_AFTER_EXPIRATION);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return errors;
|
|
}
|
|
}
|
|
```
|
|
```
|
|
|
|
|