Skip to content

Connect SPSync to SharePoint with your own app

This page is for the person who has to make the connection work: register an app, make a certificate, get consent, and grant access to the right sites. It has the commands. The admin permissions page explains why each permission exists; read that first if you are deciding what to ask your security team for.

There are three ways in.

DelegatedApplication, SPSync’s shared appApplication, your own app registration
Runs asYou, with your own SharePoint accessSPSync’s multi-tenant app, with its own certificate and no user signed inAn app registration you own, with no user signed in
Setupspsync auth login, then consentA tenant admin consents, an admin grants sites, then “Switch to unattended (application) mode” in the permissions wizardRegister an app, add a credential, consent, grant sites
Keeps running when you are awayUntil your refresh token expires, about 90 days idleYesYes
Can list every site in the tenantNo, delegated calls to getAllSites return 403; SPSync falls back to a search=* queryNo, Sites.Selected reads only granted sites; use --site-urlOnly with a tenant-wide permission such as Sites.Read.All
Good forTrying SPSync, one person’s reportsScheduled, unattended replication without registering anythingScheduled, unattended replication under your own app and policies

Sites.Selected and Lists.SelectedOperations.Selected give an app no sites at all until someone grants one, so an app holding only those cannot enumerate the tenant. Point SPSync at a site with --site-url https://contoso.sharepoint.com/sites/finance instead of asking it to list sites.

Application mode on the shared app. Once an admin has consented to Sites.Selected and granted a site, the permissions wizard offers “Switch to unattended (application) mode”. It reads one granted site with the app’s own certificate first, and changes nothing if that read is refused. In YAML it is auth: {mode: application, tenant_id: <tenant>}; common is not accepted, and the certificate comes from SPSYNC_AUTH_APP_CERTIFICATE_PRIVATE_KEY and SPSYNC_AUTH_APP_CERTIFICATE_THUMBPRINT. spsync auth status --application --tenant <tenant> prints the mode and the roles the token carries. From the admin MCP server, use create_connection with auth_mode=application. In the Azure managed application the certificate is read from Key Vault. Your own app registration (below) is still the choice when your policies only allow apps registered in your tenant.

On the CLI, spsync auth login --client-id <client-id> starts a device-code sign-in: it prints a short code and a Microsoft page, you enter the code, sign in and consent. The desktop app does the same from the Connect screen. The web UI uses the browser redirect flow (authorization code with PKCE) instead.

The consent screen asks for Sites.Read.All, User.Read and offline_access, all delegated. Any user can usually approve these for themselves.

If your tenant restricts user consent, the sign-in ends in AADSTS65001 or AADSTS90094. A tenant admin then has to approve the app once. The admin consent URL has this exact shape:

https://login.microsoftonline.com/<tenant-id>/adminconsent?client_id=<client-id>

Send it to a Global Administrator or Privileged Role Administrator. After they approve, sign in again. SPSync’s setup screen shows this link with your ids filled in, and the admin MCP server’s admin_consent tool drafts the message to your admin.

Do this when you want unattended sync, or when your organisation requires every integration to use its own registration.

In the Azure portal

  1. Open Microsoft Entra admin center, then App registrations, then New registration.
  2. Name it (for example “SPSync”), choose Accounts in this organizational directory only (single tenant), and register. Skip the redirect URI for app-only use.
  3. Copy the Application (client) ID and Directory (tenant) ID from the overview.
  4. Under API permissions, add Microsoft Graph application permissions, narrowest first: Lists.SelectedOperations.Selected (per list), Sites.Selected (per site), and Sites.Read.All only as a last resort because it covers every site.
  5. Choose Grant admin consent for the tenant. Without it the permissions do nothing.

With the Azure CLI. Sign in with az login as a user who may register applications. This is the script SPSync generates, shown for the two Selected permissions:

Terminal window
GRAPH=00000003-0000-0000-c000-000000000000
APP_ID=$(az ad app create --display-name "SPSync" \
--sign-in-audience AzureADMyOrg --query appId -o tsv)
az ad app permission add --id "$APP_ID" --api $GRAPH --api-permissions \
$(az ad sp show --id $GRAPH --query "appRoles[?value=='Sites.Selected' && contains(allowedMemberTypes, 'Application')].id | [0]" -o tsv)=Role \
$(az ad sp show --id $GRAPH --query "appRoles[?value=='Lists.SelectedOperations.Selected' && contains(allowedMemberTypes, 'Application')].id | [0]" -o tsv)=Role \
>/dev/null
az ad app permission admin-consent --id "$APP_ID" # needs a tenant admin
echo "Client id: $APP_ID"

With Microsoft Graph PowerShell

Terminal window
Connect-MgGraph -Scopes "Application.ReadWrite.All"
$graph = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
$roles = 'Sites.Selected','Lists.SelectedOperations.Selected'
$access = foreach ($n in $roles) {
@{ Id = ($graph.AppRoles | Where-Object { $_.Value -eq $n -and $_.AllowedMemberTypes -contains 'Application' }).Id; Type = 'Role' }
}
$app = New-MgApplication -DisplayName "SPSync" -SignInAudience AzureADMyOrg `
-RequiredResourceAccess @(@{ ResourceAppId = '00000003-0000-0000-c000-000000000000'; ResourceAccess = $access })
Write-Host "Client id: $($app.AppId)"

Admin consent is still a separate step in both scripts. Use the portal button or the consent URL above.

Let the admin MCP server do it. An AI assistant connected to SPSync’s admin MCP server can call create_app_registration. It asks you for a second device-code sign-in that requests the delegated AppRegistration.Create permission, creates a single-tenant registration, and returns a secure entry link for the client secret so the secret never passes through the chat. It needs a connection in byo_app mode with a specific tenant id (not organizations or common), and your account must be allowed to register apps in that tenant: the default “Users can register applications” setting, or the Application Developer or Application Administrator role. If your tenant blocks the consent, the tool says an admin must approve it, and byo_app_requirements gives you the manifest and the two scripts above instead. validate_byo_app then checks the stored credential by acquiring a token. See the admin MCP server for the full tool list.

Use a certificate. It is the default in SPSync’s own forms and in the scripts it generates. A certificate keeps its private key on your side and only the public half goes to Entra, it carries its own expiry date (SPSync reads it from the certificate), and you can add a new one beside the old one before the old one ends. A secret is a password that sits in an environment variable, lasts at most 24 months in the portal, and replaces the old value the moment you change it. Either works, and PnP PowerShell’s app-only mode requires a certificate.

Let the generated script do it

The script SPSync generates for your app registration (byo_app_requirements in the admin MCP server, or the Azure CLI and PowerShell blocks in the app) already creates a self-signed certificate valid for two years, uploads it, and prints the thumbprint. It writes the private key and the certificate together to spsync-app.pem. Paste that file’s contents on the Bring your own app form (or load the file there), and SPSync takes the thumbprint and the expiry date from the certificate, so the thumbprint box can stay empty. In an AI assistant, the same PEM goes through a secure entry link, never into the chat. The PowerShell version asks for AppRegistration.Create and Application.Read.All only, not Application.ReadWrite.All.

Create a self-signed certificate with openssl

Terminal window
openssl req -x509 -newkey rsa:2048 -nodes -days 730 \
-keyout spsync.key -out spsync.crt -subj "/CN=SPSync"
cat spsync.key spsync.crt > spsync-app.pem # key and certificate together
openssl x509 -in spsync.crt -noout -fingerprint -sha1 | cut -d= -f2 | tr -d : # the thumbprint

The thumbprint is 40 hex characters with no colons, the value the Entra portal shows.

Or with PowerShell on Windows

Terminal window
$cert = New-SelfSignedCertificate -Subject "CN=SPSync" -CertStoreLocation Cert:\CurrentUser\My `
-KeyExportPolicy Exportable -KeySpec Signature -NotAfter (Get-Date).AddYears(2)
Export-Certificate -Cert $cert -FilePath spsync.cer
$cert.Thumbprint
# Export the private key as PEM (PowerShell 7, .NET 7+); this file goes in SPSYNC_APP_KEY
Set-Content spsync.key $cert.PrivateKey.ExportPkcs8PrivateKeyPem()

On Windows PowerShell 5, export a PFX with Export-PfxCertificate, then run openssl pkcs12 -in spsync.pfx -nocerts -nodes -out spsync.key to get the PEM.

Upload the public file (spsync.crt or spsync.cer) under the app’s Certificates & secrets, then Certificates, then Upload. The thumbprint is shown next to it. SPSync needs the private key (PEM) and the thumbprint, each in an environment variable on the CLI. In the app, a PEM that also holds the certificate needs no separate thumbprint.

Tenants that require subject name and issuer authentication

Some tenants only accept a certificate when the token request carries the certificate chain (x5c). Tick Send the certificate chain on the form, or pass send_x5c. This needs the certificate in the PEM, not only the key.

If you use a secret, create it under Client secrets and copy the Value at once; it is shown only once. Pasting the secret’s ID instead of its Value is the most common mistake. Type its expiry date into the form too (it is not readable from the secret itself), or SPSync cannot warn you.

Expiry warnings and renewal

SPSync records when the credential ends. Inside 30 days the Connect screen shows a warning, spsync doctor prints a WARNING on the credential-expiry check, and the admin MCP server’s describe and validate_byo_app report it. After it ends, runs fail with AADSTS7000222 (secret) or AADSTS700027 with “key used is expired” (certificate), and SPSync says so in plain words.

To renew without a gap: add the new certificate to the app registration (keep the old one), give SPSync the new private key, check that a run works, then delete the old certificate in Entra. SPSync reads the stored certificate on every run, so you do not restart anything. The tenant script infra/tenant/01-Create-SpsyncApp.ps1 -RotateCertificate does the same in order: add, wait -OverlapMinutes (15 by default), then remove the old one.

Consent only says the app may hold Sites.Selected. Each site still needs its own grant. Run these as someone with Sites.FullControl.All, or as a site owner for Sites.Selected with Full Control.

One site, with PnP PowerShell. Grant-PnPEntraIDAppSitePermission is the current name. Older posts and scripts use the alias Grant-PnPAzureADAppSitePermission; both work.

Terminal window
Connect-PnPOnline -Url https://contoso-admin.sharepoint.com -Interactive
Grant-PnPEntraIDAppSitePermission -AppId "<client-id>" -DisplayName "SPSync" `
-Site https://contoso.sharepoint.com/sites/finance -Permissions Read

SPSync’s Permissions wizard offers Read or Write and defaults to Read. A user sign-in holds Sites.Read.All only, so for those connections the wizard skips the grant and prints these commands at once with the reason. After it grants, it reads the stored role back and corrects it if Graph stored something other than the role you chose.

Use -Permissions Read for replication. Write and Manage also exist and SPSync does not need them, because it never writes to SharePoint.

One site, with Microsoft Graph PowerShell

Terminal window
Connect-MgGraph -Scopes Sites.FullControl.All
New-MgSitePermission -SiteId "<site-id>" -Roles @("read") `
-GrantedToIdentities @(@{ Application = @{ Id = "<client-id>"; DisplayName = "SPSync" } })

One list. PnP has no cmdlet for this, so call Graph directly. It needs the app to hold Lists.SelectedOperations.Selected.

Terminal window
Connect-MgGraph -Scopes Sites.FullControl.All
$body = @{ roles = @("read"); grantedToIdentities = @(@{ application = @{ id = "<client-id>"; displayName = "SPSync" } }) } | ConvertTo-Json -Depth 5
Invoke-MgGraphRequest -Method POST `
-Uri "https://graph.microsoft.com/v1.0/sites/<site-id>/lists/<list-id>/permissions" -Body $body

Warning. A list-level grant breaks that list’s permission inheritance from its site. Site-level grants do not. Decide before you run it, and tell the list’s owners.

Check a grant. Run spsync doctor replication.yaml; its graph-permissions check reads the site with the app’s own credential. In the desktop app, the Permissions wizard shows the same state. Directly, GET https://graph.microsoft.com/v1.0/sites/<site-id>/permissions lists the apps granted on a site. The <site-id> is the hostname,siteGuid,webGuid form.

CLI, one-off.

Terminal window
spsync auth login --client-id <client-id> --tenant <tenant-id>

For app-only calls name environment variables, never the value itself:

Terminal window
# certificate
export SPSYNC_APP_KEY="$(cat spsync.key)"
export SPSYNC_APP_THUMBPRINT="<thumbprint>"
spsync discover sites --client-id <client-id> --tenant <tenant-id> --all \
--certificate-private-key-env SPSYNC_APP_KEY \
--certificate-thumbprint-env SPSYNC_APP_THUMBPRINT
# or a secret
spsync discover sites --client-id <client-id> --tenant <tenant-id> --all \
--client-secret-env SPSYNC_APP_CLIENT_SECRET

Pass exactly one kind of credential, and always pass --tenant: the client-credentials grant has no common authority to fall back on. --all without --filter needs an app-only credential and a tenant-wide permission. With only Sites.Selected, use --site-url instead.

CLI, in a replication file.

auth:
mode: byo_app
tenant_id: <tenant-id>
client_id: <client-id>
certificate_private_key_env: SPSYNC_APP_KEY
certificate_thumbprint_env: SPSYNC_APP_THUMBPRINT
# or: client_secret_env: SPSYNC_APP_CLIENT_SECRET

Set exactly one of the secret or the certificate pair.

Desktop. On the Connect screen choose the bring-your-own-app option, enter the tenant id and client id, and supply the secret or certificate. SPSync acquires a token with it before saving anything, so a wrong secret fails there.

Azure managed application. The deployment has an authClientId parameter. Set it to your own registration for delegated sign-in: add the redirect URI https://<worker-url>/auth/callback (web platform), grant the delegated Sites.Read.All, User.Read and offline_access, and redeploy. Do not grant app roles such as Sites.Selected to authClientId expecting unattended sync. It feeds delegated sign-in only and the roles have no effect. Unattended sync is set up per connection, with that connection’s own app registration as above.

Admin MCP server. create_connection with auth_mode=byo_app, then create_app_registration or byo_app_requirements, grant_site_access, validate_byo_app and permissions_status. Tools that need a person, such as sign-in, consent and grants, stop for one.

Sign-in goes through Microsoft Entra ID at login.microsoftonline.com, so Conditional Access policies for the user, the device or the client app apply as they do to any other application. SPSync does not implement or bypass them. With your own app registration the sign-in stays inside an app your tenant controls, which is what Tenant Restrictions v2 needs. We have not tested this against a live policy-restricted tenant, so try your policy in a test tenant first.

If a proxy re-signs TLS traffic, point SPSYNC_CA_BUNDLE at a PEM file. SPSync then trusts only the certificates in that file, not the system roots, so the file must hold every root needed for the hosts you reach (your proxy’s root, and any direct routes).

Allow outbound HTTPS to graph.microsoft.com and login.microsoftonline.com. A deployment also needs the other hosts listed in the Azure deployment guide.

A US Government or China tenant signs in and calls Graph on different hosts. Choose the cloud when you add the connection (the Microsoft cloud list on the Connect screen), or pass --cloud to the CLI (or set SPSYNC_CLOUD), or set auth.cloud in the config file. The flag or variable also fills in a config file that has no auth.cloud; a value written in the file wins.

Cloudcloud valueSign-in hostGraph hostSharePoint domain
Commercial (default)globallogin.microsoftonline.comgraph.microsoft.comsharepoint.com
US Government GCC Highus_govlogin.microsoftonline.usgraph.microsoft.ussharepoint.us
US Government DoDus_gov_dodlogin.microsoftonline.usdod-graph.microsoft.ussharepoint-mil.us
Microsoft 365 operated by 21Vianet (China)chinalogin.chinacloudapi.cnmicrosoftgraph.chinacloudapi.cnsharepoint.cn

Your own app registration must be created in that cloud’s Entra admin centre; an app made in the commercial cloud does not exist there. Allow outbound HTTPS to that cloud’s hosts instead of the commercial ones. Sovereign-cloud support is built from Microsoft’s published endpoints and tested against a simulated Graph; it has only been verified against a simulated Graph, not a real sovereign tenant, so try it in a test tenant first.

Corporate proxy and private certificate authority

Section titled “Corporate proxy and private certificate authority”

If your network sends traffic through a proxy, or a proxy re-signs HTTPS with your organisation’s own certificate authority, tell SPSync explicitly:

  • SPSYNC_HTTPS_PROXY: the proxy URL, for example http://proxy.example.com:8080.
  • SPSYNC_CA_BUNDLE: the path to a PEM file holding your organisation’s CA certificate(s).

In the desktop app, set both in Settings, Network. For the CLI, export them in the environment. Both apply to sign-in and every Graph call. Left blank, SPSync uses the computer’s own proxy settings (HTTPS_PROXY). spsync doctor prints the proxy and CA bundle in effect, and a certificate failure caused by a re-signing proxy names SPSYNC_CA_BUNDLE as the setting to fix. Proxies for database destinations are configured on the destination itself, not here.

Where tokens are kept differs by edition. The CLI stores its token cache in the operating system keychain. The desktop app keeps the token cache and any client secret in its local control store, spsync.db, which is created readable by the owner only on macOS and Linux. Encrypting those values at rest is planned work, so treat the machine and its backups as holding secrets.

ErrorUsual cause and fix
AADSTS65001, AADSTS90094, AADSTS650053, AADSTS70011, AADSTS65005Consent is missing or user consent is blocked. A tenant admin approves the app with the admin consent URL above.
AADSTS7000215Wrong client secret. Use the secret’s Value, not its ID.
AADSTS7000222The client secret has expired. Create a new one.
AADSTS700016No app with that client id in the tenant. Check the client id and tenant id.
AADSTS90002The tenant was not found. Check the tenant id or domain.
AADSTS700027The certificate does not match the one on the app. Upload the public part.
AADSTS700024The signed assertion is outside its valid time range, usually a wrong clock on the SPSync machine.
AADSTS50034The account does not exist in the tenant.

403 after consent. Consent for Sites.Selected grants no site. Run the site grant above, then spsync doctor. A grant can take a few minutes to take effect.

403 on getAllSites. Delegated sign-in always gets this, whatever the app holds. Use --filter or --site-url, or an app-only credential with a tenant-wide permission. An app that holds only Sites.Selected cannot list sites either.

More fixes are on the troubleshooting page.