Skip to content

Instantly share code, notes, and snippets.

@smaring
Last active February 17, 2025 17:07
Show Gist options
  • Save smaring/3a3a6779a809beecc39624aada6e2b88 to your computer and use it in GitHub Desktop.
Save smaring/3a3a6779a809beecc39624aada6e2b88 to your computer and use it in GitHub Desktop.
Construct a JWT assertion in bash using client-credential with cert to get an access token from Microsoft Azure Directory
#!/bin/bash
# Copyright 2022 Hitachi Vantara. All rights reserved.
# Author: Steve Maring <[email protected]>
#
# Description: Given an Azure Directory app's tenant id, client id, and a pfx file, this provides
# a mechanism to obtain a temporary access token via command line. The access token may be
# used to do things like send email via the Graph API.
#
# System Requirements: openssl, sed, awk, curl, jq
#
# Preparation Instructions:
# 1) acquire the pem, with public cert and private key, from your pfx
#
# $ openssl pkcs12 -in <your-app>.pfx -out <your-app>.pem
# enter the "import" password provided and a new PEM "pass phrase"; recommend using the same password
#
# if you have access to Azure Directory in a tenant, and would like to add the public cert to an app
# you have registered, you can create your own pfx with a self-signed cert file yourself with:
# $ openssl genrsa -out private-key.pem 3072
# $ openssl rsa -in private-key.pem -pubout -out public-key.pem
# $ openssl req -new -x509 -key private-key.pem -out cert.pem -days 360
# $ openssl pkcs12 -export -inkey private-key.pem -in cert.pem -out <your-app>.pfx
#
# 2) extract the public cert from the pem
# $ openssl x509 -in <your-app>.pem -out <your-app>.crt
#
# <your-app>.crt will be the PUBLIC_CERT_FILE used below
#
# 3) extract the private key to a file that doesn't require the pass phrase; PROTECT this file!
# $ openssl rsa -in <your-app>.pem -out <your-app>.key
#
# <your-app>.key will be the PRIVATE_KEY_FILE used below
#
#
# execution:
#
# $ ./get-microsoft-access-token.sh \
# <TENANT_ID> \
# <CLIENT_ID> \
# <PUBLIC_CERT_FILE> \
# <PRIVATE_KEY_FILE> \
# <TIMEOUT> \
# <DEBUG>
#
# where:
#
# <TIMEOUT> is the number of minutes before the access token will expire
# <DEBUG> is true or false.
# If true, you will see the JWT formed and the full response.
# If false, you will only receive the access token to stdout.
#
# references:
# https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials
# https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate
# https://jwt.io/
export LANG=C.UTF-8
if [ "$#" -ne 6 ]; then
echo "Missing arguments to script. Format is ..."
echo " $ ./get-microsoft-access-token.sh \ "
echo " <TENANT_ID> \ "
echo " <CLIENT_ID> \ "
echo " <PUBLIC_CERT_FILE> \ "
echo " <PRIVATE_KEY_FILE> \ "
echo " <TIMEOUT> \ "
echo " <DEBUG>"
exit 1
fi
tenantId=$1
clientId=$2
publicCertFile=$3
privateKeyFile=$4
timeout=$5
debug=$6
thumbprint=$(openssl x509 -in ${publicCertFile} -fingerprint -noout | awk '{split($0,a,"="); print a[2]}' )
thumbprintHex=$(echo -e "\\x${thumbprint}" | sed 's/:/\\x/g' )
thumbprintHexString=$(echo -e "${thumbprint}" | sed 's/://g' )
if [[ "${debug}" == "true" ]]; then
echo -e "\nPUBLIC CERT THUMBPRINT: ${thumbprintHexString}"
fi
x5t=$(echo -ne "${thumbprintHex}" | openssl base64 | sed s/\+/-/g | sed 's/\//_/g' | sed -E s/=+$// )
read -r -d '' header <<EOF
{
"alg": "RS256",
"typ": "JWT",
"x5t": "${x5t}"
}
EOF
if [[ "${debug}" == "true" ]]; then
echo -e "\nJWT HEADER:\n${header}"
fi
header_no_whitespace=$(echo "${header}" | sed ':a; N; s/[[:space:]]//g; ta')
base64_encoded_header=$(echo ${header_no_whitespace} | \
openssl base64 | \
sed s/\+/-/g | sed 's/\//_/g' | sed -E s/=+$// | \
sed ':a; N; s/[[:space:]]//g; ta')
nbf=$(date +%s)
exp=$(date --date="+ ${timeout} minutes" +%s)
jti="$(hexdump -vn16 -e'4/4 "%08X" 1 "\n"' /dev/urandom)"
read -r -d '' payload <<EOF
{
"aud": "https: //login.microsoftonline.com/${tenantId}/oauth2/v2.0/token",
"exp": ${exp},
"iss": "${clientId}",
"jti": "${jti}",
"nbf": ${nbf},
"sub": "${clientId}"
}
EOF
if [[ "${debug}" == "true" ]]; then
echo -e "\nJWT CLAIMS PAYLOAD:\n${payload}"
fi
payload_no_whitespace=$(echo "${payload}" | sed ':a; N; s/[[:space:]]//g; ta')
base64_encoded_payload=$(echo ${payload_no_whitespace} | \
openssl base64 | \
sed s/\+/-/g | sed 's/\//_/g' | sed -E s/=+$// | \
sed ':a; N; s/[[:space:]]//g; ta')
signature=$( echo -n "${base64_encoded_header}.${base64_encoded_payload}" | \
openssl dgst -sha256 -sign ${privateKeyFile} | \
openssl base64 | \
sed s/\+/-/g |sed 's/\//_/g' | sed -E s/=+$// | \
sed ':a; N; s/[[:space:]]//g; ta')
jwt="${base64_encoded_header}.${base64_encoded_payload}.${signature}"
if [[ "${debug}" == "true" ]]; then
echo "\nJWT: ${jwt}"
fi
request="curl -s -X POST \
--header \"Content-Type: application/x-www-form-urlencoded\" \
-d \"\
scope=https%3A%2F%2Fgraph.microsoft.com%2F.default&\
grant_type=client_credentials&\
client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&\
client_id=${clientId}&\
client_assertion=${jwt}\" \
https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token"
if [[ "${debug}" == "true" ]]; then
echo -e "\nREQUEST:\n${request}"
fi
response=$(eval "$request 2>&1")
if [[ "${debug}" == "true" ]]; then
echo -e "\nRESPONSE:\n"
echo "${response}" | jq .
fi
echo "${response}" | jq '.access_token' | sed 's/\"//g'
@smaring
Copy link
Author

smaring commented Dec 24, 2022

getting ...

{"error":"invalid_client","error_description":"AADSTS700027: The certificate with identifier used to sign the client assertion is not registered on application. [Reason - The key was not found., Thumbprint of key used by client: '35613 ...

@smaring
Copy link
Author

smaring commented Dec 31, 2022

I have fixed the thumbprint generation process and this script is now working.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment