Skip to content

Commit

Permalink
[iap] use google-auth for id token samples (#3444)
Browse files Browse the repository at this point in the history
  • Loading branch information
arithmetic1728 authored and leahecole committed Apr 24, 2020
1 parent 8d00e5c commit db83df1
Show file tree
Hide file tree
Showing 3 changed files with 19 additions and 109 deletions.
104 changes: 6 additions & 98 deletions iap/make_iap_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -108,48 +51,13 @@ 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(
resp.status_code, resp.headers, resp.text))
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]
3 changes: 1 addition & 2 deletions iap/requirements.txt
Original file line number Diff line number Diff line change
@@ -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
21 changes: 12 additions & 9 deletions iap/validate_jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
App Engine's Users API instead.
"""
# [START iap_validate_jwt]
import jwt
from google.auth import jwt
import requests


Expand Down Expand Up @@ -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))


Expand Down

0 comments on commit db83df1

Please sign in to comment.