diff --git a/iap/README.md b/iap/README.md index 75238df3966..06325915b13 100644 --- a/iap/README.md +++ b/iap/README.md @@ -1,5 +1,8 @@ # Cloud Identity-Aware Proxy Java Samples -Cloud Identity-Aware Proxy (Cloud IAP) lets you manage access to applications running in Compute Engine, App Engine standard environment, and Container Engine. Cloud IAP establishes a central authorization layer for applications accessed by HTTPS, enabling you to adopt an application-level access control model instead of relying on network-level firewalls. When you enable Cloud IAP, you must also use signed headers or the App Engine standard environment Users API to secure your app. +Cloud Identity-Aware Proxy (Cloud IAP) lets you manage access to applications running in Compute Engine, App Engine standard environment, and Container Engine. +Cloud IAP establishes a central authorization layer for applications accessed by HTTPS, +enabling you to adopt an application-level access control model instead of relying on network-level firewalls. + When you enable Cloud IAP, you must also use signed headers or the App Engine standard environment Users API to secure your app. ## Setup - A Google Cloud project with billing enabled @@ -29,6 +32,9 @@ It will be used to test both the authorization of an incoming request to an IAP ``` ## References -[JWT library for Java](https://github.com/auth0/java-jwt) -[Cloud IAP docs](https://cloud.google.com/iap/docs/) -[Service account credentials](https://cloud.google.com/docs/authentication#getting_credentials_for_server-centric_flow) +- [JWT library for Java (jjwt)](https://github.com/jwtk/jjwt) +- [Cloud IAP docs](https://cloud.google.com/iap/docs/) +- [Service account credentials](https://cloud.google.com/docs/authentication#getting_credentials_for_server-centric_flow) + +## Known issues +- [Auth0 JWT library](https://github.com/auth0/java-jwt) has intermittent IAP token verification issues on OpenJDK. diff --git a/iap/pom.xml b/iap/pom.xml index 4f01bf5fdde..daee8e6f3f8 100644 --- a/iap/pom.xml +++ b/iap/pom.xml @@ -52,9 +52,9 @@ 0.6.0 - com.auth0 - java-jwt - 3.2.0 + io.jsonwebtoken + jjwt + 0.7.0 diff --git a/iap/src/main/java/com/example/iap/BuildIapRequest.java b/iap/src/main/java/com/example/iap/BuildIapRequest.java index c5467f37d62..efa59ff5ecb 100644 --- a/iap/src/main/java/com/example/iap/BuildIapRequest.java +++ b/iap/src/main/java/com/example/iap/BuildIapRequest.java @@ -13,8 +13,6 @@ */ 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; @@ -28,9 +26,11 @@ import com.google.api.client.util.GenericData; import com.google.auth.oauth2.GoogleCredentials; import com.google.auth.oauth2.ServiceAccountCredentials; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + 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; @@ -71,16 +71,18 @@ private static String getSignedJWToken(ServiceAccountCredentials credentials, St 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())); + return Jwts.builder() + .setHeaderParam("kid", credentials.getPrivateKeyId()) + .setIssuer(credentials.getClientEmail()) + .setAudience(OAUTH_TOKEN_URI) + .setSubject(credentials.getClientEmail()) + .setIssuedAt(Date.from(now)) + .setExpiration(Date.from(Instant.ofEpochSecond(expirationTime))) + .claim("target_audience", baseUrl) + .signWith(SignatureAlgorithm.RS256, credentials.getPrivateKey()) + .compact(); } private static String getGoogleIdToken(String jwt) throws Exception { diff --git a/iap/src/main/java/com/example/iap/VerifyIapRequestHeader.java b/iap/src/main/java/com/example/iap/VerifyIapRequestHeader.java index 9e21fa79547..d99d7af4907 100644 --- a/iap/src/main/java/com/example/iap/VerifyIapRequestHeader.java +++ b/iap/src/main/java/com/example/iap/VerifyIapRequestHeader.java @@ -13,12 +13,6 @@ */ 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; @@ -28,13 +22,20 @@ import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.util.PemReader; import com.google.api.client.util.PemReader.Section; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.Jwt; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SigningKeyResolver; +import io.jsonwebtoken.impl.DefaultClaims; + import java.io.IOException; import java.io.StringReader; import java.net.URL; +import java.security.Key; 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; @@ -43,21 +44,32 @@ /** 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 keyCache = new HashMap<>(); + private final Map keyCache = new HashMap<>(); private final ObjectMapper mapper = new ObjectMapper(); private final TypeReference> typeRef = new TypeReference>() {}; - private ECDSAKeyProvider keyProvider = - new ECDSAKeyProvider() { + private SigningKeyResolver resolver = + new SigningKeyResolver() { + @Override + public Key resolveSigningKey(JwsHeader header, Claims claims) { + return resolveSigningKey(header); + } + @Override - public ECPublicKey getPublicKeyById(String kid) { - ECPublicKey key = keyCache.get(kid); + public Key resolveSigningKey(JwsHeader header, String payload) { + return resolveSigningKey(header); + } + + private Key resolveSigningKey(JwsHeader header) { + String keyId = header.getKeyId(); + Key key = keyCache.get(keyId); if (key != null) { return key; } @@ -72,33 +84,20 @@ public ECPublicKey getPublicKeyById(String kid) { } Map keys = mapper.readValue(response.parseAsString(), typeRef); for (Map.Entry keyData : keys.entrySet()) { - if (!keyData.getKey().equals(kid)) { + if (!keyData.getKey().equals(keyId)) { continue; } key = getKey(keyData.getValue()); if (key != null) { - keyCache.putIfAbsent(kid, key); + keyCache.putIfAbsent(keyId, key); } } } catch (IOException e) { // ignore exception } - return key; } - - @Override - public ECPrivateKey getPrivateKey() { - // ignore : only required for signing requests - return null; - } - - @Override - public String getPrivateKeyId() { - // ignore : only required for signing requests - return null; - } }; private static String getBaseUrl(URL url) throws Exception { @@ -108,7 +107,7 @@ private static String getBaseUrl(URL url) throws Exception { return (url.getProtocol() + "://" + url.getHost() + path).trim(); } - DecodedJWT verifyJWTToken(HttpRequest request) throws Exception { + Jwt verifyJWTToken(HttpRequest request) throws Exception { // Check for iap jwt header in incoming request String jwtToken = request.getHeaders().getFirstHeaderStringValue("x-goog-authenticated-user-jwt"); @@ -119,24 +118,25 @@ DecodedJWT verifyJWTToken(HttpRequest request) throws Exception { return verifyJWTToken(jwtToken, baseUrl); } - DecodedJWT verifyJWTToken(String jwtToken, String baseUrl) throws Exception { - Algorithm algorithm = Algorithm.ECDSA256(keyProvider); - - // Time constraints are automatically checked, use acceptLeeway to specify a leeway window + Jwt verifyJWTToken(String jwtToken, String baseUrl) throws Exception { + // Time constraints are automatically checked, use setAllowedClockSkewSeconds + // to specify a leeway window // The token was issued in a past date "iat" < TODAY // The token hasn't expired yet "exp" > TODAY - JWTVerifier verifier = - JWT.require(algorithm).withAudience(baseUrl).withIssuer(IAP_ISSUER_URL).build(); - - DecodedJWT decodedJWT = verifier.verify(jwtToken); - - if (decodedJWT.getSubject() == null) { - throw new JWTVerificationException("Subject expected, not found"); + Jwt jwt = + Jwts.parser() + .setSigningKeyResolver(resolver) + .requireAudience(baseUrl) + .requireIssuer(IAP_ISSUER_URL) + .parse(jwtToken); + DefaultClaims claims = (DefaultClaims) jwt.getBody(); + if (claims.getSubject() == null) { + throw new Exception("Subject expected, not found."); } - if (decodedJWT.getClaim("email") == null) { - throw new JWTVerificationException("Email expected, not found"); + if (claims.get("email") == null) { + throw new Exception("Email expected, not found."); } - return decodedJWT; + return jwt; } private ECPublicKey getKey(String keyText) throws IOException { diff --git a/iap/src/test/java/com/example/iap/BuildAndVerifyIapRequestIT.java b/iap/src/test/java/com/example/iap/BuildAndVerifyIapRequestIT.java index 3638599a5dc..89bce31fa1f 100644 --- a/iap/src/test/java/com/example/iap/BuildAndVerifyIapRequestIT.java +++ b/iap/src/test/java/com/example/iap/BuildAndVerifyIapRequestIT.java @@ -1,32 +1,29 @@ /** * 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 + *

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 + *

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 + *

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 static com.example.iap.BuildIapRequest.buildIAPRequest; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import com.auth0.jwt.interfaces.DecodedJWT; 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.HttpResponseException; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; +import io.jsonwebtoken.Jwt; import org.apache.http.HttpStatus; import org.junit.Before; import org.junit.Test; @@ -70,7 +67,7 @@ public void testGenerateAndVerifyIapRequestIsSuccessful() throws Exception { assertNotNull(split); assertEquals(split.length, 2); assertEquals(split[0].trim(), "x-goog-authenticated-user-jwt"); - DecodedJWT decodedJWT = verifyIapRequestHeader.verifyJWTToken(split[1].trim(), iapProtectedUrl); + Jwt decodedJWT = verifyIapRequestHeader.verifyJWTToken(split[1].trim(), iapProtectedUrl); assertNotNull(decodedJWT); } }