---
title: "Apple Auth"
method: POST
path: "/api/auth/apple"
tags: ["auth"]
---

# Apple Auth

`POST /api/auth/apple`

## Request

- Base URL: `https://api.stellara.natlex.it` (the document declares no server; this is the origin it was published from)
- URL: `https://api.stellara.natlex.it/api/auth/apple`
- Auth: none declared

Authenticate via Apple Sign-In identity_token (iOS-only flow).

Lookup priority (this order is load-bearing — see CLAUDE.md
"Apple Sign-In" section + S5 in docs/STELLARA_IOS_APP_STORE_SETUP.md):

  1. ``users.apple_sub`` exact match — STABLE across sign-ins; this
     is the only reliable identifier for "same Apple ID, second
     sign-in" since Apple omits ``email`` + ``fullName`` after the
     first authorization for a given (App ID, Apple ID) pair.

  2. ``users.email`` exact match (only when the JWT carries a
     VERIFIED ``email`` claim — ``body.email`` is never consulted,
     see ``_verified_email_from_claims``) — links Apple Sign-In to
     a pre-existing email/password OR Google account on the same
     email. Persists ``apple_sub`` on the existing row so step 1
     wins on subsequent sign-ins.

  3. INSERT a new user row with both ``email`` and ``apple_sub`` if
     neither lookup succeeded. ``email`` MUST be present here —
     otherwise we hit the S5 edge case (user revoked the OAuth grant
     on iPhone Settings, tries again, Apple returns a brand new
     ``sub`` with NULL email) and return 400 ``APPLE_NEW_SUB_NO_EMAIL``
     so the client can show the "Delete Stellara from Apple ID
     settings + retry" guidance.

After the user row is resolved, opportunistically exchange
``authorization_code`` for a ``refresh_token`` (Apple ``/auth/token``)
so a future ``DELETE /api/users/me`` can revoke the OAuth grant.
Failure is logged but does NOT fail this auth request — the user
signs in successfully even if Apple's token endpoint is down.

## Request body

- AppleAuthRequest: Apple Sign-In payload from `sign_in_with_apple` Flutter plugin. `identity_token` is required (the JWT we verify against Apple JWKS). `authorization_code` is optional but should always be sent — Flutter must include it so we can exchange for a `refresh_token` and persist it for revoke-on-delete (Apple Guideline 5.1.1(v)). Without it, the user signs in fine but their account stays "un-revokable" — the Stellara entry in iPhone Settings → Apple ID → Apps using Apple ID won't auto-clear on Delete Account (a soft compliance miss; Apple sometimes flags this on review). `email` and `full_name` are present ONLY at the FIRST authorization for a given (App ID, Apple ID) pair. On subsequent sign-ins, Apple returns identity_token only — `apple_sub` (the JWT's `sub` claim) is the only stable identifier that persists across sessions. `email` is accepted for wire-compat with the Flutter client but is NEVER used for account lookup or linking — the backend takes the email exclusively from the Apple-signed JWT (`claims["email"]` + `email_verified`). Trusting a client-supplied email here would let a caller with any valid identity_token attach their `apple_sub` to an arbitrary existing account. `full_name` is display-only.
  - `identity_token` string, required
  - `authorization_code` string, nullable
  - `email` string, email, nullable
  - `full_name` string, nullable

## Response `200`

Successful Response

- TokenResponse
  - `access_token` string, required
  - `refresh_token` string, required
  - `token_type` string
  - `is_new_user` boolean

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/natlex/apis/stellara-api.md) · [All operations](https://skmtc.dev/natlex/apis/stellara-api/llms.txt) · [OpenAPI document](https://skmtc.dev/natlex/apis/stellara-api/revisions/e576f046b61f?raw)
