SIWE Support
Introduction
Sign-In with Ethereum (SIWE) is a decentralized authentication protocol that allows users to authenticate using their Ethereum account.
This enables users to retain more control over their identity and provides an alternative to protocols such as OpenID Connect, which rely on a centralized identity provider.
Sign-In with Ethereum works by using off-chain services to sign a standard message format defined by EIP-4361 (https://eips.ethereum.org/EIPS/eip-4361). The user signs the SIWE message to prove ownership of the Ethereum address. This is verified by the server by extracting the Ethereum address from the signature and comparing it to the address supplied in the SIWE message.
Typically, you would rely on a browser extension such as MetaMask to provide a user-friendly way for users to sign the message with their Ethereum account.
Usage
Enabling SIWE
The Sign-In with Ethereum module can be enabled when using Standalone Jetty with.
$ java -jar $JETTY_HOME/start.jar --add-modules=siwe
If using embedded Jetty you must add the EthereumAuthenticator
to your SecurityHandler
.
Configuration
Configuration of the EthereumAuthenticator
is done through init params on the ServletContext
or SecurityHandler
. The loginPath
is the only mandatory configuration and the others have defaults that you may wish to configure.
- Login Path
-
-
Init param:
org.eclipse.jetty.security.siwe.login_path
-
Description: Unauthenticated requests are redirected to a login page where they must sign a SIWE message and send it to the server. This path represents a page in the application that contains the SIWE login page.
-
- Nonce Path
-
-
Init param:
org.eclipse.jetty.security.siwe.nonce_path
-
Description: Requests to this path will generate a random nonce string which is associated with the session. The nonce is used in the SIWE Message to avoid replay attacks. The path at which this nonce is served can be configured through the init parameter. The application does not need to implement their own nonce endpoint, they just configure this path and the Authenticator handles it. The default value for this is
/auth/nonce
if left un-configured.
-
- Authentication Path
-
-
Init param:
org.eclipse.jetty.security.siwe.authentication_path
-
Description: The authentication path is where requests containing a signed SIWE message are sent in order to authenticate the user. The default value for this is
/auth/login
.
-
- Max Message Size
-
-
Init Param:
org.eclipse.jetty.security.siwe.max_message_size
-
Description: This is the max size of the authentication message which can be read by the implementation. This limit defaults to
4 * 1024
. This is necessary because the complete request content is read into a string and then parsed.
-
- Logout Redirect Path
-
-
Init Param:
org.eclipse.jetty.security.siwe.logout_redirect_path
-
Description: Where the request is redirected to after logout. If left un-configured no redirect will be done upon logout.
-
- Error Path
-
-
Init Param:
org.eclipse.jetty.security.siwe.error_path
-
Description: Path where Authentication errors are sent, this may contain an optional query string. An error description is available on the error page through the request parameter
error_description_jetty
. If this configuration is not set Jetty will send a 403 Forbidden response upon authentication errors.
-
- Dispatch
-
-
Init Param:
org.eclipse.jetty.security.siwe.dispatch
-
Description: If set to true a dispatch will be done instead of a redirect to the login page in the case of an unauthenticated request. This defaults to false.
-
- Authenticate New Users
-
-
Init Param:
org.eclipse.jetty.security.siwe.authenticate_new_users
-
Description: This can be set to false if you have a nested
LoginService
and only want to authenticate users known by theLoginService
. This defaults totrue
meaning that any user will be authenticated regardless if they are known by the nestedLoginService
.
-
- Domains
-
-
Init Param: org.eclipse.jetty.security.siwe.domains
-
Description: This list of allowed domains to be declared in the
domain
field of the SIWE Message. If left blank this will allow all domains.
-
- Chain IDs
-
-
Init Param: org.eclipse.jetty.security.siwe.chainIds
-
Description: This list of allowed Chain IDs to be declared in the
chain-id
field of the SIWE Message. If left blank this will allow all Chain IDs.
-
Nested LoginService
A nested LoginService
may be used to assign roles to users of a known Ethereum Address. Or the nested LoginService
may be combined with the setting authenticateNewUsers == false
to only allow authentication of known users.
For example a HashLoginService
may be configured through the jetty-ee11-web.xml
file:
<Configure id="wac" class="org.eclipse.jetty.ee11.webapp.WebAppContext">
<Call id="ResourceFactory" class="org.eclipse.jetty.util.resource.ResourceFactory" name="of">
<Arg><Ref refid="Server"/></Arg>
<Call id="realmResource" name="newResource">
<Arg><SystemProperty name="jetty.base" default="."/>/etc/realm.properties</Arg>
</Call>
</Call>
<Call name="getSecurityHandler">
<Set name="loginService">
<New class="org.eclipse.jetty.security.HashLoginService">
<Set name="name">myRealm</Set>
<Set name="config"><Ref refid="realmResource"/></Set>
</New>
</Set>
</Call>
</Configure>
Application Implementation
EIP-4361 specifies the format of a SIWE Message, the overview of the Sign-In with Ethereum process, and message validation. However, it does not specify certain things like how the SIWE Message and signature are sent to the server for validation, and it does not specify the process the client acquires the nonce from the server. For this reason the EthereumAuthenticator
has been made extensible to allow different implementations.
Currently Jetty supports authentication requests of type application/x-www-form-urlencoded
or multipart/form-data
, which contains the fields message
and signature
. Where message
contains the full SIWE message, and signature
is the ERC-1271 signature of the SIWE message.
The nonce endpoint provided by the EthereumAuthenticator
returns a response with application/json
format, with a single key of nonce
.
Configuring Security Handler
// This uses jetty-core, but you can configure a ConstraintSecurityHandler for use with EE11.
SecurityHandler.PathMapped securityHandler = new SecurityHandler.PathMapped();
securityHandler.setHandler(handler);
securityHandler.put("/*", Constraint.ANY_USER);
// Add the EthereumAuthenticator to the securityHandler.
EthereumAuthenticator authenticator = new EthereumAuthenticator();
securityHandler.setAuthenticator(authenticator);
// In embedded you can configure via EthereumAuthenticator APIs.
authenticator.setLoginPath("/login.html");
// Or you can configure with parameters on the SecurityHandler.
securityHandler.setParameter(EthereumAuthenticator.LOGIN_PATH_PARAM, "/login.html");
Login Page Example
Include the Web3.js
library to interact with the users Ethereum wallet.
<script src="https://cdn.jsdelivr.net/npm/web3@1.6.1/dist/web3.min.js"></script>
HTML form to submit the sign in request.
<button id="siwe">Sign-In with Ethereum</button>
<form id="loginForm" action="/auth/login" method="POST" style="display: none;">
<input type="hidden" id="signatureField" name="signature">
<input type="hidden" id="messageField" name="message">
</form>
<p class="alert" style="display: none;">Result: <span id="siweResult"></span></p>
Add script to generate and sign the SIWE message when the sign-in button is pressed.
<script>
let provider = window.ethereum;
let accounts;
if (!provider) {
document.getElementById('siweResult').innerText = 'MetaMask is not installed. Please install MetaMask to use this feature.';
} else {
document.getElementById('siwe').addEventListener('click', async () => {
try {
accounts = await provider.request({ method: 'eth_requestAccounts' });
const domain = window.location.host;
const from = accounts[0];
// Fetch nonce from the server.
const nonceResponse = await fetch('/auth/nonce');
const nonceData = await nonceResponse.json();
const nonce = nonceData.nonce;
const siweMessage = `${domain} wants you to sign in with your Ethereum account:\n${from}\n\nI accept the MetaMask Terms of Service: https://community.metamask.io/tos\n\nURI: https://${domain}\nVersion: 1\nChain ID: 1\nNonce: ${nonce}\nIssued At: ${new Date().toISOString()}`;
document.getElementById('signatureField').value = await provider.request({
method: 'personal_sign',
params: [siweMessage, from]
});
document.getElementById('messageField').value = siweMessage;
document.getElementById('loginForm').submit();
} catch (error) {
console.error('Error during login:', error);
document.getElementById('siweResult').innerText = `Error: ${error.message}`;
document.getElementById('siweResult').parentElement.style.display = 'block';
}
});
}
</script>