All Articles

Getting an App Only access token for SharePoint REST APIs

Photo by Pavel Danilyuk: https://www.pexels.com/photo/robot-holding-a-red-flower-8438966/

Objectives

Recently I was asked to access SharePoint Online APIs using App Only access. I always used libraries (like MSAL, or PnP Core) for that and they always use signed client assertions for acquiring bearer tokens.

This time I needed some deeper insights and analyzed the whole process on protocol level.

App Registration

As a pre-requisite we need an Azure Active Directory App registration with access to SharePoint. There are plenty of guides on the net for that, for example Setup Authentication.

In short, I used openssl to create self-signed certificates…

# Create the certificate
$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365  
# Convert to pfx file
$ openssl pkcs12 -export -out certificate.pfx -inkey key.pem -in cert.pem

…and the PnP PowerShell cmdlet Register-PnPAzureADApp to create the Azure AD app registration.

Register-PnPAzureADApp `
  -ApplicationName TestApp1 `
  -Tenant <tenant>.onmicrosoft.com `
  -CertificatePath certificate.pfx `
  -CertificatePassword (ConvertTo-SecureString -String "<cert password>" -AsPlainText -Force) `
  -Username "<user>@<tenant>.onmicrosoft.com" `
  -Password (ConvertTo-SecureString -String "<password>" -AsPlainText -Force) `
  -SharePointApplicationPermissions Sites.Read.All

Getting an OAuth2 Token using a Client Assertion

The Request for getting the app only access token looks like this:

POST https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

client_id=<client-id>
&grant_type=client_credentials
&scope=https://<tenant>.sharepoint.com/.default
&client_assertion=<signed jwt client assertion>
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer

The tenant-id is the id of the Azure AD tenant, client-id the app id of the Azure AD App registration. The grant type is set to client_credentials.

The scope needs to be set to the SharePoint URL, followed by .default.

The client assertion type is set to jwt-bearer, and the client assertion itself a base64 jwt similar to…

{
  "iat": 1678548857,
  "exp": 1678552457,
  "iss": "791ff0ac-1f93-47fc-8629-6b20f360036f",
  "sub": "791ff0ac-1f93-47fc-8629-6b20f360036f",
  "aud": "https://login.microsoftonline.com/0a7ab993-afa3-4a29-9b0b-f64583a4d888/oauth2/token"
}

…signed with the private key of the certificate provided to the Azure AD App registration.

The jwt contains the following claims:

  • “iat”: Issued at timestamp
  • “exp”: Expires timestamp - time window, as short as possible
  • “iss”: Issuer - the App Id
  • “sub”: Subject Claim - also the App Id
  • “aud”: Audience - the token endpoint

This is quite hard to do with command line tools, fortunately there is a handy npm package that does all that in one step:

The jwtgen Tool

$ jwtgen -a RS256 \
         -p key.pem \
         -e 3600 \
         -c "iss=<client-id>" \
         -c "sub=<client-id>" \
         -c "aud=https://login.microsoftonline.com/<tenant-id>/oauth2/token"  \
         --headers "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"x5t\":\"<base64-encoded-certificate-thumbprint>\"}" \
         -v

Note that the certificate thumbprint <base64-encoded-certificate-thumbprint> usually comes in a hexadecimal representatation, e.g. 49AF595AE9C628BCAF5AB9D36B4EEC97DFF24458

Sites like Base64.guru or this CyberChef recipe can be used to get the correct base 64 encoded representation of the certificate thumbprint: Sa9ZWunGKLyvWrnTa07sl9/yRFg=

Everything put together

$ jwtgen -a RS256 \
         -p key.pem \
         -e 3600 \
         -c "iss=791ff0ac-1f93-47fc-8629-6b20f360036f" \
         -c "sub=791ff0ac-1f93-47fc-8629-6b20f360036f" \
         -c "aud=https://login.microsoftonline.com/0a7ab993-afa3-4a29-9b0b-f64583a4d888/oauth2/token"  \
         --headers "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"x5t\":\"Sa9ZWunGKLyvWrnTa07sl9/yRFg=\"}" \
         -v

The -v commandline option produces verbose output including the encoded claims, headers and the token:

algorithm: RS256

claims:
{
  "iat": 1678549167,
  "exp": 1678552767,
  "iss": "791ff0ac-1f93-47fc-8629-6b20f360036f",
  "sub": "791ff0ac-1f93-47fc-8629-6b20f360036f",
  "aud": "https://login.microsoftonline.com/0a7ab993-afa3-4a29-9b0b-f64583a4d888/oauth2/token"
}

headers:
{
  "typ": "JWT",
  "alg": "RS256",
  "x5t": "Sa9ZWunGKLyvWrnTa07sl9/yRFg="
}

token:
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1.... many more characters

Using the client assertion - shown in the token field - we can now try to request a token from the aouth2 token endpoint:

POST https://login.microsoftonline.com:443/0a7ab993-afa3-4a29-9b0b-f64583a4d888/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

client_id=791ff0ac-1f93-47fc-8629-6b20f360036f
&grant_type=client_credentials
&scope=https://loitzl.sharepoint.com/.default
&client_assertion=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1......
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer

Et voilà, we (hopefully) got a valid access_token in the response:

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

Lets try this by…

Getting all Lists from a SharePoint Site using the REST API

Using the token requested using a client assertion we can now try to get all lists from my SharePoint root site:

GET https://loitzl.sharepoint.com/_api/web/lists
Accept: application/json;odata=verbose
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOi....

The result: the first couple of fields of the /lists request.

{
  "d": {
    "results": [
      {
        "__metadata": {
          "id": "https://loitzl.sharepoint.com/_api/Web/SiteUserInfoList",
          "uri": "https://loitzl.sharepoint.com/_api/Web/SiteUserInfoList",
          "etag": "\"82\"",
          "type": "SP.List"
        },
        "FirstUniqueAncestorSecurableObject": {
[...]

✅ Yess!

Using a client secret

My IT department asked me if it’s possible to use a client secret instead, so I tried this as well.

The token endpoint request:

POST https://login.microsoftonline.com:443/0a7ab993-afa3-4a29-9b0b-f64583a4d888/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

client_id=791ff0ac-1f93-47fc-8629-6b20f360036f
&grant_type=client_credentials
&scope=https://loitzl.sharepoint.com/.default
&client_secret=tdB8Q....

returns an access token as well:

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

If we use that token on the SharePoint REST API…

GET https://loitzl.sharepoint.com/_api/web/lists
Accept: application/json;odata=verbose
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSU...

…it replies with an “401 Unauthorized”:

Unsupported app only token.

Structure of the Bearer Token

The bearer token acquired with a client assertion contains an appidacr claim with a value of 2, the client secret acquired token has an appidacr value of 1.

ClientAssertion vs ClientSecret Token

I reckon that SharePoint uses this value to distinguish if the endpoint accepts the token or not.

Conclusion

In my opinion signed client assertions are a more secure method for getting bearer tokens than using client secrets (regardless of SharePoint).

If someone gets hold of the client secret in transport, the window of compromise is as long as the validity duration of the client secret (thus keep them as short as possible).

But if we lose a client assertion in transport, its only valid for the requested duration, probably just a couple of minutes or even seconds.

Nevertheless, we need to handle the certificate, which is another topic.

💙

References