diff --git a/README.md b/README.md
index 61281c1f694..f0920bdd291 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,7 @@ Technology Samples:
* [Bigquery](bigquery)
* [Datastore](datastore)
* [Endpoints](endpoints)
+* [Identity-Aware Proxy](iap)
* [Key Management Service](kms)
* [Logging](logging)
* [Monitoring](monitoring)
diff --git a/appengine/iap/README.md b/appengine/iap/README.md
new file mode 100644
index 00000000000..39ca52ea4f7
--- /dev/null
+++ b/appengine/iap/README.md
@@ -0,0 +1,37 @@
+# Cloud Identity-Aware Proxy sample for Google App Engine
+
+This sample demonstrates how to use the [Cloud Identity-Aware Proxy][iap-docs] on [Google App
+Engine][ae-docs].
+
+[iap-docs]: https://cloud.google.com/iap/docs/
+[ae-docs]: https://cloud.google.com/appengine/docs/java/
+
+## Setup
+
+Install the [Google Cloud SDK](https://cloud.google.com/sdk/) and run:
+```
+ gcloud init
+```
+If this is your first time creating an App engine application:
+```
+ gcloud app create
+```
+
+## Running locally
+
+This application depends on being enabled behind an IAP, so this program should not be run locally.
+
+## Deploying
+
+- Deploy the application to the project
+ ```
+ mvn clean appengine:deploy
+ ```
+- [Enable](https://cloud.google.com/iap/docs/app-engine-quickstart) Identity-Aware Proxy on the App Engine app.
+- Add the email account you'll be running the test as to the Identity-Aware Proxy access list for the project.
+
+## Test
+
+Once deployed, access `https://your-project-id.appspot.com` . This should now prompt you to sign in for access.
+Sign in with the email account that was added to the Identity-Aware proxy access list.
+You should now see the jwt token that was received from the IAP server.
diff --git a/appengine/iap/pom.xml b/appengine/iap/pom.xml
new file mode 100644
index 00000000000..15b6cd930d8
--- /dev/null
+++ b/appengine/iap/pom.xml
@@ -0,0 +1,57 @@
+
+
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.iap;
+
+import java.io.IOException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Identity Aware Proxy (IAP) Test application to reflect jwt token issued by IAP. IAP must be
+ * enabled on application. {@see https://cloud.google.com/iap/docs/app-engine-quickstart}
+ */
+@SuppressWarnings("serial")
+public class JwtServlet extends HttpServlet {
+
+ private static final String IAP_JWT_HEADER = "x-goog-authenticated-user-jwt";
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ resp.getWriter().print(IAP_JWT_HEADER + ":" + req.getHeader(IAP_JWT_HEADER));
+ }
+}
diff --git a/appengine/iap/src/main/webapp/WEB-INF/appengine-web.xml b/appengine/iap/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..b30cd159e63
--- /dev/null
+++ b/appengine/iap/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,16 @@
+
+
+
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.iap; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.UrlEncodedContent; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.JsonObjectParser; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.util.GenericData; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.ServiceAccountCredentials; +import java.io.IOException; +import java.net.URL; +import java.security.interfaces.RSAPrivateKey; +import java.time.Clock; +import java.time.Instant; +import java.util.Collections; +import java.util.Date; + +public class BuildIapRequest { + // [START generate_iap_request] + private static final String IAM_SCOPE = "https://www.googleapis.com/auth/iam"; + private static final String OAUTH_TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token"; + private static final String JWT_BEARER_TOKEN_GRANT_TYPE = + "urn:ietf:params:oauth:grant-type:jwt-bearer"; + private static final long EXPIRATION_TIME_IN_SECONDS = 3600L; + + private static final HttpTransport httpTransport = new NetHttpTransport(); + + private static Clock clock = Clock.systemUTC(); + + private BuildIapRequest() {} + + private static String getBaseUrl(URL url) throws Exception { + String urlFilePath = url.getFile(); + int pathDelim = urlFilePath.lastIndexOf('/'); + String path = (pathDelim > 0) ? urlFilePath.substring(0, pathDelim) : ""; + return (url.getProtocol() + "://" + url.getHost() + path).trim(); + } + + private static ServiceAccountCredentials getCredentials() throws Exception { + GoogleCredentials credentials = + GoogleCredentials.getApplicationDefault().createScoped(Collections.singleton(IAM_SCOPE)); + // service account credentials are required to sign the jwt token + if (credentials == null || !(credentials instanceof ServiceAccountCredentials)) { + throw new Exception("Google credentials : service accounts credentials expected"); + } + return (ServiceAccountCredentials) credentials; + } + + private static String getSignedJWToken(ServiceAccountCredentials credentials, String baseUrl) + throws IOException { + Instant now = Instant.now(clock); + long expirationTime = now.getEpochSecond() + EXPIRATION_TIME_IN_SECONDS; + // generate jwt signed by service account + return JWT.create() + .withKeyId(credentials.getPrivateKeyId()) + .withAudience(OAUTH_TOKEN_URI) + .withIssuer(credentials.getClientEmail()) + .withSubject(credentials.getClientEmail()) + .withIssuedAt(Date.from(now)) + .withExpiresAt(Date.from(Instant.ofEpochSecond(expirationTime))) + .withClaim("target_audience", baseUrl) + .sign(Algorithm.RSA256(null, (RSAPrivateKey) credentials.getPrivateKey())); + } + + private static String getGoogleIdToken(String jwt) throws Exception { + final GenericData tokenRequest = + new GenericData().set("grant_type", JWT_BEARER_TOKEN_GRANT_TYPE).set("assertion", jwt); + final UrlEncodedContent content = new UrlEncodedContent(tokenRequest); + + final HttpRequestFactory requestFactory = httpTransport.createRequestFactory(); + + final HttpRequest request = + requestFactory + .buildPostRequest(new GenericUrl(OAUTH_TOKEN_URI), content) + .setParser(new JsonObjectParser(JacksonFactory.getDefaultInstance())); + + HttpResponse response; + String idToken = null; + response = request.execute(); + GenericData responseData = response.parseAs(GenericData.class); + idToken = (String) responseData.get("id_token"); + return idToken; + } + + public static HttpRequest buildIAPRequest(HttpRequest request) throws Exception { + // get service account credentials + ServiceAccountCredentials credentials = getCredentials(); + // get the base url of the request URL + String baseUrl = getBaseUrl(request.getUrl().toURL()); + String jwt = getSignedJWToken(credentials, baseUrl); + if (jwt == null) { + throw new Exception( + "Unable to create a signed jwt token for : " + + baseUrl + + "with issuer : " + + credentials.getClientEmail()); + } + + String idToken = getGoogleIdToken(jwt); + if (idToken == null) { + throw new Exception("Unable to retrieve open id token"); + } + + // Create an authorization header with bearer token + HttpHeaders httpHeaders = request.getHeaders().clone().setAuthorization("Bearer " + idToken); + + // create request with jwt authorization header + return httpTransport + .createRequestFactory() + .buildRequest(request.getRequestMethod(), request.getUrl(), request.getContent()) + .setHeaders(httpHeaders); + } + // [END generate_iap_request] +} diff --git a/iap/src/main/java/com/example/iap/VerifyIapRequestHeader.java b/iap/src/main/java/com/example/iap/VerifyIapRequestHeader.java new file mode 100644 index 00000000000..9e21fa79547 --- /dev/null +++ b/iap/src/main/java/com/example/iap/VerifyIapRequestHeader.java @@ -0,0 +1,163 @@ +/** + * Copyright 2017 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.iap;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.JWTVerifier;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.exceptions.JWTVerificationException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.auth0.jwt.interfaces.ECDSAKeyProvider;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.HttpStatusCodes;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.util.PemReader;
+import com.google.api.client.util.PemReader.Section;
+import java.io.IOException;
+import java.io.StringReader;
+import java.net.URL;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Verify IAP authorization JWT token in incoming request. */
+public class VerifyIapRequestHeader {
+ // [START verify_iap_request]
+ private static final String PUBLIC_KEY_VERIFICATION_URL =
+ "https://www.gstatic.com/iap/verify/public_key";
+ private static final String IAP_ISSUER_URL = "https://cloud.google.com/iap";
+
+ private final Map