Skip to content

Commit 41c173e

Browse files
authored
Updated JWT validation to new library (GoogleCloudPlatform#4393)
* Updated JWT validation to new library * Fix changed variable name * Debug audience test value * Debug audience in test environment * More verbose assertion * Clearer assertion * Clearer assertion * Specified certs for validation
1 parent 5ffe808 commit 41c173e

5 files changed

Lines changed: 24 additions & 94 deletions

File tree

iap/README.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,6 @@ service account private key can impersonate that account!
7070

7171
## Using validate_jwt
7272

73-
`validate_jwt` is not compatible with App Engine standard environment;
74-
use App Engine's Users API instead. (See `app_engine_app` for an example
75-
of how to do this.)
76-
77-
For all other environments:
78-
7973
1. Install the libraries listed in `requirements.txt`, e.g. by running:
8074
```
8175
virtualenv/bin/pip install -r requirements.txt

iap/example_gce_backend.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def root():
3030
if jwt is None:
3131
return 'Unauthorized request.'
3232
user_id, user_email, error_str = (
33-
validate_jwt.validate_iap_jwt_from_compute_engine(
33+
validate_jwt.validate_iap_jwt(
3434
jwt, CLOUD_PROJECT_ID, BACKEND_SERVICE_ID))
3535
if error_str:
3636
return 'Error: {}'.format(error_str)

iap/iap_test.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,19 @@ def test_main(capsys):
4444
pytest.skip('Only passing on Kokoro.')
4545
# JWTs are obtained by IAP-protected applications whenever an
4646
# end-user makes a request. We've set up an app that echoes back
47-
# the JWT in order to expose it to this test. Thus, this test
47+
# the IAP JWT in order to expose it to this test. Thus, this test
4848
# exercises both make_iap_request and validate_jwt.
49-
iap_jwt = make_iap_request.make_iap_request(
49+
resp = make_iap_request.make_iap_request(
5050
'https://{}/'.format(REFLECT_SERVICE_HOSTNAME),
5151
IAP_CLIENT_ID)
52-
iap_jwt = iap_jwt.split(': ').pop()
53-
jwt_validation_result = validate_jwt.validate_iap_jwt_from_app_engine(
54-
iap_jwt, IAP_PROJECT_NUMBER, IAP_APP_ID)
52+
iap_jwt = resp.split(': ').pop()
53+
54+
# App Engine JWT audience format below
55+
expected_audience = '/projects/{}/apps/{}'.format(
56+
IAP_PROJECT_NUMBER, IAP_APP_ID)
57+
58+
jwt_validation_result = validate_jwt.validate_iap_jwt(
59+
iap_jwt, expected_audience)
5560

5661
assert jwt_validation_result[0]
5762
assert jwt_validation_result[1]

iap/make_iap_request.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,15 @@ def make_iap_request(url, client_id, method='GET', **kwargs):
4141

4242
# Obtain an OpenID Connect (OIDC) token from metadata server or using service
4343
# account.
44-
google_open_id_connect_token = id_token.fetch_id_token(Request(), client_id)
44+
open_id_connect_token = id_token.fetch_id_token(Request(), client_id)
4545

4646
# Fetch the Identity-Aware Proxy-protected URL, including an
4747
# Authorization header containing "Bearer " followed by a
4848
# Google-issued OpenID Connect token for the service account.
4949
resp = requests.request(
5050
method, url,
5151
headers={'Authorization': 'Bearer {}'.format(
52-
google_open_id_connect_token)}, **kwargs)
52+
open_id_connect_token)}, **kwargs)
5353
if resp.status_code == 403:
5454
raise Exception('Service account does not have permission to '
5555
'access the IAP-protected application.')

iap/validate_jwt.py

Lines changed: 11 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -16,102 +16,33 @@
1616
1717
This code should be used by applications in Google Compute Engine-based
1818
environments (such as Google App Engine flexible environment, Google
19-
Compute Engine, or Google Container Engine) to provide an extra layer
20-
of assurance that a request was authorized by IAP.
21-
22-
For applications running in the App Engine standard environment, use
23-
App Engine's Users API instead.
19+
Compute Engine, Google Container Engine, Google App Engine) to provide
20+
an extra layer of assurance that a request was authorized by IAP.
2421
"""
2522
# [START iap_validate_jwt]
26-
from google.auth import jwt
27-
import requests
28-
29-
30-
def validate_iap_jwt_from_app_engine(iap_jwt, cloud_project_number,
31-
cloud_project_id):
32-
"""Validate a JWT passed to your App Engine app by Identity-Aware Proxy.
33-
34-
Args:
35-
iap_jwt: The contents of the X-Goog-IAP-JWT-Assertion header.
36-
cloud_project_number: The project *number* for your Google Cloud project.
37-
This is returned by 'gcloud projects describe $PROJECT_ID', or
38-
in the Project Info card in Cloud Console.
39-
cloud_project_id: The project *ID* for your Google Cloud project.
40-
41-
Returns:
42-
(user_id, user_email, error_str).
43-
"""
44-
expected_audience = '/projects/{}/apps/{}'.format(
45-
cloud_project_number, cloud_project_id)
46-
return _validate_iap_jwt(iap_jwt, expected_audience)
23+
from google.auth.transport import requests
24+
from google.oauth2 import id_token
4725

4826

49-
def validate_iap_jwt_from_compute_engine(iap_jwt, cloud_project_number,
50-
backend_service_id):
51-
"""Validate an IAP JWT for your (Compute|Container) Engine service.
27+
def validate_iap_jwt(iap_jwt, expected_audience):
28+
"""Validate an IAP JWT.
5229
5330
Args:
5431
iap_jwt: The contents of the X-Goog-IAP-JWT-Assertion header.
55-
cloud_project_number: The project *number* for your Google Cloud project.
56-
This is returned by 'gcloud projects describe $PROJECT_ID', or
57-
in the Project Info card in Cloud Console.
58-
backend_service_id: The ID of the backend service used to access the
59-
application. See
32+
expected_audience: The Signed Header JWT audience. See
6033
https://cloud.google.com/iap/docs/signed-headers-howto
6134
for details on how to get this value.
6235
6336
Returns:
6437
(user_id, user_email, error_str).
6538
"""
66-
expected_audience = '/projects/{}/global/backendServices/{}'.format(
67-
cloud_project_number, backend_service_id)
68-
return _validate_iap_jwt(iap_jwt, expected_audience)
69-
7039

71-
def _validate_iap_jwt(iap_jwt, expected_audience):
7240
try:
73-
# Retrieve public key for token signature verification.
74-
key_id = jwt.decode_header(iap_jwt).get('kid')
75-
if not key_id:
76-
return (None, None, '**ERROR: no key ID**')
77-
key = get_iap_key(key_id)
78-
79-
# Verify token signature, expiry and audience.
80-
decoded_jwt = jwt.decode(iap_jwt, certs=key, audience=expected_audience)
81-
82-
# Verify token issuer.
83-
if decoded_jwt.get('iss') != 'https://cloud.google.com/iap':
84-
return (None, None, '**ERROR: invalid issuer**')
85-
41+
decoded_jwt = id_token.verify_token(
42+
iap_jwt, requests.Request(), audience=expected_audience,
43+
certs_url='https://www.gstatic.com/iap/verify/public_key')
8644
return (decoded_jwt['sub'], decoded_jwt['email'], '')
87-
except (ValueError, requests.exceptions.RequestException) as e:
45+
except Exception as e:
8846
return (None, None, '**ERROR: JWT validation error {}**'.format(e))
8947

90-
91-
def get_iap_key(key_id):
92-
"""Retrieves a public key from the list published by Identity-Aware Proxy,
93-
re-fetching the key file if necessary.
94-
"""
95-
key_cache = get_iap_key.key_cache
96-
key = key_cache.get(key_id)
97-
if not key:
98-
# Re-fetch the key file.
99-
resp = requests.get(
100-
'https://www.gstatic.com/iap/verify/public_key')
101-
if resp.status_code != 200:
102-
raise Exception(
103-
'Unable to fetch IAP keys: {} / {} / {}'.format(
104-
resp.status_code, resp.headers, resp.text))
105-
key_cache = resp.json()
106-
get_iap_key.key_cache = key_cache
107-
key = key_cache.get(key_id)
108-
if not key:
109-
raise Exception('Key {!r} not found'.format(key_id))
110-
return key
111-
112-
113-
# Used to cache the Identity-Aware Proxy public keys. This code only
114-
# refetches the file when a JWT is signed with a key not present in
115-
# this cache.
116-
get_iap_key.key_cache = {}
11748
# [END iap_validate_jwt]

0 commit comments

Comments
 (0)