0

I have this daemon application that connects to Project Online and performs some queries. It consists of getting a bearer token and then performing the queries, like in the two steps below:

  1. Request Auth Token

POST https://accounts.accesscontrol.windows.net/{tenant}/tokens/OAuth/2"

HTTP Headers:
"Content-Type": "application/x-www-form-urlencoded"
HTTP Body:
grant_type=refresh_token
&client_id={client_id}
&client_secret={client_secret}
&resource={resource}
&refresh_token={refresh_token}
&redirect_uri=https://localhost
  1. Execute Project OData query:

GET https://{tenant_name}.sharepoint.com/sites/{pwa_site}/_api/projectdata/Tasks?$select=TaskId,ProjectId,TaskName&inlinecount=allpages&$top=1

HTTP Headers:
"Authorization": "Bearer {token}"

This worked fine until now.

However, my company wants to change this authentication flow to a new Azure Application registration that uses a certificate to authenticate to the SharePoint site (it seems Microsoft will deprecate the current one on April 2nd, 2026, or so I've been told). Therefore, I've requested this new app registration using a certificate. This app has delegated permissions to the Project. Read, Project. Write, and ProjectWebApp.FullControl, under SharePoint -> Delegated Permissions.

After this new app was registered, I could successfully generate a JWT token and use it against graph.microsoft.com to retrieve a Bearer token, as follows:

  1. Generate a JWT token that has "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token" as its audience through the following script:
#!/bin/bash

# Check for the correct number of arguments
if [ "$#" -ne 5 ]; then
    echo "Incorrect number of arguments. Need to pass Tenant ID, Passphrase, Client_ID, Private_Key, and certificate for generating the JWT token."
    exit 1
fi

TENANT_ID=$1
PASSPHRASE=$2
CLIENT_ID=$3
PRIVATE_KEY=$4
CERT_PEM=$5

AUD="https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token"
JTI=$(uuidgen)
# EXP=$(($(date +%s) + 600))  # 600 veut dire 10 min => donc Expiration dans 10 minutes
EXP=$(($(date +%s) + 3599))  # 3599 veut dire 1 hr => donc Expiration dans 1 hr


b64url() {
  openssl base64 -e -A | tr '+/' '-_' | tr -d '='
}

# get thumbprint du certificat
THUMBPRINT=$(openssl x509 -in "$CERT_PEM" -noout -fingerprint -sha1 | cut -d'=' -f2 | tr -d ':' | xxd -r -p | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')

# create header JSON
HEADER=$(jq -n --arg x5t "$THUMBPRINT" '{"alg":"RS256","x5t":$x5t}')

# create payload JSON
PAYLOAD=$(jq -n \
  --arg aud "$AUD" \
  --arg iss "$CLIENT_ID" \
  --arg sub "$CLIENT_ID" \
  --arg jti "$JTI" \
  --argjson exp "$EXP" \
  '{
    "aud": $aud,
    "iss": $iss,
    "sub": $sub,
    "jti": $jti,
    "exp": $exp
  }')

# Encoder le header et le payload en base64url
HEADER_B64=$(echo -n "$HEADER" | b64url)
PAYLOAD_B64=$(echo -n "$PAYLOAD" | b64url)

# create signature
SIGNING_INPUT="$HEADER_B64.$PAYLOAD_B64"
SIGNATURE=$(echo -n "$SIGNING_INPUT" | openssl dgst -sha256 -sign "$PRIVATE_KEY" -passin pass:"$PASSPHRASE" | b64url)

# Assembler le JWT
JWT="$SIGNING_INPUT.$SIGNATURE"

echo "$JWT"
  1. Use this JWT token to request a Bearer token:

POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token

HTTP Headers:
Content-Type: application/x-www-form-urlencoded
HTTP Body:
client_id: {client_id}
client_assertion: {JWT_token_from_previous_step}
client_assertion_type: urn:ietf:params:oauth:client-assertion-type:jwt-bearer
grant_type: client_credentials
scope: https://{tenant_name}.sharepoint.com/.default

This seems to work fine and returns a valid response with a Bearer token like below:

{
    "token_type": "Bearer",
    "expires_in": 3599,
    "ext_expires_in": 3599,
    "access_token": "eyJ0eXAiOiJKV1QiLCJub25jZSI6"...
}

All good until now, but the issue starts in the next step, on which I need to use this token on Project Online:

  1. Execute Project OData query (please note this is the same query as in the current approach, since graph api seems not to support querying Project Online:

GET https://{tenant_name}.sharepoint.com/sites/{pwa_site}/_api/projectdata/Tasks?$select=TaskId,ProjectId,TaskName&inlinecount=allpages&$top=1

HTTP Headers:
"Authorization": "Bearer {token}"

This will either return HTTP 401 with payload {"error_description": "ID3035: The request was not valid or is malformed."} or HTTP 401 with payload {"error_description": "Exception of type 'Microsoft.IdentityModel.Tokens.AudienceUriValidationFailedException' was thrown."} if the scope set on step 2 is https://graph.microsoft.com/.default.

Does anyone have any idea how to solve this and successfully retrieve data from Project Online using a Token retrieved from graph.microsoft.com?

1 Answer 1

0

The problem seems to be you are trying to use Graph API token to access Project API. You need a Project token to access Project API. Graph API for Project does not exist.

Try asking for Project API token directly (try using scope "Project.Read" instead of ".default", i.e. "https://{tenant_name}.sharepoint.com/Project.Read". Of course, the "Project.Read" should be granted to your app (by admin)

Sign up to request clarification or add additional context in comments.

4 Comments

Unfortunately it didn't work. This scope seems to be invalid: { "error": "invalid_scope", "error_description": "AADSTS1002012: The provided value for scope https://{tenant_name}.sharepoint.com/Project.Read is not valid. Client credential flows must have a scope value with /.default suffixed to the resource identifier (application ID URI). ...", "error_codes": [ 1002012 ], "timestamp": "2025-08-22 08:48:47Z", "trace_id": "65c0a5a6-08e5-4575-ba79-7cd99aa71700", "correlation_id": "53c2662d-85a7-499d-91cc-db491c5432a3" }
you need to replace the {tenant_name} with your tenant name
I did that. I just masked it in the post due to my company's security policies.
I was able to connect to the Project (classic, SharePoint-based) using that approach without any issues. Could it be you still have ".default" somewhere? You should explicitly ask for the "Project" access, or convert the ".default" token to a "Project" token using "OBO" (on-behlaf-of): learn.microsoft.com/en-us/entra/identity-platform/… BTW have you considered using some library or CLI instead of crafting requests and the authentication data yourself?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.