From db83df18cbbeff86b6a331d2bc6283eaed936497 Mon Sep 17 00:00:00 2001 From: arithmetic1728 <58957152+arithmetic1728@users.noreply.github.com> Date: Thu, 23 Apr 2020 13:34:10 -0700 Subject: [PATCH] [iap] use google-auth for id token samples (#3444) --- iap/make_iap_request.py | 104 +++------------------------------------- iap/requirements.txt | 3 +- iap/validate_jwt.py | 21 ++++---- 3 files changed, 19 insertions(+), 109 deletions(-) diff --git a/iap/make_iap_request.py b/iap/make_iap_request.py index d44e827ab2c3..37321334013a 100644 --- a/iap/make_iap_request.py +++ b/iap/make_iap_request.py @@ -15,19 +15,9 @@ """Example use of a service account to authenticate to Identity-Aware Proxy.""" # [START iap_make_request] -import google.auth -import google.auth.app_engine -import google.auth.compute_engine.credentials -import google.auth.iam from google.auth.transport.requests import Request -import google.oauth2.credentials -import google.oauth2.service_account +from google.oauth2 import id_token import requests -import requests_toolbelt.adapters.appengine - - -IAM_SCOPE = 'https://www.googleapis.com/auth/iam' -OAUTH_TOKEN_URI = 'https://www.googleapis.com/oauth2/v4/token' def make_iap_request(url, client_id, method='GET', **kwargs): @@ -49,56 +39,9 @@ def make_iap_request(url, client_id, method='GET', **kwargs): if 'timeout' not in kwargs: kwargs['timeout'] = 90 - # Figure out what environment we're running in and get some preliminary - # information about the service account. - bootstrap_credentials, _ = google.auth.default( - scopes=[IAM_SCOPE]) - if isinstance(bootstrap_credentials, - google.oauth2.credentials.Credentials): - raise Exception('make_iap_request is only supported for service ' - 'accounts.') - elif isinstance(bootstrap_credentials, - google.auth.app_engine.Credentials): - requests_toolbelt.adapters.appengine.monkeypatch() - - # For service account's using the Compute Engine metadata service, - # service_account_email isn't available until refresh is called. - bootstrap_credentials.refresh(Request()) - - signer_email = bootstrap_credentials.service_account_email - if isinstance(bootstrap_credentials, - google.auth.compute_engine.credentials.Credentials): - # Since the Compute Engine metadata service doesn't expose the service - # account key, we use the IAM signBlob API to sign instead. - # In order for this to work: - # - # 1. Your VM needs the https://www.googleapis.com/auth/iam scope. - # You can specify this specific scope when creating a VM - # through the API or gcloud. When using Cloud Console, - # you'll need to specify the "full access to all Cloud APIs" - # scope. A VM's scopes can only be specified at creation time. - # - # 2. The VM's default service account needs the "Service Account Actor" - # role. This can be found under the "Project" category in Cloud - # Console, or roles/iam.serviceAccountActor in gcloud. - signer = google.auth.iam.Signer( - Request(), bootstrap_credentials, signer_email) - else: - # A Signer object can sign a JWT using the service account's key. - signer = bootstrap_credentials.signer - - # Construct OAuth 2.0 service account credentials using the signer - # and email acquired from the bootstrap credentials. - service_account_credentials = google.oauth2.service_account.Credentials( - signer, signer_email, token_uri=OAUTH_TOKEN_URI, additional_claims={ - 'target_audience': client_id - }) - - # service_account_credentials gives us a JWT signed by the service - # account. Next, we use that to obtain an OpenID Connect token, - # which is a JWT signed by Google. - google_open_id_connect_token = get_google_open_id_connect_token( - service_account_credentials) + # Obtain an OpenID Connect (OIDC) token from metadata server or using service + # account. + google_open_id_connect_token = id_token.fetch_id_token(Request(), client_id) # Fetch the Identity-Aware Proxy-protected URL, including an # Authorization header containing "Bearer " followed by a @@ -108,9 +51,8 @@ def make_iap_request(url, client_id, method='GET', **kwargs): headers={'Authorization': 'Bearer {}'.format( google_open_id_connect_token)}, **kwargs) if resp.status_code == 403: - raise Exception('Service account {} does not have permission to ' - 'access the IAP-protected application.'.format( - signer_email)) + raise Exception('Service account does not have permission to ' + 'access the IAP-protected application.') elif resp.status_code != 200: raise Exception( 'Bad response from application: {!r} / {!r} / {!r}'.format( @@ -118,38 +60,4 @@ def make_iap_request(url, client_id, method='GET', **kwargs): else: return resp.text - -def get_google_open_id_connect_token(service_account_credentials): - """Get an OpenID Connect token issued by Google for the service account. - - This function: - - 1. Generates a JWT signed with the service account's private key - containing a special "target_audience" claim. - - 2. Sends it to the OAUTH_TOKEN_URI endpoint. Because the JWT in #1 - has a target_audience claim, that endpoint will respond with - an OpenID Connect token for the service account -- in other words, - a JWT signed by *Google*. The aud claim in this JWT will be - set to the value from the target_audience claim in #1. - - For more information, see - https://developers.google.com/identity/protocols/OAuth2ServiceAccount . - The HTTP/REST example on that page describes the JWT structure and - demonstrates how to call the token endpoint. (The example on that page - shows how to get an OAuth2 access token; this code is using a - modified version of it to get an OpenID Connect token.) - """ - - service_account_jwt = ( - service_account_credentials._make_authorization_grant_assertion()) - request = google.auth.transport.requests.Request() - body = { - 'assertion': service_account_jwt, - 'grant_type': google.oauth2._client._JWT_GRANT_TYPE, - } - token_response = google.oauth2._client._token_endpoint_request( - request, OAUTH_TOKEN_URI, body) - return token_response['id_token'] - # [END iap_make_request] diff --git a/iap/requirements.txt b/iap/requirements.txt index c96a8ba7f4b2..91ff2a6af457 100644 --- a/iap/requirements.txt +++ b/iap/requirements.txt @@ -1,7 +1,6 @@ -PyJWT==1.7.1 cryptography==2.9.1 flask==1.1.2 -google-auth==1.14.0 +google-auth==1.14.1 gunicorn==20.0.4 requests==2.23.0 requests_toolbelt==0.9.1 diff --git a/iap/validate_jwt.py b/iap/validate_jwt.py index a672e1d94ae4..5ba34f7589bb 100644 --- a/iap/validate_jwt.py +++ b/iap/validate_jwt.py @@ -23,7 +23,7 @@ App Engine's Users API instead. """ # [START iap_validate_jwt] -import jwt +from google.auth import jwt import requests @@ -70,18 +70,21 @@ def validate_iap_jwt_from_compute_engine(iap_jwt, cloud_project_number, def _validate_iap_jwt(iap_jwt, expected_audience): try: - key_id = jwt.get_unverified_header(iap_jwt).get('kid') + # Retrieve public key for token signature verification. + key_id = jwt.decode_header(iap_jwt).get('kid') if not key_id: return (None, None, '**ERROR: no key ID**') key = get_iap_key(key_id) - decoded_jwt = jwt.decode( - iap_jwt, key, - algorithms=['ES256'], - issuer='https://cloud.google.com/iap', - audience=expected_audience) + + # Verify token signature, expiry and audience. + decoded_jwt = jwt.decode(iap_jwt, certs=key, audience=expected_audience) + + # Verify token issuer. + if decoded_jwt.get('iss') != 'https://cloud.google.com/iap': + return (None, None, '**ERROR: invalid issuer**') + return (decoded_jwt['sub'], decoded_jwt['email'], '') - except (jwt.exceptions.InvalidTokenError, - requests.exceptions.RequestException) as e: + except (ValueError, requests.exceptions.RequestException) as e: return (None, None, '**ERROR: JWT validation error {}**'.format(e))