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 2 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
55 changes: 35 additions & 20 deletions iap/src/main/java/com/example/iap/BuildIapRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 Down
217 changes: 87 additions & 130 deletions iap/src/main/java/com/example/iap/VerifyIapRequestHeader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<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;
boolean verifyJwt(String jwtToken, String expectedAudience) throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't there also be an expires time that should be verified?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, added


// 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]
}
Loading