From 9626e3ab0a51e87ea6d17570a8ec290abc87fa23 Mon Sep 17 00:00:00 2001 From: Daniel De Leo Date: Thu, 17 Aug 2017 12:13:41 -0400 Subject: [PATCH 1/6] Adding sample for making IAP requests using a service account with user managed keys --- iap/make_iap_request.py | 13 +++- iap/make_iap_request_external.py | 115 +++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 iap/make_iap_request_external.py diff --git a/iap/make_iap_request.py b/iap/make_iap_request.py index 0742874314f..524c66a987e 100644 --- a/iap/make_iap_request.py +++ b/iap/make_iap_request.py @@ -27,7 +27,6 @@ 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): @@ -118,7 +117,7 @@ def get_google_open_id_connect_token(service_account_credentials): 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 + 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 @@ -140,7 +139,15 @@ def get_google_open_id_connect_token(service_account_credentials): 'grant_type': google.oauth2._client._JWT_GRANT_TYPE, } token_response = google.oauth2._client._token_endpoint_request( - request, OAUTH_TOKEN_URI, body) + request, get_token_endpoint(), body) return token_response['id_token'] +def get_token_endpoint(): + """Makes a request to Google's openid endpoint and returns the oauth token uri + This function eliminates the need to hardcode the oauth token uri which is subject + to future changes. + """ + response = requests.get("https://accounts.google.com/.well-known/openid-configuration") + return json.loads(response.text)["token_endpoint"] + # [END iap_make_request] diff --git a/iap/make_iap_request_external.py b/iap/make_iap_request_external.py new file mode 100644 index 00000000000..374a2d0d626 --- /dev/null +++ b/iap/make_iap_request_external.py @@ -0,0 +1,115 @@ +# Copyright 2017 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import google.auth +from google.auth.transport.requests import Request +from google.oauth2.service_account import Credentials +import requests +import json + +def make_iap_request(url, client_id, json_private_key): + """Makes a request to an application protected by Identity-Aware Proxy. + + Args: + url: The Identity-Aware Proxy-protected URL to fetch. + client_id: The client ID used by Identity-Aware Proxy. + json_private_key: Path to the json private key file + + Returns: + The page body, or raises an exception if the page couldn't be retrieved. + """ + # Construct OAuth 2.0 service account credentials using the + # service account's associated json private key file + service_account_credentials = get_service_account_credentials( + client_id,json_private_key) + + # 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) + + # Fetch the Identity-Aware Proxy-protected URL, including an + # Authorization header containing "Bearer " followed by a + # Google-issued OpenID Connect token for the service account. + resp = requests.get( + url, + headers={'Authorization': 'Bearer {}'.format( + google_open_id_connect_token)}, + # Do not set verify=False in production!! + # This is only for sample using self-signed certs + verify=False) + return resp.text + +def get_service_account_credentials(iap_client_id, json_private_key): + """Create Service Account credential from downloaded JSON private key file. + + Args: + iap_client_id: The client ID used by Identity-Aware Proxy. + json_private_key: The json private key file for the service account + + Returns: + OAuth 2.0 signed JWT-based access token (JWT-bAT) + """ + # Create Service Account credential from downloaded JSON private key file. + # Note: Additional claim of 'target_audience' is required and must be + # set to the clientId of the IAP Service Account + credentials = Credentials.from_service_account_file(json_private_key + ).with_claims({'target_audience': iap_client_id}) + # Must change credential's token uri to point to Google's + # OpenId token endpoint instead of Google's authorization endpoint + credentials._token_uri = get_token_endpoint() + # Generate and return OAuth 2.0 signed JWT-based access token (JWT-bAT) + return credentials + +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 an OpenID Connect (OIDC) token for the Cloud IAP-secured client ID. + 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, get_token_endpoint(), body) + return token_response['id_token'] + +def get_token_endpoint(): + """Makes a request to Google's openid endpoint and returns the oauth token uri + This function eliminates the need to hardcode the oauth token uri which is subject + to future changes. + """ + response = requests.get("https://accounts.google.com/.well-known/openid-configuration") + return json.loads(response.text)["token_endpoint"] \ No newline at end of file From d3aae89147faf8ccf299fa5a908a4ae6746adbbe Mon Sep 17 00:00:00 2001 From: Daniel De Leo Date: Thu, 17 Aug 2017 12:16:57 -0400 Subject: [PATCH 2/6] add newline to end of file --- iap/make_iap_request_external.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/iap/make_iap_request_external.py b/iap/make_iap_request_external.py index 374a2d0d626..836bc6c0a81 100644 --- a/iap/make_iap_request_external.py +++ b/iap/make_iap_request_external.py @@ -112,4 +112,5 @@ def get_token_endpoint(): to future changes. """ response = requests.get("https://accounts.google.com/.well-known/openid-configuration") - return json.loads(response.text)["token_endpoint"] \ No newline at end of file + return json.loads(response.text)["token_endpoint"] + \ No newline at end of file From 29b0c6a00869774bf8256434087dd7e7bcf6cef9 Mon Sep 17 00:00:00 2001 From: Daniel De Leo Date: Thu, 17 Aug 2017 12:34:42 -0400 Subject: [PATCH 3/6] pylint cleanup --- iap/make_iap_request.py | 17 ++++--- iap/make_iap_request_external.py | 86 +++++++++++++++++--------------- 2 files changed, 55 insertions(+), 48 deletions(-) diff --git a/iap/make_iap_request.py b/iap/make_iap_request.py index 524c66a987e..92d84be1337 100644 --- a/iap/make_iap_request.py +++ b/iap/make_iap_request.py @@ -24,6 +24,7 @@ import google.oauth2.service_account import requests import requests_toolbelt.adapters.appengine +import json IAM_SCOPE = 'https://www.googleapis.com/auth/iam' @@ -80,7 +81,8 @@ def make_iap_request(url, client_id): # 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={ + signer, signer_email, token_uri=get_token_endpoint(), + additional_claims={ 'target_audience': client_id }) @@ -143,11 +145,12 @@ def get_google_open_id_connect_token(service_account_credentials): return token_response['id_token'] def get_token_endpoint(): - """Makes a request to Google's openid endpoint and returns the oauth token uri - This function eliminates the need to hardcode the oauth token uri which is subject - to future changes. - """ - response = requests.get("https://accounts.google.com/.well-known/openid-configuration") - return json.loads(response.text)["token_endpoint"] + """Makes a request to Google's openid endpoint and returns + the oauth token uri. This function eliminates the need to + hardcode the oauth token uri which is subject to future changes. + """ + response = requests.get( + "https://accounts.google.com/.well-known/openid-configuration") + return json.loads(response.text)["token_endpoint"] # [END iap_make_request] diff --git a/iap/make_iap_request_external.py b/iap/make_iap_request_external.py index 836bc6c0a81..69746139783 100644 --- a/iap/make_iap_request_external.py +++ b/iap/make_iap_request_external.py @@ -12,62 +12,66 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Example use of a service account with user-managed keys + to authenticate to Identity-Aware Proxy.""" + import google.auth from google.auth.transport.requests import Request from google.oauth2.service_account import Credentials import requests import json + def make_iap_request(url, client_id, json_private_key): - """Makes a request to an application protected by Identity-Aware Proxy. + """Makes a request to an application protected by Identity-Aware Proxy. - Args: - url: The Identity-Aware Proxy-protected URL to fetch. - client_id: The client ID used by Identity-Aware Proxy. - json_private_key: Path to the json private key file + Args: + url: The Identity-Aware Proxy-protected URL to fetch. + client_id: The client ID used by Identity-Aware Proxy. + json_private_key: Path to the json private key file - Returns: - The page body, or raises an exception if the page couldn't be retrieved. - """ - # Construct OAuth 2.0 service account credentials using the - # service account's associated json private key file - service_account_credentials = get_service_account_credentials( - client_id,json_private_key) - - # 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) - - # Fetch the Identity-Aware Proxy-protected URL, including an - # Authorization header containing "Bearer " followed by a - # Google-issued OpenID Connect token for the service account. - resp = requests.get( - url, - headers={'Authorization': 'Bearer {}'.format( - google_open_id_connect_token)}, - # Do not set verify=False in production!! - # This is only for sample using self-signed certs - verify=False) - return resp.text + Returns: + The page body, or raises an exception if the page couldn't be retrieved. + """ + # Construct OAuth 2.0 service account credentials using the + # service account's associated json private key file + service_account_credentials = get_service_account_credentials( + client_id, json_private_key) + + # 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) + + # Fetch the Identity-Aware Proxy-protected URL, including an + # Authorization header containing "Bearer " followed by a + # Google-issued OpenID Connect token for the service account. + resp = requests.get( + url, + headers={'Authorization': 'Bearer {}'.format( + google_open_id_connect_token)}, + # Do not set verify=False in production!! + # This is only for sample using self-signed certs + verify=False) + return resp.text def get_service_account_credentials(iap_client_id, json_private_key): """Create Service Account credential from downloaded JSON private key file. Args: iap_client_id: The client ID used by Identity-Aware Proxy. - json_private_key: The json private key file for the service account + json_private_key: The json private key file for the service account Returns: OAuth 2.0 signed JWT-based access token (JWT-bAT) """ # Create Service Account credential from downloaded JSON private key file. - # Note: Additional claim of 'target_audience' is required and must be + # Note: Additional claim of 'target_audience' is required and must be # set to the clientId of the IAP Service Account credentials = Credentials.from_service_account_file(json_private_key ).with_claims({'target_audience': iap_client_id}) - # Must change credential's token uri to point to Google's + # Must change credential's token uri to point to Google's # OpenId token endpoint instead of Google's authorization endpoint credentials._token_uri = get_token_endpoint() # Generate and return OAuth 2.0 signed JWT-based access token (JWT-bAT) @@ -96,7 +100,7 @@ def get_google_open_id_connect_token(service_account_credentials): """ service_account_jwt = ( service_account_credentials._make_authorization_grant_assertion()) - # Request an OpenID Connect (OIDC) token for the Cloud IAP-secured client ID. + # Request OpenID Connect (OIDC) token for Cloud IAP-secured client ID. request = google.auth.transport.requests.Request() body = { 'assertion': service_account_jwt, @@ -107,10 +111,10 @@ def get_google_open_id_connect_token(service_account_credentials): return token_response['id_token'] def get_token_endpoint(): - """Makes a request to Google's openid endpoint and returns the oauth token uri - This function eliminates the need to hardcode the oauth token uri which is subject - to future changes. - """ - response = requests.get("https://accounts.google.com/.well-known/openid-configuration") - return json.loads(response.text)["token_endpoint"] - \ No newline at end of file + """Makes a request to Google's openid endpoint and returns + the oauth token uri. This function eliminates the need to + hardcode the oauth token uri which is subject to future changes. + """ + response = requests.get( + "https://accounts.google.com/.well-known/openid-configuration") + return json.loads(response.text)["token_endpoint"] From 05373702a75781222e486c96aad1c31948b19501 Mon Sep 17 00:00:00 2001 From: Daniel De Leo Date: Thu, 17 Aug 2017 12:36:04 -0400 Subject: [PATCH 4/6] pylint cleanup --- iap/make_iap_request_external.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/iap/make_iap_request_external.py b/iap/make_iap_request_external.py index 69746139783..00d38255382 100644 --- a/iap/make_iap_request_external.py +++ b/iap/make_iap_request_external.py @@ -15,6 +15,7 @@ """Example use of a service account with user-managed keys to authenticate to Identity-Aware Proxy.""" +# [START iap_make_request_external] import google.auth from google.auth.transport.requests import Request from google.oauth2.service_account import Credentials @@ -118,3 +119,5 @@ def get_token_endpoint(): response = requests.get( "https://accounts.google.com/.well-known/openid-configuration") return json.loads(response.text)["token_endpoint"] + +# [END iap_make_request_external] \ No newline at end of file From 453c3f4889e0d2a8bc4f278e5762b1274e28c834 Mon Sep 17 00:00:00 2001 From: Daniel De Leo Date: Thu, 17 Aug 2017 12:37:06 -0400 Subject: [PATCH 5/6] pylint cleanup --- iap/make_iap_request_external.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iap/make_iap_request_external.py b/iap/make_iap_request_external.py index 00d38255382..58af301231c 100644 --- a/iap/make_iap_request_external.py +++ b/iap/make_iap_request_external.py @@ -120,4 +120,4 @@ def get_token_endpoint(): "https://accounts.google.com/.well-known/openid-configuration") return json.loads(response.text)["token_endpoint"] -# [END iap_make_request_external] \ No newline at end of file +# [END iap_make_request_external] From 633f8fd4c12c983bf64d6b8204af4ab5585d8cc6 Mon Sep 17 00:00:00 2001 From: Daniel De Leo Date: Thu, 17 Aug 2017 12:37:06 -0400 Subject: [PATCH 6/6] pylint cleanup --- iap/make_iap_request_external.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iap/make_iap_request_external.py b/iap/make_iap_request_external.py index 00d38255382..58af301231c 100644 --- a/iap/make_iap_request_external.py +++ b/iap/make_iap_request_external.py @@ -120,4 +120,4 @@ def get_token_endpoint(): "https://accounts.google.com/.well-known/openid-configuration") return json.loads(response.text)["token_endpoint"] -# [END iap_make_request_external] \ No newline at end of file +# [END iap_make_request_external]