Skip to content

Commit

Permalink
IAP samples update (#808)
Browse files Browse the repository at this point in the history
  • Loading branch information
jabubake authored Aug 15, 2017
1 parent 9f681c2 commit 3545e6c
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 170 deletions.
2 changes: 1 addition & 1 deletion iap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
7 changes: 3 additions & 4 deletions iap/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,16 @@
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>

<!-- [START dependencies] -->
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
<version>0.7.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>4.41.1</version>
</dependency>
<!-- [END dependencies] -->

Expand Down
59 changes: 37 additions & 22 deletions iap/src/main/java/com/example/iap/BuildIapRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 =
Expand All @@ -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 {
Expand All @@ -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 : "
Expand All @@ -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]
223 changes: 91 additions & 132 deletions iap/src/main/java/com/example/iap/VerifyIapRequestHeader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Key> keyCache = new HashMap<>();
private final ObjectMapper mapper = new ObjectMapper();
private final TypeReference<HashMap<String, String>> typeRef =
new TypeReference<HashMap<String, String>>() {};

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<String, String> keys = mapper.readValue(response.parseAsString(), typeRef);
for (Map.Entry<String, String> 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<String, JWK> 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]
Loading

0 comments on commit 3545e6c

Please sign in to comment.