← Blog
Mobile16 min czytania

Apple Authentication w React Native — również na Androidzie

Biblioteka Invertase daje logowanie Apple ID w React Native na iOS, Androidzie, macOS i w przeglądarce. Pokazujemy konfigurację po stronie Apple, różnicę mechanizmów między platformami i pułapki, które kosztują najwięcej czasu.

„Sign in with Apple" trafia do aplikacji rzadko z entuzjazmu, a zwykle z konieczności. Jeśli twoja aplikacja oferuje logowanie przez Google albo Facebooka, wytyczne przeglądu App Store wymagają udostępnienia także logowania Apple ID — i recenzent to sprawdza. Warto zerknąć na aktualne brzmienie wytycznych w sekcji o usługach logowania, bo szczegóły i wyjątki (na przykład dla aplikacji korzystających wyłącznie z własnego systemu kont firmowych) z czasem się zmieniają.

Potem przychodzi drugie zaskoczenie: skoro logowanie Apple ID musi być w aplikacji na iOS, a wasza aplikacja w React Native ma też wydanie na Androida, to na Androidzie ten przycisk też powinien działać. Apple nie dostarcza na Androida żadnego SDK. Biblioteka @invertase/react-native-apple-authentication rozwiązuje oba problemy jednym pakietem, ale robi to na każdej platformie zupełnie inaczej — i właśnie ta różnica jest głównym tematem tego wpisu.

Stan projektu

Repozytorium: invertase/react-native-apple-authentication. 1668 gwiazdek, 235 forków, start w grudniu 2019 — czyli krótko po tym, jak Apple udostępniło API w iOS 13. Utrzymuje je Invertase, ta sama firma, która stoi za react-native-firebase. Licencja to Apache-2.0.

Ostatnie wydanie to v2.5.1 z 9 stycznia 2026, a wcześniej v2.5.0 i v2.4.2 z listopada 2025, v2.4.1 z kwietnia 2025 i v2.4.0 z września 2024. Ostatni commit pochodzi z dnia wydania 2.5.1, więc od ośmiu miesięcy w repozytorium jest cicho. Przy bibliotece tego typu to nie jest sygnał alarmowy — to opakowanie stabilnego API Apple i ustabilizowanego przepływu webowego, więc nie ma tu czego co miesiąc przepisywać. Dla porównania: zależności deweloperskie są trzymane przy bieżącym React Native (^0.82.1) i @types/react w wersji 19.2.2, czyli projekt jest testowany na aktualnym ekosystemie, a nie zamrożony na wersjach z 2023 roku. Otwartych zgłoszeń jest 34.

Nowa Architektura — jak to naprawdę wygląda

To pytanie, które w 2026 zadaje się pierwsze, więc odpowiedź na podstawie kodu, nie obietnic. W package.json nie ma sekcji codegenConfig. Plik .podspec deklaruje zależność wyłącznie od React-Core. Nagłówek modułu iOS to @interface RNAppleAuthModule : RCTEventEmitter <RCTBridgeModule>, a komponent przycisku dziedziczy po RCTViewManager.

Innymi słowy: to moduł starej architektury — nie Turbo Module i nie komponent Fabric. Pod Nową Architekturą działa przez warstwy zgodności, które React Native dostarcza właśnie po to. Praktycznie znaczy to, że biblioteka działa, ale planując migrację na Nową Architekturę warto ją mieć na liście rzeczy do sprawdzenia po aktualizacji RN — bo utrzymanie warstw interop nie jest wieczne. Jeśli Apple ID jest krytyczną ścieżką logowania w waszej aplikacji, to jest ryzyko, o którym lepiej wiedzieć w planie na rok niż w tygodniu wydania.

Najpierw Apple, potem kod

Najczęstszy błąd przy tej integracji polega na próbie napisania kodu przed skonfigurowaniem konta developerskiego. Kolejność jest odwrotna, a konfiguracja po stronie Apple ma kilka niekomfortowych szczegółów.

Dla samego iOS trzeba: dodać w Xcode uprawnienie Sign in with Apple w zakładce Signing and capabilities, następnie w konsoli developerskiej Apple w sekcji Identifiers zaznaczyć Sign in with Apple dla swojego App ID i wybrać opcję Enable as a primary App ID, a na końcu utworzyć klucz w sekcji Keys, powiązać go z tym App ID, pobrać i zabezpieczyć. Warto zwrócić uwagę na ten wybór „primary": jeśli twój identyfikator ma być zgrupowany z innym, głównym App ID, przechodzi się przez ścieżkę Group with existing primary App ID — i wtedy do podpisywania używany jest klucz tego głównego identyfikatora, nie twojego. To źródło błędów, których treść nie wskazuje przyczyny.

Dla Androida dochodzi cała druga konfiguracja, bo tam Apple traktuje twoją aplikację jak stronę internetową. Potrzebny jest Service ID, który: jest zmapowany na twoją domenę, ma jako główny identyfikator App ID twojej aplikacji na iOS i zawiera dokładny adres zwrotny, jaki podasz w kodzie. Dokumentacja biblioteki dodaje przy tym dwie rzeczy, które oszczędzają czas.

Pierwsza: nazwa Service ID jest tym, co Apple pokaże użytkownikowi Androida podczas logowania — w komunikacie w stylu „Use your Apple ID to sign in to Example App". To nie jest identyfikator techniczny ukryty przed użytkownikiem, więc nazwij go tak, jak ma się nazywać wasza aplikacja.

Druga: adres zwrotny nigdy nie zostanie wywołany. Biblioteka przechwytuje przekierowanie do niego wewnątrz aplikacji, więc po stronie backendu może to być pusty endpoint. Musi jednak istnieć w konsoli Apple i zgadzać się dokładnie, znak w znak, bez query stringa.

iOS — natywny przepływ

Instalacja jest zwyczajna, biblioteka wspiera autolinkowanie:

yarn add @invertase/react-native-apple-authentication
cd ios && pod install

Przycisk jest komponentem natywnym i ma jedno wymaganie, o którym łatwo zapomnieć — musisz podać szerokość i wysokość, bo sam się nie rozłoży:

import { appleAuth, AppleButton } from '@invertase/react-native-apple-authentication';

<AppleButton
  buttonStyle={AppleButton.Style.WHITE}
  buttonType={AppleButton.Type.SIGN_IN}
  style={{ width: 160, height: 45 }}
  onPress={() => onAppleButtonPress()}
/>

Samo logowanie to jedno wywołanie, po którym warto sprawdzić stan poświadczeń:

const response = await appleAuth.performRequest({
  requestedOperation: appleAuth.Operation.LOGIN,
  requestedScopes: [appleAuth.Scope.FULL_NAME, appleAuth.Scope.EMAIL],
});

const credentialState = await appleAuth.getCredentialStateForUser(response.user);

if (credentialState === appleAuth.State.AUTHORIZED) {
  // użytkownik uwierzytelniony
}

I tu pierwsza pułapka, którą dokumentacja zaznacza wprost: getCredentialStateForUser zawsze rzuca błędem na symulatorze, bo weryfikuje autentyczność urządzenia. Jeśli zobaczysz com.apple.AuthenticationServices.AuthorizationError error 1000, to zanim zaczniesz szukać błędu w konfiguracji, sprawdź, czy nie testujesz na symulatorze. Ta funkcja wymaga fizycznego urządzenia. Dokumentacja podaje jeszcze jeden trop dla symulatorów: wejść na stronę zarządzania kontem Apple, znaleźć w urządzeniach pozycję „Simulator" i usunąć ją z konta.

Trzeci element to nasłuchiwanie na odebranie zgody. Użytkownik może w każdej chwili odciąć twojej aplikacji dostęp z poziomu ustawień systemu, a wtedy chcesz o tym wiedzieć:

useEffect(() => {
  return appleAuth.onCredentialRevoked(async () => {
    console.warn('Poświadczenia użytkownika zostały odebrane');
  });
}, []);

Funkcja zwraca funkcję odpinającą nasłuch, więc zwrócenie jej z useEffect załatwia porządkowanie przy odmontowaniu komponentu.

Android — to samo, tylko całkiem inaczej

Na Androidzie nie ma natywnego API Apple, więc biblioteka robi coś innego i warto rozumieć co, bo to wpływa na to, czemu można zaufać. Kod pod spodem:

  1. Buduje adres https://appleid.apple.com/auth/authorize z parametrami client_id, redirect_uri, response_type, scope, state, opcjonalnym nonce i — co kluczowe — response_mode=form_post.
  2. Otwiera ten adres w WebView umieszczonym w DialogFragment.
  3. Po zalogowaniu Apple wysyła odpowiedź metodą POST na adres zwrotny. Biblioteka wstrzykuje na tej stronie JavaScript, który odczytuje wszystkie pola formularza i przekazuje je do aplikacji przez addJavascriptInterface.
  4. Po stronie natywnej porównuje otrzymany state z oczekiwanym i albo zwraca sukces z code, id_token i danymi użytkownika, albo błąd „state does not match".

To sprytne i działa, ale ma konsekwencję, którą trzeba mieć świadomie: przepływ na Androidzie opiera się na tym, że Apple nadal zwraca odpowiedź jako form_post z polami o nazwach state, code, id_token i user. Nie jest to kontrakt API, który Apple zobowiązało się utrzymywać. Gdyby strona odpowiedzi się zmieniła, przechwytywanie przestałoby działać — i to nie byłby błąd biblioteki, a zmiana pod nią. Przy krytycznej ścieżce logowania warto mieć na to plan awaryjny i monitorować odsetek nieudanych logowań na Androidzie osobno od iOS.

Kod po stronie JavaScriptu wygląda tak:

import { appleAuthAndroid } from '@invertase/react-native-apple-authentication';
import 'react-native-get-random-values';
import { v4 as uuid } from 'uuid';

const rawNonce = uuid();
const state = uuid();

appleAuthAndroid.configure({
  clientId: 'com.example.client-android',   // Service ID, nie bundle ID
  redirectUri: 'https://example.com/auth/callback',
  responseType: appleAuthAndroid.ResponseType.ALL,
  scope: appleAuthAndroid.Scope.ALL,
  nonce: rawNonce,
  state,
});

const response = await appleAuthAndroid.signIn();

Zwróć uwagę na clientId: na Androidzie i w wersji webowej podaje się Service ID, a nie bundle identifier aplikacji natywnej. Pomylenie tych dwóch daje w webview błąd invalid_client — i to jest pierwszy z trzech powodów tego błędu, które wymienia dokumentacja. Dwa pozostałe: Service ID przypięty do niewłaściwego głównego App ID (czyli używany jest zły klucz) oraz adres zwrotny, który nie zgadza się dokładnie z tym z konsoli.

Przycisk na Androidzie warto pokazywać warunkowo, bo logowanie wymaga API 19 lub wyższego:

{appleAuthAndroid.isSupported && (
  <AppleButton onPress={() => onAppleButtonPress()} />
)}

W wersji 2.5.0 dorzucono opcjonalną właściwość fullScreen, która decyduje, czy okno logowania zakrywa cały ekran wraz z paskiem statusu.

Nonce i weryfikacja po stronie serwera

To najczęstsze źródło cichych błędów w tej integracji, więc sprawdziłem to w kodzie, nie tylko w dokumentacji. Biblioteka haszuje nonce algorytmem SHA-256 przed wysłaniem do Apple, na obu platformach. Na iOS robi to RNAppleAuthModule.m, przypisując do żądania wynik stringBySha256HashingString. Na Androidzie ten sam efekt osiąga MessageDigest.getInstance("SHA-256"), z komentarzem w kodzie mówiącym, że chodzi o zgodność z zachowaniem iOS „i uniknięcie zamieszania". Typy w index.d.ts również to dokumentują notatką, że wartość będzie zahaszowana przed wysłaniem.

W odpowiedzi dostajesz natomiast nonce surowy. Konsekwencja jest jednoznaczna: żeby zweryfikować nonce po stronie serwera, musisz najpierw go zahaszować:

import crypto from 'crypto';
import appleSigninAuth from 'apple-signin-auth';

const claims = await appleSigninAuth.verifyIdToken(id_token, {
  nonce: nonce
    ? crypto.createHash('sha256').update(nonce).digest('hex')
    : undefined,
});

Jest też przełącznik nonceEnabled, domyślnie true. Gdy nie podasz nonce, biblioteka wygeneruje go sama (na iOS losowy 32-znakowy, na Androidzie UUID). Ustawienie nonceEnabled: false wyłącza mechanizm całkowicie — nie rób tego bez świadomej decyzji, bo nonce jest tym, co wiąże token z konkretnym żądaniem.

Druga rzecz, o której trzeba pamiętać: weryfikacja state na Androidzie odbywa się w aplikacji, w kodzie natywnym, przez porównanie z wartością oczekiwaną. To sensowna pierwsza linia obrony przed CSRF, ale nie zastępuje weryfikacji serwerowej. Autorytatywne sprawdzenie id_token — podpisu, odbiorcy i nonce — musi się dziać na backendzie. Dokumentacja mówi to zresztą wprost w komentarzu przy przykładzie: „wyślij kod autoryzacyjny do swojego backendu w celu weryfikacji". Nie traktuj wyniku z urządzenia jako dowodu tożsamości.

Pułapka, która kosztuje najwięcej: imię i e-mail tylko raz

Apple zwraca imię, nazwisko i adres e-mail wyłącznie przy pierwszym logowaniu danego użytkownika w danej aplikacji. Przy każdym następnym te pola są null. Nie ma sposobu, by poprosić o nie ponownie.

Praktyczne skutki są poważniejsze, niż brzmią. Jeśli twój backend przyjmie pierwsze logowanie, ale przewróci się przy zapisie profilu — imienia tego użytkownika już nie zobaczysz. Jeśli w pierwszej wersji aplikacji nie zapisywałeś nazwiska, bo „dodamy później", to dla wszystkich dotychczasowych użytkowników nigdy go nie zdobędziesz. Wniosek jest jeden: dane z pierwszego logowania zapisz natychmiast i w transakcji, przed czymkolwiek innym.

Dochodzi do tego szczegół, który wygląda na przesąd, a jest udokumentowanym zachowaniem: kolejność zakresów w żądaniu ma znaczenie. FAQ biblioteki, powołując się na zgłoszenie numer 293, instruuje, by FULL_NAME podawać jako pierwszy:

requestedScopes: [appleAuth.Scope.FULL_NAME, appleAuth.Scope.EMAIL]

Warto o tym wiedzieć tym bardziej, że dokumentacja integracji z Firebase w tym samym repozytorium podaje w przykładzie kolejność odwrotną — [EMAIL, FULL_NAME]. Jeśli skopiujesz przykład z przewodnika Firebase, dostaniesz konfigurację, przed którą ostrzega FAQ z README. To niespójność w dokumentacji projektu, nie w API Apple, ale skutkiem jest brak imienia i nazwiska.

Do testowania jest obejście: w ustawieniach urządzenia wejdź w Apple ID → Password & Security → Apps Using Your Apple ID, wybierz swoją aplikację i użyj opcji Stop Using Apple ID. Następne logowanie znów będzie „pierwsze". Bez tego nie przetestujesz ścieżki rejestracji więcej niż raz na konto.

Adres e-mail można natomiast odzyskać zawsze, bo siedzi w tokenie:

import { jwtDecode } from 'jwt-decode';

const { email, email_verified, is_private_email, sub } =
  jwtDecode(response.identityToken);

Imienia i nazwiska w tokenie nie ma — dokumentacja zaznacza to osobno. Token daje e-mail, informację o jego weryfikacji, informację, czy jest to adres prywatnego przekaźnika, oraz stabilny identyfikator użytkownika w polu sub.

Wylogowanie, które nie działa

W API istnieje operacja appleAuth.Operation.LOGOUT i dokumentacja odradza jej używanie w wyjątkowo bezpośrednich słowach: nie działa zgodnie z oczekiwaniami i nie jest nawet używana przez Apple w ich własnym przykładowym kodzie. Zalecenie brzmi: przy wylogowaniu po prostu usuń wszystkie dane, jakie zebrałeś o użytkowniku podczas logowania. To rzadki i doceniany przypadek, gdy biblioteka mówi „ta część naszego API jest bezużyteczna", zamiast pozwolić ci ją odkryć samodzielnie na produkcji.

Prywatny przekaźnik e-mail i konsekwencje

Pole is_private_email w tokenie to nie ciekawostka, a informacja operacyjna. Użytkownik logujący się Apple ID może wybrać opcję ukrycia adresu — wtedy dostajesz adres w domenie przekaźnika Apple, a nie jego prawdziwą skrzynkę. Poczta wysłana na taki adres dotrze tylko wtedy, gdy twoja domena wysyłająca jest zarejestrowana w usłudze prywatnego przekazywania poczty w konsoli Apple. Jeśli nie jest, wiadomości transakcyjne — potwierdzenia zamówień, resety hasła — po prostu nie dojdą, a w logach zobaczysz odrzucenia, których nikt nie połączy z brakiem konfiguracji u Apple.

Z perspektywy ochrony danych ta funkcja działa na waszą korzyść: użytkownik, który ukrył adres, zminimalizował zakres danych, jakie o nim przetwarzacie. Warto natomiast pamiętać, że identyfikator z pola sub jest stabilny i pozwala rozpoznać powracającego użytkownika, więc traktujcie go jak dane osobowe, a nie jak techniczny klucz bez znaczenia.

Język przycisku

Drobiazg, który przy polskiej aplikacji zauważy każdy tester. Natywny przycisk Apple na iOS nie czyta języka z ustawień aplikacji — bierze go z klucza CFBundleDevelopmentRegion w pliku Info.plist. Zmiana tej wartości zmienia domyślny język napisu na przycisku:

<key>CFBundleDevelopmentRegion</key>
<string>pl</string>

A jeśli aplikacja ma być wielojęzyczna, dokumentacja wskazuje dodatkowy klucz:

<key>CFBundleAllowMixedLocalizations</key>
<string>true</string>

Expo, macOS, visionOS i web

W Expo nie da się tego zrobić bez kroku prebuild, bo Apple wymaga uprawnienia. Konfiguruje się je w app.json:

{
  "ios": {
    "entitlements": {
      "com.apple.developer.applesignin": ["Default"]
    }
  }
}

Po czym trzeba uruchomić npx expo prebuild. Warto pamiętać, że Expo ma też własny moduł expo-apple-authentication — jeśli pracujecie w pełni w ekosystemie Expo i nie potrzebujecie Androida, jest to alternatywa warta porównania. Ta biblioteka wygrywa wtedy, gdy potrzebujesz jednego API dla iOS i Androida.

Zasięg platform jest przy okazji szerszy, niż sugeruje nazwa. Plik .podspec deklaruje iOS od 9.0, macOS od 10.15 i visionOS od 1.0, a w repozytorium jest osobny plik AppleButton.macos.js — na macOS biblioteka działa w połączeniu z react-native-macos. Dla przeglądarki autorzy nie udają, że mają rozwiązanie: odsyłają do zewnętrznej biblioteki react-apple-signin-auth, z uwagą, że trzeba tam podać ten sam Service ID i ten sam adres zwrotny co na Androidzie, bo obie ścieżki korzystają z tej samej konfiguracji webowej.

Pułapki w jednym miejscu

  • Imię, nazwisko i e-mail przychodzą tylko przy pierwszym logowaniu. Zapisz je natychmiast i transakcyjnie — drugiej okazji nie będzie.
  • Kolejność zakresów ma znaczenie — FULL_NAME przed EMAIL. Przykład w przewodniku Firebase w tym samym repozytorium podaje odwrotną kolejność.
  • getCredentialStateForUser zawsze zawodzi na symulatorze. Błąd 1000 z AuthorizationServices to najczęściej właśnie to, nie zła konfiguracja.
  • Operation.LOGOUT nie działa i sama dokumentacja odradza jego użycie. Czyść dane u siebie.
  • Na Androidzie clientId to Service ID, nie bundle ID. Pomyłka daje invalid_client w webview.
  • Adres zwrotny musi zgadzać się dokładnie z wpisem w konsoli Apple i nie może zawierać query stringa — nawet jeśli nigdy nie zostanie wywołany.
  • Nonce jest haszowany SHA-256 przed wysłaniem, a zwracany surowy. Na serwerze porównuj hash, nie wartość surową.
  • Weryfikacja state dzieje się w aplikacji. Autorytatywna weryfikacja id_token należy do backendu.
  • Przycisk na iOS wymaga jawnej szerokości i wysokości — bez nich się nie pokaże.
  • Język przycisku bierze się z CFBundleDevelopmentRegion, nie z ustawień aplikacji.
  • Adresy z prywatnego przekaźnika Apple wymagają rejestracji domeny wysyłającej, inaczej poczta transakcyjna nie dojdzie.
  • To moduł starej architektury — brak codegenConfig, RCTBridgeModule i RCTViewManager. Pod Nową Architekturą działa przez warstwy zgodności.
  • Przepływ na Androidzie zależy od kształtu odpowiedzi form_post Apple, przechwytywanej wstrzykniętym JavaScriptem. Monitoruj skuteczność logowań na Androidzie osobno.

Podsumowanie

Ta biblioteka robi rzecz, której nie da się obejść: sprowadza wymóg App Store i dwie zupełnie różne technologie po stronie Apple do jednego API w React Native. Nie jest to projekt, który zachwyca tempem rozwoju — i dobrze, bo opakowuje stabilne API, a nie goni ruchomy cel.

  • Jedno API na cztery platformy — iOS natywnie, Android przez przepływ webowy, macOS z react-native-macos, visionOS w podspecu.
  • Kompletne typy TypeScript — index.d.ts z dokumentacją zachowań, w tym haszowania nonce, plus generowana dokumentacja API i osobny plik testów typów.
  • Natywny przycisk we wszystkich wariantach Apple, ze stylami i typami wymaganymi przez wytyczne — to część, którą recenzent App Store obejrzy najpierw.
  • Uczciwa dokumentacja, która mówi, że LOGOUT nie działa, że symulator zwróci błąd i że kolejność zakresów ma znaczenie — zamiast zostawiać to do odkrycia na produkcji.
  • Gotowe przewodniki integracji z React Native Firebase i Auth0, plus przykładowe aplikacje w wariantach z hookami i klasami.
  • Utrzymanie przez Invertase — ten sam zespół, który prowadzi react-native-firebase, z automatyzacją wydań i zależnościami trzymanymi przy bieżącym React Native.

Biblioteka jest wydana na licencji Apache-2.0, z prawami po stronie Invertase Limited. To licencja permisywna: możesz jej używać komercyjnie, modyfikować i dostarczać klientom bez obowiązku otwierania własnego kodu. Apache-2.0 dodaje wobec MIT dwie rzeczy warte odnotowania w kontekście firmowym — wyraźne udzielenie licencji patentowej oraz wymóg odnotowania zmian w plikach, które zmodyfikujesz. Przy zwykłym użyciu jako zależności nie ma to znaczenia; przy forku utrzymywanym u siebie warto o obu pamiętać.