From 52c6c88de63fd8651c7a93def075190eba36b00d Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 23 Jul 2024 11:11:18 +1000 Subject: [PATCH] add siwe.mod, distribution tests and documentation Signed-off-by: Lachlan Roberts --- .../jetty/modules/code/examples/pom.xml | 4 + .../security/siwe/SignInWithEthereum.java | 58 +++++ .../jetty/modules/programming-guide/nav.adoc | 2 + .../pages/security/index.adoc | 16 ++ .../pages/security/openid-support.adoc | 134 ----------- .../pages/security/siwe-support.adoc | 216 +++++++++++------- jetty-core/jetty-bom/pom.xml | 2 +- jetty-core/jetty-keystore/pom.xml | 4 - .../jetty/security/SecurityHandler.java | 2 + jetty-core/jetty-siwe/pom.xml | 7 - .../src/main/config/modules/siwe.mod | 10 +- .../security/siwe/EthereumAuthenticator.java | 94 +++++--- .../internal/SignInWithEthereumToken.java | 12 +- .../security/siwe/SignInWithEthereumTest.java | 27 +-- .../siwe/SignInWithEthereumTokenTest.java | 44 +--- .../SignInWithEthereumEmbeddedExample.java | 28 ++- .../security/ConstraintSecurityHandler.java | 17 ++ .../jetty-ee10-test-siwe-webapp/pom.xml | 27 +++ .../src/main/java/com/acme/AdminServlet.java | 32 +++ .../src/main/java/com/acme/ErrorServlet.java | 37 +++ .../main/java/com/acme/ForbiddenServlet.java | 33 +++ .../src/main/java/com/acme/HomeServlet.java | 37 +++ .../src/main/java/com/acme/LogoutServlet.java | 34 +++ .../main/webapp/WEB-INF/jetty-ee10-web.xml | 20 ++ .../src/main/webapp/WEB-INF/web.xml | 69 ++++++ .../src/main/webapp/favicon.ico | Bin 0 -> 1150 bytes .../src/main/webapp/login.html | 59 +++++ jetty-ee10/jetty-ee10-tests/pom.xml | 1 + jetty-home/pom.xml | 4 + pom.xml | 17 ++ .../test-ee10-distribution/pom.xml | 5 + .../ee10/tests/distribution/SiweTests.java | 153 +++++++++++++ .../siwe/EthereumCredentials.java | 126 ++++++++++ .../siwe/SignInWithEthereumGenerator.java | 104 +++++++++ 34 files changed, 1098 insertions(+), 337 deletions(-) create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/security/siwe/SignInWithEthereum.java create mode 100644 documentation/jetty/modules/programming-guide/pages/security/index.adoc delete mode 100644 documentation/jetty/modules/programming-guide/pages/security/openid-support.adoc create mode 100644 jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/pom.xml create mode 100644 jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/AdminServlet.java create mode 100644 jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/ErrorServlet.java create mode 100644 jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/ForbiddenServlet.java create mode 100644 jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/HomeServlet.java create mode 100644 jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/LogoutServlet.java create mode 100644 jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/webapp/WEB-INF/jetty-ee10-web.xml create mode 100644 jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/webapp/WEB-INF/web.xml create mode 100644 jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/webapp/favicon.ico create mode 100644 jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/webapp/login.html create mode 100644 tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/SiweTests.java create mode 100644 tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/siwe/EthereumCredentials.java create mode 100644 tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/siwe/SignInWithEthereumGenerator.java diff --git a/documentation/jetty/modules/code/examples/pom.xml b/documentation/jetty/modules/code/examples/pom.xml index 8afed6d5ec9..f74d7288c4a 100644 --- a/documentation/jetty/modules/code/examples/pom.xml +++ b/documentation/jetty/modules/code/examples/pom.xml @@ -54,6 +54,10 @@ org.eclipse.jetty jetty-session + + org.eclipse.jetty + jetty-siwe + org.eclipse.jetty jetty-unixdomain-server diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/security/siwe/SignInWithEthereum.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/security/siwe/SignInWithEthereum.java new file mode 100644 index 00000000000..beb4a0987db --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/security/siwe/SignInWithEthereum.java @@ -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; + } +} \ No newline at end of file diff --git a/documentation/jetty/modules/programming-guide/nav.adoc b/documentation/jetty/modules/programming-guide/nav.adoc index ac50c0d7f13..e0dfa38d086 100644 --- a/documentation/jetty/modules/programming-guide/nav.adoc +++ b/documentation/jetty/modules/programming-guide/nav.adoc @@ -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[] diff --git a/documentation/jetty/modules/programming-guide/pages/security/index.adoc b/documentation/jetty/modules/programming-guide/pages/security/index.adoc new file mode 100644 index 00000000000..aec04e758f7 --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/security/index.adoc @@ -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 \ No newline at end of file diff --git a/documentation/jetty/modules/programming-guide/pages/security/openid-support.adoc b/documentation/jetty/modules/programming-guide/pages/security/openid-support.adoc deleted file mode 100644 index 82efc614e61..00000000000 --- a/documentation/jetty/modules/programming-guide/pages/security/openid-support.adoc +++ /dev/null @@ -1,134 +0,0 @@ -// -// ======================================================================== -// 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 -// ======================================================================== -// - -[[openid-support]] -=== OpenID Support - -==== External Setup - -===== Registering an App with OpenID Provider -You must register the app with an OpenID Provider such as link:https://developers.google.com/identity/protocols/OpenIDConnect#authenticatingtheuser[Google] or link:https://images-na.ssl-images-amazon.com/images/G/01/lwa/dev/docs/website-developer-guide._TTH_.pdf[Amazon.] -This will give you a Client ID and Client Secret. -Once set up you must also register all the possible URI's for your webapp with the path `/j_security_check` so that the OpenId Provider will allow redirection back to the webapp. - -These may look like - - * `http://localhost:8080/openid-webapp/j_security_check` - - * `https://example.com/j_security_check` - -==== Distribution Configuration - -===== OpenID Provider Configuration -To enable OpenID support, you first need to activate the `openid` module in your implementation. - -[source, screen, subs="{sub-order}"] ----- -$ java -jar $JETTY_HOME/start.jar --add-to-start=openid ----- - -To configure OpenID Authentication with Jetty you will need to specify the OpenID Provider's issuer identifier (case sensitive URL using the `https` scheme) and the OAuth 2.0 Client ID and Client Secret. -If the OpenID Provider does not allow metadata discovery you will also need to specify the token endpoint and authorization endpoint of the OpenID Provider. -These can be set as properties in the `start.ini` or `start.d/openid.ini` files. - -===== WebApp Specific Configuration in web.xml - -The `web.xml` file needs some specific configuration to use OpenID. -There must be a `login-config` element with an `auth-method` value of `OPENID`, and a `realm-name` value of the exact URL string used to set the OpenID Provider. - -To set the error page, an init param is set at `"org.eclipse.jetty.security.openid.error_page"`, its value should be a path relative to the webapp where authentication errors should be redirected. - -Example: - -[source, xml, subs="{sub-order}"] ----- - - OPENID - https://accounts.google.com - - - org.eclipse.jetty.security.openid.error_page - /error - ----- - -==== Embedded Configuration - -===== Define the `OpenIdConfiguration` for a specific OpenID Provider. - -If the OpenID Provider allows metadata discovery then you can use. - -[source, java, subs="{sub-order}"] ----- -OpenIdConfiguration openIdConfig = new OpenIdConfiguration(ISSUER, CLIENT_ID, CLIENT_SECRET); ----- - -Otherwise you can manually enter the necessary information: - -[source, java, subs="{sub-order}"] ----- -OpenIdConfiguration openIdConfig = new OpenIdConfiguration(ISSUER, TOKEN_ENDPOINT, AUTH_ENDPOINT, CLIENT_ID, CLIENT_SECRET); ----- - -===== Configuring an `OpenIdLoginService` -[source, java, subs="{sub-order}"] ----- -LoginService loginService = new OpenIdLoginService(openIdConfig); -securityHandler.setLoginService(loginService); ----- - -===== Configuring an `OpenIdAuthenticator` with `OpenIdConfiguration` and Error Page Redirect -[source, java, subs="{sub-order}"] ----- -Authenticator authenticator = new OpenIdAuthenticator(openIdConfig, "/error"); -securityHandler.setAuthenticator(authenticator); -servletContextHandler.setSecurityHandler(securityHandler); ----- - -===== Usage - -====== Claims and Access Token -Claims about the user can be found using attributes on the session attribute `"org.eclipse.jetty.security.openid.claims"`, and the full response containing the OAuth 2.0 Access Token can be found with the session attribute `"org.eclipse.jetty.security.openid.response"`. - -Example: -[source, java, subs="{sub-order}"] ----- -Map claims = (Map)request.getSession().getAttribute("org.eclipse.jetty.security.openid.claims"); -String userId = claims.get("sub"); - -Map response = (Map)request.getSession().getAttribute("org.eclipse.jetty.security.openid.response"); -String accessToken = response.get("access_token"); ----- - -==== Scopes -The OpenID scope is always used but additional scopes can be requested which can give you additional resources or privileges. -For the Google OpenID Provider it can be useful to request the scopes `profile` and `email` which will give you additional user claims. - -Additional scopes can be requested through the `start.ini` or `start.d/openid.ini` files, or with `OpenIdConfiguration.addScopes(...);` in embedded code. - -==== Roles - -If security roles are required they can be configured through a wrapped `LoginService` which is deferred to for role information by the `OpenIdLoginService`. - -This can be configured in XML through `etc/openid-baseloginservice.xml` in the Distribution, or in embedded code using the constructor for the `OpenIdLoginService`. - -[source, java, subs="{sub-order}"] ----- -LoginService wrappedLoginService = ...; // Optional LoginService for Roles -LoginService loginService = new OpenIdLoginService(openIdConfig, wrappedLoginService); ----- - -When using authorization roles, the setting `authenticateNewUsers` becomes significant. -If set to `true` users not found by the wrapped `LoginService` will still be authenticated but will have no roles. -If set to `false` those users will be not be allowed to authenticate and are redirected to the error page. -This setting is configured through the property `jetty.openid.authenticateNewUsers` in the `start.ini` or `start.d/openid.ini` file, or with `OpenIdLoginService.setAuthenticateNewUsers(...);` in embedded code. diff --git a/documentation/jetty/modules/programming-guide/pages/security/siwe-support.adoc b/documentation/jetty/modules/programming-guide/pages/security/siwe-support.adoc index 82efc614e61..7a075ee2f8b 100644 --- a/documentation/jetty/modules/programming-guide/pages/security/siwe-support.adoc +++ b/documentation/jetty/modules/programming-guide/pages/security/siwe-support.adoc @@ -11,124 +11,170 @@ // ======================================================================== // -[[openid-support]] -=== OpenID Support +[[siwe-support]] += SIWE Support -==== External Setup +== Introduction -===== Registering an App with OpenID Provider -You must register the app with an OpenID Provider such as link:https://developers.google.com/identity/protocols/OpenIDConnect#authenticatingtheuser[Google] or link:https://images-na.ssl-images-amazon.com/images/G/01/lwa/dev/docs/website-developer-guide._TTH_.pdf[Amazon.] -This will give you a Client ID and Client Secret. -Once set up you must also register all the possible URI's for your webapp with the path `/j_security_check` so that the OpenId Provider will allow redirection back to the webapp. +Sign-In with Ethereum (SIWE) is a decentralized authentication protocol that allows users to authenticate using their Ethereum account. -These may look like +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. - * `http://localhost:8080/openid-webapp/j_security_check` +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. - * `https://example.com/j_security_check` +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. -==== Distribution Configuration +=== Support -===== OpenID Provider Configuration -To enable OpenID support, you first need to activate the `openid` module in your implementation. +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. -[source, screen, subs="{sub-order}"] +== 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-to-start=openid +$ java -jar $JETTY_HOME/start.jar --add-modules=siwe ---- -To configure OpenID Authentication with Jetty you will need to specify the OpenID Provider's issuer identifier (case sensitive URL using the `https` scheme) and the OAuth 2.0 Client ID and Client Secret. -If the OpenID Provider does not allow metadata discovery you will also need to specify the token endpoint and authorization endpoint of the OpenID Provider. -These can be set as properties in the `start.ini` or `start.d/openid.ini` files. +If using embedded Jetty you must add the `EthereumAuthenticator` to your `SecurityHandler`. -===== WebApp Specific Configuration in web.xml +=== Configuration -The `web.xml` file needs some specific configuration to use OpenID. -There must be a `login-config` element with an `auth-method` value of `OPENID`, and a `realm-name` value of the exact URL string used to set the OpenID Provider. +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. -To set the error page, an init param is set at `"org.eclipse.jetty.security.openid.error_page"`, its value should be a path relative to the webapp where authentication errors should be redirected. +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. -Example: +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. -[source, xml, subs="{sub-order}"] +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] ---- - - OPENID - https://accounts.google.com - - - org.eclipse.jetty.security.openid.error_page - /error - + + + + + /etc/realm.properties + + + + + + + myRealm + + + + + ---- -==== Embedded Configuration +=== 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. -===== Define the `OpenIdConfiguration` for a specific OpenID Provider. +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. -If the OpenID Provider allows metadata discovery then you can use. +The nonce endpoint provided by the `EthereumAuthenticator` returns a response with `application/json` format, with a single key of `nonce`. -[source, java, subs="{sub-order}"] +=== Configuring Security Handler +[,java,indent=0] ---- -OpenIdConfiguration openIdConfig = new OpenIdConfiguration(ISSUER, CLIENT_ID, CLIENT_SECRET); +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/security/siwe/SignInWithEthereum.java[tags=configureSecurityHandler] ---- -Otherwise you can manually enter the necessary information: +=== Login Page Example -[source, java, subs="{sub-order}"] +Include the `Web3.js` library to interact with the users Ethereum wallet. +[,html,indent=0] ---- -OpenIdConfiguration openIdConfig = new OpenIdConfiguration(ISSUER, TOKEN_ENDPOINT, AUTH_ENDPOINT, CLIENT_ID, CLIENT_SECRET); + ---- -===== Configuring an `OpenIdLoginService` -[source, java, subs="{sub-order}"] +HTML form to submit the sign in request. +[,html,indent=0] ---- -LoginService loginService = new OpenIdLoginService(openIdConfig); -securityHandler.setLoginService(loginService); + + + ---- -===== Configuring an `OpenIdAuthenticator` with `OpenIdConfiguration` and Error Page Redirect -[source, java, subs="{sub-order}"] ----- -Authenticator authenticator = new OpenIdAuthenticator(openIdConfig, "/error"); -securityHandler.setAuthenticator(authenticator); -servletContextHandler.setSecurityHandler(securityHandler); +Add script to generate and sign the SIWE message when the sign-in button is pressed. +[,html,indent=0] ---- + +---- \ No newline at end of file diff --git a/jetty-core/jetty-bom/pom.xml b/jetty-core/jetty-bom/pom.xml index 900d89b8b4d..6f6c9e1422a 100644 --- a/jetty-core/jetty-bom/pom.xml +++ b/jetty-core/jetty-bom/pom.xml @@ -133,7 +133,7 @@ org.eclipse.jetty jetty-siwe - 12.0.11-SNAPSHOT + 12.0.12-SNAPSHOT org.eclipse.jetty diff --git a/jetty-core/jetty-keystore/pom.xml b/jetty-core/jetty-keystore/pom.xml index 502c770a005..3f25310f9be 100644 --- a/jetty-core/jetty-keystore/pom.xml +++ b/jetty-core/jetty-keystore/pom.xml @@ -12,7 +12,6 @@ Test keystore with self-signed SSL Certificate. - 1.78.1 ${project.groupId}.keystore @@ -20,17 +19,14 @@ org.bouncycastle bcpkix-jdk15to18 - ${bouncycastle.version} org.bouncycastle bcprov-jdk15to18 - ${bouncycastle.version} org.bouncycastle bcutil-jdk15to18 - ${bouncycastle.version} org.eclipse.jetty diff --git a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java index 88ff06cf900..2f9bf3b0078 100644 --- a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java +++ b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java @@ -313,6 +313,8 @@ public abstract class SecurityHandler extends Handler.Wrapper implements Configu protected void doStart() throws Exception { + Context context1 = ContextHandler.getCurrentContext(); + // complicated resolution of login and identity service to handle // many different ways these can be constructed and injected. diff --git a/jetty-core/jetty-siwe/pom.xml b/jetty-core/jetty-siwe/pom.xml index 6302c0dc856..b9538d3d4de 100644 --- a/jetty-core/jetty-siwe/pom.xml +++ b/jetty-core/jetty-siwe/pom.xml @@ -12,20 +12,13 @@ Jetty Sign-In with Ethereum - 1.78.1 ${project.groupId}.siwe - - org.bouncycastle - bcpkix-jdk15to18 - ${bouncycastle.version} - org.bouncycastle bcprov-jdk15to18 - ${bouncycastle.version} org.eclipse.jetty diff --git a/jetty-core/jetty-siwe/src/main/config/modules/siwe.mod b/jetty-core/jetty-siwe/src/main/config/modules/siwe.mod index 8e011925b0e..72196c15137 100644 --- a/jetty-core/jetty-siwe/src/main/config/modules/siwe.mod +++ b/jetty-core/jetty-siwe/src/main/config/modules/siwe.mod @@ -6,5 +6,13 @@ Adds Sign-In with Ethereum (SIWE) authentication to the server. [depend] security +[files] +maven://org.bouncycastle/bcprov-jdk15to18/${bouncycastle.version}|lib/bouncycastle/bcprov-jdk15to18-${bouncycastle.version}.jar + [lib] -lib/jetty-siwe-${jetty.version}.jar \ No newline at end of file +lib/jetty-siwe-${jetty.version}.jar +lib/bouncycastle/bcprov-jdk15to18-${bouncycastle.version}.jar + +[ini] +bouncycastle.version?=@bouncycastle.version@ +jetty.webapp.addHiddenClasses+=,${jetty.base.uri}/lib/bouncycastle/ \ No newline at end of file diff --git a/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/EthereumAuthenticator.java b/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/EthereumAuthenticator.java index 72730757e3f..d6ed143411f 100644 --- a/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/EthereumAuthenticator.java +++ b/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/EthereumAuthenticator.java @@ -56,34 +56,38 @@ import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.CharsetStringBuilder.Iso88591StringBuilder; import org.eclipse.jetty.util.Fields; import org.eclipse.jetty.util.IncludeExcludeSet; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.UrlEncoded; +import org.eclipse.jetty.util.component.Dumpable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.eclipse.jetty.server.FormFields.getFormEncodedCharset; -public class EthereumAuthenticator extends LoginAuthenticator +public class EthereumAuthenticator extends LoginAuthenticator implements Dumpable { private static final Logger LOG = LoggerFactory.getLogger(EthereumAuthenticator.class); public static final String LOGIN_PATH_PARAM = "org.eclipse.jetty.security.siwe.login_path"; public static final String AUTHENTICATION_PATH_PARAM = "org.eclipse.jetty.security.siwe.authentication_path"; public static final String NONCE_PATH_PARAM = "org.eclipse.jetty.security.siwe.nonce_path"; - public static final String MAX_MESSAGE_SIZE_PARAM = "org.eclipse.jetty.security.siwe.max_message_size"; public static final String LOGOUT_REDIRECT_PARAM = "org.eclipse.jetty.security.siwe.logout_redirect_path"; - public static final String DISPATCH_PARAM = "org.eclipse.jetty.security.siwe.dispatch"; - public static final String ERROR_PAGE = "org.eclipse.jetty.security.siwe.error_page"; - public static final String J_URI = "org.eclipse.jetty.security.siwe.URI"; - public static final String J_POST = "org.eclipse.jetty.security.siwe.POST"; - public static final String J_METHOD = "org.eclipse.jetty.security.siwe.METHOD"; + public static final String ERROR_PATH_PARAM = "org.eclipse.jetty.security.siwe.error_path"; public static final String ERROR_PARAMETER = "error_description_jetty"; + public static final String MAX_MESSAGE_SIZE_PARAM = "org.eclipse.jetty.security.siwe.max_message_size"; + public static final String DISPATCH_PARAM = "org.eclipse.jetty.security.siwe.dispatch"; + public static final String AUTHENTICATE_NEW_USERS_PARAM = "org.eclipse.jetty.security.siwe.authenticate_new_users"; + public static final String CHAIN_IDS_PARAM = "org.eclipse.jetty.security.siwe.chainIds"; + public static final String DOMAINS_PARAM = "org.eclipse.jetty.security.siwe.domains"; + private static final String J_URI = "org.eclipse.jetty.security.siwe.URI"; + private static final String J_POST = "org.eclipse.jetty.security.siwe.POST"; + private static final String J_METHOD = "org.eclipse.jetty.security.siwe.METHOD"; private static final String DEFAULT_AUTHENTICATION_PATH = "/auth/login"; private static final String DEFAULT_NONCE_PATH = "/auth/nonce"; private static final String NONCE_SET_ATTR = "org.eclipse.jetty.security.siwe.nonce"; private final IncludeExcludeSet _chainIds = new IncludeExcludeSet<>(); - private final IncludeExcludeSet _schemes = new IncludeExcludeSet<>(); private final IncludeExcludeSet _domains = new IncludeExcludeSet<>(); private String _loginPath; @@ -91,11 +95,10 @@ public class EthereumAuthenticator extends LoginAuthenticator private String _noncePath = DEFAULT_NONCE_PATH; private int _maxMessageSize = 4 * 1024; private String _logoutRedirectPath; - private String _errorPage; private String _errorPath; private String _errorQuery; private boolean _dispatch; - private boolean authenticateNewUsers = true; + private boolean _authenticateNewUsers = true; public EthereumAuthenticator() { @@ -107,11 +110,6 @@ public class EthereumAuthenticator extends LoginAuthenticator _domains.include(domains); } - public void includeSchemes(String... schemes) - { - _schemes.include(schemes); - } - public void includeChainIds(String... chainIds) { _chainIds.include(chainIds); @@ -140,7 +138,7 @@ public class EthereumAuthenticator extends LoginAuthenticator if (logout != null) setLogoutRedirectPath(logout); - String error = authConfig.getParameter(ERROR_PAGE); + String error = authConfig.getParameter(ERROR_PATH_PARAM); if (error != null) setErrorPage(error); @@ -148,7 +146,19 @@ public class EthereumAuthenticator extends LoginAuthenticator if (dispatch != null) setDispatch(Boolean.parseBoolean(dispatch)); - if (authenticateNewUsers) + String authenticateNewUsers = authConfig.getParameter(AUTHENTICATE_NEW_USERS_PARAM); + if (authenticateNewUsers != null) + setAuthenticateNewUsers(Boolean.parseBoolean(authenticateNewUsers)); + + String chainIds = authConfig.getParameter(CHAIN_IDS_PARAM); + if (chainIds != null) + includeChainIds(StringUtil.csvSplit(chainIds)); + + String domains = authConfig.getParameter(DOMAINS_PARAM); + if (domains != null) + includeDomains(StringUtil.csvSplit(domains)); + + if (isAuthenticateNewUsers()) { LoginService loginService = new AnyUserLoginService(authConfig.getRealmName(), authConfig.getLoginService()); authConfig = new Configuration.Wrapper(authConfig) @@ -161,6 +171,8 @@ public class EthereumAuthenticator extends LoginAuthenticator }; } + if (_loginPath == null) + throw new IllegalStateException("No loginPath"); super.setConfiguration(authConfig); } @@ -172,7 +184,7 @@ public class EthereumAuthenticator extends LoginAuthenticator public boolean isAuthenticateNewUsers() { - return authenticateNewUsers; + return _authenticateNewUsers; } /** @@ -186,7 +198,7 @@ public class EthereumAuthenticator extends LoginAuthenticator */ public void setAuthenticateNewUsers(boolean authenticateNewUsers) { - this.authenticateNewUsers = authenticateNewUsers; + this._authenticateNewUsers = authenticateNewUsers; } public void setLoginPath(String loginPath) @@ -263,7 +275,6 @@ public class EthereumAuthenticator extends LoginAuthenticator if (path == null || path.trim().isEmpty()) { _errorPath = null; - _errorPage = null; } else { @@ -272,15 +283,14 @@ public class EthereumAuthenticator extends LoginAuthenticator LOG.warn("error-page must start with /"); path = "/" + path; } - _errorPage = path; _errorPath = path; _errorQuery = ""; int queryIndex = _errorPath.indexOf('?'); if (queryIndex > 0) { - _errorPath = _errorPage.substring(0, queryIndex); - _errorQuery = _errorPage.substring(queryIndex + 1); + _errorPath = path.substring(0, queryIndex); + _errorQuery = path.substring(queryIndex + 1); } } } @@ -417,6 +427,8 @@ public class EthereumAuthenticator extends LoginAuthenticator return Constraint.Authorization.ANY_USER; if (isLoginPage(pathInContext) || isErrorPage(pathInContext)) return Constraint.Authorization.ALLOWED; + if (isNonceRequest(pathInContext)) + return Constraint.Authorization.ANY_USER; return existing; } @@ -433,7 +445,7 @@ public class EthereumAuthenticator extends LoginAuthenticator break; totalRead += len; - if (totalRead > _maxMessageSize) + if (_maxMessageSize >= 0 && totalRead > _maxMessageSize) throw new BadMessageException("SIWE Message Too Large"); out.append(buffer, 0, len); } @@ -494,6 +506,7 @@ public class EthereumAuthenticator extends LoginAuthenticator protected AuthenticationState handleNonceRequest(Request request, Response response, Callback callback) { String nonce = createNonce(request.getSession(false)); + response.getHeaders().put(HttpHeader.CONTENT_TYPE, "application/json"); ByteBuffer content = BufferUtil.toBuffer("{ \"nonce\": \"" + nonce + "\" }"); response.write(true, content, callback); return AuthenticationState.CHALLENGE; @@ -510,7 +523,7 @@ public class EthereumAuthenticator extends LoginAuthenticator try { - siwe.validate(signedMessage, nonce -> redeemNonce(session, nonce), _schemes, _domains, _chainIds); + siwe.validate(signedMessage, nonce -> redeemNonce(session, nonce), _domains, _chainIds); } catch (Throwable t) { @@ -580,9 +593,6 @@ public class EthereumAuthenticator extends LoginAuthenticator return formAuth; } - // not authenticated - if (LOG.isDebugEnabled()) - LOG.debug("auth failed {}=={}", address, _errorPage); sendError(request, response, callback, "auth failed"); return AuthenticationState.SEND_FAILURE; } @@ -661,9 +671,9 @@ public class EthereumAuthenticator extends LoginAuthenticator private void sendError(Request request, Response response, Callback callback, String message) { if (LOG.isDebugEnabled()) - LOG.debug("OpenId authentication FAILED: {}", message); + LOG.debug("Authentication FAILED: {}", message); - if (_errorPage == null) + if (_errorPath == null) { if (LOG.isDebugEnabled()) LOG.debug("auth failed 403"); @@ -673,10 +683,10 @@ public class EthereumAuthenticator extends LoginAuthenticator else { if (LOG.isDebugEnabled()) - LOG.debug("auth failed {}", _errorPage); + LOG.debug("auth failed {}", _errorPath); String contextPath = Request.getContextPath(request); - String redirectUri = URIUtil.addPaths(contextPath, _errorPage); + String redirectUri = URIUtil.addPaths(contextPath, _errorPath); if (message != null) { String query = URIUtil.addQueries(ERROR_PARAMETER + "=" + UrlEncoded.encodeString(message), _errorQuery); @@ -732,6 +742,8 @@ public class EthereumAuthenticator extends LoginAuthenticator public boolean isErrorPage(String pathInContext) { + if (_errorPath == null) + return false; return pathInContext != null && (pathInContext.equals(_errorPath)); } @@ -762,6 +774,24 @@ public class EthereumAuthenticator extends LoginAuthenticator } } + @Override + public void dump(Appendable out, String indent) throws IOException + { + Dumpable.dumpObjects(out, indent, this, + "loginPath=" + _loginPath, + "authenticationPath=" + _authenticationPath, + "noncePath=" + _noncePath, + "errorPath=" + _errorPath, + "errorQuery=" + _errorQuery, + "dispatch=" + _dispatch, + "authenticateNewUsers=" + _authenticateNewUsers, + "logoutRedirectPath=" + _logoutRedirectPath, + "maxMessageSize=" + _maxMessageSize, + "chainIds=" + _chainIds, + "domains=" + _domains + ); + } + public static class FixedSizeSet extends LinkedHashSet { private final int maxSize; diff --git a/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/internal/SignInWithEthereumToken.java b/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/internal/SignInWithEthereumToken.java index 24fcbc8acaf..b351a05b4a3 100644 --- a/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/internal/SignInWithEthereumToken.java +++ b/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/internal/SignInWithEthereumToken.java @@ -56,24 +56,22 @@ public record SignInWithEthereumToken(String scheme, /** * @param signedMessage the {@link SignedMessage}. * @param validateNonce a {@link Predicate} used to validate the nonce. - * @param schemes the {@link IncludeExcludeSet} used to validate the scheme. * @param domains the {@link IncludeExcludeSet} used to validate the domain. * @param chainIds the {@link IncludeExcludeSet} used to validate the chainId. * @throws ServerAuthException if the {@link SignedMessage} fails validation. */ public void validate(SignedMessage signedMessage, Predicate validateNonce, - IncludeExcludeSet schemes, IncludeExcludeSet domains, IncludeExcludeSet chainIds) throws ServerAuthException { if (validateNonce != null && !validateNonce.test(nonce())) - throw new ServerAuthException("invalid nonce"); + throw new ServerAuthException("invalid nonce " + nonce); if (!StringUtil.asciiEqualsIgnoreCase(signedMessage.recoverAddress(), address())) throw new ServerAuthException("signature verification failed"); if (!"1".equals(version())) - throw new ServerAuthException("unsupported version"); + throw new ServerAuthException("unsupported version " + version); LocalDateTime now = LocalDateTime.now(); if (StringUtil.isNotBlank(expirationTime())) @@ -90,11 +88,9 @@ public record SignInWithEthereumToken(String scheme, throw new ServerAuthException("SIWE message not yet valid"); } - if (schemes != null && !schemes.test(scheme())) - throw new ServerAuthException("unregistered scheme"); if (domains != null && !domains.test(domain())) - throw new ServerAuthException("unregistered domain"); + throw new ServerAuthException("unregistered domain: " + domain()); if (chainIds != null && !chainIds.test(chainId())) - throw new ServerAuthException("unregistered chainId"); + throw new ServerAuthException("unregistered chainId: " + chainId()); } } diff --git a/jetty-core/jetty-siwe/src/test/java/org/eclipse/jetty/security/siwe/SignInWithEthereumTest.java b/jetty-core/jetty-siwe/src/test/java/org/eclipse/jetty/security/siwe/SignInWithEthereumTest.java index afa69e3d9cb..f577acb36c3 100644 --- a/jetty-core/jetty-siwe/src/test/java/org/eclipse/jetty/security/siwe/SignInWithEthereumTest.java +++ b/jetty-core/jetty-siwe/src/test/java/org/eclipse/jetty/security/siwe/SignInWithEthereumTest.java @@ -70,6 +70,11 @@ public class SignInWithEthereumTest public boolean handle(Request request, Response response, Callback callback) throws Exception { String pathInContext = Request.getPathInContext(request); + if ("/error".equals(pathInContext)) + { + response.write(true, BufferUtil.toBuffer("ERROR"), callback); + return true; + } if ("/login".equals(pathInContext)) { response.write(true, BufferUtil.toBuffer("Please Login"), callback); @@ -89,11 +94,11 @@ public class SignInWithEthereumTest }; _authenticator = new EthereumAuthenticator(); - _authenticator.setLoginPath("/login"); SecurityHandler.PathMapped securityHandler = new SecurityHandler.PathMapped(); securityHandler.setAuthenticator(_authenticator); securityHandler.setHandler(handler); + securityHandler.setParameter(EthereumAuthenticator.LOGIN_PATH_PARAM, "/login"); securityHandler.put("/*", Constraint.ANY_USER); SessionHandler sessionHandler = new SessionHandler(); @@ -224,26 +229,6 @@ public class SignInWithEthereumTest assertThat(response.getContentAsString(), containsString("UserPrincipal: " + _credentials.getAddress())); } - @Test - public void testEnforceScheme() throws Exception - { - _authenticator.includeSchemes("https"); - - // Test login with invalid scheme. - String nonce = getNonce(); - String siweMessage = SignInWithEthereumGenerator.generateMessage("http", "localhost", _credentials.getAddress(), nonce); - ContentResponse response = sendAuthRequest(_credentials.signMessage(siweMessage)); - assertThat(response.getStatus(), equalTo(HttpStatus.FORBIDDEN_403)); - assertThat(response.getContentAsString(), containsString("unregistered scheme")); - - // Test login with valid scheme. - nonce = getNonce(); - siweMessage = SignInWithEthereumGenerator.generateMessage("https", "localhost", _credentials.getAddress(), nonce); - response = sendAuthRequest(_credentials.signMessage(siweMessage)); - assertThat(response.getStatus(), equalTo(HttpStatus.OK_200)); - assertThat(response.getContentAsString(), containsString("UserPrincipal: " + _credentials.getAddress())); - } - @Test public void testEnforceChainId() throws Exception { diff --git a/jetty-core/jetty-siwe/src/test/java/org/eclipse/jetty/security/siwe/SignInWithEthereumTokenTest.java b/jetty-core/jetty-siwe/src/test/java/org/eclipse/jetty/security/siwe/SignInWithEthereumTokenTest.java index b7018010ead..de6f2d72944 100644 --- a/jetty-core/jetty-siwe/src/test/java/org/eclipse/jetty/security/siwe/SignInWithEthereumTokenTest.java +++ b/jetty-core/jetty-siwe/src/test/java/org/eclipse/jetty/security/siwe/SignInWithEthereumTokenTest.java @@ -55,7 +55,7 @@ public class SignInWithEthereumTokenTest assertNotNull(siwe); Throwable error = assertThrows(Throwable.class, () -> - siwe.validate(signedMessage, null, null, null, null)); + siwe.validate(signedMessage, null, null, null)); assertThat(error.getMessage(), containsString("unsupported version")); } @@ -84,7 +84,7 @@ public class SignInWithEthereumTokenTest assertNotNull(siwe); Throwable error = assertThrows(Throwable.class, () -> - siwe.validate(signedMessage, null, null, null, null)); + siwe.validate(signedMessage, null, null, null)); assertThat(error.getMessage(), containsString("expired SIWE message")); } @@ -114,7 +114,7 @@ public class SignInWithEthereumTokenTest assertNotNull(siwe); Throwable error = assertThrows(Throwable.class, () -> - siwe.validate(signedMessage, null, null, null, null)); + siwe.validate(signedMessage, null, null, null)); assertThat(error.getMessage(), containsString("SIWE message not yet valid")); } @@ -144,40 +144,10 @@ public class SignInWithEthereumTokenTest domains.include("example.org"); Throwable error = assertThrows(Throwable.class, () -> - siwe.validate(signedMessage, null, null, domains, null)); + siwe.validate(signedMessage, null, domains, null)); assertThat(error.getMessage(), containsString("unregistered domain")); } - @Test - public void testInvalidScheme() throws Exception - { - EthereumCredentials credentials = new EthereumCredentials(); - LocalDateTime issuedAt = LocalDateTime.now(); - String message = SignInWithEthereumGenerator.generateMessage( - "https", - "example.com", - credentials.getAddress(), - "hello this is the statement", - "https://example.com", - "1", - "1", - EthereumUtil.createNonce(), - issuedAt, - null, null, null, null - ); - - SignedMessage signedMessage = credentials.signMessage(message); - SignInWithEthereumToken siwe = SignInWithEthereumParser.parse(message); - assertNotNull(siwe); - - IncludeExcludeSet schemes = new IncludeExcludeSet<>(); - schemes.include("wss"); - - Throwable error = assertThrows(Throwable.class, () -> - siwe.validate(signedMessage, null, schemes, null, null)); - assertThat(error.getMessage(), containsString("unregistered scheme")); - } - @Test public void testInvalidChainId() throws Exception { @@ -204,7 +174,7 @@ public class SignInWithEthereumTokenTest chainIds.include("1337"); Throwable error = assertThrows(Throwable.class, () -> - siwe.validate(signedMessage, null, null, null, chainIds)); + siwe.validate(signedMessage, null, null, chainIds)); assertThat(error.getMessage(), containsString("unregistered chainId")); } @@ -232,7 +202,7 @@ public class SignInWithEthereumTokenTest Predicate nonceValidation = nonce -> false; Throwable error = assertThrows(Throwable.class, () -> - siwe.validate(signedMessage, nonceValidation, null, null, null)); + siwe.validate(signedMessage, nonceValidation, null, null)); assertThat(error.getMessage(), containsString("invalid nonce")); } @@ -260,6 +230,6 @@ public class SignInWithEthereumTokenTest Predicate nonceValidation = nonce -> true; assertDoesNotThrow(() -> - siwe.validate(signedMessage, nonceValidation, null, null, null)); + siwe.validate(signedMessage, nonceValidation, null, null)); } } diff --git a/jetty-core/jetty-siwe/src/test/java/org/eclipse/jetty/security/siwe/example/SignInWithEthereumEmbeddedExample.java b/jetty-core/jetty-siwe/src/test/java/org/eclipse/jetty/security/siwe/example/SignInWithEthereumEmbeddedExample.java index 3ee3ad35704..ee64848e2c6 100644 --- a/jetty-core/jetty-siwe/src/test/java/org/eclipse/jetty/security/siwe/example/SignInWithEthereumEmbeddedExample.java +++ b/jetty-core/jetty-siwe/src/test/java/org/eclipse/jetty/security/siwe/example/SignInWithEthereumEmbeddedExample.java @@ -79,13 +79,7 @@ public class SignInWithEthereumEmbeddedExample } }; - EthereumAuthenticator authenticator = new EthereumAuthenticator(); - authenticator.setLoginPath("/login.html"); - SecurityHandler.PathMapped securityHandler = new SecurityHandler.PathMapped(); - securityHandler.setAuthenticator(authenticator); - securityHandler.setHandler(handler); - securityHandler.put("/*", Constraint.ANY_USER); - + SecurityHandler securityHandler = createSecurityHandler(handler); SessionHandler sessionHandler = new SessionHandler(); sessionHandler.setHandler(securityHandler); @@ -98,4 +92,24 @@ public class SignInWithEthereumEmbeddedExample System.err.println(resourceHandler.getBaseResource()); server.join(); } + + public static SecurityHandler createSecurityHandler(Handler handler) + { + // 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"); + + return securityHandler; + } } \ No newline at end of file diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/security/ConstraintSecurityHandler.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/security/ConstraintSecurityHandler.java index 035486a1d49..83e74fe072b 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/security/ConstraintSecurityHandler.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/security/ConstraintSecurityHandler.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -29,6 +30,7 @@ import java.util.stream.Stream; import jakarta.servlet.HttpConstraintElement; import jakarta.servlet.HttpMethodConstraintElement; +import jakarta.servlet.ServletContext; import jakarta.servlet.ServletSecurityElement; import jakarta.servlet.annotation.ServletSecurity.EmptyRoleSemantic; import jakarta.servlet.annotation.ServletSecurity.TransportGuarantee; @@ -40,6 +42,7 @@ import org.eclipse.jetty.http.pathmap.PathSpec; import org.eclipse.jetty.security.Constraint; import org.eclipse.jetty.security.Constraint.Transport; import org.eclipse.jetty.security.SecurityHandler; +import org.eclipse.jetty.server.Context; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandler; @@ -373,6 +376,20 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr //Servlet Spec 3.1 pg 147 sec 13.8.4.2 log paths for which there are uncovered http methods checkPathsWithUncoveredHttpMethods(); + Context context = ContextHandler.getCurrentContext(); + if (context instanceof ServletContextHandler.ServletScopedContext servletScopedContext) + { + ServletContext servletContext = servletScopedContext.getServletContext(); + Enumeration names = servletContext.getInitParameterNames(); + while (names != null && names.hasMoreElements()) + { + String name = names.nextElement(); + if (name.startsWith("org.eclipse.jetty.security.") && + getParameter(name) == null) + setParameter(name, servletContext.getInitParameter(name)); + } + } + super.doStart(); } diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/pom.xml b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/pom.xml new file mode 100644 index 00000000000..b8cf0c2b4a4 --- /dev/null +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/pom.xml @@ -0,0 +1,27 @@ + + + + 4.0.0 + + org.eclipse.jetty.ee10 + jetty-ee10-tests + 12.0.12-SNAPSHOT + + jetty-ee10-test-siwe-webapp + war + + EE10 :: Tests :: SIWE WebApp + + + + org.eclipse.jetty + jetty-slf4j-impl + compile + + + jakarta.servlet + jakarta.servlet-api + provided + + + diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/AdminServlet.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/AdminServlet.java new file mode 100644 index 00000000000..5949b1d0a54 --- /dev/null +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/AdminServlet.java @@ -0,0 +1,32 @@ +// +// ======================================================================== +// 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 com.acme; + +import java.io.IOException; + +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@WebServlet("/admin") +public class AdminServlet extends HttpServlet +{ + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException + { + response.setContentType("text/plain"); + response.getWriter().print("adminPage userPrincipal: " + request.getUserPrincipal().getName()); + } +} diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/ErrorServlet.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/ErrorServlet.java new file mode 100644 index 00000000000..2a0644b1bca --- /dev/null +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/ErrorServlet.java @@ -0,0 +1,37 @@ +// +// ======================================================================== +// 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 com.acme; + +import java.io.IOException; + +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@WebServlet("/error") +public class ErrorServlet extends HttpServlet +{ + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException + { + + response.setContentType("text/html"); + response.getWriter().println("

error: not authorized

"); + response.getWriter().println("

" + request.getUserPrincipal() + "

"); + response.getWriter().println("

" + request.getParameter("error_description_jetty") + "

"); + String home = request.getContextPath().isEmpty() ? "/" : request.getContextPath(); + response.getWriter().println("Home
"); + } +} diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/ForbiddenServlet.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/ForbiddenServlet.java new file mode 100644 index 00000000000..1e7c2ec3ac0 --- /dev/null +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/ForbiddenServlet.java @@ -0,0 +1,33 @@ +// +// ======================================================================== +// 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 com.acme; + +import java.io.IOException; + +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@WebServlet("/forbidden") +public class ForbiddenServlet extends HttpServlet +{ + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException + { + String home = request.getContextPath().isEmpty() ? "/" : request.getContextPath(); + response.getWriter().println("

Not authorized to access this page.

"); + response.getWriter().println("Home
"); + } +} diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/HomeServlet.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/HomeServlet.java new file mode 100644 index 00000000000..6edf8f94d04 --- /dev/null +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/HomeServlet.java @@ -0,0 +1,37 @@ +// +// ======================================================================== +// 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 com.acme; + +import java.io.IOException; +import java.security.Principal; + +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@WebServlet("") +public class HomeServlet extends HttpServlet +{ + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException + { + response.setContentType("text/plain"); + Principal userPrincipal = request.getUserPrincipal(); + if (userPrincipal != null) + response.getWriter().print("userPrincipal: " + userPrincipal.getName()); + else + response.getWriter().print("not authenticated"); + } +} diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/LogoutServlet.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/LogoutServlet.java new file mode 100644 index 00000000000..055be42607b --- /dev/null +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/java/com/acme/LogoutServlet.java @@ -0,0 +1,34 @@ +// +// ======================================================================== +// 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 com.acme; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@WebServlet("/logout") +public class LogoutServlet extends HttpServlet +{ + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + request.logout(); + if (!response.isCommitted()) + response.sendRedirect(request.getContextPath()); + } +} diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/webapp/WEB-INF/jetty-ee10-web.xml b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/webapp/WEB-INF/jetty-ee10-web.xml new file mode 100644 index 00000000000..6da307c0395 --- /dev/null +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/webapp/WEB-INF/jetty-ee10-web.xml @@ -0,0 +1,20 @@ + + + + + + + + /etc/realm.properties + + + + + + + myRealm + + + + + \ No newline at end of file diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/webapp/WEB-INF/web.xml b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..da4fe0c2b68 --- /dev/null +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,69 @@ + + + + SIWE Authentication Webapp + + + SIWE + myRealm + + + + org.eclipse.jetty.security.siwe.login_path + /login.html + + + org.eclipse.jetty.security.siwe.error_path + /error?foobar=3 + + + org.eclipse.jetty.security.siwe.chainIds + 2,3,4,1,5,7 + + + + admin + + + forbidden + + + ** + + + + + User Pages + /login + /expiry + + + ** + + + + + + Admin Page + /admin + + + admin + + + + + + Forbidden Page + /forbidden + + + forbidden + + + + + diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/webapp/favicon.ico b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/webapp/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..ea9e174b48b0130d4294813a6b5d487f35b436cc GIT binary patch literal 1150 zcmbtT*-leY6um|h48jdk3$&%Q(B4i^j0|O{SS`gNpioe>$}E$@lZJ1QNB+PF^2Rq1 z-~0si(Ki_^w7s|RV6t5MRE#FRd7IU<_sLm%SZ5ziJI7yDmBxRswtPX;YBWs?5!~WV zX`jnKzth8Su;vVy6F!(zey)BLp7>CB>W4KOr0qw3w28if>`f;Q+U+p8Z%!Bx?#XHx z){FsbAqZ=}v|o5k{<>)7Pyike?g%;$(&VSpzKYFT`wR zDrX^M;CRr3qk($tMJ~gfG?=p)=1f4>OZ>eMmFKZ z{#6_645KLgIrf(Ep}0){MOd9rM%JLm7=wuWGDy2FVWh5HVfe~LjL`pd&kbuajQk^N z@{D>BMfSbkW<%5whu%d#k%;ZURdyb!Ya7ZI-%;ttJJuxJLz0cWW_*%td~zN%2=2D-ky` z(BtG2iSC_JOXHW6$^Qk_6M1L8#NWE%;LLH} zWIj0`Qp53(2ibm?;-@;O`5*Ztr|<+(CpGA+Q$34j#g!J$e~9zt#$j(AUOLK=Y~>vE zu?9lyH@sWXP4aHWYq5K$p7{f?rUNjigPh|g$c%mOb6~yGu6q2P&lSgd%&yLxlcl$% z^3Jn=&e49e4f|0W4m)dTSK~8hVtc3o?+0Ai>UUzBcl=?*!x|+Q-zjIsoMPQ_f7DOj qU^eMe?`tnuMcawfS5dwZz6;KEaVb>dF}Ls}x8hTO;ZooK`hEgZQ79q+ literal 0 HcmV?d00001 diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/webapp/login.html b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/webapp/login.html new file mode 100644 index 00000000000..8a358c741ea --- /dev/null +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-siwe-webapp/src/main/webapp/login.html @@ -0,0 +1,59 @@ + + + + + + Sign-In with Ethereum + + + +

Sign-In with Ethereum

+ + + + + + + diff --git a/jetty-ee10/jetty-ee10-tests/pom.xml b/jetty-ee10/jetty-ee10-tests/pom.xml index 0e4620bbd75..66b5164ba2c 100644 --- a/jetty-ee10/jetty-ee10-tests/pom.xml +++ b/jetty-ee10/jetty-ee10-tests/pom.xml @@ -27,6 +27,7 @@ jetty-ee10-test-log4j2-webapp jetty-ee10-test-loginservice jetty-ee10-test-openid-webapp + jetty-ee10-test-siwe-webapp jetty-ee10-test-owb-cdi-webapp jetty-ee10-test-quickstart jetty-ee10-test-sessions diff --git a/jetty-home/pom.xml b/jetty-home/pom.xml index 070415d1e50..017530f1d6d 100644 --- a/jetty-home/pom.xml +++ b/jetty-home/pom.xml @@ -104,6 +104,10 @@ org.eclipse.jetty jetty-security
+ + org.eclipse.jetty + jetty-siwe + org.eclipse.jetty jetty-start diff --git a/pom.xml b/pom.xml index 31c385d1ba7..533efde7859 100644 --- a/pom.xml +++ b/pom.xml @@ -169,6 +169,7 @@ 3.0.0-alpha.2 9.7 4.2.1 + 1.78.1 7.0.0 3.6.0 1.5 @@ -1303,6 +1304,22 @@ wildfly-elytron-sasl-scram ${wildfly.elytron.version} + + + org.bouncycastle + bcpkix-jdk15to18 + ${bouncycastle.version} + + + org.bouncycastle + bcprov-jdk15to18 + ${bouncycastle.version} + + + org.bouncycastle + bcutil-jdk15to18 + ${bouncycastle.version} +
diff --git a/tests/test-distribution/test-ee10-distribution/pom.xml b/tests/test-distribution/test-ee10-distribution/pom.xml index 53783eae17e..3628f0b9298 100644 --- a/tests/test-distribution/test-ee10-distribution/pom.xml +++ b/tests/test-distribution/test-ee10-distribution/pom.xml @@ -37,6 +37,11 @@ jetty-openid test
+ + org.eclipse.jetty + jetty-siwe + test + org.eclipse.jetty jetty-slf4j-impl diff --git a/tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/SiweTests.java b/tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/SiweTests.java new file mode 100644 index 00000000000..e7936f868fd --- /dev/null +++ b/tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/SiweTests.java @@ -0,0 +1,153 @@ +// +// ======================================================================== +// 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.ee10.tests.distribution; + +import java.io.FileWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.FormRequestContent; +import org.eclipse.jetty.ee10.tests.distribution.siwe.EthereumCredentials; +import org.eclipse.jetty.ee10.tests.distribution.siwe.SignInWithEthereumGenerator; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.security.siwe.SignedMessage; +import org.eclipse.jetty.tests.distribution.AbstractJettyHomeTest; +import org.eclipse.jetty.tests.testers.JettyHomeTester; +import org.eclipse.jetty.tests.testers.Tester; +import org.eclipse.jetty.util.Fields; +import org.eclipse.jetty.util.ajax.JSON; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Isolated; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Isolated +public class SiweTests extends AbstractJettyHomeTest +{ + private final EthereumCredentials _credentials = new EthereumCredentials(); + + @Test + public void testSiwe() throws Exception + { + Path jettyBase = newTestJettyBaseDirectory(); + String jettyVersion = System.getProperty("jettyVersion"); + JettyHomeTester distribution = JettyHomeTester.Builder.newInstance() + .jettyVersion(jettyVersion) + .jettyBase(jettyBase) + .build(); + + String[] args1 = { + "--create-startd", + "--approve-all-licenses", + "--add-to-start=http,ee10-webapp,ee10-deploy,ee10-annotations,siwe" + }; + + try (JettyHomeTester.Run run1 = distribution.start(args1)) + { + assertTrue(run1.awaitFor(START_TIMEOUT, TimeUnit.SECONDS)); + assertEquals(0, run1.getExitValue()); + + Path webApp = distribution.resolveArtifact("org.eclipse.jetty.ee10:jetty-ee10-test-siwe-webapp:war:" + jettyVersion); + distribution.installWar(webApp, "test"); + Files.createDirectory(jettyBase.resolve("etc")); + Path realmProperties = Files.createFile(jettyBase.resolve("etc/realm.properties")); + try (FileWriter fw = new FileWriter(realmProperties.toFile())) + { + fw.write(_credentials.getAddress() + ":,admin\n"); + } + + int port = Tester.freePort(); + String[] args2 = { + "jetty.http.port=" + port, + "jetty.ssl.port=" + port, + "jetty.server.dumpAfterStart=true", + }; + +// System.setProperty("distribution.debug.port", "5005"); + try (JettyHomeTester.Run run2 = distribution.start(args2)) + { + assertTrue(run2.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS)); + startHttpClient(false); + String uri = "http://localhost:" + port + "/test"; + + // Initially not authenticated. + ContentResponse response = client.GET(uri + "/"); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + String content = response.getContentAsString(); + assertThat(content, containsString("not authenticated")); + + // Request to /admin redirects to loginPage. + client.setFollowRedirects(false); + response = client.GET(uri + "/admin"); + assertThat(response.getStatus(), is(HttpStatus.FOUND_302)); + assertThat(response.getHeaders().get(HttpHeader.LOCATION), + containsString(uri + "/login.html")); + + // Fetch a nonce from the server. + response = client.GET(uri + "/auth/nonce"); + String nonce = parseNonce(response.getContentAsString()); + assertThat(nonce.length(), equalTo(8)); + + // Request to authenticate redirects to /admin page. + FormRequestContent authRequestContent = getAuthRequestContent(port, nonce); + response = client.POST(uri + "/auth/login").body(authRequestContent).send(); + assertThat(response.getStatus(), is(HttpStatus.SEE_OTHER_303)); + assertThat(response.getHeaders().get(HttpHeader.LOCATION), + containsString(uri + "/admin")); + + // We can access /admin as user has the admin role. + response = client.GET(uri + "/admin"); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + content = response.getContentAsString(); + assertThat(content, containsString("adminPage userPrincipal: " + _credentials.getAddress())); + + // We can't access /forbidden as user does not have the correct role. + response = client.GET(uri + "/forbidden"); + assertThat(response.getStatus(), is(HttpStatus.FORBIDDEN_403)); + + // Logout and we can no longer get the userPrincipal. + client.setFollowRedirects(true); + response = client.GET(uri + "/logout"); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + content = response.getContentAsString(); + assertThat(content, containsString("not authenticated")); + } + } + } + + private FormRequestContent getAuthRequestContent(int port, String nonce) throws Exception + { + SignedMessage signedMessage = _credentials.signMessage( + SignInWithEthereumGenerator.generateMessage(port, _credentials.getAddress(), nonce)); + Fields fields = new Fields(); + fields.add("signature", signedMessage.signature()); + fields.add("message", signedMessage.message()); + return new FormRequestContent(fields); + } + + @SuppressWarnings("rawtypes") + private String parseNonce(String responseContent) + { + return (String)((Map)new JSON().parse(new JSON.StringSource(responseContent))).get("nonce"); + } +} diff --git a/tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/siwe/EthereumCredentials.java b/tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/siwe/EthereumCredentials.java new file mode 100644 index 00000000000..80a5d6957bf --- /dev/null +++ b/tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/siwe/EthereumCredentials.java @@ -0,0 +1,126 @@ +// +// ======================================================================== +// 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.ee10.tests.distribution.siwe; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.spec.ECGenParameterSpec; +import java.util.Arrays; + +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.math.ec.ECPoint; +import org.bouncycastle.util.encoders.Hex; +import org.eclipse.jetty.security.siwe.SignedMessage; +import org.eclipse.jetty.security.siwe.internal.EthereumSignatureVerifier; + +import static org.eclipse.jetty.security.siwe.internal.EthereumSignatureVerifier.keccak256; + +public class EthereumCredentials +{ + private final PrivateKey privateKey; + private final PublicKey publicKey; + private final String address; + private final BouncyCastleProvider provider = new BouncyCastleProvider(); + + public EthereumCredentials() + { + try + { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC", provider); + ECGenParameterSpec ecGenParameterSpec = new ECGenParameterSpec("secp256k1"); + keyPairGenerator.initialize(ecGenParameterSpec, new SecureRandom()); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + this.privateKey = keyPair.getPrivate(); + this.publicKey = keyPair.getPublic(); + this.address = EthereumSignatureVerifier.toAddress(((BCECPublicKey)publicKey).getQ()); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + public String getAddress() + { + return address; + } + + public SignedMessage signMessage(String message) throws Exception + { + byte[] messageBytes = message.getBytes(StandardCharsets.ISO_8859_1); + String prefix = "\u0019Ethereum Signed Message:\n" + messageBytes.length + message; + byte[] messageHash = keccak256(prefix.getBytes(StandardCharsets.ISO_8859_1)); + + Signature ecdsaSign = Signature.getInstance("NONEwithECDSA", provider); + ecdsaSign.initSign(privateKey); + ecdsaSign.update(messageHash); + byte[] encodedSignature = ecdsaSign.sign(); + byte[] r = getR(encodedSignature); + byte[] s = getS(encodedSignature); + + byte[] signature = new byte[65]; + System.arraycopy(r, 0, signature, 0, 32); + System.arraycopy(s, 0, signature, 32, 32); + signature[64] = (byte)(calculateV(messageHash, r, s) + 27); + return new SignedMessage(message, Hex.toHexString(signature)); + } + + private byte[] getR(byte[] encodedSignature) + { + int rLength = encodedSignature[3]; + byte[] r = Arrays.copyOfRange(encodedSignature, 4, 4 + rLength); + return ensure32Bytes(r); + } + + private byte[] getS(byte[] encodedSignature) + { + int rLength = encodedSignature[3]; + int sLength = encodedSignature[5 + rLength]; + byte[] s = Arrays.copyOfRange(encodedSignature, 6 + rLength, 6 + rLength + sLength); + return ensure32Bytes(s); + } + + private byte[] ensure32Bytes(byte[] bytes) + { + if (bytes.length == 32) + return bytes; + if (bytes.length > 32) + return Arrays.copyOfRange(bytes, bytes.length - 32, bytes.length); + else + { + byte[] padded = new byte[32]; + System.arraycopy(bytes, 0, padded, 32 - bytes.length, bytes.length); + return padded; + } + } + + private byte calculateV(byte[] hash, byte[] r, byte[] s) + { + ECPoint publicKeyPoint = ((BCECPublicKey)publicKey).getQ(); + for (int v = 0; v < 4; v++) + { + ECPoint qPoint = EthereumSignatureVerifier.ecRecover(hash, v, new BigInteger(1, r), new BigInteger(1, s)); + if (qPoint != null && qPoint.equals(publicKeyPoint)) + return (byte)v; + } + throw new RuntimeException("Could not recover public key from signature"); + } +} diff --git a/tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/siwe/SignInWithEthereumGenerator.java b/tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/siwe/SignInWithEthereumGenerator.java new file mode 100644 index 00000000000..8f507b5f813 --- /dev/null +++ b/tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/siwe/SignInWithEthereumGenerator.java @@ -0,0 +1,104 @@ +// +// ======================================================================== +// 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.ee10.tests.distribution.siwe; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class SignInWithEthereumGenerator +{ + private SignInWithEthereumGenerator() + { + } + + public static String generateMessage(int port, String address, String nonce) + { + return generateMessage(null, "localhost:" + port, address, nonce, null, null); + } + + public static String generateMessage(String scheme, String domain, String address, String nonce) + { + return generateMessage(scheme, domain, address, nonce, null, null); + } + + public static String generateMessage(String scheme, String domain, String address, String nonce, String chainId) + { + return generateMessage(scheme, + domain, + address, + "I accept the MetaMask Terms of Service: https://community.metamask.io/tos", + "http://" + domain, + "1", + chainId, + nonce, + LocalDateTime.now(), + null, + null, + null, + null); + } + + public static String generateMessage(String scheme, String domain, String address, String nonce, LocalDateTime expiresAt, LocalDateTime notBefore) + { + return generateMessage(scheme, + domain, + address, + "I accept the MetaMask Terms of Service: https://community.metamask.io/tos", + "http://" + domain, + "1", + "1", + nonce, + LocalDateTime.now(), + expiresAt, + notBefore, + null, + null); + } + + public static String generateMessage(String scheme, + String domain, + String address, + String statement, + String uri, + String version, + String chainId, + String nonce, + LocalDateTime issuedAt, + LocalDateTime expirationTime, + LocalDateTime notBefore, + String requestId, + String resources) + { + StringBuilder sb = new StringBuilder(); + if (scheme != null) + sb.append(scheme).append("://"); + sb.append(domain).append(" wants you to sign in with your Ethereum account:\n"); + sb.append(address).append("\n\n"); + sb.append(statement).append("\n\n"); + sb.append("URI: ").append(uri).append("\n"); + sb.append("Version: ").append(version).append("\n"); + sb.append("Chain ID: ").append(chainId).append("\n"); + sb.append("Nonce: ").append(nonce).append("\n"); + sb.append("Issued At: ").append(issuedAt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + if (expirationTime != null) + sb.append("\nExpiration Time: ").append(expirationTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + if (notBefore != null) + sb.append("\nNot Before: ").append(notBefore.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + if (requestId != null) + sb.append("\nRequest ID: ").append(requestId); + if (resources != null) + sb.append("\nResources:").append(resources); + return sb.toString(); + } +}