Last active
February 17, 2025 17:07
-
-
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
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 | |
# 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' |
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
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 ...