SIWE Authentication in Blazor
Implement Sign-In with Ethereum (EIP-4361) authentication in Blazor. Supports Blazor Server (direct signing) and Blazor WASM (REST API + JWT).
NuGet: Nethereum.Siwe, Nethereum.Blazor
A complete starter template is available:
dotnet new install Nethereum.Templates.Pack
dotnet new nethereum-siwe
Template repo: https://github.com/Nethereum/Nethereum.Templates.Pack (templates/Nethereum.Templates.Siwe)
SIWE Flow
- Server creates a
SiweMessagewith a random nonce - Client signs the message with their wallet
- Server verifies signature matches the claimed address
- Server issues a JWT (WASM) or stores session (Server)
- Blazor
<AuthorizeView>checks claims
Blazor Server (Simplest)
Everything runs in one process. No REST API needed.
Setup (Program.cs)
builder.Services.AddSingleton<SiweMessageService>();
builder.Services.AddSingleton<NethereumSiweAuthenticatorService>();
builder.Services.AddScoped<IAccessTokenService, ProtectedSessionStorageAccessTokenService>();
builder.Services.AddScoped<AuthenticationStateProvider, SiweAuthenticationServerStateProvider>();
Authentication Flow
The SiweAuthenticationServerStateProvider orchestrates the full flow:
public async Task AuthenticateAsync(string address = null)
{
if (string.IsNullOrEmpty(address))
address = await EthereumHostProvider.GetProviderSelectedAccountAsync();
var siweMessage = new DefaultSiweMessage();
siweMessage.Address = address.ConvertToEthereumChecksumAddress();
siweMessage.SetExpirationTime(DateTime.UtcNow.AddMinutes(10));
siweMessage.SetNotBefore(DateTime.UtcNow);
var fullMessage = await nethereumSiweAuthenticatorService.AuthenticateAsync(siweMessage);
await _accessTokenService.SetAccessTokenAsync(SiweMessageStringBuilder.BuildMessage(fullMessage));
await MarkUserAsAuthenticated();
}
The NethereumSiweAuthenticatorService (from Nethereum.UI) handles:
- Building the message string with nonce
- Prompting the wallet to sign via
IEthereumHostProvider - Verifying the signature
Blazor WASM + REST API
REST API: Generate SIWE Message
[AllowAnonymous]
[HttpPost("newsiwemessage")]
public IActionResult GenerateNewSiweMessage([FromBody] string address)
{
var message = new DefaultSiweMessage();
message.SetExpirationTime(DateTime.UtcNow.AddMinutes(10));
message.SetNotBefore(DateTime.UtcNow);
message.Address = address.ConvertToEthereumChecksumAddress();
return Ok(_siweMessageService.BuildMessageToSign(message));
}
REST API: Authenticate and Issue JWT
[AllowAnonymous]
[HttpPost("authenticate")]
public async Task<IActionResult> Authenticate(AuthenticateRequest request)
{
var siweMessage = SiweMessageParser.Parse(request.SiweEncodedMessage);
if (!await _siweMessageService.IsUserAddressRegistered(siweMessage))
return Unauthorized("Invalid User");
if (!await _siweMessageService.IsMessageSignatureValid(siweMessage, request.Signature))
return Unauthorized("Invalid Signature");
if (!_siweMessageService.IsMessageTheSameAsSessionStored(siweMessage))
return Unauthorized("Nonce mismatch");
if (!_siweMessageService.HasMessageDateStartedAndNotExpired(siweMessage))
return Unauthorized("Expired");
var token = _siweJwtAuthorisationService.GenerateToken(siweMessage, request.Signature);
return Ok(new AuthenticateResponse { Address = siweMessage.Address, Jwt = token });
}
WASM Client: Sign and Authenticate
public async Task AuthenticateAsync(string address)
{
var siweMessage = await _siweUserLoginService.GenerateNewSiweMessage(address);
var signedMessage = await EthereumHostProvider.SignMessageAsync(siweMessage);
var response = await _siweUserLoginService.Authenticate(
SiweMessageParser.Parse(siweMessage), signedMessage);
if (response.Jwt != null)
{
await _accessTokenService.SetAccessTokenAsync(response.Jwt);
await MarkUserAsAuthenticated();
}
}
Using AuthorizeView
Two roles are available:
| Role | Meaning |
|---|---|
EthereumConnected |
Wallet is connected (from EthereumAuthenticationStateProvider) |
SiweAuthenticated |
SIWE signature verified (from SiweAuthentication*StateProvider) |
<AuthorizeView Roles="EthereumConnected">
<Authorized>
<p>Wallet connected: @context.User.Identity?.Name</p>
<AuthorizeView Roles="SiweAuthenticated">
<NotAuthorized>
<button @onclick="LoginAsync">Sign In</button>
</NotAuthorized>
</AuthorizeView>
</Authorized>
<NotAuthorized>
<button @onclick="ConnectWalletAsync">Connect Wallet</button>
</NotAuthorized>
</AuthorizeView>
<AuthorizeView Roles="SiweAuthenticated">
<Authorized>
<p>Authenticated as @context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value</p>
<MyProtectedComponent />
</Authorized>
</AuthorizeView>
NFT-Gated Access
Use ERC721BalanceEthereumUserService to require users hold a specific NFT:
var userService = new ERC721BalanceEthereumUserService(web3, nftContractAddress);
var siweService = new SiweMessageService(
new InMemorySessionNonceStorage(),
userService);
IsUserAddressRegistered will check the user's NFT balance before allowing authentication.
Smart Contract Wallets (ERC-1271 / ERC-6492)
Pass a Web3 instance to SiweMessageService for smart contract wallet signature verification:
var siweService = new SiweMessageService(
new InMemorySessionNonceStorage(),
ethereumUserService: null,
web3ForERC1271Validation: web3);
IsMessageSignatureValid automatically falls back to ERC-1271 on-chain verification, and supports ERC-6492 for counterfactual (not-yet-deployed) smart wallets.
SiweMessageService API
| Method | Purpose |
|---|---|
BuildMessageToSign(message) |
Build message string, assign nonce, store in session |
IsMessageSignatureValid(message, signature) |
Verify ECDSA or ERC-1271/ERC-6492 signature |
IsValidMessage(message, signature) |
Full validation: timing + session + signature |
IsUserAddressRegistered(message) |
Check user via IEthereumUserService |
HasMessageDateStartedAndNotExpired(message) |
Check NotBefore and ExpirationTime |
IsMessageTheSameAsSessionStored(message) |
Verify message matches stored nonce |
InvalidateSession(message) |
Remove session (logout) |
Claims Structure
new Claim(ClaimTypes.Name, userName);
new Claim(ClaimTypes.NameIdentifier, ethereumAddress);
new Claim(ClaimTypes.Role, "EthereumConnected");
new Claim(ClaimTypes.Role, "SiweAuthenticated");
For full documentation, see: https://docs.nethereum.com/docs/blazor-dapp-integration/guide-blazor-authentication