Skip to content

Instantly share code, notes, and snippets.

@mlgrm
Last active February 19, 2019 14:58
Show Gist options
  • Save mlgrm/5ecb9012a8d54590bbedbfcb9b03acdb to your computer and use it in GitHub Desktop.
Save mlgrm/5ecb9012a8d54590bbedbfcb9b03acdb to your computer and use it in GitHub Desktop.
make calls to the google api using oauth2
#!/bin/bash
# google-auth [options] [[http[s]://]api-server]endpoint [key=value ...]
#
# ###########
# # options #
# ###########
#
# all options can be set as environment variables, i.e.:
# > google-api --option-name "option-value" ...
# is equivalent to
# > OPTION_NAME="option-value" google-auth ...
#
#
# --api-server # server to run queries against
#
# the following two can be found in a google credentials json file or copied
# and pasted from the credentials page. see below.
# -i --client-id # client id from google credentials
# -s --client-secred # client secret from google credentials
#
# the following three can also be found in a credentials file, but the
# default values should work:
# --auth-uri # uri to request a google authorization from
# --token-uri # uri from which to request a token once you have an
# # authorization code
# --redirect-uri # uri where the auth server will send the auth code
# # by default it will display the code and you will
# # have to copy and paste it into the terminal
# -c --credentials # the location of a file in json format containing
# # (and replacing) the previous five parameters
# -t --token # the location of a valid token file obviating the
# # need for credentials. the token will be
# # automatically refreshed on first use
# --scope # the url or endpoint of a scope to request. the
# # default is
# # full privileges on the user's google drive:
# # https://www.googleapis.com/auth/drive
# # to get multiple scopes, use this flag repeatedly
# # if a scope is specified, authentication is required
# -l --list # some api calls may return partial lists
# # give the name of the list here (e.g. "files"),
# # and we'll iterate and concatenate until the list
# # is complete
#
# ###############
# # credentials #
# ###############
#
# to do anything with this script you need a credentials file from google
# go to https://console.cloud.google.com/apis/credentials
# click create credentials -> OAuth client ID
# choose Application type: Other
# then click on your credentials and click "download json"
# if you save the file as ~/.config/google-api/credentials.json, the script
# will find it automagically, otherwise, set -c <file-location>
#
# to make a call to the api, run
# google-api [[http[s]://]api-server]api-endpoint [key=value ...]
#
# results are written to stdout in json format
#
# example:
# google-api "https://www.googleapis.com/drive/v3/files" \
# "corpora=user,allTeamDrives" \
# "corpus=DEFAULT" \
# "pageSize=100" \
# "q=owner:mlgrm"
#
# see: https://developers.google.com/drive/api/v3/reference/files/list
#############
# functions #
#############
# encode_url [[https://]api-server]endpoint [key=value ...]
# make a url where the first argument is the domain and endpoint,
# and each additional argument is a parameter of the form
# key=value
encode_url () {
url="$1"
if [[ $# -gt 1 ]]; then url="$url?"; fi
# raw endpoint
if grep -qE '^\/' <<< $url; then url="$API_SERVER$url"; fi
# missing protocol
if ! grep -qE '^http[s]?:\/\/' <<< $url; then url="https://$url"; fi
shift
while [[ $# -gt 1 ]]; do
url="$url$1&"
shift
done
url="$url$1"
echo $url
}
# encode_post [key=value ...]
# make a data string for use by curl -d from the arguments of the form
# key=value
encode_post () {
data="$1"
shift
while [[ $# -gt 0 ]]; do
data="$data&$1"
shift
done
echo $data
}
encode_json () {
jq
}
# oauth
# use oauth 2 interactively to obtain and an api token
# no arguments
oauth () {
consent_url=$(
encode_url \
$AUTH_URI \
"client_id=$CLIENT_ID" \
"redirect_uri=$REDIRECT_URI" \
"response_type=code" \
"scope=$scope_str"
)
if [[ -z $(which xdg-open) ]]; then
(warn "copy and paste the following url into your browser")
(warn $consent_url)
else
xdg-open "$consent_url" > /dev/null
fi
echo "consent to drive access and copy code." >&2
read -p "code: " code
token=$(
curl -s $TOKEN_URI \
-d $(encode_post \
code=$code \
client_id=$CLIENT_ID \
client_secret=$CLIENT_SECRET \
redirect_uri=$REDIRECT_URI \
grant_type=authorization_code
)
)
[[ -z $(jq 'select(.access_token != null)' <<< $token) ||
-z $(jq 'select(.access_token != null)' <<< $token) ]] &&
die 1 "invalid or emptry token"
# offer to cache the token for the future
default_file="$HOME/.config/google-api/token.json"
read -p "write token to file [Yn]?" to_file
if [[ -z $to_file ]] || grep -i "^y" <<< $to_file; then
read -p "file location [$default_file]:" TOKEN_FILE
[[ -z $TOKEN_FILE ]] && TOKEN_FILE=$default_file
mkdir -p $(dirname $TOKEN_FILE)
echo $token > $TOKEN_FILE
fi
token_time=$(date +%s)
}
# oauth_refresh
# get a new oauth token from the google api using the refresh token in
# the $token environment variable
oauth_refresh () {
refresh_token=$(jq -r '.refresh_token' <<< $token)
if [[ $refresh_token = null ]]; then
die 1 "can't find a refresh_token"
fi
token=$( \
curl -s ${TOKEN_URI:-"https://www.googleapis.com/oauth2/v4/token"} \
-d $(encode_post \
refresh_token=$refresh_token \
client_id=$CLIENT_ID \
client_secret=$CLIENT_SECRET \
grant_type=refresh_token
)
)
token_time=$(date +%s)
}
# api_query_try
# try to run a query using the current token
api_query_try () {
if [[ -z $token ]]; then die 1 "have no access token in apt_query"; fi
access_token=$(jq -r '.access_token' <<< $token)
cmd="curl -s"
if [[ -n $REQUEST ]]; then cmd+=(" --request $REQUEST"); fi
cmd+=("'$(encode_url $@)'")
cmd+=("--header 'Authorization: Bearer $access_token'")
if [[ -n $POST_DATA ]]; then
cmd+=("--header 'Accept: application/json'")
cmd+=("--header 'Content-Type: application/json'")
cmd+=("--data '$POST_DATA'")
cmd+=("--compressed")
fi
if [[ $DEBUG = "true" ]]; then warn "cmd: ${cmd[@]}"; fi
response=$(eval "${cmd[@]}")
echo $response
if [[ -n $response && $(jq '.error == null' <<< $response) = "false" ]]; then
return 1
fi
}
# api_query
# keep trying to run a query until there is no error. expired tokens
# provoke a refresh, rate exceeded provokes exponential backoff
api_query () {
pause=1
until result=$(
api_query_try $@
); do
reason=$(jq -r '.error.errors[0].reason' <<< $result)
message=$(jq -r '.error.errors[0].message' <<< $result)
case $reason in
"authError")
warn "token expired, refreshing"
oauth_refresh
;;
"rateLimitExceeded")
warn "rate limit exceeded, backing off..."
sleep $pause
if [[ $pause -lt 512 ]]; then pause *= 2; fi
;;
*)
die 1 "unknown error: $result"
;;
esac
done
if [[ -n $LIST ]] &&
[[ -n $(jq 'select(.nextPageToken != null)' <<< $result) ]]; then
nextPageToken=$(jq -r '.nextPageToken' <<< $result)
result2=$(api_query \
$(for i in $@; do echo $i; done | grep -v '^pageToken=') \
pageToken=$nextPageToken)
list="["
list+=$(jq ".$LIST" <<< $result)
list+=","
list+=$(jq ".$LIST" <<< $result2)
list+="]"
list=$(jq 'flatten' <<< $list)
result=$(jq ".$LIST = $list" <<< $result2)
fi
echo $result
}
# get_token
# look in a few places for credentials or a token, dying if unsuccessful
get_token () {
if [[ -n $TOKEN_FILE ]]; then
# file specified
[[ ! -s $file ]] && \
die 1 "$type file $file missing or empty"
token=$(< $TOKEN_FILE)
return 0
fi
if [[ -n $METADATA_URL ]] && curl -sH \
"Metadata-flavor: Google" \
"$METADATA_URL/google-api-token" > /dev/null; then
# token in metadata
token=$(curl -sH "Metadata-flavor: Google" \
"$METADATA_URL/google-api-token")
return 0
fi
if [[ -s "$HOME/.config/google-api/token.json" ]]; then
# file in config
token=$(< "$HOME/.config/google-api/token.json")
return 0
fi
oauth || die 1 "failed to create token"
}
# get_cred
# look in a few places for credentials or a token, dying if unsuccessful
get_cred () {
if [[ -n $CRED_FILE ]]; then
# file specified
[[ ! -s $CRED_FILE ]] && \
die 1 "credentials file $file missing or empty"
cred=$(jq ".installed" $CRED_FILE)
return 0
fi
if [[ -n $METADATA_URL ]] && curl -sH \
"Metadata-flavor: Google" \
"$METADATA_URL/google-api-credentials" > /dev/null; then
# token in metadata
cred=$(curl -sH "Metadata-flavor: Google" \
"$METADATA_URL/google-api-credentials" | jq ".installed")
return 0
fi
if [[ -s "$HOME/.config/google-api/credentials.json" ]]; then
# file in config
cred=$(jq ".installed" "$HOME/.config/google-api/credentials.json")
return 0
fi
die 1 "no credentials found"
}
# refresh_if_stale
# if the token is going to expire in less than a minute, refresh it
refresh_if_stale () {
if [[ -z $token_time ]] || [[ $(($(date +%s) + 60)) -gt \
$(($token_time + $(jq -r '.expires_in' <<< $token))) ]]; then
oauth_refresh
fi
token_time=$(date +%s)
}
warn () {
echo "$0:" "$@" >&2
}
die () {
rc=$1
shift
warn "$@"
exit $rc
}
trap 'echo "died an ignominious death code $?"' ERR
#########
# flags #
#########
# set environment variables from flag values. flag values override
# exported environment variables, which override defaults
POSITIONAL=()
POST_DATA=()
while [[ $# -gt 0 ]]
do
key="$1"
case $key in
--api-server)
API_SERVER=$2
shift
shift
;;
-i|--client-id)
CLIENT_ID="$2"
shift
shift
;;
-s|--client-secret)
CLIENT_SECRET="$2"
shift
shift
;;
--auth-uri)
AUTH_URI="$2"
shift
shift
;;
--token-uri)
TOKEN_URI="$2"
shift
shift
;;
--redirect-uri)
REDIRECT_URI="$2"
shift
shift
;;
-c|--credentials)
CRED_FILE="$2"
shift
shift
;;
-t|--token)
TOKEN_FILE="$2"
shift
shift
;;
--scope)
SCOPE+=("$2")
shift
shift
;;
-l|--list)
LIST="$2"
shift
shift
;;
-p|--post-data)
POST_DATA="$2"
shift
shift
;;
-X|--request)
REQUEST=$2
shift
shift
;;
*)
POSITIONAL+=("$1")
shift
;;
esac
done
set -- "${POSITIONAL[@]}"
##################
# default values #
##################
# these default values should give reasonable behaviour
# note that there is no default for CLIENT_ID or CLIENT_SECRET
API_SERVER=${API_SERVER:-"https://www.googleapis.com"}
SCOPE=${SCOPE:-"$API_SERVER/auth/drive"}
if curl -s metadata.google.internal > /dev/null; then
METADATA_URL="metadata.google.internal/"
METADATA_URL+="computeMetadata/v1/project/attributes"
fi
get_cred
CLIENT_ID=$(jq -r '.client_id' <<< $cred)
CLIENT_SECRET=$(jq -r '.client_secret' <<< $cred)
AUTH_URI=$(jq -r '.auth_uri' <<< $cred)
TOKEN_URI=$(jq -r '.token_uri' <<< $cred)
REDIRECT_URI=$(jq -r '.redirect_uris[0]' <<< $cred)
#################
# handle scopes #
#################
# if we request scopes, do oauth to be sure the token has the necessary
# scope
if [[ -${#SCOPE[@]} -gt 0 ]]; then
# prepend server url if necessary
for i in ${!SCOPE[@]}; do
if ! $grep "^$API_SERVER" <<< ${SCOPE[$i]}; then
SCOPE[$i]=$API_SERVER${SCOPE[$i]}
fi
done
scope_str=${SCOPE[0]}
# if we have multiple scopes, paste them together with sep="%20"
if [[ ${#SCOPE[@]} -gt 1 ]]; then
scope_str+=$(printf "%%20%s" ${SCOPE[@]:1})
fi
oauth
# otherwise look for a cached token
else
SCOPE="$API_SERVER/auth/drive"
get_token
fi
if [[ -z $token ]]; then die 1 "failed to set token..."; fi
refresh_if_stale
if [[ $# -eq 0 ]]; then
set -- https://www.googleapis.com/drive/v3/about fields=user
fi
api_query $@
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment