Skip to content

Commit 3545e6c

Browse files
authored
IAP samples update (GoogleCloudPlatform#808)
1 parent 9f681c2 commit 3545e6c

File tree

5 files changed

+144
-170
lines changed

5 files changed

+144
-170
lines changed

iap/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ It will be used to test both the authorization of an incoming request to an IAP
4646
```
4747
4848
## References
49-
- [JWT library for Java (jjwt)](https://github.com/jwtk/jjwt)
49+
- [Nimbus JOSE jwt library](https://bitbucket.org/connect2id/nimbus-jose-jwt/wiki/Home)
5050
- [Cloud IAP docs](https://cloud.google.com/iap/docs/)
5151
- [Service account credentials](https://cloud.google.com/docs/authentication#getting_credentials_for_server-centric_flow)
5252

iap/pom.xml

+3-4
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,16 @@
5252
<artifactId>javax.servlet-api</artifactId>
5353
<version>3.1.0</version>
5454
</dependency>
55-
5655
<!-- [START dependencies] -->
5756
<dependency>
5857
<groupId>com.google.auth</groupId>
5958
<artifactId>google-auth-library-oauth2-http</artifactId>
6059
<version>0.7.1</version>
6160
</dependency>
6261
<dependency>
63-
<groupId>io.jsonwebtoken</groupId>
64-
<artifactId>jjwt</artifactId>
65-
<version>0.7.0</version>
62+
<groupId>com.nimbusds</groupId>
63+
<artifactId>nimbus-jose-jwt</artifactId>
64+
<version>4.41.1</version>
6665
</dependency>
6766
<!-- [END dependencies] -->
6867

iap/src/main/java/com/example/iap/BuildIapRequest.java

+37-22
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
* express or implied. See the License for the specific language governing permissions and
1212
* limitations under the License.
1313
*/
14+
1415
package com.example.iap;
16+
// [START generate_iap_request]
1517

1618
import com.google.api.client.http.GenericUrl;
1719
import com.google.api.client.http.HttpHeaders;
@@ -26,18 +28,18 @@
2628
import com.google.api.client.util.GenericData;
2729
import com.google.auth.oauth2.GoogleCredentials;
2830
import com.google.auth.oauth2.ServiceAccountCredentials;
29-
import io.jsonwebtoken.Jwts;
30-
import io.jsonwebtoken.SignatureAlgorithm;
31-
32-
import java.io.IOException;
33-
import java.net.URL;
31+
import com.nimbusds.jose.JWSAlgorithm;
32+
import com.nimbusds.jose.JWSHeader;
33+
import com.nimbusds.jose.JWSSigner;
34+
import com.nimbusds.jose.crypto.RSASSASigner;
35+
import com.nimbusds.jwt.JWTClaimsSet;
36+
import com.nimbusds.jwt.SignedJWT;
3437
import java.time.Clock;
3538
import java.time.Instant;
3639
import java.util.Collections;
3740
import java.util.Date;
3841

3942
public class BuildIapRequest {
40-
// [START generate_iap_request]
4143
private static final String IAM_SCOPE = "https://www.googleapis.com/auth/iam";
4244
private static final String OAUTH_TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token";
4345
private static final String JWT_BEARER_TOKEN_GRANT_TYPE =
@@ -60,22 +62,33 @@ private static ServiceAccountCredentials getCredentials() throws Exception {
6062
return (ServiceAccountCredentials) credentials;
6163
}
6264

63-
private static String getSignedJWToken(ServiceAccountCredentials credentials, String iapClientId)
64-
throws IOException {
65+
private static String getSignedJwt(ServiceAccountCredentials credentials, String iapClientId)
66+
throws Exception {
6567
Instant now = Instant.now(clock);
6668
long expirationTime = now.getEpochSecond() + EXPIRATION_TIME_IN_SECONDS;
6769

6870
// generate jwt signed by service account
69-
return Jwts.builder()
70-
.setHeaderParam("kid", credentials.getPrivateKeyId())
71-
.setIssuer(credentials.getClientEmail())
72-
.setAudience(OAUTH_TOKEN_URI)
73-
.setSubject(credentials.getClientEmail())
74-
.setIssuedAt(Date.from(now))
75-
.setExpiration(Date.from(Instant.ofEpochSecond(expirationTime)))
76-
.claim("target_audience", iapClientId)
77-
.signWith(SignatureAlgorithm.RS256, credentials.getPrivateKey())
78-
.compact();
71+
// header must contain algorithm ("alg") and key ID ("kid")
72+
JWSHeader jwsHeader =
73+
new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(credentials.getPrivateKeyId()).build();
74+
75+
// set required claims
76+
JWTClaimsSet claims =
77+
new JWTClaimsSet.Builder()
78+
.audience(OAUTH_TOKEN_URI)
79+
.issuer(credentials.getClientEmail())
80+
.subject(credentials.getClientEmail())
81+
.issueTime(Date.from(now))
82+
.expirationTime(Date.from(Instant.ofEpochSecond(expirationTime)))
83+
.claim("target_audience", iapClientId)
84+
.build();
85+
86+
// sign using service account private key
87+
JWSSigner signer = new RSASSASigner(credentials.getPrivateKey());
88+
SignedJWT signedJwt = new SignedJWT(jwsHeader, claims);
89+
signedJwt.sign(signer);
90+
91+
return signedJwt.serialize();
7992
}
8093

8194
private static String getGoogleIdToken(String jwt) throws Exception {
@@ -100,16 +113,18 @@ private static String getGoogleIdToken(String jwt) throws Exception {
100113

101114
/**
102115
* Clone request and add an IAP Bearer Authorization header with signed JWT token.
116+
*
103117
* @param request Request to add authorization header
104118
* @param iapClientId OAuth 2.0 client ID for IAP protected resource
105119
* @return Clone of request with Bearer style authorization header with signed jwt token.
106-
* @throws Exception
120+
* @throws Exception exception creating signed JWT
107121
*/
108-
public static HttpRequest buildIAPRequest(HttpRequest request, String iapClientId) throws Exception {
122+
public static HttpRequest buildIapRequest(HttpRequest request, String iapClientId)
123+
throws Exception {
109124
// get service account credentials
110125
ServiceAccountCredentials credentials = getCredentials();
111126
// get the base url of the request URL
112-
String jwt = getSignedJWToken(credentials, iapClientId);
127+
String jwt = getSignedJwt(credentials, iapClientId);
113128
if (jwt == null) {
114129
throw new Exception(
115130
"Unable to create a signed jwt token for : "
@@ -132,5 +147,5 @@ public static HttpRequest buildIAPRequest(HttpRequest request, String iapClientI
132147
.buildRequest(request.getRequestMethod(), request.getUrl(), request.getContent())
133148
.setHeaders(httpHeaders);
134149
}
135-
// [END generate_iap_request]
136150
}
151+
// [END generate_iap_request]

iap/src/main/java/com/example/iap/VerifyIapRequestHeader.java

+91-132
Original file line numberDiff line numberDiff line change
@@ -11,162 +11,121 @@
1111
* express or implied. See the License for the specific language governing permissions and
1212
* limitations under the License.
1313
*/
14+
1415
package com.example.iap;
16+
// [START verify_iap_request]
1517

16-
import com.fasterxml.jackson.core.type.TypeReference;
17-
import com.fasterxml.jackson.databind.ObjectMapper;
18-
import com.google.api.client.http.GenericUrl;
1918
import com.google.api.client.http.HttpRequest;
20-
import com.google.api.client.http.HttpResponse;
21-
import com.google.api.client.http.HttpStatusCodes;
22-
import com.google.api.client.http.javanet.NetHttpTransport;
23-
import com.google.api.client.util.PemReader;
24-
import com.google.api.client.util.PemReader.Section;
25-
import io.jsonwebtoken.Claims;
26-
import io.jsonwebtoken.JwsHeader;
27-
import io.jsonwebtoken.Jwt;
28-
import io.jsonwebtoken.Jwts;
29-
import io.jsonwebtoken.SigningKeyResolver;
30-
import io.jsonwebtoken.impl.DefaultClaims;
31-
32-
import java.io.IOException;
33-
import java.io.StringReader;
34-
import java.security.Key;
35-
import java.security.KeyFactory;
36-
import java.security.NoSuchAlgorithmException;
37-
import java.security.PublicKey;
19+
import com.google.common.base.Preconditions;
20+
import com.nimbusds.jose.JWSHeader;
21+
import com.nimbusds.jose.JWSVerifier;
22+
import com.nimbusds.jose.crypto.ECDSAVerifier;
23+
import com.nimbusds.jose.jwk.ECKey;
24+
import com.nimbusds.jose.jwk.JWK;
25+
import com.nimbusds.jose.jwk.JWKSet;
26+
import com.nimbusds.jwt.JWTClaimsSet;
27+
import com.nimbusds.jwt.SignedJWT;
28+
import java.net.URL;
3829
import java.security.interfaces.ECPublicKey;
39-
import java.security.spec.InvalidKeySpecException;
40-
import java.security.spec.X509EncodedKeySpec;
30+
import java.time.Clock;
31+
import java.time.Instant;
32+
import java.util.Date;
4133
import java.util.HashMap;
4234
import java.util.Map;
4335

4436
/** Verify IAP authorization JWT token in incoming request. */
4537
public class VerifyIapRequestHeader {
4638

47-
// [START verify_iap_request]
4839
private static final String PUBLIC_KEY_VERIFICATION_URL =
49-
"https://www.gstatic.com/iap/verify/public_key";
40+
"https://www.gstatic.com/iap/verify/public_key-jwk";
41+
5042
private static final String IAP_ISSUER_URL = "https://cloud.google.com/iap";
5143

52-
private final Map<String, Key> keyCache = new HashMap<>();
53-
private final ObjectMapper mapper = new ObjectMapper();
54-
private final TypeReference<HashMap<String, String>> typeRef =
55-
new TypeReference<HashMap<String, String>>() {};
56-
57-
private SigningKeyResolver resolver =
58-
new SigningKeyResolver() {
59-
@Override
60-
public Key resolveSigningKey(JwsHeader header, Claims claims) {
61-
return resolveSigningKey(header);
62-
}
63-
64-
@Override
65-
public Key resolveSigningKey(JwsHeader header, String payload) {
66-
return resolveSigningKey(header);
67-
}
68-
69-
private Key resolveSigningKey(JwsHeader header) {
70-
String keyId = header.getKeyId();
71-
Key key = keyCache.get(keyId);
72-
if (key != null) {
73-
return key;
74-
}
75-
try {
76-
HttpRequest request =
77-
new NetHttpTransport()
78-
.createRequestFactory()
79-
.buildGetRequest(new GenericUrl(PUBLIC_KEY_VERIFICATION_URL));
80-
HttpResponse response = request.execute();
81-
if (response.getStatusCode() != HttpStatusCodes.STATUS_CODE_OK) {
82-
return null;
83-
}
84-
Map<String, String> keys = mapper.readValue(response.parseAsString(), typeRef);
85-
for (Map.Entry<String, String> keyData : keys.entrySet()) {
86-
if (!keyData.getKey().equals(keyId)) {
87-
continue;
88-
}
89-
key = getKey(keyData.getValue());
90-
if (key != null) {
91-
keyCache.putIfAbsent(keyId, key);
92-
}
93-
}
94-
95-
} catch (IOException e) {
96-
// ignore exception
97-
}
98-
return key;
99-
}
100-
};
44+
// using a simple cache with no eviction for this sample
45+
private final Map<String, JWK> keyCache = new HashMap<>();
46+
47+
private static Clock clock = Clock.systemUTC();
48+
49+
private ECPublicKey getKey(String kid, String alg) throws Exception {
50+
JWK jwk = keyCache.get(kid);
51+
if (jwk == null) {
52+
// update cache loading jwk public key data from url
53+
JWKSet jwkSet = JWKSet.load(new URL(PUBLIC_KEY_VERIFICATION_URL));
54+
for (JWK key : jwkSet.getKeys()) {
55+
keyCache.put(key.getKeyID(), key);
56+
}
57+
jwk = keyCache.get(kid);
58+
}
59+
// confirm that algorithm matches
60+
if (jwk != null && jwk.getAlgorithm().getName().equals(alg)) {
61+
return ECKey.parse(jwk.toJSONString()).toECPublicKey();
62+
}
63+
return null;
64+
}
10165

10266
// Verify jwt tokens addressed to IAP protected resources on App Engine.
103-
// The project *number* for your Google Cloud project available via 'gcloud projects describe $PROJECT_ID'
104-
// or in the Project Info card in Cloud Console.
67+
// The project *number* for your Google Cloud project via 'gcloud projects describe $PROJECT_ID'
68+
// The project *number* can also be retrieved from the Project Info card in Cloud Console.
10569
// projectId is The project *ID* for your Google Cloud Project.
106-
Jwt verifyJWTTokenForAppEngine(HttpRequest request, long projectNumber, String projectId) throws Exception {
70+
boolean verifyJwtForAppEngine(HttpRequest request, long projectNumber, String projectId)
71+
throws Exception {
10772
// Check for iap jwt header in incoming request
108-
String jwtToken =
109-
request.getHeaders().getFirstHeaderStringValue("x-goog-iap-jwt-assertion");
110-
if (jwtToken == null) {
111-
return null;
73+
String jwt = request.getHeaders().getFirstHeaderStringValue("x-goog-iap-jwt-assertion");
74+
if (jwt == null) {
75+
return false;
11276
}
113-
return verifyJWTToken(jwtToken, String.format("/projects/%s/apps/%s",
114-
Long.toUnsignedString(projectNumber),
115-
projectId));
77+
return verifyJwt(
78+
jwt,
79+
String.format("/projects/%s/apps/%s", Long.toUnsignedString(projectNumber), projectId));
11680
}
11781

118-
Jwt verifyJWTTokenForComputeEngine(HttpRequest request, long projectNumber, long backendServiceId) throws Exception {
82+
boolean verifyJwtForComputeEngine(
83+
HttpRequest request, long projectNumber, long backendServiceId) throws Exception {
11984
// Check for iap jwt header in incoming request
120-
String jwtToken =
121-
request.getHeaders().getFirstHeaderStringValue("x-goog-iap-jwt-assertion");
85+
String jwtToken = request.getHeaders()
86+
.getFirstHeaderStringValue("x-goog-iap-jwt-assertion");
12287
if (jwtToken == null) {
123-
return null;
124-
}
125-
return verifyJWTToken(jwtToken, String.format("/projects/%s/global/backendServices/%s",
126-
Long.toUnsignedString(projectNumber),
127-
Long.toUnsignedString(backendServiceId)));
128-
}
129-
130-
Jwt verifyJWTToken(String jwtToken, String expectedAudience) throws Exception {
131-
// Time constraints are automatically checked, use setAllowedClockSkewSeconds
132-
// to specify a leeway window
133-
// The token was issued in a past date "iat" < TODAY
134-
// The token hasn't expired yet "exp" > TODAY
135-
Jwt jwt =
136-
Jwts.parser()
137-
.setSigningKeyResolver(resolver)
138-
.requireAudience(expectedAudience)
139-
.requireIssuer(IAP_ISSUER_URL)
140-
.parse(jwtToken);
141-
DefaultClaims claims = (DefaultClaims) jwt.getBody();
142-
if (claims.getSubject() == null) {
143-
throw new Exception("Subject expected, not found.");
88+
return false;
14489
}
145-
if (claims.get("email") == null) {
146-
throw new Exception("Email expected, not found.");
147-
}
148-
return jwt;
90+
return verifyJwt(
91+
jwtToken,
92+
String.format(
93+
"/projects/%s/global/backendServices/%s",
94+
Long.toUnsignedString(projectNumber), Long.toUnsignedString(backendServiceId)));
14995
}
15096

151-
private ECPublicKey getKey(String keyText) throws IOException {
152-
StringReader reader = new StringReader(keyText);
153-
Section section = PemReader.readFirstSectionAndClose(reader, "PUBLIC KEY");
154-
if (section == null) {
155-
throw new IOException("Invalid data.");
156-
} else {
157-
byte[] bytes = section.getBase64DecodedBytes();
158-
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes);
159-
try {
160-
KeyFactory kf = KeyFactory.getInstance("EC");
161-
PublicKey publicKey = kf.generatePublic(keySpec);
162-
if (publicKey instanceof ECPublicKey) {
163-
return (ECPublicKey) publicKey;
164-
}
165-
} catch (InvalidKeySpecException | NoSuchAlgorithmException var7) {
166-
throw new IOException("Unexpected exception reading data", var7);
167-
}
168-
}
169-
return null;
97+
private boolean verifyJwt(String jwtToken, String expectedAudience) throws Exception {
98+
99+
// parse signed token into header / claims
100+
SignedJWT signedJwt = SignedJWT.parse(jwtToken);
101+
JWSHeader jwsHeader = signedJwt.getHeader();
102+
103+
// header must have algorithm("alg") and "kid"
104+
Preconditions.checkNotNull(jwsHeader.getAlgorithm());
105+
Preconditions.checkNotNull(jwsHeader.getKeyID());
106+
107+
JWTClaimsSet claims = signedJwt.getJWTClaimsSet();
108+
109+
// claims must have audience, issuer
110+
Preconditions.checkArgument(claims.getAudience().contains(expectedAudience));
111+
Preconditions.checkArgument(claims.getIssuer().equals(IAP_ISSUER_URL));
112+
113+
// claim must have issued at time in the past
114+
Date currentTime = Date.from(Instant.now(clock));
115+
Preconditions.checkArgument(claims.getIssueTime().before(currentTime));
116+
// claim must have expiration time in the future
117+
Preconditions.checkArgument(claims.getExpirationTime().after(currentTime));
118+
119+
// must have subject, email
120+
Preconditions.checkNotNull(claims.getSubject());
121+
Preconditions.checkNotNull(claims.getClaim("email"));
122+
123+
// verify using public key : lookup with key id, algorithm name provided
124+
ECPublicKey publicKey = getKey(jwsHeader.getKeyID(), jwsHeader.getAlgorithm().getName());
125+
126+
Preconditions.checkNotNull(publicKey);
127+
JWSVerifier jwsVerifier = new ECDSAVerifier(publicKey);
128+
return signedJwt.verify(jwsVerifier);
170129
}
171-
// [END verify_iap_request]
172130
}
131+
// [END verify_iap_request]

0 commit comments

Comments
 (0)