4
votes

I would like to be able to programmatically generate an id token for iap using the user default credential on a dev environment (i.e. my own laptop with google cloud sdk installed and logged in).

When following the documentation, I managed to generate an authorization token using a service account file.

When using google.auth.default on my personal computer, I can see the credentials of type google.oauth2.credentials.Credentials have a refresh_token. I wanted to use it to generate the token as it is done with curl in the documentation under Authenticating from a desktop app -> Accessing the application but I could not make it work. Does someone know if there is a way to authenticate this way ?

2
Are you able to elaborate on why it didn't work? Perhaps you saw some errors?nbryans

2 Answers

8
votes

As Matthew stated, project for Client ID used to obtain refresh token should match project for IAP Client ID. Gcloud uses Client ID and secret defined in path_to/google-cloud-sdk/lib/googlecloudsdk/api_lib/auth/util.py for default credentials (DEFAULT_CREDENTIALS_DEFAULT_CLIENT_ID and DEFAULT_CREDENTIALS_DEFAULT_CLIENT_SECRET). Because of that, you can't use refresh token from google.auth.default() without util.py change, as an attempt to obtain ID token will fail with:

{
 "error": "invalid_audience",
 "error_description": "The audience client and the client need to be in the same project."
}

Your options are:

  1. Obtain refresh token (cache it to avoid the need to do user grant every time) and ID token as per Matthew's reply / documentation.
  2. Patch Client ID and secret present in the gcloud util.py (might be changed with gcloud updates).

Sample code for both options:

import google.auth
import requests
import json

from webbrowser import open_new_tab
from time import sleep

# use gcloud app default credentials if gcloud's util.py is patched
def id_token_from_default_creds(audience): 
    cred, proj = google.auth.default()
    # data necessary for ID token
    client_id = cred.client_id
    client_secret= cred.client_secret
    refresh_token = str(cred.refresh_token)
    return id_token_from_refresh_token(client_id, client_secret, refresh_token, audience)

def id_token_from_refresh_token(client_id, client_secret, refresh_token, audience):
    oauth_token_base_URL = "https://www.googleapis.com/oauth2/v4/token"
    payload = {"client_id": client_id, "client_secret": client_secret,
                "refresh_token": refresh_token, "grant_type": "refresh_token",
                "audience": audience}
    res = requests.post(oauth_token_base_URL, data=payload)
    return (str(json.loads(res.text)[u"id_token"]))

# obtain ID token for provided Client ID: get authorization code -> exchange for refresh token -> obtain and return ID token
def id_token_from_client_id(client_id, client_secret, audience):
    auth_code = get_auth_code(client_id)
    refresh_token = get_refresh_token_from_code(auth_code, client_id, client_secret)
    return id_token_from_refresh_token(client_id, client_secret, refresh_token, audience)

def get_auth_code(client_id):
    auth_url = "https://accounts.google.com/o/oauth2/v2/auth?client_id=%s&response_type=code&scope=openid%%20email&access_type=offline&redirect_uri=urn:ietf:wg:oauth:2.0:oob"%client_id
    open_new_tab(auth_url)
    sleep(1)
    return raw_input("Authorization code: ")

def get_refresh_token_from_code(auth_code, client_id, client_secret):
    oauth_token_base_URL = 'https://www.googleapis.com/oauth2/v4/token'
    payload = {"code": auth_code, "client_id": client_id, "client_secret": client_secret,
                "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", "grant_type": "authorization_code"}
    res = requests.post(oauth_token_base_URL, data=payload)
    return (str(json.loads(res.text)[u"refresh_token"]))

print("ID token from client ID: %s" % id_token_from_client_id("<Other client ID>", "<Other client secret>", "<IAP Client ID>")) # other client ID should be from the same project as IAP Client ID 
print("ID token from \"default\" credentials: %s" % id_token_from_default_creds("<IAP Client ID>"))
2
votes

thanks for pointing this out! I'd love it if we had code samples for this, but as you discovered, at least for Python the code sample we have for service account auth doesn't work with user accounts.

I'm not familiar enough with our Python client libraries to tell you the whether they can help you with any of this, but the gist of what https://cloud.google.com/iap/docs/authentication-howto#authenticating_from_a_desktop_app is guiding you through is:

  1. Make a new client ID (in the same project as the IAP-secured app) for your client app, and get a refresh token using that client ID. You can't just use the refresh token from application default credentials, since that's going to have the wrong client ID and probably scopes. Instead, you'll need to add functionality like "gcloud auth login" to your application and persist the refresh token.

  2. Once you have a refresh token, when your client app wants to access the IAP app: POST to https://www.googleapis.com/oauth2/v4/token with the client ID and secret for your app's OAuth client, the refresh token, and the IAP client ID. This returns an OpenID Connect token that will be valid to authenticate to IAP for one hour.

Is that at least enough to get started?