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..d727c237364 100644
--- a/iap/src/main/java/com/example/iap/BuildIapRequest.java
+++ b/iap/src/main/java/com/example/iap/BuildIapRequest.java
@@ -11,7 +11,9 @@
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.example.iap;
+// [START generate_iap_request]
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpHeaders;
@@ -26,18 +28,18 @@
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;
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 =
@@ -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 : "
@@ -132,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 fa10e607f9d..4e97f9ec387 100644
--- a/iap/src/main/java/com/example/iap/VerifyIapRequestHeader.java
+++ b/iap/src/main/java/com/example/iap/VerifyIapRequestHeader.java
@@ -11,162 +11,121 @@
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.example.iap;
+// [START verify_iap_request]
-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;
/** 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";
+ "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;
+ private 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 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());
+ 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]
}
+// [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);
}
}