Add work-account or Apple sign-in
Prompt:
Let our staff sign in with their work accounts.
Also works:
- "Only people at our company should be able to use the app."
- "Add Sign in with Apple to the sign-in page."
What your tool does
- Reads the
add-sign-inskill (read_context, idskill:add-sign-in) and follows it. - Stores the secret or key by name with
store_secret, as Store a secret describes, so its value never passes through your tool. - Sends the
entraorapplemembers with the route, throughconfigure_realmor the manifest'srealmmember. - Gives you the callback address the answer returns, for you to list in the company's app registration or Apple's Services ID.
- Asks you for the registration's details, listed under What you need, and for the name to store the client secret or signing key under. It never asks for the secret itself.
What you need
- For work accounts: an app registration in the company's Microsoft Entra tenant, with its tenant identifier (a GUID), its client identifier, and a client secret. You add a redirect URI and two optional claims to it in step 2.
- For Sign in with Apple: in your Apple developer account, a Services ID, a Sign in with Apple key as its
.p8file, its key identifier, and your team identifier.
Before your AI starts
This section is for your AI tool: what it checks and gathers before it begins. You don't need to do these steps yourself.
- A connected, signed-in tool (Connect Claude Code or Codex).
- The application's identifier from
list_applications, and a manifest that declares the accounts service (Add sign-in to your app). - Whether the realm is invitation-only: the work-account route cannot be used with invitation-only sign-up (Make sign-up invitation-only).
Steps
1. Store the client secret or the signing key
Store the work-account client secret, or the Apple key's .p8 file, by name at the application's scope in the realm's environment, as A key that a sign-in method uses describes. Development, once turned on, takes a second call with environment: "development".
configure_realm then moves the value into the realm vault, where it stays under that name and only the accounts service reads it. You rotate it by name as before. After a change of name, the earlier name stays in the realm vault. No upstream declaration can use the name, and a name an upstream of this application already uses is refused.
2. Add work-account sign-in
A work account is a person's Microsoft Entra account at one company. Call configure_realm with entra and with entra in sign_in_methods. A test checks these members against the action's request.
{
"application": "<application id>",
"sign_in_methods": ["entra"],
"entra": {
"tenant": "00000000-0000-4000-8000-000000000001",
"client_id": "00000000-0000-4000-8000-000000000002",
"client_secret_name": "entra-client-secret"
}
}
tenantis the company's tenant identifier, a GUID;client_idis the client identifier of the app registration in that tenant;client_secret_nameis the name step 1 stored the client secret under.
Saving entra stores the work-account details, and the route appears on the sign-in page once sign_in_methods includes entra. The list replaces the current one, so name the other methods beside entra to keep them (Offer other sign-in methods).
In the app registration, add the address in callbacks.entra as a web redirect URI, exactly as returned. read_realm and configure_realm both return it once the manifest declares the accounts service, even before the route is set up. A submit_manifest whose realm member names entra returns it too, as realm.entra_callback. The address is on the platform's own origin: https://turnzero.ai/auth/entra/callback on the platform at https://turnzero.ai, and https://dev.turnzero.ai/auth/entra/callback on Turn Zero's development platform. It is never your application's hostname, and both environments of every application use it.
In the app registration's Token configuration, add the optional claims email and xms_edov to the ID token. A work-account user's address is the email claim the tenant sends. It is verified only where the token also carries xms_edov as true, meaning the address's domain owner is verified, and unverified otherwise. The address comes marked source: "tenant", and a route that needs a verified address reads verified first.
A verified work address is one account's address. At its first sign-in, a person whose address another sign-in method already holds is asked to link, not given a second account. A work account whose verified address another account already holds can sign in with that address unverified. That happens on a realm without the emailed code, and where the other account holds the address through a work account alone. A code sent to an address that only a work account holds verified asks the person to sign in with that work account to link.
The work-account route cannot be used with invitation-only sign-up, whether the realm's creation or the invited audience makes it so.
3. Allow only one company's staff
To let only one company's staff reach the application, declare the workforce audience with that tenant and the realm's entra route together. The audience decides who may reach the app, and the realm's entra route, set up with the same tenant, is what signs them in, so declare both.
A workforce audience allows only the entra route of its declared tenant, so its realms offer no other method, and configure_realm refuses email there. The platform refuses a request without a session before it reaches your code, and sends a browser to the sign-in page. A submission declaring the audience without the route is still accepted, because configure_realm can add the route later. Until then no one can sign in, and the response's detail names each realm still missing the route.
A test validates the sample below against the manifest schema.
{
"manifest_version": 1,
"services": [{ "kind": "accounts" }],
"health": "/health",
"region": "usa",
"egress": [],
"audience": { "kind": "workforce", "tenant": "00000000-0000-4000-8000-000000000001" },
"packages": [],
"realm": {
"sign_in_methods": ["entra"],
"entra": { "tenant": "00000000-0000-4000-8000-000000000001", "client_id": "00000000-0000-4000-8000-000000000002", "client_secret_name": "entra-client-secret" }
}
}
The manifest's realm member sets the route on every realm the application has at each submission, and a client secret not stored yet is accepted there (Configure sign-in and push in the manifest). Sign in from a native app compares public and workforce for a mobile backend.
4. Add Sign in with Apple
Call configure_realm with apple and with apple in sign_in_methods:
services_id, the Services ID the web sign-in uses;team_idandkey_id, each ten characters;key_secret_name, the name step 1 stored the signing key under.
The platform reads the key only to sign Apple's client secret. The apple route signs people in on the sign-in page with Apple's own pages. It can be used with invitation-only sign-up, because Apple verifies the address.
Apple posts its response back to the platform's callback, the address read_realm returns in callbacks.apple. The Services ID lists that address, exactly as returned, as its return URL, and the address's host as its domain. The address is on the platform's own origin: https://turnzero.ai/auth/apple/callback on the platform at https://turnzero.ai, and https://dev.turnzero.ai/auth/apple/callback on Turn Zero's development platform. It is never your application's hostname, and both environments of every application use it.
A person may hide their address behind Apple's private relay, which the realm accepts as any verified address. For a relay address to receive the platform's mail, register donotreply@turnzero.ai and its domain, turnzero.ai, with Apple's private email relay for your team. A native app can also exchange Apple's own ID token for a session (Sign in from a native app).
Expected result
configure_realm returns the realm with its entra or apple settings, never the secret, and callbacks, the two addresses the registrations list. Its detail says the route is set, and that it is offered once sign_in_methods includes it. The sign-in page then shows the new route. A work-account user signs in with the company's account, and GET /__account/me returns their address marked "source": "tenant". Under the workforce audience, a visitor without a session is sent to the sign-in page.
Refusals
This table lists the refusals these steps most often meet; the refusals page lists every refusal with its cause and remedy.
Where the manifest's realm member sets the credential, submit_manifest or create_environment returns custody_entry_missing, custody_entry_unmovable, and name_bound_to_setting instead. The manifest stays recorded. Your tool then stores the value again, stores it under a new name that the manifest names, or removes the binding, and submits the manifest again.
| Refusal | Status | Cause | Remedy |
|---|---|---|---|
custody_entry_missing |
409 | configure_realm named a client secret or Apple signing key that is not stored at the application's scope in the realm's environment. |
Store it with store_secret, as Store a secret describes, naming the application and environment, then call configure_realm again. |
platform_minted_name |
409 | configure_realm named a credential the platform minted, credential-<application id>, database-<application id>, or issue-tracking-<application id>, as the client secret or the Apple signing key. |
Store the client secret or the key under your own name with store_secret, as Store a secret describes, and name that. |
name_bound_to_upstream |
409 | configure_realm named, as the client secret or the Apple signing key, a secret that an upstream of this application uses. |
Store the client secret or the key under another name at the application's scope, and name that. |
name_bound_to_push |
409 | configure_realm named, as the client secret or the Apple signing key, a secret that a push configuration of this application uses as a provider credential. |
Store the client secret or the key under another name at the application's scope, and name that. |
name_bound_to_setting |
409 | configure_realm named a client secret or Apple signing key that a setting binds: the manifest's settings, the running copy of either environment, or a deploy, promote, or redeploy in flight to one. The detail names the setting and where it is bound. |
Store the client secret or the key under another name at the application's scope, and name that. A running copy keeps its binding until that environment's next deploy or promote of a manifest without it, and an action in flight keeps it until the action ends. |
realm_vault_unregistered |
409 | The platform has no realm vault set up for the application's hosting cell, so the client secret or the Apple signing key has nowhere to move. | Report it to platform staff; nothing on your side fixes it. |
custody_entry_unmovable |
409 | The realm vault already has an entry under the name of the client secret or the Apple signing key, live or soft-deleted, at another version. Nothing moved. | Store the value under a new name with store_secret, as Store a secret describes, and name that. |
invited_realm_refuses_work_account |
409 | configure_realm combined invitation-only creation, by creation: "invited" or by the invited audience, with the entra route. |
Leave entra out of sign_in_methods. Where the realm's own creation makes it invitation-only, set creation: "open" under a public audience. |
declared_tenant_contradicts_route |
409 | The workforce audience gives a different tenant from the work-account route of one of the application's realms, and the refusal's environment names which. |
Declare that route's tenant, or call configure_realm for the named environment with the audience's tenant. |
route_set_contradicts_audience |
409 | Under a workforce audience, configure_realm named a route such as email, or an entra.tenant other than the declared one. |
Name entra alone with the declared tenant, or change the audience and resubmit the manifest. |
manifest_owned_field |
409 | configure_realm would change entra, apple, or sign_in_methods while the manifest's realm member names it. |
Change the field in the manifest and submit it, or remove it from the realm member first (Configure the realm). |
Related
- Add sign-in to your app turns sign-in on and tells your backend which user sent a request.
- Store a secret stores the client secret and the signing key by name.
- Manage end users lists every realm setting and which ones the manifest owns.
- Make sign-up invitation-only covers the other audience that limits who may reach the app.
- Sign in from a native app signs in a mobile app's users, and exchanges Apple's own ID token.
- Build a mobile app's backend joins these steps for a mobile backend that signs in one company's staff.
- configure_realm in the generated reference gives the
entraandapplemembers' exact forms.