Access Google Cloud IAP-Protected resources via CLI
I’ve always known network security and identity management to be separate domains. Like those awkward cousins at a family reunion — they knew each other existed, but rarely spoke.
Until I discovered Google Cloud’s Identity Aware Proxy or IAP.
IAP provides an identity-based firewall to protect applications. It’s like giving your firewall a superpower — the ability to recognize who’s knocking before opening the door!
Setting up IAP for web clients (access via the browser) is a breeze. But if you have APIs that users need to access via the command line interface (CLI), the setup can be tricky. This blog simplifies the steps to do that.
Creating the API
First, let’s deploy a simple API on AppEngine that our users need to access.
Create the following files in a directory using your favorite editor.
mkdir iap-test && cd iap-test
requirements.txt
Flask==3.0.0
main.py
import flask
import json
app = flask.Flask(__name__)
@app.get("/")
def hello():
return {
'statusCode': 200,
'body': json.dumps('Function Succeeded!')
}
if __name__ == "__main__":
app.run(host="localhost", port=8080, debug=True)
app.yaml
runtime: python312
service: iap-test
Let’s deploy this to AppEngine via the gcloud command.
gcloud app deploy
On successful deployment, my test app is live at https://iap-test-dot-pensande.el.r.appspot.com/
Secure the API using IAP
Go to the IAP console and enable IAP on the AppEngine app. Details here.
Now grant the IAP-secured Web App User
role to your user account.
Finally, go to the Credentials console and create an OAuth 2.0 Client of type Desktop app and give it a name. Once created, download the OAuth Client credentials and store it locally as a client_secret.json
file.
Create the Local CLI Application
Now create two files requirements.txt
and secure-desktop-cli.py
using your favorite editor.
requirements.txt
google-auth-oauthlib
keyrings.cryptfile
keyring
secure-desktop-cli.py
from google_auth_oauthlib.flow import InstalledAppFlow
import requests
import keyring
import getpass
SCOPES = [
'openid',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/cloud-platform'
]
service_name = 'iap-desktop-cli'
user_name = getpass.getuser()
url = input("Enter your api: ")
def fetch_token():
# use the client secret to fetch user creds
flow = InstalledAppFlow.from_client_secrets_file('client_secret.json', SCOPES)
creds = flow.run_local_server(host='localhost', port=4545)
token = creds.id_token
keyring.set_password(service_name, username=user_name, password=token)
return token
def fetch_response(token):
# use the user creds to fetch the api response
headers = {
"Authorization": "Bearer %s" %token,
}
response = requests.get(url,headers=headers)
if response.status_code == 200:
print(response.json())
elif response.status_code == 401:
token = fetch_token()
fetch_response(token)
else:
print(f"Error code: ${response.status_code}")
try:
token = keyring.get_password(service_name, user_name)
if token is None:
token = fetch_token()
fetch_response(token)
except Exception as e:
print(e)
Notice that we are using a non-standard port (4545
) on the localhost
to listen for the authorization code returned by the authorization server.
Notice also how we are storing the credentials token in a local vault enabled by keyrings.cryptfile — an encrypted text file storage. For production usage, you should replace it with a keyring backend that uses an OS-native vault such as the macOS Keychain or a Linux or Windows equivalent.
Test and Verify
Let’s set up a python virtual environment to install dependencies and run the code.
sudo apt install python3.11-venv
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python3 secure-desktop-cli.py
If everything is set up correctly, you’ll be prompted for the app-engine URL and your vault password. If you are doing this for the first time, a browser window pops up where you are prompted to authenticate (if you are not already logged in with an active session) and then grant consent to the App.
Finally, the JSON output is printed on your terminal. \o/
Securing the Client Credentials
A security nerd would be skeptic about the above implementation as installed apps (e.g. mobile, desktop and single-page apps) cannot securely store client credentials. This is where the Proof Key for Code Exchange (PKCE) protocol comes in to enhance the security of the installed app flow.
Simply by enabling the autogenerate_code_verifier
flag, the google-auth SDK generates a random, high-entropy string called code_verifier
for every authorization request. Its transformed value, called code_challenge
, is sent to the authorization server to obtain the auth_code
.
The SDK then sends the auth_code
, the code_verifier
, and client credentials to the token endpoint which verifies the code_verifier
against the previously submitted code_challenge
before issuing a token.
Even if a malicious actor were to steal your client credentials and intercept an
auth_code
, it wouldn’t be able to exchange it for a token, as they would not have thecode_verifier
.
So we modify our InstalledAppFlow call as follows.
flow = InstalledAppFlow.from_client_secrets_file(
'client_secret.json',
scopes=SCOPES,
autogenerate_code_verifier=True
)
When we re-run our CLI app, notice how the authorization URL below includes the code_challenge
and code_challenge_method
query parameters.