Skip to main content

[Beta] Wallet Authentication

This feature is in beta and is still susceptible to API changes. Please refrain from using it in production environments until we have a stable version.

Overview

The following is a technical document outling thirdwebs new wallet authentication flows which enable users to verify and authenticate to backends using just their connect client-side wallet.

This functionality opens the door to a new realm of possibilities regarding the integration of web3 into all applications, including the following potential use cases:

  • Fully self-custodied authentication to any website backend with just a wallet (no third party dependencies or database requirements)
  • Securely verify existing users wallet addresses to use for reading associated on-chain data
  • Connect user accounts to wallet addresses to allow users to receive and trasfer digital assets
  • Integration of any web2 infrastructure into web3 applications including payments, databases, and other services

The following resources can be referenced in relation to this document:

As this feature is in beta, we appreciate any feedback or questions you may have while reading through this document, and encourage you to raise any concerns in our community discord

Background

The web3 technology stack enables a variety of offerings like verifiable ownership and decentralized public data that would stand to benefit many applications.

However, because most traditional/web2 applications rely on a client-server model and often a database, they are limited from accessing these capabilities as they are faced with the challenge of securely obtaining their users on-chain identities to use for ownership and data-reading purposes. We aim to remove this challenge with our new sign-in with ethereum based on-chain identity verification flow.

Furthermore, web3 now presents a unique opportunity where the first time, private keys used for security no longer need to lie hidden away in a backend or database, but are actually in the hands of users themselves via wallets. This technology opens the possibilities for a new form of self-custodied authentication in which users and applications are able to provide a new wallet based authentication that doesn’t rely on any external services or databases.

The following is a technical document outlining our solutions to the two considerations detailed above, specifically (1) a new method to securely transfer users wallet identities between clients and servers and (2) the creation of a new fully self-custodied wallet based authentication method

Goal

  • Identification: Enable services to securely verify a users on-chain identity
  • Authentication: Enable users to authenticate to any service using just their wallet

Implementation

The thirdweb authentication flow enables backends to identify and authenticate users wallets

auth.png

Secure Identification

The identification flow lets backends securely verify the wallet address of a client-side user. The flow takes advantage of the sign-in with ethereum flow (EIP-4361) to verify the users wallet.

Screen Shot 2022-06-14 at 10.24.12 PM.png

Interface

**sdk.auth.login(domain: string, options?: LoginOptions): LoginPayload**

  • Generates a payload signed by the connected wallet that can be used to provide a verifiable wallet address to a backend
type LoginOptions = {
expirationTime?: Date; // Defaults to 5 minutes from now
nonce?: string; // Defaults to UUIDv4
chainId?: number; // Defaults to empty
};

type LoginPayload = {
payload: {
address: string;
nonce: string;
issuedAt: Date;
expirationTime: Date;
chainId?: number;
};
signature: string;
};

const payload = await sdk.auth.login("thirdweb.com");

The message to sign should be of the following EIP-4361 compliant format, of which the rationale is described below:

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

Make sure that the requesting domain above matches the URL of the current website.

Chain ID: ${chainId}
Nonce: ${nonce}
Expiration Time: ${expirationTime}
  • domain - Ensures that the user isn’t signing on an unintended website (phishing attack)
  • address - Shows the user which wallet they are authenticating with + saves address in the payload to show intent
  • nonce - Provide a nonce in case backends want to make payloads only one-time use (will be a UUID nonce, generated by SDK)
  • expirationTime - Prevent a single payload from being used forever
  • chainId - Optionally pass a chain ID if the payload is being used to read on-chain data

sdk.auth.verify(domain: string, payload: LoginPayload): string

  • Returns the address of the login payload, or throws an error if payload is invalid
// Generate a payload from the connected wallet on the client side
const payload = await sdk.auth.login("thirdweb.com");

// Check if the login payload is valid and get the address
const address = sdk.auth.verify("thirdweb.com", payload);

The verification makes a few key checks to ensure the validity of the payload:

  • Ensure that the domain of the signed message matches the domain passed to the SDK
  • Ensure that the address in the payload, the address in the message, and the address that signed the message are all the same
  • Ensure that the expiration time has not come yet

If any of these conditions are not met, this method will throw an error.

Basic Authentication

Screen Shot 2022-06-14 at 10.25.32 PM.png

Considerations:

  • Phishing attacks are the main security concern for this flow. Signed messages should make it clear what users are signing so that attackers can’t prompt users to sign an ambiguous message and use it to authenticate with the users address.
    • The EIP-4361 specifies a message structure that provides sufficient information to mitigate phishing attacks
  • This flow is similar to JWT in that it uses an access token (payload) to authenticate. It’s important to note that if this access token is ever stolen, the users account will be hacked.
    • Thus, it’s important that the access token is securely stored on the client side for later use.
    • Typical JWT providers mitigate this issue by issuing JWT access tokens with very short validity times, along with refresh tokens which allow authenticated users to get a new payload when an old one expires. This flow is currently not implemented with our basic authentication flow, but it can be implemented as elaborated below in the “Using Auth Providers” section.

Interface:

**sdk.auth.generateAuthToken(domain: string, payload: LoginPayload, options?: AuthenticationOptions): string**

  • Generate a JWT token that can be used to send authenticated requests to the specified backend service
type AuthenticationOptions = {
expirationTime?: Date | number; // Defaults to 5 hours
invalidBefore?: Date | number; // Defaults to current time
};

// Generate a payload from the connected wallet on the client side
const loginPayload = await sdk.auth.login("thirdweb.com");

// Generate an authentication payload with the login payload
const authToken = await sdk.auth.generateAuthToken(
"thirdweb.com",
loginPayload,
);

The authenticate function takes the following steps

  • First calls sdk.auth.verify(domain, payload) to ensure that the login request is valid
  • If it is, generate a JWT payload structure (structure described below)

The JWT payload to be signed will take the following structure

{
"iss": "0x...", // address of the admin wallet generating the payload
"sub": "0x...", // Address of the user wallet logging in
"aud": "thirdweb.com", // Domain that the payload was intended for
"iat": 1653884584, // Unix time (epoch seconds) when payload was issued
"exp": 1653884613, // Unix time (epoch seconds) when payload expires
"nbf": 1653884584, // Time before which payload is invalid, should default to iat
"jti": "82da3d83-9b7b-4cdd-a890-f4d9b25415be" // uuidv4
}
  • Sign the JWT payload with the connected backend admin wallet, which can later be used for verification
  • In addition to the payload and signature, we use the following JWT header for our token:
{
// Specify that we are using the ECDSA SHA-256 hashing algorithm
"alg": "ES256",
// Standard specification for JWT token
"typ": "JWT",
}
  • Construct an actual JWT token from the payload and signature with the following steps, using the following process:
    • First we JSON.stringify both the header and the payload
    • Next, we base64 encode the stringified header, the stringified payload, and the raw signature.
    • Finally, we can construct the JWT token by creating the following . separated structure: {encodedHeader}.{encodedPayload}.{encodedSignature} (without the curly brackets)
    • The following is an example JWT token generated with this method:
    eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIweDllMWI4QTg2ZkZFRTRhNzE3NURBRTRiREIxY0MxMmQxMTFEY2IzRDYiLCJzdWIiOiIweDllMWI4QTg2ZkZFRTRhNzE3NURBRTRiREIxY0MxMmQxMTFEY2IzRDYiLCJhdWQiOiJ0aGlyZHdlYi5jb20iLCJleHAiOjE2NTU1MTcwNjYsIm5iZiI6MTY1NTQ5OTA2NiwiaWF0IjoxNjU1NDk5MDY2LCJqdGkiOiI1M2I1M2JlOC0zMWVmLTQzZjAtYjE4OS0zYTFiMWZjOTU4MjUifQ.MHg5ZTlkZTkxYmVmZTlmZGRjOWQzYWQxY2M5ODNjOWIzNzFhMmUwNGU2OTM1OGVkNjg1MWU5ZDM2ZTEzMWJmNjU4NDUwNDYxOTM1ZDlkMjcxODgwYTRkMTE1ZGVlNjk0OTkwNzMwN2ViZWM4ZmRkZTI3NTgxMDczNjllMTFkNWU5ZTFi
    • You can verify that the above token has a format compliant with the JWT standard by plugging it in to the JWT token debugger on the official JWT website https://jwt.io

sdk.auth.authenticate(domain: string, token: string): string

  • Authenticates a JWT token generated with the generateAuthToken function. Throws an error if the token is invalid, otherwise returns the address of the authenticating wallet.
// Generate a payload from the connected wallet on the client side
const loginPayload = await sdk.auth.login("thirdweb.com");

// Generate a JWT token
const token = await sdk.auth.generateAuthToken("thirdweb.com", loginPayload);

// Verify the auth token and extract the authenticating address
const address = sdk.auth.authenticate("thirdweb.com", token);

The verification step should verify all of the following:

  • Ensure that the signing authority matches the iss field
  • Check that the current time is after nbf and before exp
  • Check that the provided domain matches the aud field

Security Considerations

1. Secure Client-Side Token Storage

Just like the standard JWT flow, the authentication token is generated by the backend service and can be sent to the client-side user. The client-side user can then use it for authentication for as long as the payload is valid.

This means that if anyone obtains the auth token through any means, they can also authenticate with the token, which is why it’s crucial that the auth token is stored securely on the client-side.

Storing JWT tokens in localStorage or ordinary cookies is not a secure way to store tokens on the client-side, as these methods are prone to XSS attacks.

Our current recommend method of using these tokens is by setting httpOnly and secure headers on cookies from the backend - this lets the backend send secure cookies to the client-side that the frontend actually has no access to, which mitigates the risk of XSS attacks.

You can see an example implementation of this setup in our example authentication repository: https://github.com/thirdweb-example/sign-in-with-ethereum

2. Phishing on Login Request

The authentication flow relies on the user signing a message on the frontend to prove their identity and sending this to the backend. The backend can then use this message to verify the users identity and issue an authentication token.

This means that if a malicious website is able to get a user to sign a message with the same structure as that accepted by the backend, then they can use this signature to authenticate as the user.

Thus, it’s important that the signed message provides sufficient information to prevent such phishing attacks, which is why the message has the domain and the message indicating the user to check that the domain field matches the website so they are aware of what they are signing.

3. One Time Use Signatures

Login request signatures (created by sdk.auth.login()) have a 5 minute expiration time by default to minimize the amount of time that they are valid for making repeat authentication attempts.

It may be optimal in some cases to only enable signatures to be used once each. Because of this consideration, we’ve included a nonce on each signature so that backends that want to enforce this functionality can store nonces in their database and ensure that no previously used nonces can be used again.

In the future, we’ll offer a self-hosted option (as mentioned below) where we can handle this for users.

Using Auth Providers

Alternatively, you can take advantage of existing token-based authentication flows:

  • Use the sdk.auth.login() function and sdk.auth.verify() functions to securely get the users wallet address to the backend.
  • Then, you can use this requesting user to generate an authentication payload with a third party auth flow like auth0, and relay the requested access and refresh tokens to the user.

This enables users to take advantage of more advanced token-based auth flows that provide refresh token functionality.

  • Eventually, we’ll integrate this functionality directly into our authentication service - specifically, we’ll offer a self-hosted option that lets user plug in their database to get access/refresh token setup and one-time use authentication tokens out of the box.

Screen Shot 2022-06-14 at 10.25.15 PM.png