# Apple Sign-In

Exchange a Sign in with Apple identity token for Aurea tokens. The linked user is signed in, or a new user is created on first use.

## Overview

Your app obtains an Apple identity token and sends it here. Aurea verifies it and returns the same token set as [Login](https://docs.aureahub.com/docs/auth-login.md): `accessToken`, `refreshToken`, `expiresIn` and `user`, plus `isNewUser: true` when this call created the account.

- **Tenant.** The user belongs to the tenant identified by the `x-tenant-api-key` signing header. A `tenantApiKey` body field is accepted but does not change the tenant.
- **Tenant configuration.** The tenant's configuration must contain an Apple *bundle ID*. Without one the call returns `400` *Apple Sign-In is not enabled for this tenant*.
- **Verification.** The token's signature is checked against Apple's public keys (`https://appleid.apple.com/auth/keys`). The issuer must be `https://appleid.apple.com` and the audience must be the tenant's bundle ID. Expiry is checked with 30 seconds of clock tolerance.
- **Email.** If the token carries an email with `email_verified` set to false, the call returns `422`.
- **No wallet** is created by this endpoint.

> ⚠️ This endpoint requires **tenant request signing**: send `x-tenant-api-key`, `x-timestamp` and `x-signature` as described in [Authentication](https://docs.aureahub.com/docs/authentication.md). It is rate-limited to **20 requests per minute per client IP**.

## Endpoint

### `POST /v1/auth/apple`

Verifies an Apple identity token, then signs in the linked user or creates a new one. Requires tenant HMAC signing headers.

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `idToken` | string | yes | Identity token from Sign in with Apple, issued for the tenant's configured bundle ID |
| `tenantApiKey` | string | no | Accepted (at least 32 characters) but not used: the tenant comes from the x-tenant-api-key header |

**Responses**

`200` OK

```json
{
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
  "expiresIn": 900,
  "user": {
    "id": "<user-id>",
    "username": "apple_001234ab",
    "email": "alice@example.com",
    "qrCode": "<qr-code>",
    "role": "user",
    "createdAt": "2026-09-01T10:00:00.000Z"
  },
  "isNewUser": true
}
```

`400` Not Enabled

```json
{ "error": "BadRequestError", "message": "Apple Sign-In is not enabled for this tenant" }
```

`401` Invalid Token

```json
{ "error": "UnauthorizedError", "message": "Invalid Apple identity token" }
```

`422` Email Not Verified

```json
{ "error": "ValidationError", "message": "Apple account email is not verified" }
```

`429` Too Many Requests

```json
{ "statusCode": 429, "error": "Error", "message": "Rate limit exceeded, retry in 1 minute" }
```

**Example request**

```bash
curl -X POST https://api.aureahub.com/v1/auth/apple \
  -H "Content-Type: application/json" \
  -H "x-tenant-api-key: <your-tenant-api-key>" \
  -H "x-timestamp: <unix-seconds>" \
  -H "x-signature: <hex-hmac-sha256>" \
  -d '{"idToken":"<apple-identity-token>"}'
```

## Account Matching

1. **Linked account.** If the Apple account (the token's `sub`) is already linked to an active user in the tenant, that user is signed in.
2. **New account.** Otherwise a user is created with no password. The username is `apple_` followed by the first 8 letters and digits of the Apple user ID; if that username is taken, `_1`, `_2`, … is appended. The token's email is stored when present. The response includes `isNewUser: true`.

Apple sign-in does not look up existing users by email, so it never links to an account created with a password or with Google. `isNewUser` is omitted when an existing user was signed in. Users created here cannot use password [Login](https://docs.aureahub.com/docs/auth-login.md) (`401` *This account uses social sign-in*).

## Errors

On this endpoint, `400`, `401` and `422` bodies contain only `error` and `message`; `404` and `429` use the full envelope described in [Error Handling](https://docs.aureahub.com/docs/errors.md).

| Status | message | When |
| --- | --- | --- |
| 400 | `Request validation failed` | idToken is missing or empty. |
| 400 | `Apple Sign-In is not enabled for this tenant` | The tenant has no Apple bundle ID configured. |
| 401 | `Invalid Apple identity token` | Verification failed: for example a bad signature, the wrong issuer or audience, an expired token, or no subject. |
| 401 | `Missing required HMAC headers: x-tenant-api-key, x-timestamp, x-signature` | Signature problem. Other messages: Invalid x-timestamp header, Request expired, Invalid signature, Replay detected. See Authentication. |
| 404 | `Invalid API key` | The x-tenant-api-key header does not match an active tenant. |
| 422 | `Apple account email is not verified` | The token carries an email with email_verified set to false. |
| 429 | `Rate limit exceeded, retry in 1 minute` | More than 20 requests in one minute from the same client IP. See the retry-after header. |

## Implementation

Forward the identity token with the tenant signature. Sign and send the same body string:

```javascript
import { createHash, createHmac } from 'node:crypto';

const API = 'https://api.aureahub.com';

// Tenant request signing: see Authentication → HMAC Request Signing
function signedHeaders(method, pathWithQuery, body = '') {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const bodyHash = createHash('sha256').update(body).digest('hex');
  const signature = createHmac('sha256', process.env.AUREA_TENANT_API_SECRET)
    .update(`${method}\n${pathWithQuery}\n${timestamp}\n${bodyHash}`)
    .digest('hex');
  return {
    'x-tenant-api-key': process.env.AUREA_TENANT_API_KEY,
    'x-timestamp': timestamp,
    'x-signature': signature,
  };
}

// identityToken: the Sign in with Apple identity token your app obtained
export async function signInWithApple(identityToken) {
  const path = '/v1/auth/apple';
  const body = JSON.stringify({ idToken: identityToken });

  const res = await fetch(API + path, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', ...signedHeaders('POST', path, body) },
    body,
  });
  const data = await res.json();

  if (!res.ok) {
    throw new Error(`${res.status}: ${data.message ?? data.error}`);
  }

  const { accessToken, refreshToken, expiresIn, user } = data;
  return { accessToken, refreshToken, expiresIn, user, isNewUser: data.isNewUser === true };
}
```

---

Web version: https://docs.aureahub.com/#auth-apple
