Skip to content

Instantly share code, notes, and snippets.

@jay0lee
Last active January 29, 2025 20:45
Show Gist options
  • Save jay0lee/e35b6cd3f594ea13ff3d2a34c269e629 to your computer and use it in GitHub Desktop.
Save jay0lee/e35b6cd3f594ea13ff3d2a34c269e629 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
###
### Shell script to use Google service account and optionally
### domain-wide delegation (DwD).
###
### This script will ultimately output an access token that can be used to
### call Google APIs as the service account or the Workspace user in DwD.
###
### Example to show a Workspace user's IMAP settings:
###
### export access_token=$(./sa_and_dwd.sh \
### --credentials-file oauth2service.json \
### --scope https://www.googleapis.com/auth/gmail.settings.basic \
### --user a_user@workspace_domain.com)
### curl -vvvv -H "Authorization: Bearer ${access_token}" \
### -H "accept: application/json" \
### https://gmail.googleapis.com/gmail/v1/users/me/settings/imap
###
### Example to list GCP Projects visible to a Cloud Identity user with no parent organization or folder
###
### export access_token=$(./sa_and_dwd.sh \
### --credentials-file oauth2service.json \
### --scope https://www.googleapis.com/auth/cloud-platform \
### --user a_user@workspace_domain.com)
### curl -vvvv -H "Authorization: Bearer ${access_token}" \
### 'https://cloudresourcemanager.googleapis.com/v1/projects?filter=parent.type:""'
###
### Example to move a GCP project a user controls into a folder
### export access_token=$(./sa_and_dwd.sh \
### --credentials-file oauth2service.json \
### --scope https://www.googleapis.com/auth/cloud-platform \
### --user a_user@workspace_domain.com)
### curl -vvvv -X PUT -H "Authorization: Bearer ${access_token}" \
### -H "Content-Type: application/json" \
### --data '{"parent": {"type": "folder", "id": "69836513752"}}' \
### 'https://cloudresourcemanager.googleapis.com/v1/projects/MY-PROJECT-ID'
###
### Example to create a Drive doc as the service account user (no DwD user impersonation):
###
### create_url="https://www.googleapis.com/drive/v3/files?uploadType=resumable&supportsAllDrives=true"
### curl -vvvv -H "Authorization: Bearer $(./sa_and_dwd.sh --credentials-file oauth2service.json \
### --scope https://www.googleapis.com/auth/drive)" \
### -d '{"parents": ["0AI2c0GeS7Yg1Uk9PVA"], "name": "test doc", "mimeType": "application/vnd.google-apps.document"}' \
### -H "Content-Type: application/json" \
### $create_url
### Arguments:
###
### --credentials-file <pathtofile> - Required. A service account private key file in JSON format.
###
### --scope - Required. OAuth 2.0 scopes which service account is authorized to use.
###
### --user - Optional. When doing DwD, the Google Workspace user for whom the script requests an access token.
###
### --debug - Optional. Output verbose curl messages and results of API calls for troubleshooting.
###
token_endpoint="https://oauth2.googleapis.com/token"
PARAMS=""
while (( "$#" )); do
case "$1" in
-d|--debug)
# Shows curl headers and outputs verbose results.
DEBUG=true
shift
;;
-c|--credentials-file)
# Local credentials file to be used. Should be an OAuth service account
# file in JSON format.
if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then
credentials_file=$2
shift 2
else
echo "Error: Argument for $1 is missing" >&2
exit 1
fi
;;
# OAuth scopes to be used for Domain-Wide Delegation.
-s|--scope)
if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then
scope=$2
shift 2
else
echo "Error: Argument for $1 is missing" >&2
exit 1
fi
;;
# Email address of the Workspace user which the service account should
# impersonate via domain-wide delegation.
-u|--user)
if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then
user=$2
shift 2
else
echo "Error: Argument for $1 is missing" >&2
exit 1
fi
;;
*)
echo "Error: Unsupported argument $1" >&2
exit 1
;;
esac
done
# set positional arguments in their proper place
eval set -- "$PARAMS"
if ! [ -x "$(command -v jq)" ]; then
echo 'Error: jq is not installed and is needed for JSON parsing.' >&2
exit 1
fi
if [ -z ${credentials_file+x} ]; then
echo "ERROR: you need to specify --credentials-file"
exit 1
fi
if [[ ! -r $credentials_file ]]; then
echo "ERROR: cannot read $credentials_file"
exit 1
fi
if [ -n "${DEBUG}" ]; then
curl_verbosity="-v"
else
curl_verbosity="-s"
fi
# Create a signed JWT for the local service account.
# https://developers.google.com/identity/protocols/oauth2/service-account#authorizingrequests
iss="$(jq -r ".client_email" "${credentials_file}")"
# The only difference between standard service account auth and DwD is
# the value of the sub(ject) parameter. If the --user argument is provided we
# set sub to it and do DwD, otherwise sub is the same as iss(uer) which is the service
# account email address.
if [ -z ${user+x} ]; then
sub="${user}"
else
sub=$iss
fi
iat="$(date +%s)"
exp="$((iat + 3600))"
# Form the JWT JSON header
json_header=$(jq --null-input --compact-output \
--arg alg RS256 \
--arg typ JWT \
'{"alg": $alg, "typ": $typ}')
if [ -n "${DEBUG}" ]; then
echo "JSON JWT header is:"
echo " ${json_header}"
echo
fi
# The header should be encoded in URL-safe base64
header=$(echo -n "${json_header}" | \
openssl base64 | \
tr -d '=' | \
tr '/+' '_-' | \
tr -d '\n')
if [ -n "${DEBUG}" ]; then
echo "Encoded JWT header is:"
echo " ${header}"
echo
fi
# Form the JWT claim set
# iss is client_email value from credentials file
# scope is read from command arguments
# aud is the token endpoint static string
# iat is current unix timestamp
# exp is current unix timestamp + 3600 seconds (1 hour)
# sub is either the service account email or for DwD, the Workspace user to impersonate
json_claims=$(jq --null-input --compact-output \
--arg iss "$(jq -r '.client_email' "$credentials_file")" \
--arg scope "$scope" \
--arg aud "$token_endpoint" \
--arg iat "$(date +%s)" \
--arg exp "$(($(date +%s) + 3600))" \
--arg sub "$user" \
'{"iss": $iss, "scope": $scope, "aud": $aud, "iat": $iat|tonumber, "exp": $exp|tonumber, "sub": $sub}')
if [ -n "${DEBUG}" ]; then
echo "JWT claim set is:"
echo " ${json_claims}"
echo
fi
claims=$(echo -n "${json_claims}" | \
openssl base64 | \
tr -d '=' | \
tr '/+' '_-' | \
tr -d '\n')
if [ -n "${DEBUG}" ]; then
echo "Encoded JWT claims is:"
echo " ${claims}"
echo
fi
unsigned_jwt="${header}.${claims}"
if [ -n "${DEBUG}" ]; then
echo "Unsigned JWT is:"
echo " ${unsigned_jwt}"
echo
fi
private_key="$(jq -r ".private_key" "${credentials_file}")"
if [ -n "${DEBUG}" ]; then
echo "Private key is:"
echo " ${private_key}"
echo
fi
signature=$(openssl dgst -sha256 \
-sign <(echo -ne "${private_key}") \
<(echo -n "${unsigned_jwt}") | \
openssl base64 | \
tr -d '=' | \
tr '/+' '_-' | \
tr -d '\n' )
if [ -n "${DEBUG}" ]; then
echo "Signature is:"
echo " ${signature}"
echo
fi
assertion="${unsigned_jwt}.${signature}"
if [ -n "${DEBUG}" ]; then
echo
echo -e "Assertion is ${assertion}"
echo
fi
# Make the access token request. Note we POST JSON data here which works fine.
json_data=$(jq --null-input --compact-output \
--arg grant_type "urn:ietf:params:oauth:grant-type:jwt-bearer" \
--arg assertion "$assertion" \
'{"grant_type": $grant_type, "assertion": $assertion}')
token_result=$(curl $curl_verbosity \
-H "Content-Type: application/json" \
-d "$json_data" \
"$token_endpoint")
access_token=$(jq -r ".access_token" <<< "${token_result}")
if [ -n "$DEBUG" ]; then
echo "Response was: ${token_result}"
echo
echo -e "Access token is ${access_token}"
echo
else
echo "$access_token"
fi
if [ -n "$DEBUG" ]; then
tokeninfo_result=$(curl $curl_verbosity "https://oauth2.googleapis.com/tokeninfo?access_token=${access_token}")
echo -e "$tokeninfo_result"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment