DEV Community

Miro
Miro

Posted on

Oauth2 certificate authentication in bash script

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

#!/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
Enter fullscreen mode Exit fullscreen mode

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

#!/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

Enter fullscreen mode Exit fullscreen mode

Source:

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

Code

Top comments (0)