certs = appIdentity.getPublicCertificatesForApp();
+ PublicCertificate publicCert = certs.iterator().next();
+ return publicCert.getX509CertificateInPemFormat().getBytes(java.nio.charset.StandardCharsets.UTF_8);
+ }
+
+ private Certificate parsePublicCertificate(byte[] publicCert)
+ throws CertificateException, NoSuchAlgorithmException {
+ InputStream stream = new ByteArrayInputStream(publicCert);
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ return cf.generateCertificate(stream);
+ }
+
+ private boolean verifySignature(byte[] blob, byte[] blobSignature, PublicKey pk)
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+ Signature signature = Signature.getInstance("SHA256withRSA");
+ signature.initVerify(pk);
+ signature.update(blob);
+ return signature.verify(blobSignature);
+ }
+
+ private String simulateIdentityAssertion()
+ throws CertificateException, UnsupportedEncodingException, NoSuchAlgorithmException,
+ InvalidKeyException, SignatureException {
+ // Simulate the sending app.
+ String message = "abcdefg " + java.time.Instant.now().toString();
+ byte[] blob = message.getBytes();
+ byte[] blobSignature = signBlob(blob);
+ byte[] publicCert = getPublicCertificate();
+
+ // Simulate the receiving app, which gets the certificate, blob, and signature.
+ Certificate cert = parsePublicCertificate(publicCert);
+ PublicKey pk = cert.getPublicKey();
+ boolean isValid = verifySignature(blob, blobSignature, pk);
+
+ return String.format(
+ "isValid=%b for message: %s\n\tsignature: %s\n\tpublic cert: %s",
+ isValid, message, Arrays.toString(blobSignature), Arrays.toString(publicCert));
+ }
+ // [END gae_java8_app_identity_other_services]
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ resp.setContentType("text/plain");
+ try {
+ resp.getWriter().println(simulateIdentityAssertion());
+ } catch (Exception e) {
+ throw new javax.servlet.ServletException(e);
+ }
+ }
+}
diff --git a/appengine-java21/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortener.java b/appengine-java21/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortener.java
new file mode 100644
index 00000000000..c6c6df50f9b
--- /dev/null
+++ b/appengine-java21/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortener.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * 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.appidentity;
+
+import com.google.appengine.api.appidentity.AppIdentityService;
+import com.google.appengine.api.appidentity.AppIdentityServiceFactory;
+import com.google.common.io.CharStreams;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+@SuppressWarnings("serial")
+class UrlShortener {
+ // [START gae_java21_app_identity_google_apis]
+
+ /**
+ * Returns a shortened URL by calling the Google URL Shortener API.
+ *
+ * Note: Error handling elided for simplicity.
+ */
+ public String createShortUrl(String longUrl) throws IOException {
+ ArrayList scopes = new ArrayList<>();
+ scopes.add("https://www.googleapis.com/auth/urlshortener");
+ final AppIdentityService appIdentity = AppIdentityServiceFactory.getAppIdentityService();
+ final AppIdentityService.GetAccessTokenResult accessToken = appIdentity.getAccessToken(scopes);
+ // The token asserts the identity reported by appIdentity.getServiceAccountName()
+ JSONObject request = new JSONObject();
+ request.put("longUrl", longUrl);
+
+ URL url = new URL("https://www.googleapis.com/urlshortener/v1/url?pp=1");
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setDoOutput(true);
+ connection.setRequestMethod("POST");
+ connection.addRequestProperty("Content-Type", "application/json");
+ connection.addRequestProperty("Authorization", "Bearer " + accessToken.getAccessToken());
+
+ OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream(), StandardCharsets.UTF_8);
+ request.write(writer);
+ writer.close();
+
+ if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
+ // Note: Should check the content-encoding.
+ // Any JSON parser can be used; this one is used for illustrative purposes.
+ JSONTokener responseTokens = new JSONTokener(connection.getInputStream());
+ JSONObject response = new JSONObject(responseTokens);
+ return (String) response.get("id");
+ } else {
+ try (InputStream s = connection.getErrorStream();
+ InputStreamReader r = new InputStreamReader(s, StandardCharsets.UTF_8)) {
+ throw new RuntimeException(
+ String.format(
+ "got error (%d) response %s from %s",
+ connection.getResponseCode(), CharStreams.toString(r), connection.toString()));
+ }
+ }
+ }
+ // [START gae_java21_app_identity_google_apis]
+}
diff --git a/appengine-java21/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortenerServlet.java b/appengine-java21/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortenerServlet.java
new file mode 100644
index 00000000000..41bf8aebf8d
--- /dev/null
+++ b/appengine-java21/appidentity/src/main/java/com/example/appengine/appidentity/UrlShortenerServlet.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * 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.appidentity;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@SuppressWarnings("serial")
+// With @WebServlet annotation the webapp/WEB-INF/web.xml is no longer required.
+@WebServlet(
+ name = "UrlShortener",
+ description = "AppIdentity: Url Shortener",
+ urlPatterns = "/appidentity/shorten"
+)
+public class UrlShortenerServlet extends HttpServlet {
+
+ private final UrlShortener shortener;
+
+ public UrlShortenerServlet() {
+ shortener = new UrlShortener();
+ }
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ PrintWriter writer = resp.getWriter();
+ writer.println("");
+ writer.println("");
+ writer.println(
+ "Asserting Identity to Google APIs - App Engine App Identity Example");
+ writer.println("");
+ }
+
+ @Override
+ public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ resp.setContentType("text/plain");
+ String longUrl = req.getParameter("longUrl");
+ if (longUrl == null) {
+ resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "missing longUrl parameter");
+ return;
+ }
+
+ String shortUrl;
+ PrintWriter writer = resp.getWriter();
+ try {
+ shortUrl = shortener.createShortUrl(longUrl);
+ } catch (Exception e) {
+ resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ writer.println("error shortening URL: " + longUrl);
+ e.printStackTrace();
+ return;
+ }
+
+ writer.print("long URL: ");
+ writer.println(longUrl);
+ writer.print("short URL: ");
+ writer.println(shortUrl);
+ }
+}
diff --git a/appengine-java21/appidentity/src/main/webapp/WEB-INF/appengine-web.xml b/appengine-java21/appidentity/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..0f80dd41fc5
--- /dev/null
+++ b/appengine-java21/appidentity/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,5 @@
+
+
+ true
+ java21
+
diff --git a/appengine-java21/appidentity/src/main/webapp/WEB-INF/web.xml b/appengine-java21/appidentity/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..21e14d226e3
--- /dev/null
+++ b/appengine-java21/appidentity/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,8 @@
+
+
+
+ appidentity/identity
+
+
diff --git a/appengine-java21/appidentity/src/test/java/com/example/appengine/appidentity/IdentityServletTest.java b/appengine-java21/appidentity/src/test/java/com/example/appengine/appidentity/IdentityServletTest.java
new file mode 100644
index 00000000000..58c57c457c4
--- /dev/null
+++ b/appengine-java21/appidentity/src/test/java/com/example/appengine/appidentity/IdentityServletTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * 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.appidentity;
+
+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.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 IdentityServlet}.
+ */
+@RunWith(JUnit4.class)
+public class IdentityServletTest {
+
+ // Set up a helper so that the ApiProxy returns a valid environment for local testing.
+ private final LocalServiceTestHelper helper = new LocalServiceTestHelper();
+
+ @Mock
+ private HttpServletRequest mockRequest;
+ @Mock
+ private HttpServletResponse mockResponse;
+ private StringWriter responseWriter;
+ private IdentityServlet servletUnderTest;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.openMocks(this);
+ helper.setUp();
+
+ // Set up a fake HTTP response.
+ responseWriter = new StringWriter();
+ when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter));
+
+ servletUnderTest = new IdentityServlet();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void doGet_defaultEnvironment_writesResponse() throws Exception {
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ // We don't have any guarantee over what the local App Engine environment returns for
+ // "com.google.appengine.runtime.default_version_hostname". Only assert that the response
+ // contains part of the string we have control over.
+ assertWithMessage("IdentityServlet response")
+ .that(responseWriter.toString())
+ .contains("default_version_hostname:");
+ }
+}
diff --git a/appengine-java21/appidentity/src/test/java/com/example/appengine/appidentity/SignForAppServletTest.java b/appengine-java21/appidentity/src/test/java/com/example/appengine/appidentity/SignForAppServletTest.java
new file mode 100644
index 00000000000..cf2488005f1
--- /dev/null
+++ b/appengine-java21/appidentity/src/test/java/com/example/appengine/appidentity/SignForAppServletTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * 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.appidentity;
+
+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.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 SignForAppServlet}.
+ */
+@RunWith(JUnit4.class)
+public class SignForAppServletTest {
+
+ private final LocalServiceTestHelper helper = new LocalServiceTestHelper();
+
+ @Mock
+ private HttpServletRequest mockRequest;
+ @Mock
+ private HttpServletResponse mockResponse;
+ private StringWriter responseWriter;
+ private SignForAppServlet servletUnderTest;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.openMocks(this);
+ helper.setUp();
+
+ // Set up a fake HTTP response.
+ responseWriter = new StringWriter();
+ when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter));
+
+ servletUnderTest = new SignForAppServlet();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void doGet_defaultEnvironment_successfullyVerifiesSignature() throws Exception {
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ assertWithMessage("SignForAppServlet response")
+ .that(responseWriter.toString())
+ .contains("isValid=true for message: abcdefg");
+ }
+}