0

We are trying to implement server to server authentication and access an IAP resource per documentation here.

url = "https://project-name-B.appspot.com" # This is the IAP resource. This application is hosted in a different project. Lets call this as "Project B"
client_id = "client-id-number"                # This is the client id when you click on "Edit Oauth Client" in GCP console for the IAP resource.

response = authenticate_obj.make_iap_request(url, client_id)

We get the following error when we execute the above code.

Traceback (most recent call last): File "/env/lib/python3.7/site-packages/gunicorn/workers/gthread.py", line 271, in handle keepalive = self.handle_request(req, conn) File "/env/lib/python3.7/site-packages/gunicorn/workers/gthread.py", line 320, in handle_request respiter = self.wsgi(environ, resp.start_response) 
File "/env/lib/python3.7/site-packages/flask/app.py", line 2463, in __call__ return self.wsgi_app(environ, start_response) File "/env/lib/python3.7/site-packages/flask/app.py", 
line 2449, in wsgi_app response = self.handle_exception(e) 
File "/env/lib/python3.7/site-packages/flask/app.py", 
line 1866, in handle_exception reraise(exc_type, exc_value, tb) 
File "/env/lib/python3.7/site-packages/flask/_compat.py", 
line 39, in reraise raise value File "/env/lib/python3.7/site-packages/flask/app.py", 
line 2446, in wsgi_app response = self.full_dispatch_request() 
File "/env/lib/python3.7/site-packages/flask/app.py", 
line 1951, in full_dispatch_request rv = self.handle_user_exception(e) 
File "/env/lib/python3.7/site-packages/flask/app.py", line 1820, in handle_user_exception reraise(exc_type, exc_value, tb) 
File "/env/lib/python3.7/site-packages/flask/_compat.py", line 39, in reraise raise value File "/env/lib/python3.7/site-packages/flask/app.py", line 1949, in full_dispatch_request rv = self.dispatch_request() File "/env/lib/python3.7/site-packages/flask/app.py", line 1935, in dispatch_request return self.view_functions[rule.endpoint](**req.view_args) File "/srv/controllers/migratedata.py", line 84, in migrate_data response = authenticate_obj.make_iap_request(url, client_id) 
File "/srv/controllers/authentication/iap_authentication.py", line 107, in make_iap_request resp.status_code, resp.headers, resp.text)) 
Exception: Bad response from application: 502 / {'Content-Type': 'text/html; charset=UTF-8', 'Referrer-Policy': 'no-referrer', 'Content-Length': '1613', 'Date': 'Fri, 15 Nov 2019 00:44:06 GMT', 'Alt-Svc': 'quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000'} / '<!DOCTYPE html>\n<html lang=en>\n <meta charset=utf-8>\n <meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
\n <title>Error 502 (Server Error)!!1</title>\n <style>\n *{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}\n </style>\n <a href=//www.google.com/><span id=logo aria-label=Google></span></a>\n <p><b>502.</b> <ins>That’s an error.</ins>
\n <p>The server encountered a temporary error and could not complete your request.<p>Please try again in 30 seconds. <ins>That’s all we know.

The project that we are making the request from (lets call this as Project A) has a default service account project-name-A@appspot.gserviceaccount.com. We have added the "Service Account Token Creator" role to it. We are following the documentation here to create the JWT and to make the IAP request.

url = "https://project-name-B.appspot.com"
client_id = "this is the cliennt id that we got from GCP console in the IAP resource (Project name B) - Edit Oauth Client"
    authenticate_obj = authenticate_server_to_server()
    response = authenticate_obj.make_iap_request(url, client_id)        

class authenticate_server_to_server()

    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='POST', **kwargs):
    """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.
    method: The request method to use
        ('GET', 'OPTIONS', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE')
    **kwargs: Any of the parameters defined for the request function:
        https://github.com/requests/requests/blob/master/requests/api.py
        If no timeout is provided, it is set to 90 by default.

    Returns:
        The page body, or raises an exception if the page couldn't be retrieved.
    """
    # Set the default timeout, if missing
    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.

    authenticate_server_to_server_obj = authenticate_server_to_server()     
    google_open_id_connect_token = authenticate_server_to_server_obj.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.
    # url = "https://" + url
    url = "https://project-name-B.appspot.com"
    resp = requests.request(
        method, url,
        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))
    elif resp.status_code != 200:

        raise Exception(
            'Bad response from application: {!r} / {!r} / {!r}'.format(
            resp.status_code, resp.headers, resp.text))
        return resp.text
    else:
        return resp.text

We use the following function to receive the OpenID Connect token.

def get_google_open_id_connect_token(self,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 = service_account_credentials._make_authorization_grant_assertion()
    request = google.auth.transport.requests.Request()
    body = {
        'assertion': service_account,
        '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']

Please find the IAP config for Project-B here.enter image description here . The IAP secured web app user is project-A@appspot.gserviceaccount.com - the default service account of Project A.

Jack tileman
  • 813
  • 2
  • 11
  • 26
  • Could you please provide the code ( sanitized, of course ) of where are you making the authentication? How you generate the JWT, request the OIDC etc.. ? Did you created a new Service account or you are using the default one? – Andrei Tigau Nov 15 '19 at 08:37

2 Answers2

1

From the code you have provided it seems that you did not initialized the variables OAUTH_TOKEN_URI and IAM_SCOPE with the suitable paths:

IAM_SCOPE = 'https://www.googleapis.com/auth/iam'
OAUTH_TOKEN_URI = 'https://www.googleapis.com/oauth2/v4/token'

EDIT:

Could you, also confirm that you have actually enabled the IAP for the App Engine instance you are trying to access? I saw before this exact error in a situation when the IAP was not enabled.

Andrei Tigau
  • 2,010
  • 1
  • 6
  • 17
  • both the variables are in the class (I have edited my question to include them). – Jack tileman Nov 15 '19 at 15:42
  • Could you confirm that you have actually enabled the IAP for the App Engine instance you are trying to access : https://cloud.google.com/iap/docs/app-engine-quickstart#enabling_iap ? I saw before this exact error in a situation when the IAP was not enabled. – Andrei Tigau Nov 15 '19 at 15:49
  • confirmed. I have attached the IAP setting in GCP console to the question. – Jack tileman Nov 15 '19 at 16:10
  • I reproduced this scenario and faced the 500 response because the service account for my App Engine from project A, had not the IAM Role of Service Account Token Creator. Could you try to add this role to your service account and try again ? - https://cloud.google.com/iam/docs/granting-roles-to-service-accounts – Andrei Tigau Nov 18 '19 at 11:39
  • 1
    I was able to reproduce the exact stack trace as you. Everything seemed fined so I decided to create a Issue Tracker for further updates about this issue. You can check it out here : https://issuetracker.google.com/144749437 – Andrei Tigau Nov 19 '19 at 09:58
1

The service account permission should at least be IAP-secured web app user. For more specific IAM roles for IAP.

irvifa
  • 1,865
  • 2
  • 16
  • 20