From df06156cbe605907b560c1c9bd273f0c7d0e63bb Mon Sep 17 00:00:00 2001 From: Jisha Abubaker Date: Tue, 15 Aug 2017 10:14:07 -0700 Subject: [PATCH 1/2] switching to nimbus library adding constraints for iat, alg --- iap/README.md | 2 +- iap/pom.xml | 7 +- .../java/com/example/iap/BuildIapRequest.java | 55 +++-- .../example/iap/VerifyIapRequestHeader.java | 217 +++++++----------- .../iap/BuildAndVerifyIapRequestIT.java | 23 +- 5 files changed, 138 insertions(+), 166 deletions(-) diff --git a/iap/README.md b/iap/README.md index 6407754900e..b81c3e44ec6 100644 --- a/iap/README.md +++ b/iap/README.md @@ -46,7 +46,7 @@ It will be used to test both the authorization of an incoming request to an IAP ``` ## References -- [JWT library for Java (jjwt)](https://github.com/jwtk/jjwt) +- [Nimbus JOSE jwt library](https://bitbucket.org/connect2id/nimbus-jose-jwt/wiki/Home) - [Cloud IAP docs](https://cloud.google.com/iap/docs/) - [Service account credentials](https://cloud.google.com/docs/authentication#getting_credentials_for_server-centric_flow) diff --git a/iap/pom.xml b/iap/pom.xml index 4bcaa6a6030..f46238676b6 100644 --- a/iap/pom.xml +++ b/iap/pom.xml @@ -52,7 +52,6 @@ javax.servlet-api 3.1.0 - com.google.auth @@ -60,9 +59,9 @@ 0.7.1 - io.jsonwebtoken - jjwt - 0.7.0 + com.nimbusds + nimbus-jose-jwt + 4.41.1 diff --git a/iap/src/main/java/com/example/iap/BuildIapRequest.java b/iap/src/main/java/com/example/iap/BuildIapRequest.java index 0b52f06dbc4..45f5f45a58c 100644 --- a/iap/src/main/java/com/example/iap/BuildIapRequest.java +++ b/iap/src/main/java/com/example/iap/BuildIapRequest.java @@ -11,6 +11,7 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ + package com.example.iap; import com.google.api.client.http.GenericUrl; @@ -26,11 +27,12 @@ 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 com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; import java.time.Clock; import java.time.Instant; import java.util.Collections; @@ -60,22 +62,33 @@ private static ServiceAccountCredentials getCredentials() throws Exception { return (ServiceAccountCredentials) credentials; } - private static String getSignedJWToken(ServiceAccountCredentials credentials, String iapClientId) - throws IOException { + private static String getSignedJwt(ServiceAccountCredentials credentials, String iapClientId) + throws Exception { Instant now = Instant.now(clock); long expirationTime = now.getEpochSecond() + EXPIRATION_TIME_IN_SECONDS; // generate jwt signed by service account - 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", iapClientId) - .signWith(SignatureAlgorithm.RS256, credentials.getPrivateKey()) - .compact(); + // header must contain algorithm ("alg") and key ID ("kid") + JWSHeader jwsHeader = + new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(credentials.getPrivateKeyId()).build(); + + // set required claims + JWTClaimsSet claims = + new JWTClaimsSet.Builder() + .audience(OAUTH_TOKEN_URI) + .issuer(credentials.getClientEmail()) + .subject(credentials.getClientEmail()) + .issueTime(Date.from(now)) + .expirationTime(Date.from(Instant.ofEpochSecond(expirationTime))) + .claim("target_audience", iapClientId) + .build(); + + // sign using service account private key + JWSSigner signer = new RSASSASigner(credentials.getPrivateKey()); + SignedJWT signedJwt = new SignedJWT(jwsHeader, claims); + signedJwt.sign(signer); + + return signedJwt.serialize(); } private static String getGoogleIdToken(String jwt) throws Exception { @@ -100,16 +113,18 @@ private static String getGoogleIdToken(String jwt) throws Exception { /** * Clone request and add an IAP Bearer Authorization header with signed JWT token. + * * @param request Request to add authorization header * @param iapClientId OAuth 2.0 client ID for IAP protected resource * @return Clone of request with Bearer style authorization header with signed jwt token. - * @throws Exception + * @throws Exception exception creating signed JWT */ - public static HttpRequest buildIAPRequest(HttpRequest request, String iapClientId) throws Exception { + public static HttpRequest buildIapRequest(HttpRequest request, String iapClientId) + throws Exception { // get service account credentials ServiceAccountCredentials credentials = getCredentials(); // get the base url of the request URL - String jwt = getSignedJWToken(credentials, iapClientId); + String jwt = getSignedJwt(credentials, iapClientId); if (jwt == null) { throw new Exception( "Unable to create a signed jwt token for : " diff --git a/iap/src/main/java/com/example/iap/VerifyIapRequestHeader.java b/iap/src/main/java/com/example/iap/VerifyIapRequestHeader.java index fa10e607f9d..983cf179445 100644 --- a/iap/src/main/java/com/example/iap/VerifyIapRequestHeader.java +++ b/iap/src/main/java/com/example/iap/VerifyIapRequestHeader.java @@ -11,33 +11,24 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ + package com.example.iap; -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 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.security.Key; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; +import com.google.common.base.Preconditions; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.ECDSAVerifier; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import java.net.URL; import java.security.interfaces.ECPublicKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; +import java.time.Clock; +import java.time.Instant; +import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -46,127 +37,93 @@ public class VerifyIapRequestHeader { // [START verify_iap_request] private static final String PUBLIC_KEY_VERIFICATION_URL = - "https://www.gstatic.com/iap/verify/public_key"; + "https://www.gstatic.com/iap/verify/public_key-jwk"; + private static final String IAP_ISSUER_URL = "https://cloud.google.com/iap"; - private final Map keyCache = new HashMap<>(); - private final ObjectMapper mapper = new ObjectMapper(); - private final TypeReference> typeRef = - new TypeReference>() {}; - - private SigningKeyResolver resolver = - new SigningKeyResolver() { - @Override - public Key resolveSigningKey(JwsHeader header, Claims claims) { - return resolveSigningKey(header); - } - - @Override - 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; - } - try { - HttpRequest request = - new NetHttpTransport() - .createRequestFactory() - .buildGetRequest(new GenericUrl(PUBLIC_KEY_VERIFICATION_URL)); - HttpResponse response = request.execute(); - if (response.getStatusCode() != HttpStatusCodes.STATUS_CODE_OK) { - return null; - } - Map keys = mapper.readValue(response.parseAsString(), typeRef); - for (Map.Entry keyData : keys.entrySet()) { - if (!keyData.getKey().equals(keyId)) { - continue; - } - key = getKey(keyData.getValue()); - if (key != null) { - keyCache.putIfAbsent(keyId, key); - } - } - - } catch (IOException e) { - // ignore exception - } - return key; - } - }; + // using a simple cache with no eviction for this sample + private final Map keyCache = new HashMap<>(); + + private static Clock clock = Clock.systemUTC(); + + private ECPublicKey getKey(String kid, String alg) throws Exception { + JWK jwk = keyCache.get(kid); + if (jwk == null) { + // update cache loading jwk public key data from url + JWKSet jwkSet = JWKSet.load(new URL(PUBLIC_KEY_VERIFICATION_URL)); + for (JWK key : jwkSet.getKeys()) { + keyCache.put(key.getKeyID(), key); + } + jwk = keyCache.get(kid); + } + // confirm that algorithm matches + if (jwk != null && jwk.getAlgorithm().getName().equals(alg)) { + return ECKey.parse(jwk.toJSONString()).toECPublicKey(); + } + return null; + } // Verify jwt tokens addressed to IAP protected resources on App Engine. - // The project *number* for your Google Cloud project available via 'gcloud projects describe $PROJECT_ID' - // or in the Project Info card in Cloud Console. + // The project *number* for your Google Cloud project via 'gcloud projects describe $PROJECT_ID' + // The project *number* can also be retrieved from the Project Info card in Cloud Console. // projectId is The project *ID* for your Google Cloud Project. - Jwt verifyJWTTokenForAppEngine(HttpRequest request, long projectNumber, String projectId) throws Exception { + boolean verifyJwtForAppEngine(HttpRequest request, long projectNumber, String projectId) + throws Exception { // Check for iap jwt header in incoming request - String jwtToken = - request.getHeaders().getFirstHeaderStringValue("x-goog-iap-jwt-assertion"); - if (jwtToken == null) { - return null; + String jwt = request.getHeaders().getFirstHeaderStringValue("x-goog-iap-jwt-assertion"); + if (jwt == null) { + return false; } - return verifyJWTToken(jwtToken, String.format("/projects/%s/apps/%s", - Long.toUnsignedString(projectNumber), - projectId)); + return verifyJwt( + jwt, + String.format("/projects/%s/apps/%s", Long.toUnsignedString(projectNumber), projectId)); } - Jwt verifyJWTTokenForComputeEngine(HttpRequest request, long projectNumber, long backendServiceId) throws Exception { + boolean verifyJwtForComputeEngine( + HttpRequest request, long projectNumber, long backendServiceId) throws Exception { // Check for iap jwt header in incoming request - String jwtToken = - request.getHeaders().getFirstHeaderStringValue("x-goog-iap-jwt-assertion"); + String jwtToken = request.getHeaders() + .getFirstHeaderStringValue("x-goog-iap-jwt-assertion"); if (jwtToken == null) { - return null; - } - return verifyJWTToken(jwtToken, String.format("/projects/%s/global/backendServices/%s", - Long.toUnsignedString(projectNumber), - Long.toUnsignedString(backendServiceId))); - } - - Jwt verifyJWTToken(String jwtToken, String expectedAudience) 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 - Jwt jwt = - Jwts.parser() - .setSigningKeyResolver(resolver) - .requireAudience(expectedAudience) - .requireIssuer(IAP_ISSUER_URL) - .parse(jwtToken); - DefaultClaims claims = (DefaultClaims) jwt.getBody(); - if (claims.getSubject() == null) { - throw new Exception("Subject expected, not found."); + return false; } - if (claims.get("email") == null) { - throw new Exception("Email expected, not found."); - } - return jwt; + return verifyJwt( + jwtToken, + String.format( + "/projects/%s/global/backendServices/%s", + Long.toUnsignedString(projectNumber), Long.toUnsignedString(backendServiceId))); } - private ECPublicKey getKey(String keyText) throws IOException { - StringReader reader = new StringReader(keyText); - Section section = PemReader.readFirstSectionAndClose(reader, "PUBLIC KEY"); - if (section == null) { - throw new IOException("Invalid data."); - } else { - byte[] bytes = section.getBase64DecodedBytes(); - X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes); - try { - KeyFactory kf = KeyFactory.getInstance("EC"); - PublicKey publicKey = kf.generatePublic(keySpec); - if (publicKey instanceof ECPublicKey) { - return (ECPublicKey) publicKey; - } - } catch (InvalidKeySpecException | NoSuchAlgorithmException var7) { - throw new IOException("Unexpected exception reading data", var7); - } - } - return null; + boolean verifyJwt(String jwtToken, String expectedAudience) throws Exception { + + // parse signed token into header / claims + SignedJWT signedJwt = SignedJWT.parse(jwtToken); + JWSHeader jwsHeader = signedJwt.getHeader(); + + // header must have algorithm("alg") and "kid" + Preconditions.checkNotNull(jwsHeader.getAlgorithm()); + Preconditions.checkNotNull(jwsHeader.getKeyID()); + + JWTClaimsSet claims = signedJwt.getJWTClaimsSet(); + + // claims must have audience, issuer + Preconditions.checkArgument(claims.getAudience().contains(expectedAudience)); + Preconditions.checkArgument(claims.getIssuer().equals(IAP_ISSUER_URL)); + + // claim must have issued at time and must be before current time + Date currentTime = Date.from(Instant.now(clock)); + Preconditions.checkArgument(claims.getIssueTime().before(currentTime)); + + // must have subject, email + Preconditions.checkNotNull(claims.getSubject()); + Preconditions.checkNotNull(claims.getClaim("email")); + + // verify using public key : lookup with key id, algorithm name provided + ECPublicKey publicKey = getKey(jwsHeader.getKeyID(), jwsHeader.getAlgorithm().getName()); + + Preconditions.checkNotNull(publicKey); + JWSVerifier jwsVerifier = new ECDSAVerifier(publicKey); + return signedJwt.verify(jwsVerifier); } // [END verify_iap_request] } diff --git a/iap/src/test/java/com/example/iap/BuildAndVerifyIapRequestIT.java b/iap/src/test/java/com/example/iap/BuildAndVerifyIapRequestIT.java index 5183b0053da..c511597fa84 100644 --- a/iap/src/test/java/com/example/iap/BuildAndVerifyIapRequestIT.java +++ b/iap/src/test/java/com/example/iap/BuildAndVerifyIapRequestIT.java @@ -11,11 +11,12 @@ * 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 static org.junit.Assert.assertTrue; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpHeaders; @@ -24,7 +25,6 @@ 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.Test; import org.junit.runner.RunWith; @@ -43,7 +43,6 @@ public class BuildAndVerifyIapRequestIT { private HttpTransport httpTransport = new NetHttpTransport(); private VerifyIapRequestHeader verifyIapRequestHeader = new VerifyIapRequestHeader(); - // Access an IAP protected url without signed jwt authorization header @Test public void accessIapProtectedResourceFailsWithoutJwtHeader() throws Exception { @@ -61,7 +60,7 @@ public void accessIapProtectedResourceFailsWithoutJwtHeader() throws Exception { public void testGenerateAndVerifyIapRequestIsSuccessful() throws Exception { HttpRequest request = httpTransport.createRequestFactory().buildGetRequest(new GenericUrl(IAP_PROTECTED_URL)); - HttpRequest iapRequest = buildIAPRequest(request, IAP_CLIENT_ID); + HttpRequest iapRequest = BuildIapRequest.buildIapRequest(request, IAP_CLIENT_ID); HttpResponse response = iapRequest.execute(); assertEquals(response.getStatusCode(), HttpStatus.SC_OK); String headerWithtoken = response.parseAsString(); @@ -71,12 +70,14 @@ public void testGenerateAndVerifyIapRequestIsSuccessful() throws Exception { assertEquals("x-goog-authenticated-user-jwt", split[0].trim()); String jwtToken = split[1].trim(); - HttpRequest verifyJwtRequest = httpTransport - .createRequestFactory() - .buildGetRequest(new GenericUrl(IAP_PROTECTED_URL)).setHeaders( - new HttpHeaders().set("x-goog-iap-jwt-assertion", jwtToken)); - Jwt decodedJWT = verifyIapRequestHeader.verifyJWTTokenForAppEngine( - verifyJwtRequest, IAP_PROJECT_NUMBER, IAP_PROJECT_ID); - assertNotNull(decodedJWT); + HttpRequest verifyJwtRequest = + httpTransport + .createRequestFactory() + .buildGetRequest(new GenericUrl(IAP_PROTECTED_URL)) + .setHeaders(new HttpHeaders().set("x-goog-iap-jwt-assertion", jwtToken)); + boolean verified = + verifyIapRequestHeader.verifyJwtForAppEngine( + verifyJwtRequest, IAP_PROJECT_NUMBER, IAP_PROJECT_ID); + assertTrue(verified); } } From 6e7f49e4834c0a100fa81773e512ace192401101 Mon Sep 17 00:00:00 2001 From: Jisha Abubaker Date: Tue, 15 Aug 2017 13:12:55 -0700 Subject: [PATCH 2/2] adding expiration check --- iap/src/main/java/com/example/iap/BuildIapRequest.java | 4 ++-- .../java/com/example/iap/VerifyIapRequestHeader.java | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/iap/src/main/java/com/example/iap/BuildIapRequest.java b/iap/src/main/java/com/example/iap/BuildIapRequest.java index 45f5f45a58c..d727c237364 100644 --- a/iap/src/main/java/com/example/iap/BuildIapRequest.java +++ b/iap/src/main/java/com/example/iap/BuildIapRequest.java @@ -13,6 +13,7 @@ */ package com.example.iap; +// [START generate_iap_request] import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpHeaders; @@ -39,7 +40,6 @@ 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 = @@ -147,5 +147,5 @@ public static HttpRequest buildIapRequest(HttpRequest request, String iapClientI .buildRequest(request.getRequestMethod(), request.getUrl(), request.getContent()) .setHeaders(httpHeaders); } - // [END generate_iap_request] } +// [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 index 983cf179445..4e97f9ec387 100644 --- a/iap/src/main/java/com/example/iap/VerifyIapRequestHeader.java +++ b/iap/src/main/java/com/example/iap/VerifyIapRequestHeader.java @@ -13,6 +13,7 @@ */ package com.example.iap; +// [START verify_iap_request] import com.google.api.client.http.HttpRequest; import com.google.common.base.Preconditions; @@ -35,7 +36,6 @@ /** 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-jwk"; @@ -94,7 +94,7 @@ boolean verifyJwtForComputeEngine( Long.toUnsignedString(projectNumber), Long.toUnsignedString(backendServiceId))); } - boolean verifyJwt(String jwtToken, String expectedAudience) throws Exception { + private boolean verifyJwt(String jwtToken, String expectedAudience) throws Exception { // parse signed token into header / claims SignedJWT signedJwt = SignedJWT.parse(jwtToken); @@ -110,9 +110,11 @@ boolean verifyJwt(String jwtToken, String expectedAudience) throws Exception { Preconditions.checkArgument(claims.getAudience().contains(expectedAudience)); Preconditions.checkArgument(claims.getIssuer().equals(IAP_ISSUER_URL)); - // claim must have issued at time and must be before current time + // claim must have issued at time in the past Date currentTime = Date.from(Instant.now(clock)); Preconditions.checkArgument(claims.getIssueTime().before(currentTime)); + // claim must have expiration time in the future + Preconditions.checkArgument(claims.getExpirationTime().after(currentTime)); // must have subject, email Preconditions.checkNotNull(claims.getSubject()); @@ -125,5 +127,5 @@ boolean verifyJwt(String jwtToken, String expectedAudience) throws Exception { JWSVerifier jwsVerifier = new ECDSAVerifier(publicKey); return signedJwt.verify(jwsVerifier); } - // [END verify_iap_request] } +// [END verify_iap_request]