Spring Authentication With MetaMask - DZone (2024)

When choosing a user authentication method for your application, you usually have several options: develop your own system for identification, authentication, and authorization, or use a ready-made solution. A ready-made solution means that the user already has an account on an external system such as Google, Facebook, or GitHub, and you use the appropriate mechanism, most likely OAuth, to provide limited access to the user’s protected resources without transferring the username and password to it. The second option with OAuth is easier to implement, but there is a risk for your user if the user's account is blocked and the user will lose access to your site. Also, if I, as a user, want to enter a site that I do not trust, I have to provide my personal information, such as my email and full name, sacrificing my anonymity.

In this article, we’ll build an alternative login method for Spring using the MetaMask browser extension. MetaMask is a cryptocurrency wallet used to manage Ethereum assets and interact with the Ethereum blockchain. Unlike the OAuth provider, only the necessary set of data can be stored on the Ethereum network. We must take care not to store secret information in the public data, but since any wallet on the Ethereum network is in fact a cryptographic strong key pair, in which the public key determines the wallet address and the private key is never transmitted over the network and is known only by the owner, we can use asymmetric encryption to authenticate users.

Authentication Flow

Spring Authentication With MetaMask - DZone (1)

  1. Connect to MetaMask and receive the user’s address.
  2. Obtain a one-time code (nonce) for a user address.
  3. Sign a message containing nonce with a private key using MetaMask.
  4. Authenticate the user by validating the user's signature on the back end.
  5. Generate a new nonce to prevent your signature from being compromised.

Step 1: Project Setup

To quickly build a project, we can use Spring Initializr. Let’s add the following dependencies:

  • Spring Web
  • Spring Security
  • Thymeleaf
  • Lombok

Download the generated project and open it with a convenient IDE. In the pom.xml, we add the following dependency to verify the Ethereum signature:

XML

<dependency><groupId>org.web3j</groupId><artifactId>core</artifactId><version>4.10.2</version></dependency>

Step 2: User Model

Let’s create a simple User model containing the following fields: address and nonce. The nonce, or one-time code, is a random number we will use for authentication to ensure the uniqueness of each signed message.

Java

public class User { private final String address; private Integer nonce; public User(String address) { this.address = address; this.nonce = (int) (Math.random() * 1000000); } // getters}

To store users, for simplicity, I’ll be using an in-memory Map with a method to retrieve User by address, creating a new User instance in case the value is missing:

Java

@Repositorypublic class UserRepository { private final Map<String, User> users = new ConcurrentHashMap<>(); public User getUser(String address) { return users.computeIfAbsent(address, User::new); }}

Let's define a controller allowing users to fetch nonce by their public address:

Java

@RestControllerpublic class NonceController { @Autowired private UserRepository userRepository; @GetMapping("/nonce/{address}") public ResponseEntity<Integer> getNonce(@PathVariable String address) { User user = userRepository.getUser(address); return ResponseEntity.ok(user.getNonce()); }}

Step 3: Authentication Filter

To implement a custom authentication mechanism with Spring Security, first, we need to define our AuthenticationFilter. Spring filters are designed to intercept requests for certain URLs and perform some actions. Each filter in the chain can process the request, pass it to the next filter in the chain, or not pass it, immediately sending a response to the client.

Java

public class MetaMaskAuthenticationFilter extends AbstractAuthenticationProcessingFilter { protected MetaMaskAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { UsernamePasswordAuthenticationToken authRequest = getAuthRequest(request); authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); return this.getAuthenticationManager().authenticate(authRequest); } private UsernamePasswordAuthenticationToken getAuthRequest(HttpServletRequest request) { String address = request.getParameter("address"); String signature = request.getParameter("signature"); return new MetaMaskAuthenticationRequest(address, signature); }}

Our MetaMaskAuthenticationFilter will intercept requests with the POST "/login" pattern. In the attemptAuthentication(HttpServletRequest request, HttpServletResponse response) method, we extract address and signature parameters from the request. Next, these values are used to create an instance of MetaMaskAuthenticationRequest, which we pass as a login request to the authentication manager:

Java

public class MetaMaskAuthenticationRequest extends UsernamePasswordAuthenticationToken { public MetaMaskAuthenticationRequest(String address, String signature) { super(address, signature); super.setAuthenticated(false); } public String getAddress() { return (String) super.getPrincipal(); } public String getSignature() { return (String) super.getCredentials(); }}

Step 4: Authentication Provider

Our MetaMaskAuthenticationRequest should be processed by a custom AuthenticationProvider, where we can validate the user's signature and return a fully authenticated object. Let’s create an implementation of AbstractUserDetailsAuthenticationProvider, which is designed to work with UsernamePasswordAuthenticationToken instances:

Java

@Componentpublic class MetaMaskAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { @Autowired private UserRepository userRepository; @Override protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { MetaMaskAuthenticationRequest auth = (MetaMaskAuthenticationRequest) authentication; User user = userRepository.getUser(auth.getAddress()); return new MetaMaskUserDetails(auth.getAddress(), auth.getSignature(), user.getNonce()); } @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { MetaMaskAuthenticationRequest metamaskAuthenticationRequest = (MetaMaskAuthenticationRequest) authentication; MetaMaskUserDetails metamaskUserDetails = (MetaMaskUserDetails) userDetails; if (!isSignatureValid(authentication.getCredentials().toString(), metamaskAuthenticationRequest.getAddress(), metamaskUserDetails.getNonce())) { logger.debug("Authentication failed: signature is not valid"); throw new BadCredentialsException("Signature is not valid"); } } ...}

The first method, retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) should load the User entity from our UserRepository and compose the UserDetails instance containing address, signature, and nonce:

Java

public class MetaMaskUserDetails extends User { private final Integer nonce; public MetaMaskUserDetails(String address, String signature, Integer nonce) { super(address, signature, Collections.emptyList()); this.nonce = nonce; } public String getAddress() { return getUsername(); } public Integer getNonce() { return nonce; }}

The second method, additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) will do the signature verification using the Elliptic Curve Digital Signature Algorithm (ECDSA). The idea of this algorithm is to recover the wallet address from a given message and signature. If the recovered address matches our address from MetaMaskUserDetails, then the user can be authenticated.

1. Get the message hash by adding a prefix to make the calculated signature recognizable as an Ethereum signature:

Java

String prefix = "\u0019Ethereum Signed Message:\n" + message.length();byte[] msgHash = Hash.sha3((prefix + message).getBytes());

2. Extract the r, s and v components from the Ethereum signature and create a SignatureData instance:

Java

byte[] signatureBytes = Numeric.hexStringToByteArray(signature);byte v = signatureBytes[64];if (v < 27) {v += 27;}byte[] r = Arrays.copyOfRange(signatureBytes, 0, 32);byte[] s = Arrays.copyOfRange(signatureBytes, 32, 64);Sign.SignatureData data = new Sign.SignatureData(v, r, s);

3. Using the method Sign.recoverFromSignature(), retrieve the public key from the signature:

Java

BigInteger publicKey = Sign.signedMessageHashToKey(msgHash, sd);

4. Finally, get the wallet address and compare it with the initial address:

Java

String recoveredAddress = "0x" + Keys.getAddress(publicKey);if (address.equalsIgnoreCase(recoveredAddress)) { // Signature is valid.} else { // Signature is not valid.}

There is a complete implementation of isSignatureValid(String signature, String address, Integer nonce) method with nonce:

Java

public boolean isSignatureValid(String signature, String address, Integer nonce) { // Compose the message with nonce String message = "Signing a message to login: %s".formatted(nonce); // Extract the ‘r’, ‘s’ and ‘v’ components byte[] signatureBytes = Numeric.hexStringToByteArray(signature); byte v = signatureBytes[64]; if (v < 27) { v += 27; } byte[] r = Arrays.copyOfRange(signatureBytes, 0, 32); byte[] s = Arrays.copyOfRange(signatureBytes, 32, 64); Sign.SignatureData data = new Sign.SignatureData(v, r, s); // Retrieve public key BigInteger publicKey; try { publicKey = Sign.signedPrefixedMessageToKey(message.getBytes(), data); } catch (SignatureException e) { logger.debug("Failed to recover public key", e); return false; } // Get recovered address and compare with the initial address String recoveredAddress = "0x" + Keys.getAddress(publicKey); return address.equalsIgnoreCase(recoveredAddress);}

Step 5: Security Configuration

In the Security Configuration, besides the standard formLogin setup, we need to insert our MetaMaskAuthenticationFilter into the filter chain before the default:

Java

@Beanpublic SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception { return http .authorizeHttpRequests(customizer -> customizer .requestMatchers(HttpMethod.GET, "/nonce/*").permitAll() .anyRequest().authenticated()) .formLogin(customizer -> customizer.loginPage("/login") .failureUrl("/login?error=true") .permitAll()) .logout(customizer -> customizer.logoutUrl("/logout")) .csrf(AbstractHttpConfigurer::disable) .addFilterBefore(authenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class) .build();}private MetaMaskAuthenticationFilter authenticationFilter(AuthenticationManager authenticationManager) { MetaMaskAuthenticationFilter filter = new MetaMaskAuthenticationFilter(); filter.setAuthenticationManager(authenticationManager); filter.setAuthenticationSuccessHandler(new MetaMaskAuthenticationSuccessHandler(userRepository)); filter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler("/login?error=true")); filter.setSecurityContextRepository(new HttpSessionSecurityContextRepository()); return filter;}

To prevent replay attacks in case the user’s signature gets compromised, we will create the AuthenticationSuccessHandler implementation, in which we change the user’s nonce and make the user sign the message with a new nonce next login:

Java

public class MetaMaskAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final UserRepository userRepository; public MetaMaskAuthenticationSuccessHandler(UserRepository userRepository) { super("/"); this.userRepository = userRepository; } @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { super.onAuthenticationSuccess(request, response, authentication); MetaMaskUserDetails principal = (MetaMaskUserDetails) authentication.getPrincipal(); User user = userRepository.getUser(principal.getAddress()); user.changeNonce(); }}

Java

public class User { ... public void changeNonce() { this.nonce = (int) (Math.random() * 1000000); }}

We also need to configure the AuthenticationManager bean injecting our MetaMaskAuthenticationProvider:

Java

@Beanpublic AuthenticationManager authenticationManager(List<AuthenticationProvider> authenticationProviders) { return new ProviderManager(authenticationProviders);}

Step 6: Templates

Java

@Controllerpublic class WebController { @RequestMapping("/") public String root() { return "index"; } @RequestMapping("/login") public String login() { return "login"; }}

Our WebController contains two templates: login.html andindex.html:

1. The first template will be used to authenticate with MetaMask.

To prompt a user to connect to MetaMask and receive a wallet address, we can use the eth_requestAccounts method:

JavaScript

const accounts = await window.ethereum.request({method: 'eth_requestAccounts'});const address = accounts[0];


Spring Authentication With MetaMask - DZone (2)

Next, having connected the MetaMask and received the nonce from the back end, we request the MetaMask to sign a message using the personal_sign method:

JavaScript

const nonce = await getNonce(address);const message = `Signing a message to login: ${nonce}`;const signature = await window.ethereum.request({method: 'personal_sign', params: [message, address]});


Spring Authentication With MetaMask - DZone (3)

Finally, we send the calculated signature with the address to the back end. There is a complete template templates/login.html:

HTML

<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" lang="en"><head> <title>Login page</title> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous"> <link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous"/></head><body><div class="container"> <div class="form-signin"> <h3 class="form-signin-heading">Please sign in</h3> <p th:if="${param.error}" class="text-danger">Invalid signature</p> <button class="btn btn-lg btn-primary btn-block" type="submit" onclick="login()">Login with MetaMask</button> </div></div><script th:inline="javascript"> async function login() { if (!window.ethereum) { console.error('Please install MetaMask'); return; } // Prompt user to connect MetaMask const accounts = await window.ethereum.request({method: 'eth_requestAccounts'}); const address = accounts[0]; // Receive nonce and sign a message const nonce = await getNonce(address); const message = `Signing a message to login: ${nonce}`; const signature = await window.ethereum.request({method: 'personal_sign', params: [message, address]}); // Login with signature await sendLoginData(address, signature); } async function getNonce(address) { return await fetch(`/nonce/${address}`) .then(response => response.text()); } async function sendLoginData(address, signature) { return fetch('/login', { method: 'POST', headers: {'content-type': 'application/x-www-form-urlencoded'}, body: new URLSearchParams({ address: encodeURIComponent(address), signature: encodeURIComponent(signature) }) }).then(() => window.location.href = '/'); }</script></body></html>

2. The second templates/index.html template will be protected by our Spring Security configuration, displaying the Principal name as the wallet address after the person gets signed up:

HTML

<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security" lang="en"><head> <title>Spring Authentication with MetaMask</title> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous"> <link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous"/></head><body><div class="container" sec:authorize="isAuthenticated()"> <form class="form-signin" method="post" th:action="@{/logout}"> <h3 class="form-signin-heading">This is a secured page!</h3> <p>Logged in as: <span sec:authentication="name"></span></p> <button class="btn btn-lg btn-secondary btn-block" type="submit">Logout</button> </form></div></body></html>

The full source code is provided on GitHub.

In this article, we developed an alternative authentication mechanism with Spring Security and MetaMask using asymmetric encryption. This method can fit into your application, but only if your target audience is using cryptocurrency and has the MetaMask extension installed in their browser.

Opinions expressed by DZone contributors are their own.

Spring Authentication With MetaMask - DZone (2024)
Top Articles
Advisory Committee Roles and Expectations | Athens-Clarke County, GA
The biggest risks of using Bluetooth trackers like Apple AirTag, Tile
St Thomas Usvi Craigslist
Knoxville Tennessee White Pages
What to Serve with Lasagna (80+ side dishes and wine pairings)
OSRS Fishing Training Guide: Quick Methods To Reach Level 99 - Rune Fanatics
Bellinghamcraigslist
Phenix Food Locker Weekly Ad
Ribbit Woodbine
Big Y Digital Coupon App
Zachary Zulock Linkedin
Umn Biology
Myunlb
Robot or human?
Signs Of a Troubled TIPM
Seafood Bucket Cajun Style Seafood Restaurant in South Salt Lake - Restaurant menu and reviews
Colts Snap Counts
Buy PoE 2 Chaos Orbs - Cheap Orbs For Sale | Epiccarry
Bcbs Prefix List Phone Numbers
Rufus Benton "Bent" Moulds Jr. Obituary 2024 - Webb & Stephens Funeral Homes
Never Give Up Quotes to Keep You Going
Dcf Training Number
Providence Medical Group-West Hills Primary Care
Zillow Group Stock Price | ZG Stock Quote, News, and History | Markets Insider
Boston Dynamics’ new humanoid moves like no robot you’ve ever seen
When Does Subway Open And Close
Victory for Belron® company Carglass® Germany and ATU as European Court of Justice defends a fair and level playing field in the automotive aftermarket
Publix Near 12401 International Drive
Nottingham Forest News Now
TMO GRC Fortworth TX | T-Mobile Community
Remnants of Filth: Yuwu (Novel) Vol. 4
Pokémon Unbound Starters
Viduthalai Movie Download
Taylored Services Hardeeville Sc
Deepwoken: Best Attunement Tier List - Item Level Gaming
Pfcu Chestnut Street
Boneyard Barbers
Springfield.craigslist
Rust Belt Revival Auctions
Www Craigslist Com Shreveport Louisiana
Great Clips On Alameda
Enjoy4Fun Uno
Pensacola Cars Craigslist
Ashoke K Maitra. Adviser to CMD&#39;s. Received Lifetime Achievement Award in HRD on LinkedIn: #hr #hrd #coaching #mentoring #career #jobs #mba #mbafreshers #sales…
San Bernardino Pick A Part Inventory
Post A Bid Monticello Mn
Human Resources / Payroll Information
Beds From Rent-A-Center
Rise Meadville Reviews
Www.card-Data.com/Comerica Prepaid Balance
Latest Posts
Article information

Author: Duncan Muller

Last Updated:

Views: 6228

Rating: 4.9 / 5 (59 voted)

Reviews: 82% of readers found this page helpful

Author information

Name: Duncan Muller

Birthday: 1997-01-13

Address: Apt. 505 914 Phillip Crossroad, O'Konborough, NV 62411

Phone: +8555305800947

Job: Construction Agent

Hobby: Shopping, Table tennis, Snowboarding, Rafting, Motor sports, Homebrewing, Taxidermy

Introduction: My name is Duncan Muller, I am a enchanting, good, gentle, modern, tasty, nice, elegant person who loves writing and wants to share my knowledge and understanding with you.