Skip to content

Identifying your app

Before your app can ask a user for access, ShiftSync needs to know who it is. There are two ways to arrange that, and you pick one.

Section titled “Option 1 — a client ID metadata document (recommended)”

Host a JSON document at an HTTPS URL you control, and use that URL as your client_id. There is no registration step and nothing to store: ShiftSync fetches the URL when your app first sends a user to authorise, and reads your app’s details from it.

This is the mechanism the MCP specification now prefers, defined by draft-ietf-oauth-client-id-metadata-document. Because your identity is a URL you host, the same client_id works against any authorization server that supports it — there is nothing to re-register when you add one.

Check that ShiftSync supports it before you rely on it:

GET https://api.shiftsync.app/.well-known/oauth-authorization-server
{
"client_id_metadata_document_supported": true
}
{
"client_id": "https://mcp.example.com/oauth/client.json",
"client_name": "Example MCP Client",
"client_uri": "https://mcp.example.com",
"logo_uri": "https://mcp.example.com/logo.png",
"redirect_uris": ["http://127.0.0.1:3000/callback"],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}

client_id, client_name and redirect_uris are required. client_id must be exactly the URL the document is served from — byte for byte, same case, same spelling. Anything else is refused.

  • HTTPS, with a path (https://example.com/client.json, not https://example.com).
  • No fragment, no query string, no . or .. path segments, no username or password.
  • Already in canonical form. https://Example.com/c.json and https://example.com:443/c.json are refused — write them the way a URL parser would.
  • At most 512 characters.

Two identifiers that differ in any way are two different apps, with separate user grants. Pick one spelling and keep it.

  • Exactly one request, with Accept: application/json. Redirects are not followed — serve the document at the URL itself, with a 200 and a JSON content type.
  • At most 5 kB is read.
  • Private, loopback and link-local addresses are refused.

The result is cached, honouring your Cache-Control (s-maxage first, then max-age) or Expires, clamped to between 5 minutes and 24 hours. A static file on a CDN is exactly right. If you change the document, allow up to your cache lifetime for it to take effect — and note that changing your redirect_uris while a user is midway through authorising will not affect that in-flight authorisation.

Field Limit
Whole document 5 kB
redirect_uris 5 entries, 512 characters each
client_name 255 characters
logo_uri, client_uri HTTPS only
scope Every value must be a scope ShiftSync issues

scope is optional. Leave it out and your app may request any scope — which is usually what you want for a document used against several authorization servers.

The document is public, so it can never carry a secret. client_secret, client_secret_expires_at and any client_secret_* value for token_endpoint_auth_method are refused outright.

private_key_jwt with jwks/jwks_uri is the specification’s answer for a confidential client. ShiftSync does not support it yet and refuses a document that declares it, with "private_key_jwt is not supported yet". Use "token_endpoint_auth_method": "none" and PKCE, which is mandatory regardless.

Use the App console to create an app and get a client_id (and, for a confidential app, a client_secret). This is the right choice when you want an app you manage from a UI, with an icon and a fixed set of scopes.

Dynamic Client Registration (POST /oauth/register, RFC 7591) also still works and is still advertised, for clients that cannot host a document. The MCP specification deprecates it in favour of the option above.

Whichever option you pick, the redirect_uri on an authorization request must match one you declared, by exact string comparison — except for a loopback URI over plain HTTP (http://127.0.0.1…, http://[::1]…, http://localhost…), where any port is accepted, as RFC 8252 §7.3 requires. A native app can therefore declare http://127.0.0.1:3000/callback and bind whatever port is free at runtime. Scheme, host, path and query must still match exactly.

A rejected authorization request returns 400 with an OAuth error body, and — for a metadata document problem — an error_description naming the cause:

{
"error": "invalid_request",
"error_description": "client_id in the metadata document does not match the requested client_id"
}

Others you may see: client_id is not a valid client identifier URL, the client_id metadata document could not be retrieved, the client_id metadata document is not valid JSON, the client_id metadata document is missing required fields, the client_id metadata document contains invalid client metadata, client_secret must not appear in a client_id metadata document, and temporarily unable to retrieve the client_id metadata document (back off and retry).