### **PR Type** Enhancement, Tests ___ ### **Description** - Add multi-framework auth demos and examples - React Native, React, Next.js SSR, Vue, SvelteKit, Express - Email/password, magic link, MFA, WebAuthn flows - File upload, profile, logs viewing ___ <details> <summary><h3> File Walkthrough</h3></summary> <table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><details><summary>18 files</summary><table> <tr> <td><strong>MFASettings.tsx</strong><dd><code>Add React Native MFA settings screen</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-5d346e2efc6817f6c3de4360c27031e4d5a4f37423aedb329f645aa9ac33d6e8">+594/-0</a> </td> </tr> <tr> <td><strong>upload.tsx</strong><dd><code>Add React Native file upload screen</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-3763882523f551153b1520c05a953fdd2b3e7e0c37e2db53318e6a2f05d09b9f">+552/-0</a> </td> </tr> <tr> <td><strong>Upload.tsx</strong><dd><code>Add React SPA file upload page</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-aadcba440d58667ce429ae0caad0695a7dddc8d91e1b8f0dc52ce6633b0eddc4">+444/-0</a> </td> </tr> <tr> <td><strong>SecurityKeys.tsx</strong><dd><code>Add React SPA WebAuthn security keys</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-3f9cf11913bfd3220c4c220618b5575c6a3ed59ffa42d6d5be32c77edfc7d610">+404/-0</a> </td> </tr> <tr> <td><strong>client.tsx</strong><dd><code>Add Next.js SSR file upload client</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-f6fa7dea8d8baf6d7fcefaf4517a492dbfca213afcf6b5d08d180a889947c0e5">+399/-0</a> </td> </tr> <tr> <td><strong>SecurityKeyClient.tsx</strong><dd><code>Add Next.js SSR WebAuthn key client</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-c05c21b6e3c47e8d17ba69ebba93e3ccd4266e26fd4062e6de552ed98d46c603">+351/-0</a> </td> </tr> <tr> <td><strong>signup.tsx</strong><dd><code>Add React Native signup screen with tabs</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-262b09b9dd7234cad96fe092d7131a23451c9e50b98c126c9e36599b3a127ac6">+387/-0</a> </td> </tr> <tr> <td><strong>signin.tsx</strong><dd><code>Add React Native signin screen with MFA</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-3a4105c32f4aa4290e4fc0a12c2cc3121b4b0901c1429917087e26569c1c0a7a">+367/-0</a> </td> </tr> <tr> <td><strong>MFASettings.tsx</strong><dd><code>Add React SPA MFA settings component</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-e43f9e6aec21b8620c86b9cdf9986e2e8dace52d87609b1c74cf6ab9a0639d21">+288/-0</a> </td> </tr> <tr> <td><strong>mfa-settings.tsx</strong><dd><code>Add Next.js SSR MFA settings client</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-75767c82cc4c7eb779f3beb7ca5c5f9b2cc090347029ab44cc7f93b8b881c85b">+292/-0</a> </td> </tr> <tr> <td><strong>profile.tsx</strong><dd><code>Add React Native profile screen</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-e996eb818728427e1c131ac6f3cba0bd506ad8f28486ee47c9ceb5b33dbc7869">+262/-0</a> </td> </tr> <tr> <td><strong>verify.tsx</strong><dd><code>Add React Native email verify screen</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-93aeb3cfb31ac602e11e6031bb4860e81385c3c9693e5285c8f69988341cd045">+265/-0</a> </td> </tr> <tr> <td><strong>index.ts</strong><dd><code>Add Express server Nhost client example</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-337bf1c26c434751c7c3e356598278e8e05482e41936a30c587cf68fd375092f">+126/-0</a> </td> </tr> <tr> <td><strong>email-confirm-change.tsx</strong><dd><code>Add email confirm-change template generator</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-5ee9a92531c20cb74b9a49335eb0a65cdd73efa7d8c59020ea80163bba2cec72">+129/-0</a> </td> </tr> <tr> <td><strong>AppleSignIn.tsx</strong><dd><code>Add Expo Apple Sign In integration</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-8fcf005753999f7770661afbc82063eb0637a2f959f03a77a94d4eef55b1d9cf">+112/-0</a> </td> </tr> <tr> <td><strong>render-emails.ts</strong><dd><code>Add email templates render script</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-760314d4066ab3171a8dd9ec712e5e6b4e373f86531e66c502df06dab8f9dc7d">+82/-0</a> </td> </tr> <tr> <td><strong>auth.ts</strong><dd><code>Add Vue demo Nhost auth store</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-db6008d5d08993870173045b4d2e978832e91943222dad7f6093537c48277e4f">+75/-0</a> </td> </tr> <tr> <td><strong>App.tsx</strong><dd><code>Set up React SPA routes and auth provider</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-849f3aa52970f348de49a27094aac4e4b8cb8cf29580cada70d37f1a04249725">+77/-0</a> </td> </tr> </table></details></td></tr><tr><td><strong>Tests</strong></td><td><details><summary>2 files</summary><table> <tr> <td><strong>DateTimePicker.test.tsx</strong><dd><code>Simplify DateTimePicker test awaits</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-c7076012eb33d6f60049710638b5ad19c2f310b8c250c79f1905be7e0a30b00a">+12/-12</a> </td> </tr> <tr> <td><strong>useProjectLogs.test.ts</strong><dd><code>Update project logs tests to use CoreLogService</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-13d900aa08d06962a09628136b893801ad62a96c3ff89d380c5c4b7ae92d891e">+9/-9</a> </td> </tr> </table></details></td></tr><tr><td><strong>Additional files</strong></td><td><details><summary>101 files</summary><table> <tr> <td><strong>examples_demos_checks.yaml</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-48c5a14a5d1da9f35b409ecc95fae8f3a319f97bffbf0020efcb8c360347dc02">+94/-0</a> </td> </tr> <tr> <td><strong>biome.json</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-2bc8a1f5e9380d5a187a4e90f11b4dd36c3abad6aea44c84be354a4f44cdec55">+43/-0</a> </td> </tr> <tr> <td><strong>LogsServiceFilter.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-a590a7298a9f040df9f26c4eb37d10fc36f47c32996f71aec47796f08c44e892">+8/-7</a> </td> </tr> <tr> <td><strong>DeploymentServcieLogsHeader.test.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-46fb5d0c9528168323c0e16ef4186d91fe6274b64292f43841258bdfc45dd581">+81/-0</a> </td> </tr> <tr> <td><strong>DeploymentServiceLogs.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-333a9783713e9a4bad1b5327e117cbe69148091abe8b9038d36132b5f4635bbe">+3/-3</a> </td> </tr> <tr> <td><strong>DeploymentServiceLogsHeader.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-4f102c06ed32bb3d8245e415e76b0b14d2d4ae3abca6e234edf69278325c7a95">+3/-3</a> </td> </tr> <tr> <td><strong>useProjectLogs.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-10efc67700b3f024dd03442eacd339802e951696d04caa76bd5a864bd5c7c83f">+3/-3</a> </td> </tr> <tr> <td><strong>LogsBody.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-b628e511a7fb9b237ac691b27ab9585eed0d0803144cde66c3af7fa6f9a2dc40">+2/-2</a> </td> </tr> <tr> <td><strong>LogsHeader.test.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-6a348a6b3f868aac854020f2b85ff9a7cf5d61f362a5201e77681e4d5a576f20">+86/-0</a> </td> </tr> <tr> <td><strong>LogsHeader.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-ebb3285aa776c9c5ea8b72672c4aafd55994c6c694998bbf56ca9c56d1e77664">+3/-3</a> </td> </tr> <tr> <td><strong>services.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-8fcdaed33322718091b613ae22c65cc3eb61972904b5af46866b160c9bbbe48c">+13/-13</a> </td> </tr> <tr> <td><strong>logs.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-77489a68a7526d74f06d59019ad68c44728b7620637308d70fba38d6649b73fa">+3/-3</a> </td> </tr> <tr> <td><strong>Makefile</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-80fe306cf2e39e0852a6033f331195be7942ed7fb54b2cac6bd0139cc34d1bb6">+17/-0</a> </td> </tr> <tr> <td><strong>APPLE_SIGN_IN_SETUP.md</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-eeb0deb7c8dae0e6b49462b92e5e7176ee11236759847ea59253375ba48d6f5f">+95/-0</a> </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-29693cbc361101a671771703a3097f97b0ad6c344435cb3fa6d19b00c030ecac">+169/-0</a> </td> </tr> <tr> <td><strong>README_MAGIC_LINKS.md</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-9232aee57e45778533da1f08d80d794f612f3672b4e2fc0a77c4ca6e03e27d3a">+410/-0</a> </td> </tr> <tr> <td><strong>README_NATIVE_AUTHENTICATION.md</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-2bfce492dbe0824f4e47907f7269b57b6f8f092550647afc46cff13903a34750">+381/-0</a> </td> </tr> <tr> <td><strong>README_PROTECTED_ROUTES.md</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-25f106def41f290d4c471f95f4a25db4c7d411407f7937035f5e3e53ff90aa6f">+357/-0</a> </td> </tr> <tr> <td><strong>README_SOCIAL_SIGNIN.md</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-10c036291fe61ed79d9ce5c1764f10e44b6cc57232b0b6b981c19f84760f5e8e">+530/-0</a> </td> </tr> <tr> <td><strong>app.json</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-12f6ad4bacc187ec247d9ee19b2ed6a5b49c4304508fd3ba4321b265e0eec36f">+53/-0</a> </td> </tr> <tr> <td><strong>_layout.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-9d1054a3d9a27191c4056537dea211e6a19610581557e756db81677e49e7b929">+54/-0</a> </td> </tr> <tr> <td><strong>MagicLinkForm.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-169e79f10d79f5ff7df3e910f042005f0b139246098ad4a1d2284ed450409b42">+158/-0</a> </td> </tr> <tr> <td><strong>NativeLoginForm.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-46f2e1cd61d0d53b7fde9a0056f78e07a317e8fe60b6b237baa0a13d6cd4a4e0">+93/-0</a> </td> </tr> <tr> <td><strong>ProtectedScreen.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-4c71e8ae5499fc1dda82b020fd0b1f2ce2f972d18298af69d6c3c553742d2ad0">+40/-0</a> </td> </tr> <tr> <td><strong>SocialLoginForm.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-2e46da6c133f57ab9748231ab7985a6704893b6d99c2981007a3cfeaeaee2b79">+91/-0</a> </td> </tr> <tr> <td><strong>index.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-0db4f13bb27522261f48250563b1f9d6e6cd5805c9b89caf2bce331b229a4cca">+120/-0</a> </td> </tr> <tr> <td><strong>AsyncStorage.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-7c877423128ee8dadc8bafb89ea8a98b6c1b05130949a6c6d9f3f325a4ccacc1">+93/-0</a> </td> </tr> <tr> <td><strong>AuthProvider.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-72e3c353a156c42747b082a51cf6ab9e59a8b5009f8cb637d9e75fb9c31a7212">+110/-0</a> </td> </tr> <tr> <td><strong>utils.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-4091a110ae026c9f130fb654f47381b503f7a04a1cefc3020831ecb3f0d971ad">+44/-0</a> </td> </tr> <tr> <td><strong>mfa.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-477afc36e3e2e62727b11e6d1f77592c89d120d8dfd676a05756dcff7625825a">+233/-0</a> </td> </tr> <tr> <td><strong>package.json</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-9d05689c57c194cb000eb7ff276476869ba4ee40a730c23e47016671d7f17521">+57/-0</a> </td> </tr> <tr> <td><strong>tsconfig.json</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-1d10ade4fc1aa2dce579be648ec4420554d626d814d1971a6ec8d59018eb3bc8">+15/-0</a> </td> </tr> <tr> <td><strong>.secrets.example</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-c47199e16c186acba1e9f82c541362d1a6ff05066299b04f61f358d636c3f5d0">+16/-0</a> </td> </tr> <tr> <td><strong>Makefile</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-5c1f8270d9eede9a24fa01068ede09fa13a8fca85eeda73328701b5781500ebe">+7/-0</a> </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-9a3489ab94ccfa9504db04213d3dbb603c609abf1435e5844d911a03210c3515">+29/-0</a> </td> </tr> <tr> <td><strong>env-up.sh</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-de9763dc0910faec77518ee1eacfdf7a74a257b88e6055a421f37b9ca85d4280">+8/-0</a> </td> </tr> <tr> <td><strong>package.json</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-c602bad387df10731905ad3a49ee0a1ba256823a7e1a4b8ca262fc2f407fea4b">+13/-0</a> </td> </tr> <tr> <td><strong>tsconfig.json</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-7635e254c708cde29d62250387c92e13e0d2a7f4180b11ba11b288a7205ed00a">+11/-0</a> </td> </tr> <tr> <td><strong>config.yaml</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-4446c65612d85e309d41f928643d796ca2d3970f2ac122559a2c3ced6cfd5a77">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-ebd226650ec2ccfdfa181eb52ac57c538d1258efcdd5eefa6521d661014b0beb">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-f4eec235a2ce395d582333d50a2358e5b4b4758a0d6de2d80bb9b983bb5788c3">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-546c1e26f4f5a58d38dab39ffcd1f1e14ecc6867fd1bece4b6a1dd76a310fe3b">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-a62a136fad466ff5def2190737d76bdb54d3ca7c31d5ed85c0c7b9bdb4adc33a">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-c70655567313cbdee9e332a73651c1e91e33f57fe8a168cb064f4206aca66a20">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-6a415c40c7cfaa325f53043b71d709714101b9f629f954095bcc64862abc744b">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-9fd0c953500d416a457aa8dfcfb3659a517ee82a964b860b1b8afeb8d09c098f">+43/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-fd472f081db09177df1548bebdf3141328273c89d3e76b14e01e62bfdfbe2443">+1/-0</a> </td> </tr> <tr> <td><strong>body.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-236393a4f47ed882febf9eb4dd24d5525bc3f20e5e474f5e34ecdff091555965">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-8f0998f6df2005ee1436d74ad2ce2cd1bf91eb053a304160130d24ac64ecfae1">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-401c18113470ff2d38ad5b98eb3fc4fbb3aa1226c2db81b425a75eec28072607">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-3005f541570bcb879d647fbee1161de8b18981e4b07b8eac07a2250835f605a9">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-ed0a3cf5dabcbecde2cb9e914d827ca6a92cd0818aff7b45e51e8b5bdeec27df">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-b7ec4c42b59e6f2e23f1ba196befd43d08632b13747b48c6ceaba3d6d1e38209">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-026c60a66006fe593b8c30cba71ef52d4968f22f004c68bcde8356312de00dce">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-9e40803f1b207ababf5640dec47083efa84dfbc201e968b37730e515081a3101">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-143e6ac6a6fff08a7d6a68f98cae196d569fe4f30fc0215633c5b653ce349181">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-613a3b66d30c6e4b93257765df35c2d0be6379b6f7ab6f3478b4731c1d21ee96">+43/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-6f9c1433fcaabe15051bad4b1d888203d928a30ab2da829c724d54f74ecc818f">+1/-0</a> </td> </tr> <tr> <td><strong>body.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-1727f5ec1686c6079eb733d7f3c5cc60a6f047635241bbf4c63fc5985a78b13e">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-f65964bb501d3baeba4974daaab7c0e6edaec0645860b119633d09424deb995a">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-b911f5720caca8a68bc5ba072284c4d40e1e6b3283a2070c17e84a3d9ba55f79">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-7f1525fdd0e82fd57d181d56acb7828c5838ef635403c560b1dfb88834cc5996">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-afb123c7657dcf08247b52027d73e4d9dcee51d0cb7e37a1de0f31200c4dc535">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-877b5d458de6de8bb1287075e3eb6a6f8aaba807ddd2db78705640512ce038a6">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-01135b2530cf6bf46ae7398f1ed4e1a684cbfff1af5fc7dc28eacbd3b1c66caf">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-efa400ab45cf1d64d014c44c6308be2146eb1657c02f8ac7e47c9f71fb34bd4f">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-a7a3eec9c3ce1f9d763a196f2a6af166c07c3c87f05967261847cb40219814fe">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-344d73792431a98012dcfe76f75525a383827ef6f81ab1688b46ba504c34a903">+43/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-d01fea24e87babd4f89d3ce27578f96a35578511bebad2c0c2f2689160c0fc63">+1/-0</a> </td> </tr> <tr> <td><strong>body.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-dd12ae6e043b5085514f43691a710f3e65a0c3df93c8847ffd9678c8fae36c08">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-fa998d4e425d971dd546898cac4d1183dd4c10d4d95f19d4f4650d322a58505c">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-1c7f1643eece2cc9fdad3cc032d60826d541d6ad9123c9674ea1de20f4ef429f">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-bd2c5decf64f436932af01d32dc08a35fb745c8aacf97952be9f8925fb94eed4">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-f4187b87464106220110abbc03675a30e6e5154248be0880f75e7b976419e3ef">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-9d893b6b685830b2cdce2b05a925482b93c5407a7d7e2f2f76d159e55a089d6a">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-05b560e90f649549a7d98396179732b15c4af26b8df6fa5dc3b594b49eb548d0">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-2bef9e62baac1dabaf929cbb9113ad6c5711eb8ba364defebefd9cb9e65703e8">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-d0ca2cbd1895c7841755f2f8bb109ba17f6018a53f6615ab45445660c947742d">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-2df8e46ef47cbebf23bfe3bba1e963ebc93d1bde4c09d6bc18cafd008810fe11">+43/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-21ce711d17cbc8006a363b11e33f0b9009b3c7c7ba6885e9b6fccb64755ac0cd">+1/-0</a> </td> </tr> <tr> <td><strong>body.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-692abf5d4051aa5afc5342cdd36c24879777744678ea1cf6a1a246f2d4cb60ea">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-1443b315ed3da71a04c3d532a46dd6fc837cbf67a9813d6d9b7dcaad4fe8f617">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-b034b26b4c5e31f76049c742cf80148731a686744a3dd0e440399521ad2491d9">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-52643cd4c649348b055c9659cda85a7570f974c47aaa7af473814cea851fb9a7">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-c1538ae90cc62160dbcb4be240cef50eaf241b88ce4cb391e5851e8b346d73bf">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-5e695a8cb0adf5ff5a530a922bc8f5b52d00972a99951f2bd690e4f44ccefae8">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-532dbd0e9c1f0d9d76715a27d174745824cfc68d0f7e499afa7e7e2c150b8d19">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-07b82a5305d283da9c6781f44a88c0ff4131b575fe5ee78a15d69abf6bd91b87">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-9b9ee0fb48f9f8815980bfe94d7d93d41bb9f1511cad6852a6d4f7e9192faf47">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-d3755355865e28005c894a10824920696b06e64e7c1c8765268908d56aa12261">+43/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-d5c3314a4bfa1667da59c2072178f80a4654ac0f10f714b378496a69b6e21dcd">+1/-0</a> </td> </tr> <tr> <td><strong>body.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-a0fda535693ff92e05311d4a63884ae0063e90eadeba20e958a7f9da51016ec5">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-de84bf86da7ce4481559b7037412128a4928746589b35575b9dd811c9f50b391">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-d17f4b74ce523c6ed011f1d7a3426dcbe430716a50803ee2afdb0f81e5d047fe">+1/-0</a> </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-3266bd27efc68f506fa6c7d9ab41567c63a9fabf3d2d247f5719be551efbf250">+23/-0</a> </td> </tr> <tr> <td><strong>email-verify.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-80cd5ff8c6c8b4f7a588058618ed27b3316304b1786a92ea8ffbb4c3fda96ac5">+127/-0</a> </td> </tr> <tr> <td><strong>password-reset.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-cef64f7d4eab756b7c68477e4fcc323cf8197832c92fd07e65f24dc71c9e3e80">+127/-0</a> </td> </tr> <tr> <td><strong>signin-otp.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-a2a5f9edf38127ed8168a60248e51e6ba1b4026fe7e8366111f28e0502749e6f">+125/-0</a> </td> </tr> <tr> <td><strong>signin-passwordless.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-38ef73aa255432a72a30ab3e63da415b26598ce7b267a1e39957c26aefac1d88">+127/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-1c287ea5a500ae373cd8c021d07dc0f3994db71077e892d7b5761baf5c2ef037">+8/-0</a> </td> </tr> <tr> <td><strong>Additional files not shown</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3459/files#diff-2f328e4cd8dbe3ad193e49d92bcf045f47a6b72b1e9487d366f6b8288589b4ca"></a></td> </tr> </table></details></td></tr></tr></tbody></table> </details> ___
11 KiB
Native Authentication - Apple Sign-In
This document explains how native authentication with Apple Sign-In is implemented in the Nhost React Native demo, including deep linking, nonce generation, ID tokens, and security considerations.
Overview
Apple Sign-In provides a secure, privacy-focused authentication method for iOS users. The implementation uses cryptographic nonces, identity tokens, and deep linking to ensure a secure authentication flow between the app, Apple's servers, and Nhost.
Architecture
Authentication Flow
- Nonce Generation: Create a cryptographic nonce for request verification
- Apple Authentication: Request user authentication from Apple
- Identity Token: Receive signed JWT from Apple containing user information
- Nhost Verification: Send identity token and nonce to Nhost for verification
- Session Creation: Nhost validates the token and creates a user session
Implementation Details
Apple Sign-In Component
// app/components/AppleSignIn.tsx
const AppleSignIn: React.FC<AppleSignInProps> = ({ setIsLoading }) => {
const { nhost } = useAuth();
const [appleAuthAvailable, setAppleAuthAvailable] = useState(false);
// Check Apple authentication availability
useEffect(() => {
const checkAvailability = async () => {
if (Platform.OS === "ios") {
const isAvailable = await AppleAuthentication.isAvailableAsync();
setAppleAuthAvailable(isAvailable);
}
};
checkAvailability();
}, []);
const handleAppleSignIn = async () => {
try {
// Generate cryptographic nonce
const nonce = Math.random().toString(36).substring(2, 15);
const hashedNonce = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
nonce,
);
// Request Apple authentication
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL,
],
nonce: hashedNonce,
});
// Authenticate with Nhost
if (credential.identityToken) {
const response = await nhost.auth.signInIdToken({
provider: "apple",
idToken: credential.identityToken,
nonce, // Original unhashed nonce
});
if (response.body?.session) {
router.replace("/profile");
}
}
} catch (error) {
// Handle authentication errors
}
};
};
Security Mechanisms
Cryptographic Nonce
The nonce prevents replay attacks and ensures request authenticity:
// Generate random nonce
const nonce = Math.random().toString(36).substring(2, 15);
// Hash nonce for Apple (SHA256)
const hashedNonce = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
nonce,
);
// Send hashed nonce to Apple
const credential = await AppleAuthentication.signInAsync({
nonce: hashedNonce,
// ...
});
// Send original nonce to Nhost for verification
await nhost.auth.signInIdToken({
provider: "apple",
idToken: credential.identityToken,
nonce, // Original unhashed nonce
});
Why Nonce is Important
- Replay Attack Prevention: Ensures each authentication request is unique
- Request Binding: Links the Apple response to the specific app request
- Tampering Detection: Detects if the response has been modified
- Time-bound Security: Nonces typically have short lifespans
Identity Token Structure
Apple returns a JWT (JSON Web Token) containing:
{
"iss": "https://appleid.apple.com",
"aud": "com.nhost.reactnativewebdemo",
"exp": 1634567890,
"iat": 1634564290,
"sub": "000123.abc456def789...",
"nonce": "hashed_nonce_value",
"email": "user@example.com",
"email_verified": "true",
"real_user_indicator": "true"
}
Platform Requirements
iOS Configuration
The app must be properly configured for Apple Sign-In:
// app.json
{
"expo": {
"ios": {
"bundleIdentifier": "com.nhost.reactnativewebdemo",
"infoPlist": {
"NSFaceIDUsageDescription": "This app uses Face ID for signing in"
}
},
"plugins": ["expo-apple-authentication"]
}
}
Availability Check
Apple Sign-In is only available on iOS 13+ devices:
const checkAvailability = async () => {
if (Platform.OS === "ios") {
const isAvailable = await AppleAuthentication.isAvailableAsync();
setAppleAuthAvailable(isAvailable);
}
};
Nhost Configuration
Apple Provider Setup
Configure Apple as an authentication provider in your Nhost dashboard:
- Team ID: Your Apple Developer Team ID
- Service ID: Apple Services ID for your app
- Key ID: Apple Sign-In key identifier
- Private Key: Apple Sign-In private key (P8 file content)
Server-Side Verification
Nhost performs server-side verification of the identity token:
- Signature Verification: Validates JWT signature using Apple's public keys
- Nonce Verification: Compares hashed nonce in token with provided nonce
- Audience Verification: Ensures token is intended for your app
- Expiration Check: Validates token hasn't expired
- Issuer Validation: Confirms token comes from Apple
Deep Linking Integration
URL Scheme Configuration
The app is configured with custom URL schemes for deep linking:
// app.json
{
"expo": {
"scheme": "reactnativewebdemo",
"ios": {
"infoPlist": {
"CFBundleURLTypes": [
{
"CFBundleURLSchemes": ["reactnativewebdemo"]
}
]
}
}
}
}
Handling Deep Links
While Apple Sign-In typically doesn't require custom deep linking (it's handled within the app), the configuration supports it for other authentication flows:
// app/verify.tsx - Used by other auth methods
useEffect(() => {
const subscription = Linking.addEventListener("url", handleDeepLink);
return () => subscription?.remove();
}, []);
const handleDeepLink = (event: { url: string }) => {
// Handle incoming deep links from authentication providers
};
Error Handling
Common Apple Sign-In Errors
const handleAppleSignIn = async () => {
try {
// ... authentication logic
} catch (error: any) {
if (error.code === "ERR_CANCELED") {
// User canceled authentication
return;
}
if (error.code === "ERR_INVALID_RESPONSE") {
Alert.alert("Error", "Invalid response from Apple");
return;
}
if (error.code === "ERR_NOT_AVAILABLE") {
Alert.alert("Error", "Apple Sign-In not available on this device");
return;
}
// Generic error handling
Alert.alert("Authentication Error", error.message || "Unknown error");
}
};
Nhost Integration Errors
const response = await nhost.auth.signInIdToken({
provider: "apple",
idToken: credential.identityToken,
nonce,
});
if (response.error) {
switch (response.error.message) {
case "Invalid identity token":
Alert.alert("Error", "Authentication failed. Please try again.");
break;
case "Invalid nonce":
Alert.alert("Error", "Security verification failed");
break;
default:
Alert.alert("Error", "Authentication error occurred");
}
}
Privacy Features
Apple's Privacy Protection
Apple Sign-In provides enhanced privacy features:
- Email Relay: Apple can provide relay emails to protect user's real email
- Minimal Data: Only requests necessary user information
- User Control: Users can choose what information to share
- Private Email: Option to hide real email address
Handling Private Emails
// Handle Apple's private relay emails
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.EMAIL,
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
],
nonce: hashedNonce,
});
// Email might be a private relay address
console.log("Email:", credential.email); // Could be privaterelay@example.com
Testing
Development Testing
- iOS Simulator: Apple Sign-In works in iOS Simulator (iOS 14+)
- Physical Device: Test on real iOS devices for complete functionality
- Xcode Console: Monitor authentication flow through Xcode logs
Test Scenarios
// Test different authentication states
const testScenarios = [
"First-time sign in with Apple ID",
"Returning user authentication",
"User cancels authentication",
"Network connection issues",
"Invalid Apple ID credentials",
"Apple ID with 2FA enabled",
];
Security Best Practices
Implementation Guidelines
- Always Use Nonce: Never skip nonce generation for production apps
- Validate Server-Side: Let Nhost handle token validation
- Handle Errors Gracefully: Provide clear feedback to users
- Secure Storage: Let Nhost handle session storage securely
- Regular Updates: Keep Apple authentication libraries updated
Production Considerations
- Apple Developer Account: Requires paid Apple Developer membership
- App Store Review: Apple Sign-In must be implemented if other social logins exist
- Bundle ID Matching: Ensure bundle ID matches Apple configuration
- Certificate Management: Keep Apple certificates and keys updated
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| "Not Available" Error | iOS version < 13 or not configured | Check device compatibility and configuration |
| Invalid Identity Token | Incorrect Nhost Apple configuration | Verify Apple provider settings in Nhost dashboard |
| Nonce Mismatch | Sending hashed nonce to Nhost | Send original unhashed nonce to Nhost |
| Bundle ID Mismatch | App bundle ID doesn't match Apple config | Ensure bundle IDs match in all configurations |
Debug Tools
// Enable debug logging
if (__DEV__) {
console.log("Apple Auth Available:", appleAuthAvailable);
console.log("Generated Nonce:", nonce);
console.log("Hashed Nonce:", hashedNonce);
console.log("Identity Token:", credential.identityToken);
}
Related Documentation
External Resources
- Apple Sign-In Documentation
- Expo Apple Authentication
- Nhost Apple Provider Setup
- JWT Token Inspector - For debugging identity tokens