diff --git a/appengine-java21/ee8/users/README.md b/appengine-java21/ee8/users/README.md
new file mode 100644
index 00000000000..e8a3c04aa60
--- /dev/null
+++ b/appengine-java21/ee8/users/README.md
@@ -0,0 +1,23 @@
+# Users Authentication sample for Google App Engine
+
+
+
+
+This sample demonstrates how to use the [Users API][appid] on [Google App
+Engine][ae-docs].
+
+[appid]: https://cloud.google.com/appengine/docs/java/users/
+[ae-docs]: https://cloud.google.com/appengine/docs/java/
+
+## Running locally
+This example uses the
+[Maven gcloud plugin](https://cloud.google.com/appengine/docs/legacy/standard/java/using-maven).
+To run this sample locally:
+
+ $ mvn appengine:run
+
+## Deploying
+In the following command, replace YOUR-PROJECT-ID with your
+[Google Cloud Project ID](https://developers.google.com/console/help/new/#projectnumber).
+
+ $ mvn clean package appengine:deploy
diff --git a/appengine-java21/ee8/users/pom.xml b/appengine-java21/ee8/users/pom.xml
new file mode 100644
index 00000000000..3c65115463e
--- /dev/null
+++ b/appengine-java21/ee8/users/pom.xml
@@ -0,0 +1,133 @@
+
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-users-j21
+
+
+
+ com.google.cloud.samples
+ shared-configuration
+ 1.2.0
+
+
+ 21
+ 21
+
+
+
+
+
+ libraries-bom
+ com.google.cloud
+ import
+ pom
+ 26.28.0
+
+
+
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ 2.0.39
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ 4.0.4
+ jar
+ provided
+
+
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+ org.mockito
+ mockito-core
+ 4.11.0
+ test
+
+
+ com.google.appengine
+ appengine-testing
+ 2.0.39
+ test
+
+
+ com.google.appengine
+ appengine-api-stubs
+ 2.0.39
+ test
+
+
+ com.google.appengine
+ appengine-tools-sdk
+ 2.0.39
+ test
+
+
+ com.google.truth
+ truth
+ 1.4.4
+ test
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+ 3.4.0
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.13
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ 2.5.0
+
+ GCLOUD_CONFIG
+ GCLOUD_CONFIG
+ true
+ true
+
+
+
+
+
diff --git a/appengine-java21/ee8/users/src/main/java/com/example/appengine/users/UsersServlet.java b/appengine-java21/ee8/users/src/main/java/com/example/appengine/users/UsersServlet.java
new file mode 100644
index 00000000000..11a5aafd91b
--- /dev/null
+++ b/appengine-java21/ee8/users/src/main/java/com/example/appengine/users/UsersServlet.java
@@ -0,0 +1,58 @@
+/* Copyright 2016 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// [START gae_java21_users_api]
+
+package com.example.appengine.users;
+
+import com.google.appengine.api.users.UserService;
+import com.google.appengine.api.users.UserServiceFactory;
+import java.io.IOException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+// With @WebServlet annotation the webapp/WEB-INF/web.xml is no longer required.
+@WebServlet(
+ name = "UserAPI",
+ description = "UserAPI: Login / Logout with UserService",
+ urlPatterns = "/userapi"
+)
+public class UsersServlet extends HttpServlet {
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ UserService userService = UserServiceFactory.getUserService();
+
+ String thisUrl = req.getRequestURI();
+
+ resp.setContentType("text/html");
+ if (req.getUserPrincipal() != null) {
+ resp.getWriter()
+ .println(
+ "
Hello, "
+ + req.getUserPrincipal().getName()
+ + "! You can sign out.
");
+ } else {
+ resp.getWriter()
+ .println(
+ "Please sign in.
");
+ }
+ }
+}
+// [END gae_java21_users_api]
diff --git a/appengine-java21/ee8/users/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java21/ee8/users/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..71f00b07474
--- /dev/null
+++ b/appengine-java21/ee8/users/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,8 @@
+
+
+ java21
+
+
+
+ true
+
diff --git a/appengine-java21/ee8/users/src/main/webapp/WEB-INF/web.xml b/appengine-java21/ee8/users/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..5fece3ce81b
--- /dev/null
+++ b/appengine-java21/ee8/users/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,26 @@
+
+
+
+
+ userapi
+
+ true
+
diff --git a/appengine-java21/ee8/users/src/test/java/com/example/appengine/users/UsersServletTest.java b/appengine-java21/ee8/users/src/test/java/com/example/appengine/users/UsersServletTest.java
new file mode 100644
index 00000000000..e7195d3f2f2
--- /dev/null
+++ b/appengine-java21/ee8/users/src/test/java/com/example/appengine/users/UsersServletTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2015 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine.users;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.mockito.Mockito.when;
+
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import javax.management.remote.JMXPrincipal;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link UsersServlet}.
+ */
+@RunWith(JUnit4.class)
+public class UsersServletTest {
+
+ private static final String FAKE_URL = "fakey.fake.fak";
+ private static final String FAKE_NAME = "Fake";
+ // Set up a helper so that the ApiProxy returns a valid environment for local testing.
+ private final LocalServiceTestHelper helper = new LocalServiceTestHelper();
+
+ @Mock
+ private HttpServletRequest mockRequestNotLoggedIn;
+ @Mock
+ private HttpServletRequest mockRequestLoggedIn;
+ @Mock
+ private HttpServletResponse mockResponse;
+ private StringWriter responseWriter;
+ private UsersServlet servletUnderTest;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.openMocks(this);
+ helper.setUp();
+
+ // Set up some fake HTTP requests
+ // If the user isn't logged in, use this request
+ when(mockRequestNotLoggedIn.getRequestURI()).thenReturn(FAKE_URL);
+ when(mockRequestNotLoggedIn.getUserPrincipal()).thenReturn(null);
+
+ // If the user is logged in, use this request
+ when(mockRequestLoggedIn.getRequestURI()).thenReturn(FAKE_URL);
+ // Most of the classes that implement Principal have been
+ // deprecated. JMXPrincipal seems like a safe choice.
+ when(mockRequestLoggedIn.getUserPrincipal()).thenReturn(new JMXPrincipal(FAKE_NAME));
+
+ // Set up a fake HTTP response.
+ responseWriter = new StringWriter();
+ when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter));
+
+ servletUnderTest = new UsersServlet();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void doGet_userNotLoggedIn_writesResponse() throws Exception {
+ servletUnderTest.doGet(mockRequestNotLoggedIn, mockResponse);
+
+ // If a user isn't logged in, we expect a prompt
+ // to login to be returned.
+ assertWithMessage("UsersServlet response")
+ .that(responseWriter.toString())
+ .contains("Please .
");
+ }
+
+ @Test
+ public void doGet_userLoggedIn_writesResponse() throws Exception {
+ servletUnderTest.doGet(mockRequestLoggedIn, mockResponse);
+
+ // If a user is logged in, we expect a prompt
+ // to logout to be returned.
+ assertWithMessage("UsersServlet response")
+ .that(responseWriter.toString())
+ .contains("Hello, " + FAKE_NAME + "!");
+ assertWithMessage("UsersServlet response").that(responseWriter.toString()).contains("sign out");
+ }
+}