Last active
February 19, 2019 14:58
-
-
Save mlgrm/5ecb9012a8d54590bbedbfcb9b03acdb to your computer and use it in GitHub Desktop.
make calls to the google api using oauth2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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