Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IAP samples update #808

Merged
merged 4 commits into from
Aug 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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