How to authenticate programmatically to a Cloud Identity-Aware Proxy (Cloud IAP)-secured resource using user default credentials?
Asked Answered
M

2

7

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 ?

Memorial answered 2/3, 2018 at 14:48 Comment(1)
Are you able to elaborate on why it didn't work? Perhaps you saw some errors?Leukemia
I
9

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>"))
Isochroous answered 6/3, 2018 at 10:51 Comment(2)
Thanks from the code it loads the credentials from ~/.config/gcloud/application_default_credentials.json and you can use the GOOGLE_APPLICATION_CREDENTIALS in your environment to specify a new file. I added the other client credential but I'm having a 401 with the unauthorized client message. I am currently looking into it.Memorial
Looking at the library, GCloud credentials are of "authorized_user" type, so you can indeed potentially try to set GOOGLE_APPLICATION_CREDENTIALS environment variable to a path of a json file that contains "refresh_token": "your_refresh_token", "client_id": "your_client_id", "client_secret": "your_client_secret" and "type": "authorized_user". I don't understand why do you want to do it though, as you first need to obtain refresh token to be able to populate the file, so you would essentially just use google.auth library to read from this json.Isochroous
S
3

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?

Shurwood answered 6/3, 2018 at 0:50 Comment(4)
Thanks a lot ! I am not sure I understand why the client id is needed for users and not a user since they both have the IAP-Secured Web App User role ?Memorial
One way to look at it is the client app's client ID is telling the authentication system what app is authenticating the user, and the IAP app's client ID is indicating which app the user wants to access. The client app client ID is needed in the "user auth" case but not the "service auth" case because in the service account flow, the app uses a non-OAuth-based mechanism (such as possession of a private key, or GCP platform magic) to prove its authority to act as the service account.Shurwood
Ok I understand it better, it would be great if there was a way to use the "GCP platform magic" so that gcloud command authenticated user with access to IAP could connect in a programmatic way without having to log again.Memorial
I get the general idea about needing another client id. What I don't understand is the following: to what extent do we need to protect the id and secret of this secondary client? Should this be considered as sensitive data? It seems unlikely as otherwise the data could never be packaged into a desktop style client. Would it be safe to hard code these credentials and commit to version control? @Matthew, what are your thoughts on this?Codie

© 2022 - 2024 — McMap. All rights reserved.