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-in skill (read_context, id skill: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 entra or apple members with the route, through configure_realm or the manifest's realm member.
  • 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 .p8 file, 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.

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"
  }
}
  • tenant is the company's tenant identifier, a GUID;
  • client_id is the client identifier of the app registration in that tenant;
  • client_secret_name is 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_id and key_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).