3

I am trying to programmatically access an IAP-protected App Engine Standard app via Python from outside of the GCP environment. I have tried various methods, including the method shown in the docs here: https://cloud.google.com/iap/docs/authentication-howto#iap-make-request-python. Here is my code:

from google.auth.transport.requests import Request
from google.oauth2 import id_token
import requests

def make_iap_request(url, client_id, method='GET', **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

    # Obtain an OpenID Connect (OIDC) token from metadata server or using service
    # account.
    open_id_connect_token = id_token.fetch_id_token(Request(), client_id)
    print(f'{open_id_connect_token=}')

    # 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.request(
        method, url,
        headers={'Authorization': 'Bearer {}'.format(
            open_id_connect_token)}, **kwargs)
    print(f'{resp=}')
    if resp.status_code == 403:
        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

if __name__ == '__main__':
    res = make_iap_request(
        'https://MYAPP.ue.r.appspot.com/',
        'Client ID from IAP>App Engine app>Edit OAuth Client>Client ID'
        )
    print(res)

When I run it locally, I have the GOOGLE_APPLICATION_CREDENTIALS environment variable set to a local JSON credential file containing the keys for the service account I want to use. I have also tried running this in Cloud Functions so it would presumably use the metadata service to pick up the App Engine default service account (I think?).

In both cases, I am able to generate a token that appears valid. Using jwt.io, I see that it contains the expected data and the signature is valid. However, when I make a request to the app using the token, I always get this exception:

Bad response from application: 401 / {'X-Goog-IAP-Generated-Response': 'true', 'Date': 'Tue, 09 Feb 2021 19:25:43 GMT', 'Content-Type': 'text/html', 'Server': 'Google Frontend', 'Content-Length': '47', 'Alt-Svc': 'h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"'} / 'Invalid GCIP ID token: JWT signature is invalid'

What could I be doing wrong?

John Hanley
  • 74,467
  • 6
  • 95
  • 159
rocklobster
  • 445
  • 1
  • 4
  • 8
  • Are you creating an Access Token or Identity Token? Edit your question with the code you have written (links are not OK except as additional references). Show how you are making the request to App Engine. Tips. IAP requires an Identity Token in the HTTP header `Authorization: Bearer `. The error `JWT signature is invalid` means you that you probably having an error in how you are using the token in your API/REST call. – John Hanley Feb 09 '21 at 21:00
  • Thank you for the posting tips, @JohnHanley. I have updated my question with code and some additional details. – rocklobster Feb 09 '21 at 21:33
  • @JohnHanley To answer your initial question, I am attempting to generate an identity token, but it's not clear to me where the access token comes into play in this scenario. Am I missing a step and need to generate an access token first? – rocklobster Feb 09 '21 at 22:12
  • What is the output from this statement in your code (just the first and last 10 characters): `print(f'{open_id_connect_token=}')`? I have a feeling that you are appending the string `Client ID from IAP>App Engine app>Edit OAuth Client>Client ID` to the Identity token. If true, remove `kwargs` from `headers={'Authorization': 'Bearer {}'.format(open_id_connect_token)}, **kwargs)` – John Hanley Feb 09 '21 at 22:23
  • That line of output looks like this: open_id_connect_token='eyJhbGciOi...s-2_BI_drw' I have also added extra logging so I can see exactly what requests is sending and it seems correct to me: DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): MYAPP.ue.r.appspot.com:443 send: b'GET / HTTP/1.1\r\nHost: MYAPP.ue.r.appspot.com\r\nUser-Agent: python-requests/2.25.1\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nConnection: keep-alive\r\nAuthorization: Bearer eyJhbGciOi...s-2_BI_drw\r\n\r\n' I tried removing the **kwargs as you suggested, but that did not make a difference. – rocklobster Feb 10 '21 at 02:05
  • The idenity token looks correct. The first few bytes are the JWT header which is usually the same key/value pairs for the first part. The error messages states that the token signature is invalid. Either the identity token is being truncated or transformed somehow. I do not see an issue in your code. – John Hanley Feb 10 '21 at 02:17
  • As a debugging test, manually create an Identity Token using the CLI and hardcode that token in your code. Use this CLI command: https://cloud.google.com/sdk/gcloud/reference/auth/print-identity-token The `audience` is the URL of your app engine service. Note: this token will expire in one hour (3600 seconds) then you will need to create a new one. – John Hanley Feb 10 '21 at 02:22
  • You will need to add the identity that the CLI is using to App Engine. Use the command `gcloud auth list` to see the identity. – John Hanley Feb 10 '21 at 02:23
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/228504/discussion-between-rocklobster-and-john-hanley). – rocklobster Feb 10 '21 at 02:53
  • @rocklobster It appears that your JWT payload contains `gcip`, meaning you've configured IAP to use external identities. Is this correct? If that's the case, IAM will not work. You'll have to use Identity Platform. – Donnald Cucharo Feb 10 '21 at 07:21
  • @DonnaldCucharo - the OP tried using his Identity platform users also and the result was the exact same error. I forgot about internal/external and how that makes a difference. – John Hanley Feb 10 '21 at 07:32
  • @JohnHanley there are a few differences as [explained in this doc](https://cloud.google.com/iap/docs/signed-headers-howto#jwts_for_external_identities). – Donnald Cucharo Feb 10 '21 at 09:07
  • @DonnaldCucharo - I know the differences. However, do you have a reference for your comment. I just used a service account with Identity Platform configured with external identities. Therefore, I am not sure your comment is correct. The key is that I added Google as a provider in addition to Email/Password. – John Hanley Feb 10 '21 at 09:07
  • @JohnHanley my comment is based here https://cloud.google.com/iap/docs/enable-external-identities#switching_back_to_google_identities – Donnald Cucharo Feb 10 '21 at 09:17
  • @DonnaldCucharo - Your link does not preclude using a service account with external identities. – John Hanley Feb 10 '21 at 09:21
  • @DonnaldCucharo - It is getting late and I have spent hours on the OP's question/problem. I have code and configuration that works but I have fiddled around testing everything. Tomorrow I will start over and verify the correct steps to configure IDP and the Python code that works correctly in a precise/controlled manner. – John Hanley Feb 10 '21 at 09:23
  • 2
    Thank you both for your time and insights. Ultimately, the solution as identified by @JohnHanley was to perform a token exchange for an Identity Platform token (https://cloud.google.com/iap/docs/service-accounts-external-identities#python). – rocklobster Feb 10 '21 at 15:05

1 Answers1

6

The solution to this problem is to exchange the Google Identity Token for an Identity Platform Identity Token.

The reason for the error Invalid GCIP ID token: JWT signature is invalid is caused by using a Google Identity Token which is signed by a Google RSA private key and not by a Google Identity Platform RSA private key. I overlooked GCIP in the error message, which would have told me the solution once we validated that the token was not corrupted in use.

In the question, this line of code fetches the Google Identity Token:

open_id_connect_token = id_token.fetch_id_token(Request(), client_id)

The above line of code requires that Google Cloud Application Default Credentials are setup. Example: set GOOGLE_APPLICATION_CREDENTIALS=c:\config\service-account.json

The next step is to exchange this token for an Identity Platform token:

def exchange_google_id_token_for_gcip_id_token(google_open_id_connect_token):
    SIGN_IN_WITH_IDP_API = 'https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp'
    API_KEY = '';

    url = SIGN_IN_WITH_IDP_API + '?key=' + API_KEY;

    data={
        'requestUri': 'http://localhost',
        'returnSecureToken': True,
        'postBody':'id_token=' + google_open_id_connect_token + '&providerId=google.com'}

    try:
        resp = requests.post(url, data)

        res = resp.json()

        if 'error' in res:
            print("Error: {}".format(res['error']['message']))
            exit(1)

        # print(res)

        return res['idToken']
    except Exception as ex:
        print("Exception: {}".format(ex))
        exit(1)

The API Key can be found in the Google Cloud Console -> Identity Platform. Top right "Application Setup Details". This will show the apiKey and authDomain.

More information can be found at this link:

Exchanging a Google token for an Identity Platform token

John Hanley
  • 74,467
  • 6
  • 95
  • 159
  • Thank you for the solution! One thing I did slightly differently was to create a dedicated API Key from Cloud Console > API > Credentials > API Keys and then restrict it to only the Identity Toolkit API. – rocklobster Feb 10 '21 at 21:55
  • @rocklobster - Adding restrictions to the API key is a very good idea. – John Hanley Feb 10 '21 at 21:56