Apple

Sign in with Apple

Fluxo OIDC form_post para web e mobile — configuração no Apple Developer, JWT client secret, callback POST, captura de nome no primeiro login, e-mail private relay, referência da API e exemplos de frontend.

Visão geral

O Sign in with Apple é opcional e controlado por APPLE_AUTH_ENABLED. Quando desligado, os endpoints Apple respondem 503 Service Unavailable. O Apple usa OpenID Connect com response_mode=form_post, ou seja, o callback é um POST (não um GET). O client secret é um JWT assinado com sua chave .p8 — a lib passport-apple cuida disso automaticamente. O ID estável do usuário (appleId) é o claim sub do id_token. O Apple envia nome e e-mail somente no primeiro login; logins seguintes entregam apenas o id_token. O e-mail pode ser um endereço private relay (*@privaterelay.appleid.com). JWTs nunca vão na URL de redirect; a API emite um exchangeCode de uso único (Redis, 60 segundos). O mesmo POST /auth/exchange é compartilhado com os demais provedores sociais.

  • • Ativado por feature flag no ambiente
  • • Strategy passport-apple + AppleAuthGuard
  • • id_token decodificado para extrair sub (appleId) e e-mail
  • • Callback POST (form_post) — diferente do GET redirect dos demais provedores OAuth2
  • • Nome do usuário enviado pelo Apple apenas no primeiro login via req.body.user (JSON)
  • • E-mail pode ser endereço private relay — ainda utilizável como identificador de conta
  • • Client secret é um JWT gerado a partir da chave .p8 — gerenciado pelo passport-apple
  • • Allowlist de redirect evita open redirects
  • • Mesmo formato de tokens do POST /auth/login

Apple Developer console

Configure o Sign in with Apple em developer.apple.com. Você precisará de um App ID, um Services ID (client ID) e uma chave privada.

  1. Abra Certificates, Identifiers & Profiles no Apple Developer
  2. Crie um App ID (Identifiers → App IDs) e ative a capability 'Sign In with Apple'
  3. Crie um Services ID (Identifiers → Services IDs) — este é seu APPLE_CLIENT_ID (ex.: com.example.service). Ative 'Sign In with Apple', configure Domains & Subdomains e Return URLs para corresponder a APPLE_CALLBACK_URL
  4. Crie uma Key (Keys → +) e ative 'Sign In with Apple'. Baixe o arquivo .p8 — disponível apenas uma vez
  5. Anote: Team ID (canto superior direito de developer.apple.com/account), Key ID (listado ao lado da chave), Services ID
  6. Cole o conteúdo do arquivo .p8 em APPLE_PRIVATE_KEY, substituindo quebras de linha por \n literais (Docker-safe)
  7. APPLE_CALLBACK_URL deve ser uma URL HTTPS registrada nas Return URLs do Services ID — localhost só é aceito em desenvolvimento com ngrok ou similar

Variáveis de ambiente

Carregadas do .env via src/config/apple-oauth.config.ts (namespace appleOAuth no ConfigService). Veja .env.example. Armazene APPLE_PRIVATE_KEY com \n literais entre as linhas — o config substitui por quebras reais na inicialização.

Environment.env
APPLE_AUTH_ENABLED=true
APPLE_CLIENT_ID=com.example.service
APPLE_TEAM_ID=TEAM1234XX
APPLE_KEY_ID=ABCDEFGHIJ
APPLE_PRIVATE_KEY=-----BEGIN EC PRIVATE KEY-----\nMHQC...\n-----END EC PRIVATE KEY-----
APPLE_CALLBACK_URL=https://api.example.com/api/v1/auth/apple/callback
APPLE_REDIRECT_ALLOWLIST=myapp://success,https://app.example.com/auth/callback
APPLE_OAUTH_DEFAULT_ROLES=

Roles padrão para novos usuários Apple

Novos usuários Apple recebem roles: [] por padrão (igual ao register). Defina APPLE_OAUTH_DEFAULT_ROLES no .env como lista separada por vírgula (ex.: user ou user,manager). Usuários existentes vinculados por appleId mantêm as roles atuais.

Sequência

  1. Cliente abre GET /api/v1/auth/apple?redirect=<URL na allowlist>
  2. Usuário autoriza no Apple; Apple faz POST em /api/v1/auth/apple/callback (response_mode=form_post)
  3. API decodifica id_token (sub → appleId, e-mail), lê nome de req.body.user no primeiro login, faz upsert, grava exchangeCode no Redis e redireciona para redirect?code=...
  4. Cliente faz POST { code } em /api/v1/auth/exchange e recebe JWTs

Referência da API

1. Iniciar Sign In

Abre o consentimento do Apple Sign In. O query redirect deve bater com um prefixo de APPLE_REDIRECT_ALLOWLIST (ou omita para usar a primeira entrada). Scope: name email.

HTTPGET /api/v1/auth/apple
GET /api/v1/auth/apple?redirect=http%3A%2F%2Flocalhost%3A5173%2Fauth%2Fcallback

400 redirect inválido · 503 feature desligada

2. Callback (POST — form_post)

O Apple faz POST neste endpoint após o consentimento. Ao contrário dos demais provedores OAuth2, é um POST. O parâmetro state (com a URL de redirect) vem em req.body.state. O nome do usuário vem em req.body.user (string JSON) somente no primeiro login. A API decodifica o id_token e redireciona para seu app com ?code=.

HTTPPOST /api/v1/auth/apple/callback
POST /api/v1/auth/apple/callback
Content-Type: application/x-www-form-urlencoded
(sent by Apple — form_post response mode)
→ 302 Location: http://localhost:5173/auth/callback?code=EXCHANGE_CODE

3. Trocar code

Endpoint público compartilhado com outros provedores sociais. Consome o code (uso único) e retorna o mesmo formato do login.

HTTP RequestPOST /api/v1/auth/exchange
POST /api/v1/auth/exchange
Content-Type: application/json

{
  "code": "EXCHANGE_CODE"
}
HTTP Response200 OK
{
  "user": {
    "id": "...",
    "email": "[email protected]",
    "firstName": "Jane",
    "lastName": "Doe",
    "roles": [],
    "appleId": "apple-sub-001",
    "isActive": true
  },
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}

401 code inválido/expirado/usado · 503 social auth desligado

Frontend — Web (SPA)

Mesmo padrão dos demais provedores OAuth: iniciar login no browser; sua rota de callback lê code da query e chama exchange. Nota: o callback do Apple é um POST tratado pelo servidor — seu frontend só recebe o redirect com ?code=.

JavaScriptIniciar Apple Sign In
const API = 'http://localhost:3000/api/v1';
const redirect = encodeURIComponent(
  'http://localhost:5173/auth/callback'
);

// Redirect full-page para Nest → Apple → volta ao SPA
window.location.href = `${API}/auth/apple?redirect=${redirect}`;
JavaScriptPágina de callback /auth/callback
// /auth/callback no SPA
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
if (!code) throw new Error('Missing exchange code');

const res = await fetch('http://localhost:3000/api/v1/auth/exchange', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ code }),
});
if (!res.ok) throw new Error(await res.text());

const { user, accessToken, refreshToken } = await res.json();
// persista tokens conforme sua aplicação exige
sessionStorage.setItem('accessToken', accessToken);
// Use appleId como identificador estável — e-mail pode ser private relay
console.log('signed in', user.appleId);
  • • Adicione a URL de callback do frontend em APPLE_REDIRECT_ALLOWLIST (ex.: https://app.example.com/auth/callback)
  • • APPLE_CALLBACK_URL deve ser HTTPS apontando para a API Nest (registrada no Apple Developer)
  • • O Apple envia nome apenas no primeiro login — salve imediatamente; não será enviado novamente
  • • E-mail pode ser private relay — use appleId como identificador estável, não o e-mail
  • • Armazene accessToken / refreshToken com segurança (memória + httpOnly cookie conforme sua preferência)

Frontend — Mobile (deep link)

Use um custom scheme (ou universal link) na allowlist. Após Apple Sign In, a API redireciona para myapp://success?code=.... O app abre, parseia o code e chama exchange. No iOS também é possível usar a ASWebAuthenticationSession nativa.

React NativeAbrir Sign In no browser
import * as Linking from 'expo-linking';
import * as WebBrowser from 'expo-web-browser';

const API = 'https://api.example.com/api/v1';
const redirect = encodeURIComponent('myapp://success');

async function signInWithApple() {
  const url = `${API}/auth/apple?redirect=${redirect}`;
  await WebBrowser.openAuthSessionAsync(url, 'myapp://success');
}
React NativeTratar deep link + exchange
import * as Linking from 'expo-linking';

async function exchangeFromUrl(url) {
  const { queryParams } = Linking.parse(url);
  const code = queryParams?.code;
  if (!code) return;

  const res = await fetch(`${API}/auth/exchange`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ code }),
  });
  const data = await res.json();
  // data.accessToken, data.refreshToken, data.user
  return data;
}

Linking.addEventListener('url', ({ url }) => {
  exchangeFromUrl(url);
});
  • • Registre o scheme no SO / Expo app.json
  • • Prefira Linking.addEventListener('url', ...) e getInitialURL()
  • • Exchange via HTTPS contra a URL base da API
  • • Identifique usuários por appleId — não pelo e-mail (pode ser private relay ou mudar se o usuário revogar)

Notas de segurança

  • • URLs de redirect devem bater com prefixos de APPLE_REDIRECT_ALLOWLIST
  • • Exchange codes são de uso único e expiram em 60 segundos (Redis)
  • • APPLE_PRIVATE_KEY é segredo — nunca versione no repositório
  • • O JWT client secret é gerado por requisição a partir da chave privada pelo passport-apple
  • • Apple envia nome e e-mail somente no primeiro consentimento — logins seguintes só entregam id_token
  • • E-mails private relay (*@privaterelay.appleid.com) são reais e entregáveis, mas ligados ao Apple ID do usuário
  • • Usuários podem revogar o Apple Sign In nas configurações do dispositivo — trate a desativação via isActive
  • • Usuários só-Apple têm password null — login por senha / change-password falham até definir senha

Modelo de usuário

Campos da entidade Postgres / interface usados pelo Apple Sign In:

  • • appleId — Apple subject ID único e nullable (sub do id_token)
  • • email — do id_token; pode ser endereço private relay
  • • firstName / lastName — de req.body.user no primeiro login; padrão 'Apple' / '' nos seguintes
  • • password — nullable para contas só OAuth
  • • isActive — true para novos usuários Apple
  • • roles — de APPLE_OAUTH_DEFAULT_ROLES no env (padrão [])

Relacionado