Web3 Authentication

An Initial Look and Proof of Concept

Description

This project implements an innovative Web3 authentication strategy that leverages blockchain technology to create a secure, decentralized, and user-centric authentication system. (Note: this article is for research and concept purposes only, please ensure any implementation you derive from this article is properly designed and audited as the subject of auth. and security is a complex topic and require more considerations than mentioned in this article).

Table of Contents

  1. Introduction
  2. How The Web3 Auth Strategy Works
  3. How it Compares to Other Strategies
  4. Implementation Details
  5. Conclusion

Introduction

In the ever-evolving landscape of web development, authentication remains a critical component of user security. This project explores an innovative Web3 authentication strategy that leverages the power of blockchain technology to create a more secure, decentralized, and user-centric authentication system.


How The Web3 Auth Strategy Works

Let's break down the key components of our Web3 authentication system:

Challenge Generation

When a user initiates the authentication process, our server generates a unique challenge:

Gofunc web3Challenge(w http.ResponseWriter, r *http.Request) {
  challenge := make([]byte, 32)
  _, err := rand.Read(challenge)
  if err != nil {
    http.Error(w, "Error generating challenge", http.StatusInternalServerError)
    return
  }

  response := Web3Challenge{
    Challenge: hex.EncodeToString(challenge),
  }

  json.NewEncoder(w).Encode(response)
}

This challenge is a randomly generated string that the user will need to sign with their private key.

Signature Verification

Once the user signs the challenge with their Ethereum wallet, they send back the signed challenge along with their Ethereum address and public key:

Gofunc web3Verify(w http.ResponseWriter, r *http.Request) {
  var signedChallenge Web3SignedChallenge
  err := json.NewDecoder(r.Body).Decode(&signedChallenge)
  if err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
  }

  // Verify the signature
  address := common.HexToAddress(signedChallenge.Address)
  pubKey, err := crypto.DecompressPubkey(common.FromHex(signedChallenge.PublicKey))
  if err != nil {
    http.Error(w, "Invalid public key", http.StatusBadRequest)
    return
  }

  challengeBytes, err := hex.DecodeString(signedChallenge.Challenge)
  if err != nil {
    http.Error(w, "Invalid challenge format", http.StatusBadRequest)
    return
  }

  signatureBytes := common.FromHex(signedChallenge.Signature)

  // Add prefix to the message. This is equivalent to what eth_sign does.
  prefixedChallenge := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(challengeBytes), challengeBytes)

  // Hash the prefixed message
  hash := crypto.Keccak256Hash([]byte(prefixedChallenge))

  // Verify the signature
  if !crypto.VerifySignature(crypto.FromECDSAPub(pubKey), hash.Bytes(), signatureBytes[:len(signatureBytes)-1]) {
    http.Error(w, "Signature verification failed", http.StatusUnauthorized)
    return
  }

  // ... (user creation or update logic)
}

Our server then verifies the signature using the Ethereum cryptographic libraries. This proves that the user controls the private key associated with the provided Ethereum address.

User Management

If the signature is valid, we either create a new user or update an existing one:

Govar user User
result := db.Where("eth_address = ?", address.Hex()).First(&user)
if result.Error != nil {
  // User doesn't exist, create a new one
  user = User{
    Username:  address.Hex(), // Use the Ethereum address as the username
    EthAddress: address.Hex(),
    PublicKey: signedChallenge.PublicKey,
  }
  result = db.Create(&user)
  if result.Error != nil {
    http.Error(w, "Error creating user", http.StatusInternalServerError)
    return
  }
} else {
  // User exists, update the public key if it's different
  if user.PublicKey != signedChallenge.PublicKey {
    user.PublicKey = signedChallenge.PublicKey
    db.Save(&user)
  }
}

This approach allows for a seamless signup/login process. The first time a user authenticates, an account is created. On subsequent authentications, their existing account is used.

Token Generation

After successful authentication, we generate a pair of tokens - an access token and a refresh token:

GotokenPair, err := createTokenPair(user.Username, user.ID)
if err != nil {
  http.Error(w, "Error creating tokens", http.StatusInternalServerError)
  return
}

json.NewEncoder(w).Encode(tokenPair)

// ... (createTokenPair function)
func createTokenPair(username string, userID uint) (TokenPair, error) {
  accessToken, err := createAccessToken(username)
  if err != nil {
    return TokenPair{}, err
  }

  refreshToken := uuid.New().String()
  expiryTime := time.Now().Add(7 * 24 * time.Hour)

  newRefreshToken := RefreshToken{
    Token: refreshToken,
    UserID: userID,
    Expiry: expiryTime,
  }

  result := db.Create(&newRefreshToken)
  if result.Error != nil {
    return TokenPair{}, result.Error
  }

  return TokenPair{
    AccessToken: accessToken,
    RefreshToken: refreshToken,
  }, nil
}

The access token is a short-lived JWT that the client can use for subsequent API calls. The refresh token is a long-lived token stored in the database, which can be used to obtain new access tokens without requiring the user to re-authenticate.


Comparing Web3 Auth to Other Strategies

Traditional Username/Password

Web3 Auth Advantage: Eliminates password storage and significantly reduces the attack surface.

OAuth 2.0 / OpenID Connect

Web3 Auth Advantage: Provides SSO benefits without relying on centralized providers, enhancing privacy and reducing third-party dependencies, and reducing unwanted cross platform data-sharing (e.g. your wallet doesn't hold any of your personal data the way Google does).

Multi-Factor Authentication (MFA)

Web3 Auth Advantage: Inherently provides multi-factor security without added user friction if the user already possesses their own crypto wallet.

WebAuthn / FIDO2

Web3 Auth Advantage: Offers similar security benefits without requiring special hardware beyond what Web3 users already have.

JWT/Session-based Authentication

Web3 Auth Advantage: Incorporates JWT for session management while providing stronger initial authentication. Additionally for higher-risk/irreversible actions, a new wallet signature can be requested to perform that specific action (e.g. changing account details, or transferring account assets) thereby limiting the problems that can be caused by session/token stealing without requiring a user to re-sign-in or verify  with their email and taking them off the app.


Implementation Details

The implementation uses Go and includes the following key components:

Cryptographic Security:

This proof of concept uses Ethereum's cryptographic libraries to verify signatures, ensuring a high level of security. You could switch this out with any other chain's cryptographic library depending on your app's chosen chain.

Goif !crypto.VerifySignature(crypto.FromECDSAPub(pubKey), hash.Bytes(), signatureBytes[:len(signatureBytes)-1]) {
  http.Error(w, "Signature verification failed", http.StatusUnauthorized)
  return
}
Decentralized Identity:

Users are identified by their Ethereum address, which they control. Platforms can't import any personal details about the user and can either prompt for needed account details like a username, generate a randomized username or use the wallet address as the username.

Gouser = User{
  Username:  address.Hex(), // Use the Ethereum address as the username
  EthAddress: address.Hex(),
  PublicKey: signedChallenge.PublicKey,
}
Stateless Authentication:

We use JWTs for ongoing authentication, making our system scalable. As mentioned above, you can use signatures for more critical actions which would be less stateless, but would be less frequently used (strategies embedding public keys inside the access tokens could allow any service that needs to validate signatures to do so without needing access to the user table), 

Gofunc createAccessToken(username string) (string, error) {
  expirationTime := time.Now().Add(15 * time.Minute)
  claims := &Claims{
    Username: username,
    StandardClaims: jwt.StandardClaims{
      ExpiresAt: expirationTime.Unix(),
    },
  }

  token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
  return token.SignedString(privateKey)
}
Refresh Token Mechanism:

We implement refresh tokens for long-term sessions while keeping access tokens short-lived. (Note: this is a very basic implementation for demonstration purposes).

Gofunc createTokenPair(username string, userID uint) (TokenPair, error) {
  accessToken, err := createAccessToken(username)
  if err != nil {
    return TokenPair{}, err
  }

  refreshToken := uuid.New().String()
  expiryTime := time.Now().Add(7 * 24 * time.Hour)

  newRefreshToken := RefreshToken{
    Token: refreshToken,
    UserID: userID,
    Expiry: expiryTime,
  }

  result := db.Create(&newRefreshToken)
  if result.Error != nil {
    return TokenPair{}, result.Error
  }

  return TokenPair{
    AccessToken: accessToken,
    RefreshToken: refreshToken,
  }, nil
}

This implementation combines the strengths of various authentication strategies while leveraging the unique benefits of Web3 technology. It provides a secure, user-centric, and forward-looking approach to authentication that is well-suited for the decentralized web.


Conclusion

The Web3 authentication strategy presented in this article represents a significant leap forward in secure, decentralized, and user-centric authentication systems. By leveraging blockchain technology and cryptographic principles, this approach addresses many of the shortcomings of traditional authentication methods while introducing new benefits:

  1. Enhanced Security: By eliminating password storage and utilizing cryptographic signatures, this strategy significantly reduces the attack surface for potential breaches.
  2. User Empowerment: Users maintain full control over their identity through their blockchain wallet, aligning with the principles of self-sovereign identity.
  3. Seamless User Experience: The strategy offers a frictionless authentication process for users already engaged in the Web3 ecosystem.
  4. Privacy-Preserving: Unlike OAuth providers, this method doesn't rely on centralized identity providers, enhancing user privacy.
  5. Flexibility: The system can be adapted to work with various blockchain networks and can be integrated into both Web3 and traditional applications.
  6. Future-Proofing: As blockchain technology continues to evolve and gain adoption, this authentication method positions applications to be at the forefront of technological advancement.

While this Web3 authentication strategy offers numerous advantages, it's crucial to remember that security is an ever-evolving field. Implementers should stay informed about the latest developments in blockchain security, cryptography, and authentication best practices. Additionally, considerations such as key management, user recovery processes, and integration with existing systems should be carefully addressed in any real-world implementation.

As we move towards a more decentralized internet, authentication strategies like the one presented here will play a pivotal role in shaping the future of digital identity and security. By embracing these innovative approaches, developers can create more secure, user-centric, and privacy-preserving applications that are well-suited for the Web3 era.

Liam Barter-Browning

Intermediate Software Developer, and Business Consultant.