Skip to content

Instantly share code, notes, and snippets.

@jay0lee
Last active August 19, 2024 19:13
Show Gist options
  • Save jay0lee/69ee1972065ab500befa39cdf6e176cd to your computer and use it in GitHub Desktop.
Save jay0lee/69ee1972065ab500befa39cdf6e176cd to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
###
### Shell script to get an access token for a user
### using Google 3-legged OAuth.
###
### Example to show your quota usage:
###
### # first run is to authorize.
### bash --credentials-file drive-quota.json \
# --scope "https://www.googleapis.com/auth/drive.metadata.readonly"
###
### # store access token in a variable
### token=$(bash 3-legged-oauth.sh \
### --credentials-file drive-quota.json \
### --scope "https://www.googleapis.com/auth/drive.metadata.readonly")
###
### # call Drive API to get quota.
### curl -H "Authorization: Bearer ${token}" \
### https://www.googleapis.com/drive/v3/about?fields=storageQuota
###
### Arguments:
###
### --credentials-file <pathtofile> - Required. file where credentials are stored.
###
### --scope <scopes> - Required on first run authorization. Scope(s) to authorize. Separate scopes by a space.
###
### --code-challenge-method - Optional. PKCE method to use. Defaults to S256. Also supports plain and none though
### they are not secure options.
###
### --cert <pathtofile> - Optional. file of client certificate for mTLS. If specified --key is required also.
###
### --key <pathtofile> - Optional. file of client key for mTLS. If specified --cert is required also.
###
### --debug - Optional. Output verbose curl messages and results of API calls for troubleshooting.
###
# create your own client id/secret
# https://developers.google.com/identity/protocols/OAuth2InstalledApp#creatingcred
client_id='978969553067-73svibj9j7d5sbq6c5587qorl7qouesh.apps.googleusercontent.com'
client_secret='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'
# Takes a JWT token and returns the decoded body.
# WARNING - DOES NOT VALIDATE AUTHENTICITY OF JWT
# for this scripts purposes this is not an issue
# since we are trusting Google server response anyway.
decode_jwt() {
echo -n "$1" |
cut -d "." -f 2 |
tr '_-' '/+' |
base64 -d 2> /dev/null
}
urldecode() {
: "${*//+/ }"
echo -e "${_//%/\\x}"
}
debug=false
code_challenge_method="S256"
force_refresh=false
ip=""
PARAMS=""
while (( "$#" )); do
case "$1" in
--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.
--scope)
if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then
scope=$2
shift 2
else
echo "Error: Argument for $1 is missing" >&2
exit 1
fi
;;
# IP method to use
--ip)
if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then
ip=$2
shift 2
else
echo "Error: Argument for $1 is missing" >&2
exit 1
fi
;;
# Code challenge method
--code-challenge-method)
if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then
code_challenge_method=$2
shift 2
else
echo "Error: Argument for $1 is missing" >&2
exit 1
fi
;;
--cert)
if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then
cert=$2
shift 2
else
echo "Error: Argument for $1 is missing" >&2
exit 1
fi
;;
--force-refresh)
# force access token refresh
force_refresh=true
shift
;;
--key)
if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then
key=$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 ! [ -x "$(command -v openssl)" ]; then
echo 'Error: openssl is not installed and is needed for hashing functions.' >&2
exit 1
fi
if ! [ -x "$(command -v base64)" ]; then
echo 'Error: base64 is not installed and is needed for base64 decoding.' >&2
exit 1
fi
if [ -z ${credentials_file+x} ]; then
echo "ERROR: you need to specify --credentials-file"
exit 1
fi
if [ ! -z ${cert+x} ] && [ -z ${key+x} ]; then
token_endpoint="https://oauth2.mtls.googleapis.com/token"
curl_mtls_opts=( --cert "$cert" )
elif [ ! -z ${key+x} ] && [ -z ${cert+x} ]; then
echo "ERROR: you must also specify --cert when specifying --key."
exit 1
elif [ ! -z ${cert+x} ] && [ ! -z ${key+x} ]; then
token_endpoint="https://oauth2.mtls.googleapis.com/token"
curl_mtls_opts=( --key "$key" --cert "$cert" )
else
curl_mtls_opts=()
token_endpoint="https://oauth2.googleapis.com/token"
fi
case $ip in
"6" | "v6" | "ipv6")
curl_ipver="--ipv6"
;;
"4" | "v4" | "ipv4")
curl_ipver="--ipv4"
;;
"")
curl_ipver=""
;;
*)
echo "ERROR: --ip should be 4 or 6, got ${ip}."
exit 3
;;
esac
if $debug; then
curl_verbosity="-v"
else
curl_verbosity="-s"
fi
if ! [ -r "$credentials_file" ]; then
if [ -z ${scope+x} ]; then
echo "ERROR: you need to specify --scope on first run"
exit 2
fi
redirect_uri="https://localhost:8888"
# Form the request URL
# https://developers.google.com/identity/protocols/OAuth2InstalledApp#step-2-send-a-request-to-googles-oauth-20-server
auth_url="https://accounts.google.com/o/oauth2/v2/auth?client_id=${client_id}&scope=${scope}&response_type=code&redirect_uri=${redirect_uri}"
# Generate the PKCE and append to auth_url
# https://developers.google.com/identity/protocols/oauth2/native-app#step1-code-verifier
case $code_challenge_method in
"plain" | "S256")
code_verifier=$(openssl rand -base64 32 | tr '/ +\n' '. ~_' | tr -d '=' )
;;&
"plain")
code_challenge="$code_verifier"
;;&
"S256")
code_challenge=$(printf "%s" "$code_verifier" | \
openssl dgst -sha256 -binary | \
openssl base64 | \
tr '+/' '-_' | \
tr -d '=')
;;&
"plain" | "S256")
auth_url+="&code_challenge_method=${code_challenge_method}&code_challenge=${code_challenge}"
;;
"none")
code_verifier=""
;;
*)
echo "ERROR: --code-challenge-method should be S256, plain or none, got ${code_challenge_method}."
exit 3
;;
esac
echo "Please go to:"
echo
echo "$auth_url"
echo
echo "WARNING: after accepting you'll get a PAGE NOT FOUND error in the"
echo "browser. Copy the URL of the page here:"
read -r auth_code
if [[ $auth_code == https* ]]; then
auth_code=$(echo "$auth_code" | grep -E -o 'code=([^&]*)')
auth_code=${auth_code: 5}
fi
auth_code=$(urldecode "$auth_code")
# exchange authorization code for access and refresh tokens
# https://developers.google.com/identity/protocols/OAuth2InstalledApp#exchange-authorization-code
json_format="{code: \$code, client_id: \$client_id, client_secret: \$client_secret, redirect_uri: \$redirect_uri, grant_type: \$grant_type"
if [ -z "$code_verifier" ]; then
verifier_arg=( )
json_format+="}"
else
verifier_arg=( "--arg" "code_verifier" "$code_verifier" )
json_format+=", code_verifier: \$code_verifier}"
fi
json_data=$(jq --null-input --compact-output \
--arg code "$auth_code" \
"${verifier_arg[@]}" \
--arg client_id "$client_id" \
--arg client_secret "$client_secret" \
--arg redirect_uri "$redirect_uri" \
--arg grant_type "authorization_code" \
"$json_format")
if $debug; then
echo "JSON POST data:"
echo
echo -e "$json_data"
echo
fi
auth_result=$(curl "$curl_verbosity" "${curl_mtls_opts[@]}" "$curl_ipver" "$token_endpoint" \
-H "Content-Type: application/json" \
-d "$json_data")
if $debug; then
echo "JSON POST response:"
echo
echo -e "$auth_result"
echo
fi
expires_in=$(jq -r ".expires_in" <<< "$auth_result")
if [ -z "$expires_in" ] || [ "$expires_in" == "null" ]; then
echo -e "ERROR: Auth code exchange failed:"
echo -e "$auth_result"
exit 3
fi
time_now=$(date +%s)
expires_at=$((time_now + expires_in - 60))
id_token=$(jq -r ".id_token" <<< "$auth_result")
if [ "$id_token" == "null" ]; then
decoded_id_token="{}"
else
decoded_id_token=$(decode_jwt "$id_token")
fi
if $debug; then
echo "Decoded ID Token:"
echo
echo -e "$decoded_id_token" | jq .
echo
fi
auth_result=$(echo "$auth_result" | jq --sort-keys \
--argjson decoded_id_token "$decoded_id_token" \
--arg expires_at "$expires_at" \
'. + {expires_at: $expires_at|tonumber, decoded_id_token: $decoded_id_token}')
echo -e "$auth_result" > "$credentials_file"
fi
expires_at="$(jq -r ".expires_at" "${credentials_file}")"
access_token="$(jq -r ".access_token" "${credentials_file}")"
# if our access token is expired, use the refresh token to get a new one
# https://developers.google.com/identity/protocols/OAuth2InstalledApp#offline
time_now=$(date +%s)
if ( [ $force_refresh = true ] || [ $time_now -gt $expires_at ] ); then
refresh_token="$(jq -r ".refresh_token" "${credentials_file}")"
json_data=$(jq --null-input --compact-output \
--arg refresh_token "$refresh_token" \
--arg client_id "$client_id" \
--arg client_secret "$client_secret" \
--arg grant_type "refresh_token" \
--arg scope "$scope" \
'{"refresh_token": $refresh_token, "client_id": $client_id, "client_secret": $client_secret, "grant_type": $grant_type, "scope": $scope}')
if $debug; then
echo "Refresh JSON POST data:"
echo
echo -e "$json_data"
echo
fi
refresh_result=$(curl "$curl_verbosity" "${curl_mtls_opts[@]}" "$curl_ipver" "$token_endpoint" \
-H "Content-Type: application/json" \
-d "$json_data")
access_token=$(jq -r ".access_token" <<< "$refresh_result")
if [ -z "$access_token" ] || [ "$access_token" == "null" ]; then
echo -e "ERROR: Refresh token failed:"
echo -e "$refresh_result"
echo
exit 3
fi
expires_in=$(jq -r ".expires_in" <<< "$refresh_result")
expires_at=$((time_now + expires_in - 60))
id_token=$(jq -r ".id_token" <<< "$refresh_result")
scope=$(jq -r ".scope" <<< "$refresh_result")
if [ "$id_token" == "null" ]; then
decoded_id_token="{}"
else
decoded_id_token=$(decode_jwt "$id_token")
fi
refreshed_creds=$(jq --arg expires_at "$expires_at" \
--arg access_token "$access_token" \
--arg id_token "$id_token" \
--argjson decoded_id_token "$decoded_id_token" \
--arg scope "$scope" \
'. + {expires_at: $expires_at|tonumber, access_token: $access_token, id_token: $id_token, decoded_id_token: $decoded_id_token, scope: $scope}' \
"$credentials_file")
echo -e "$refreshed_creds" > "$credentials_file"
fi
echo "$access_token"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment