Oauth2 certificate authentication in bash script
Tags: oauth2, bash, microsoftgraph, googleapi
Published: 2021-03-08 01:00:08

I was exploring the possibility of getting the auth token in bash using certificate authentication. Here are two examples how to obtain access token, one for Microsoft Graph and the other one for Google APIs.

Both scripts are similar:

- create jwt payload/claims
- sign header +payload with private key of the certificate uploaded to Microsoft/Google
- post data to token endpoint
- acquire access_token 

### A bit of explanation

- `base64 -w 0 | tr '/+' '_-' | tr -d '=' ` - this is simple base64url encoding
  `base64 -w 0` means that data won't be broken in multiple lines, `tr '/+' '_-'` is used to replace `/` with `_` and `+` with `-`, and finally `tr -d '=' ` is used to remove `=` which is standard base64 padding
- `openssl dgst -sha256 -sign $key_file` is signing input with certificate key stored in the `$key_file`
- `date +%s` is current timestamp, and `$(($ts + 600))` adds 10 minutes of token validity

Microsoft uses certificate fingerprint (thumbprint) in jwt header and it is a binary value encoded as base64url.

- `openssl x509 -fingerprint -noout -in $cert_file` will produce something like
  `SHA1 Fingerprint=AA:BB:CC:DD:EE...` after which `cut -d '=' -f 2` is used to get everything right of the `=` sign (the fingerprint), which is then converted into binary using `xxd -r -p`  and finally converted into base64url

It also uses "a Guid" for unique identifier of a jwt where `cat /proc/sys/kernel/random/uuid` is used to create it.

In the end, `jq` is used to extract `auth_token` into the file or to display the error message in case authentication was not successful. 

It is probably easier just to use vendor provided tools or libraries, but sometimes having a simple shell script is just good enough. 



## Microsoft Graph

```bash
#!/bin/bash
set -e

client_id='<replace_with_client_id>'
tenant_id='<replace_with_tenant_id>'
cert_file='my.crt' #certificate (used for fingerprint)
key_file='my.key'  #certificate private key (for signing)
cert_hash=$(openssl x509 -fingerprint -noout -in $cert_file | cut -d '=' -f 2 | xxd -r -p | base64 -w 0| tr '/+' '_-' | tr -d '=')
jwt_header="{\"alg\":\"RS256\",\"typ\":\"JWT\",\"x5t\":\"$cert_hash\"}"

ts=$(date +%s)
part_aud="\"aud\":\"https://login.microsoftonline.com/$tenant_id/oauth2/v2.0/token\""
part_nbf="\"nbf\":$ts"
part_exp="\"exp\":$(($ts + 600))"
part_jti="\"jti\":\"$(cat /proc/sys/kernel/random/uuid)\""
part_iss="\"iss\":\"$client_id\""
part_sub="\"sub\":\"$client_id\""
jwt_payload="{$part_aud,$part_exp,$part_iss,$part_jti,$part_nbf,$part_sub}"

token_data="$(echo -n $jwt_header | base64 -w 0 | tr '/+' '_-' | tr -d '=').$(echo -n $jwt_payload | base64 -w 0 | tr '/+' '_-' | tr -d '=')"

signature=$(echo -n $token_data | openssl dgst -sha256 -sign $key_file | base64 -w 0 | tr '/+' '_-' | tr -d '=')

assertion="$token_data.$signature"

resp=$(curl -Ssl -X POST "https://login.microsoftonline.com/$tenant_id/oauth2/v2.0/token" \
  --data-urlencode "client_id=$client_id" \
  --data-urlencode 'scope=https://graph.microsoft.com/.default' \
  --data-urlencode 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' \
  --data-urlencode "client_assertion=$assertion" \
  --data-urlencode 'grant_type=client_credentials' \
)

if echo $resp | grep -q 'access_token'; then
	echo $resp | jq -j '.access_token' >access_token
else
	echo $resp | jq -r '.error + " error:\n" + .error_description'
	exit 1
fi
```

Source:

https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate

https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials



## Google APIs

```bash
#!/bin/bash
set -e

client_email='client@your-project-name.iam.gserviceaccount.com'
subject_email='subject@example.com' #user that will be impersonated
scopes='https://www.googleapis.com/auth/<scope1> https://www.googleapis.com/auth/<scope2>'
key_file='my.key' #certificate private key (for signing)

jwt_header="{\"alg\":\"RS256\",\"typ\":\"JWT\"}"

ts=$(date +%s)
part_iss="\"iss\":\"$client_email\""
part_sub="\"sub\":\"$subject_email\""
part_scope="\"scope\":\"$scopes\""
part_aud="\"aud\":\"https://oauth2.googleapis.com/token\""
part_exp="\"exp\":\"$(($ts + 600))\""
part_iat="\"iat\":\"$ts\""
jwt_payload="{$part_iss,$part_sub,$part_scope,$part_aud,$part_exp,$part_iat}"

token_data="$(echo -n $jwt_header | base64 -w 0 | tr '/+' '_-' | tr -d '=').$(echo -n $jwt_payload | base64 -w 0 | tr '/+' '_-' | tr -d '=')"

signature=$(echo -n $token_data | openssl dgst -sha256 -sign $key_file | base64 -w 0 | tr '/+' '_-' | tr -d '=')

assertion="$token_data.$signature"

resp=$(curl -Ssl -X POST 'https://oauth2.googleapis.com/token' \
  --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer' \
  --data-urlencode "assertion=$assertion" \
)

if echo $resp | grep -q 'access_token'; then
	echo $resp | jq -j '.access_token' >access_token
else
	echo $resp | jq -r '.error + " error:\n" + .error_description'
	exit 1
fi
 
```

Source:

https://developers.google.com/identity/protocols/oauth2/service-account#httprest

## Code
{% gist https://gist.github.com/milolav/da713d2e6dd4c9d32a2d3afab38c7a18 %}