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.
Choose a mode
Section titled “Choose a mode”There are three ways in.
| Delegated | Application, SPSync’s shared app | Application, your own app registration | |
|---|---|---|---|
| Runs as | You, with your own SharePoint access | SPSync’s multi-tenant app, with its own certificate and no user signed in | An app registration you own, with no user signed in |
| Setup | spsync auth login, then consent | A tenant admin consents, an admin grants sites, then “Switch to unattended (application) mode” in the permissions wizard | Register an app, add a credential, consent, grant sites |
| Keeps running when you are away | Until your refresh token expires, about 90 days idle | Yes | Yes |
| Can list every site in the tenant | No, delegated calls to getAllSites return 403; SPSync falls back to a search=* query | No, Sites.Selected reads only granted sites; use --site-url | Only with a tenant-wide permission such as Sites.Read.All |
| Good for | Trying SPSync, one person’s reports | Scheduled, unattended replication without registering anything | Scheduled, 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.
Delegated sign-in
Section titled “Delegated sign-in”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.
Register your own app (service principal)
Section titled “Register your own app (service principal)”Do this when you want unattended sync, or when your organisation requires every integration to use its own registration.
In the Azure portal
- Open Microsoft Entra admin center, then App registrations, then New registration.
- Name it (for example “SPSync”), choose Accounts in this organizational directory only (single tenant), and register. Skip the redirect URI for app-only use.
- Copy the Application (client) ID and Directory (tenant) ID from the overview.
- Under API permissions, add Microsoft Graph application permissions, narrowest first:
Lists.SelectedOperations.Selected(per list),Sites.Selected(per site), andSites.Read.Allonly as a last resort because it covers every site. - 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:
GRAPH=00000003-0000-0000-c000-000000000000APP_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/nullaz ad app permission admin-consent --id "$APP_ID" # needs a tenant adminecho "Client id: $APP_ID"With Microsoft Graph PowerShell
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.
Certificate or secret
Section titled “Certificate or secret”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
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 togetheropenssl x509 -in spsync.crt -noout -fingerprint -sha1 | cut -d= -f2 | tr -d : # the thumbprintThe thumbprint is 40 hex characters with no colons, the value the Entra portal shows.
Or with PowerShell on Windows
$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_KEYSet-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.
Grant access to sites and lists
Section titled “Grant access to sites and lists”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.
Connect-PnPOnline -Url https://contoso-admin.sharepoint.com -InteractiveGrant-PnPEntraIDAppSitePermission -AppId "<client-id>" -DisplayName "SPSync" ` -Site https://contoso.sharepoint.com/sites/finance -Permissions ReadSPSync’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
Connect-MgGraph -Scopes Sites.FullControl.AllNew-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.
Connect-MgGraph -Scopes Sites.FullControl.All$body = @{ roles = @("read"); grantedToIdentities = @(@{ application = @{ id = "<client-id>"; displayName = "SPSync" } }) } | ConvertTo-Json -Depth 5Invoke-MgGraphRequest -Method POST ` -Uri "https://graph.microsoft.com/v1.0/sites/<site-id>/lists/<list-id>/permissions" -Body $bodyWarning. 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.
Use it in SPSync
Section titled “Use it in SPSync”CLI, one-off.
spsync auth login --client-id <client-id> --tenant <tenant-id>For app-only calls name environment variables, never the value itself:
# certificateexport 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 secretspsync discover sites --client-id <client-id> --tenant <tenant-id> --all \ --client-secret-env SPSYNC_APP_CLIENT_SECRETPass 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_SECRETSet 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.
Enterprise controls
Section titled “Enterprise controls”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.
Government and China clouds
Section titled “Government and China clouds”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.
| Cloud | cloud value | Sign-in host | Graph host | SharePoint domain |
|---|---|---|---|---|
| Commercial (default) | global | login.microsoftonline.com | graph.microsoft.com | sharepoint.com |
| US Government GCC High | us_gov | login.microsoftonline.us | graph.microsoft.us | sharepoint.us |
| US Government DoD | us_gov_dod | login.microsoftonline.us | dod-graph.microsoft.us | sharepoint-mil.us |
| Microsoft 365 operated by 21Vianet (China) | china | login.chinacloudapi.cn | microsoftgraph.chinacloudapi.cn | sharepoint.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 examplehttp://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.
Troubleshooting
Section titled “Troubleshooting”| Error | Usual cause and fix |
|---|---|
AADSTS65001, AADSTS90094, AADSTS650053, AADSTS70011, AADSTS65005 | Consent is missing or user consent is blocked. A tenant admin approves the app with the admin consent URL above. |
AADSTS7000215 | Wrong client secret. Use the secret’s Value, not its ID. |
AADSTS7000222 | The client secret has expired. Create a new one. |
AADSTS700016 | No app with that client id in the tenant. Check the client id and tenant id. |
AADSTS90002 | The tenant was not found. Check the tenant id or domain. |
AADSTS700027 | The certificate does not match the one on the app. Upload the public part. |
AADSTS700024 | The signed assertion is outside its valid time range, usually a wrong clock on the SPSync machine. |
AADSTS50034 | The 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.