Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #11560 - Implement EIP-4361 Sign-In With Ethereum #11883

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
436ca41
Issue #11560 - Implement EIP-4361 Sign-In With Ethereum
lachlan-roberts Jul 5, 2024
90e5919
PR #11883 - javadoc and code cleanup
lachlan-roberts Jul 5, 2024
ac5925a
PR #11883 - test fixes and cleanup
lachlan-roberts Jul 5, 2024
70e6192
PR #11883 - fixes to EthereumCredentials
lachlan-roberts Jul 5, 2024
614025a
Add openid and siwe documentation sections.
lachlan-roberts Jul 8, 2024
4006228
Merge remote-tracking branch 'origin/jetty-12.0.x' into jetty-12.0.x-…
lachlan-roberts Jul 12, 2024
225e89c
update version for jetty-siwe pom.xml
lachlan-roberts Jul 12, 2024
aa945d5
add jetty module for siwe
lachlan-roberts Jul 15, 2024
52c6c88
add siwe.mod, distribution tests and documentation
lachlan-roberts Jul 23, 2024
9ea7431
changes from review
lachlan-roberts Jul 23, 2024
32b7043
add javadoc for test classes
lachlan-roberts Jul 23, 2024
2f66835
PR #11883 - changes from review
lachlan-roberts Jul 25, 2024
44286fe
PR #11883 - changes from review
lachlan-roberts Jul 25, 2024
f1f1602
PR #11883 - changes from review
lachlan-roberts Jul 25, 2024
9581ef1
Merge remote-tracking branch 'origin/jetty-12.0.x' into jetty-12.0.x-…
lachlan-roberts Jul 25, 2024
37af005
Merge remote-tracking branch 'origin/jetty-12.0.x' into jetty-12.0.x-…
lachlan-roberts Aug 13, 2024
cc61f78
update poms to 12.0.13-SNAPSHOT
lachlan-roberts Aug 13, 2024
a525b70
Move SignInWithEthereumEmbeddedExample to code-examples in documentation
lachlan-roberts Aug 21, 2024
063e3fc
PR #11883 - fix build issue with poms
lachlan-roberts Aug 21, 2024
37aed0e
PR #11883 - remove experimental warning and fix to documentation
lachlan-roberts Aug 21, 2024
b07a8c0
Merge remote-tracking branch 'origin/jetty-12.0.x' into jetty-12.0.x-…
lachlan-roberts Aug 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions documentation/jetty/modules/code/examples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-session</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-siwe</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-unixdomain-server</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.security.siwe.example;

import java.io.PrintWriter;
import java.nio.file.Paths;
import java.util.Objects;

import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.security.AuthenticationState;
import org.eclipse.jetty.security.Constraint;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.siwe.EthereumAuthenticator;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.session.SessionHandler;
import org.eclipse.jetty.util.Callback;

public class SignInWithEthereum
{
public static SecurityHandler createSecurityHandler(Handler handler)
{
// tag::configureSecurityHandler[]
// This uses jetty-core, but you can configure a ConstraintSecurityHandler for use with EE10.
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");
// end::configureSecurityHandler[]
return securityHandler;
}
}
2 changes: 2 additions & 0 deletions documentation/jetty/modules/programming-guide/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
** xref:troubleshooting/state-tracking.adoc[]
** xref:troubleshooting/component-dump.adoc[]
** xref:troubleshooting/debugging.adoc[]
* xref:security/index.adoc[]
** xref:security/siwe-support.adoc[]
* Migration Guides
** xref:migration/94-to-10.adoc[]
** xref:migration/11-to-12.adoc[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

= Jetty Security

TODO: introduction
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that TODO shown in the resulting documentation, or is it treated like a comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shows the todo in the documentation, but the documentation is very incomplete and doesn't yet have any other security modules. And a bunch of the other main headers have the same TODOs for the introduction page.

But I will remove this index file and just have a header without an intro page for this.

Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

[[siwe-support]]
= 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.

=== Support

Currently Jetty only provides support SIWE in Jetty 12.0+ and only for `jetty-core`, and `ee10`+ environments. It is enabled by adding the `EtheremAuthenticator` to the `SecurityHandler` of your web application.

== Usage

=== Enabling SIWE
The Sign-In with Ethereum module can be enabled when using Standalone Jetty with.
[source,subs=attributes+]
----
$ 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 the `LoginService`. This defaults to `true` meaning that any user will be authenticated regardless if they are known by the nested `LoginService`.

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-ee10-web.xml` file:
[, xml, indent=0]
----
<Configure id="wac" class="org.eclipse.jetty.ee10.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
[,java,indent=0]
----
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/security/siwe/SignInWithEthereum.java[tags=configureSecurityHandler]
----

=== Login Page Example

Include the `Web3.js` library to interact with the users Ethereum wallet.
[,html,indent=0]
----
<script src="https://cdn.jsdelivr.net/npm/web3@1.6.1/dist/web3.min.js"></script>
----

HTML form to submit the sign in request.
[,html,indent=0]
----
<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.
[,html,indent=0]
----
<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>
----
5 changes: 5 additions & 0 deletions jetty-core/jetty-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@
<artifactId>jetty-session</artifactId>
<version>12.0.12-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-siwe</artifactId>
<version>12.0.12-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
Expand Down
4 changes: 0 additions & 4 deletions jetty-core/jetty-keystore/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,21 @@
<description>Test keystore with self-signed SSL Certificate.</description>

<properties>
<bouncycastle.version>1.78.1</bouncycastle.version>
<bundle-symbolic-name>${project.groupId}.keystore</bundle-symbolic-name>
</properties>

<dependencies>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15to18</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcutil-jdk15to18</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
Expand Down
2 changes: 1 addition & 1 deletion jetty-core/jetty-openid/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<version>12.0.12-SNAPSHOT</version>
</parent>
<artifactId>jetty-openid</artifactId>
<name>EE10 :: OpenID</name>
<name>Core :: OpenID</name>
<description>Jetty OpenID Connect Infrastructure</description>

<properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public interface Authenticator
String SPNEGO_AUTH = "SPNEGO";
String NEGOTIATE_AUTH = "NEGOTIATE";
String OPENID_AUTH = "OPENID";
String SIWE_AUTH = "SIWE";

/**
* Configure the Authenticator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,8 @@ protected IdentityService findIdentityService()
protected void doStart()
throws Exception
{
Context context1 = ContextHandler.getCurrentContext();
lachlan-roberts marked this conversation as resolved.
Show resolved Hide resolved

// complicated resolution of login and identity service to handle
// many different ways these can be constructed and injected.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ public static CompletableFuture<Fields> from(Request request, Charset charset, i
* @param maxLength The maximum total size of the fields
* @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure.
*/
static CompletableFuture<Fields> from(Content.Source source, Attributes attributes, Charset charset, int maxFields, int maxLength)
public static CompletableFuture<Fields> from(Content.Source source, Attributes attributes, Charset charset, int maxFields, int maxLength)
{
Object attr = attributes.getAttribute(FormFields.class.getName());
if (attr instanceof FormFields futureFormFields)
Expand Down
Loading
Loading