Authentication Testing Guide
How to Test Sign in with Apple with Playwright: Complete 2026 Guide
A scenario-by-scenario walkthrough of testing Sign in with Apple with Playwright. Private relay email, hidden email forwarding, Apple ID popup handling, real name sharing toggles, first-login vs returning-login differences, and the pitfalls that break Apple auth test suites.
“Over one billion active Apple IDs exist worldwide, and Sign in with Apple is required for any iOS app that offers third-party social login, per App Store Review Guideline 4.8.”
Sign in with Apple End-to-End Flow
1. Why Testing Sign in with Apple Is Harder Than It Looks
Sign in with Apple looks deceptively simple from the user's perspective: click a button, authenticate with Face ID or a password, and you're in. Under the surface, five structural properties make it one of the hardest OAuth flows to test with Playwright or any browser automation tool.
First, the Apple ID authentication screen opens in a popup window (or sometimes an iframe on Safari), not a redirect. Playwright needs to listen for the popup event on the page context and then interact with a completely separate browser context hosted on appleid.apple.com. If you miss that popup event, your test hangs indefinitely waiting for elements that exist in a different window.
Second, Apple requires two-factor authentication for every Apple ID. There is no way to disable 2FA on an Apple ID once it is enabled, and all new Apple IDs have it on by default. Your test must handle the 2FA code entry screen, which means either using a dedicated test Apple ID with a phone number you control, or intercepting the 2FA code from a trusted device programmatically.
Third, Apple sends the user's name and email only on the very first authorization. If a user has previously authorized your app, the consent screen does not appear at all, and Apple does not include the name or email in subsequent identity tokens. Your backend must persist these values on first login, because you will never receive them again. This creates a fundamental asymmetry between first-login and returning-login test scenarios.
Fourth, Apple's Private Relay email system generates a unique, random email address per app per user (like abc123@privaterelay.appleid.com). Your app can send emails to this relay address, and Apple forwards them to the user's real inbox, but only if you have registered your outbound email domains in the Apple Developer portal. If that configuration is wrong, emails silently vanish.
Fifth, Apple's consent screen presents a “Share My Email” vs “Hide My Email” toggle and a name-editing field. The user can modify their displayed name or choose to hide their real email before granting consent. Your test suite must cover both paths, because the data your backend receives differs significantly.
Sign in with Apple Popup Flow
Your App
User clicks Apple button
Popup Opens
appleid.apple.com
Apple ID Login
Email + password
2FA Challenge
6-digit code from device
Consent Screen
Name + email sharing
Callback POST
id_token + code to your server
Session Created
User logged in
Private Relay Email Flow
User Hides Email
Selects Hide My Email
Apple Generates Relay
abc@privaterelay.appleid.com
Your App Sends Email
To relay address
Apple Forwards
To user's real inbox
User Receives Email
From your registered domain
A robust Sign in with Apple test suite must cover all of these surfaces. The sections below walk through each scenario, with runnable Playwright TypeScript you can copy directly into your project.
2. Setting Up Your Test Environment
Testing Sign in with Apple requires more upfront configuration than most OAuth providers. Apple does not offer a sandbox or test mode for authentication. Every Sign in with Apple flow uses production Apple ID servers and real Apple IDs. That means you need a dedicated test Apple ID, a properly configured Service ID in the Apple Developer portal, and a server that can receive Apple's callback POST.
Apple Sign-In Test Environment Checklist
- Create a dedicated test Apple ID with a phone number you control for 2FA
- Register a Service ID in Apple Developer portal with your test domain
- Configure the Return URL (callback) to match your test server
- Generate a Sign in with Apple private key (.p8 file) for token generation
- Register your outbound email domains for Private Relay forwarding
- Store the Team ID, Service ID, Key ID, and private key as environment variables
- Set up a TOTP or SMS interception mechanism for 2FA codes
- Revoke the test Apple ID's authorization for your app before first-login tests
Environment Variables
Generating the Client Secret JWT
Unlike most OAuth providers, Apple does not give you a static client secret. Instead, you must generate a signed JWT using your private key. This JWT has a maximum lifetime of six months and must be regenerated before it expires. Here is the helper that creates the client secret for token exchange.
Playwright Configuration for Apple Sign-In
The Apple ID login opens in a popup window. Playwright needs permission to handle multiple browser contexts and generous timeouts for the 2FA step. The popup domain is appleid.apple.com, which serves its own set of cookies and security headers.
3. Scenario: First-Time Login with Real Email
The first-time login is the most important scenario to test because it is the only time Apple sends the user's full name and email to your application. If your backend fails to capture and persist this data on first authorization, you lose it permanently. Apple will never send it again for that user-app pair.
Before running this test, you must revoke your test Apple ID's authorization for your app. Go to appleid.apple.com, sign in with the test account, navigate to “Sign-In and Security” then “Sign in with Apple”, find your app, and click “Stop using Sign in with Apple”. Alternatively, use the Apple REST API to revoke tokens programmatically.
First-Time Login with Real Email Shared
ComplexGoal
Complete a first-time Sign in with Apple flow where the user shares their real email address. Verify that the backend receives and persists the user's name and real email.
Preconditions
- App running at
APP_BASE_URL - Test Apple ID has never authorized your app (or authorization was revoked)
- Phone or TOTP secret available for 2FA
Playwright Implementation
What to Assert Beyond the UI
- The database record stores the user's real email, not a relay address
- The
subclaim from Apple's identity token is persisted as the unique user identifier - The user's first and last name are saved (Apple only sends them once)
- A valid refresh token is stored for future token exchanges
4. Scenario: First-Time Login with Private Relay Email
When a user selects “Hide My Email” on the consent screen, Apple generates a unique relay address in the format <random>@privaterelay.appleid.com. This address is unique per user per app. The same user authorizing a different app receives a different relay address. Your backend receives this relay address instead of the real email, and it must handle it correctly: store it, send emails to it, and never assume it looks like a standard email address.
The relay address only works for email forwarding if your sending domain is registered in the Apple Developer portal under “Services” then “Sign in with Apple for Email Communication”. If you skip this step, any emails your app sends to the relay address will bounce silently. This is one of the most common production issues with Sign in with Apple, and your tests should verify that forwarding actually works.
First-Time Login with Hide My Email
ComplexGoal
Complete a first-time Sign in with Apple flow where the user selects “Hide My Email”. Verify the backend receives and stores the private relay address.
Preconditions
- Test Apple ID authorization revoked for your app
- Email domains registered for Private Relay in Apple Developer portal
- 2FA code mechanism ready
Playwright Implementation
What to Assert Beyond the UI
- Database stores an email ending in
@privaterelay.appleid.com - The
is_private_emailclaim in the identity token istrue - Your email-sending service can resolve the relay address (DNS MX lookup)
- The user's
subidentifier is the same regardless of email choice
Private Relay Login: Playwright vs Assrt
import { test, expect } from '@playwright/test';
import * as OTPAuth from 'otpauth';
test('apple login: hide my email', async ({ page }) => {
await page.goto('/');
const popupPromise = page.waitForEvent('popup');
await page.getByRole('button', { name: /sign in with apple/i }).click();
const applePopup = await popupPromise;
await applePopup.waitForURL(/appleid.apple.com/);
await applePopup.locator('input#account_name_text_field').fill(process.env.TEST_APPLE_ID!);
await applePopup.locator('button#sign-in').click();
await applePopup.locator('input#password_text_field').fill(process.env.TEST_APPLE_PASSWORD!);
await applePopup.locator('button#sign-in').click();
const totp = new OTPAuth.TOTP({ secret: OTPAuth.Secret.fromBase32(process.env.TEST_APPLE_2FA_SECRET!) });
const codeInputs = applePopup.locator('input.verify-code-input');
for (let i = 0; i < 6; i++) { await codeInputs.nth(i).fill(totp.generate()[i]); }
await applePopup.getByText('Hide My Email').click();
await applePopup.getByRole('button', { name: /continue/i }).click();
await page.waitForURL('**/dashboard**');
await expect(page.locator('[data-testid="user-email"]')).toContainText('@privaterelay');
});5. Scenario: Returning User Login (No Consent Screen)
Once a user has authorized your app, subsequent Sign in with Apple logins skip the consent screen entirely. Apple authenticates the user (with 2FA if needed) and immediately posts the authorization code and identity token to your callback. No name, no email, no sharing toggle. Just the sub claim that identifies this user.
This creates a testing asymmetry. Your returning-login test cannot verify that the name and email are present in the callback, because Apple intentionally omits them. Instead, your test must verify that the backend matches the sub claim to the existing user record and does not create a duplicate account.
Returning User Login
ModerateGoal
Log in with an Apple ID that has previously authorized your app. Verify that no consent screen appears and the user session matches the existing account.
Playwright Implementation
What to Assert Beyond the UI
- No duplicate user record was created in the database
- The session's
submatches the original first-login record - The user's name and email are served from your database, not from the Apple callback
- The login timestamp was updated on the existing record
6. Scenario: Real Name Sharing Toggle
On the first-time consent screen, Apple presents editable first and last name fields. The user can modify these to anything they want before granting authorization. A user named “Jane Smith” could change it to “J S” or leave the fields blank entirely. Your backend must handle all of these cases gracefully: empty names, single-character names, Unicode characters, and names that differ from the Apple ID's registered name.
This scenario tests the edge case where the user modifies their name before consenting. Since Apple only sends the name on first login, whatever value the user enters (or clears) is the only name your app will ever receive from Apple for this user.
Modified Name on Consent Screen
ModerateGoal
Complete a first-time Sign in with Apple where the user edits their displayed name before consenting. Verify the backend stores the modified name, not the Apple ID's registered name.
Playwright Implementation
What to Assert Beyond the UI
- Database stores “CustomFirst” and “CustomLast”, not the Apple ID's real name
- Profile display renders the custom name throughout the app
- Empty name fields are handled gracefully with a fallback (e.g., “Apple User”)
Name Sharing: Playwright vs Assrt
import { test, expect } from '@playwright/test';
import * as OTPAuth from 'otpauth';
test('Apple login: modified name', async ({ page }) => {
await page.goto('/');
const popupPromise = page.waitForEvent('popup');
await page.getByRole('button', { name: /sign in with apple/i }).click();
const applePopup = await popupPromise;
await applePopup.waitForURL(/appleid.apple.com/);
// ... authenticate with email, password, 2FA (12 lines omitted)
await applePopup.locator('input[name="firstName"]').clear();
await applePopup.locator('input[name="firstName"]').fill('CustomFirst');
await applePopup.locator('input[name="lastName"]').clear();
await applePopup.locator('input[name="lastName"]').fill('CustomLast');
await applePopup.getByRole('button', { name: /continue/i }).click();
await page.waitForURL('**/dashboard**');
await expect(page.getByText('CustomFirst')).toBeVisible();
});7. Scenario: ID Token Validation and Claims
Apple returns an identity token (a signed JWT) in the callback POST. Your backend must validate this token before trusting its claims. The validation steps are: verify the signature using Apple's public keys (fetched from https://appleid.apple.com/auth/keys), confirm the iss is https://appleid.apple.com, confirm the aud matches your Service ID, and check that the token is not expired. Failing to validate any of these allows token forgery attacks.
This scenario intercepts the network request to your callback endpoint and inspects the identity token that Apple sends. The test validates the token structure and claims without depending on UI elements.
ID Token Validation
ModerateGoal
Intercept the Apple callback and validate that the identity token has the correct issuer, audience, and signature.
Playwright Implementation
What to Assert Beyond the UI
- Token signature validates against Apple's published JWK set
- The
issclaim is exactlyhttps://appleid.apple.com - The
audclaim matches your registered Service ID - The
exptimestamp is in the future - The
email_verifiedclaim istrue
8. Scenario: Testing Private Relay Email Forwarding
Once a user has signed up with “Hide My Email”, your app needs to send transactional emails (welcome emails, password resets, notifications) to their private relay address. Apple forwards these emails to the user's real inbox, but only if your sending domain is registered and the email passes SPF, DKIM, and DMARC checks. A misconfigured sending domain causes emails to silently fail with no bounce notification.
This scenario verifies the complete email forwarding pipeline: sign up with Hide My Email, trigger a transactional email from your app, and confirm the email arrives at the real inbox. You need either access to the real inbox behind the Apple ID or a mail interception service.
Private Relay Email Forwarding
ComplexGoal
After a user signs up with Hide My Email, send a transactional email to the relay address and verify it is delivered to the user's real inbox.
Playwright Implementation
What to Assert Beyond the UI
- Email arrives at the real inbox within 60 seconds
- The “From” address matches your registered sending domain
- SPF, DKIM, and DMARC headers pass validation
- The email body renders correctly (no relay header corruption)
Email Forwarding: Playwright vs Assrt
import { test, expect } from '@playwright/test';
import { ImapFlow } from 'imapflow';
test('private relay: email forwarding', async ({ page, request }) => {
const user = await (await request.get('/api/test/current-user', {
headers: { Authorization: `Bearer ${process.env.TEST_SESSION_TOKEN}` },
})).json();
expect(user.email).toContain('@privaterelay.appleid.com');
await request.post('/api/test/send-welcome-email', { data: { userId: user.id } });
const client = new ImapFlow({ host: 'imap.mail.me.com', port: 993, secure: true,
auth: { user: process.env.TEST_APPLE_ID!, pass: process.env.TEST_APPLE_APP_PASSWORD! }
});
await client.connect();
const lock = await client.getMailboxLock('INBOX');
// ... poll for email arrival (15+ lines)
lock.release(); await client.logout();
});9. Common Pitfalls That Break Apple Auth Test Suites
These are real issues sourced from Apple Developer Forums, Stack Overflow, and GitHub issues on popular Sign in with Apple libraries. Each one has broken production test suites.
Pitfalls to Watch For
- Missing the popup event: if you click the Apple button without calling page.waitForEvent('popup') first, Playwright loses the popup window and your test hangs forever
- Apple sends name and email only on first authorization. If you forget to persist them, they are gone permanently. Re-authorizing does not resend them.
- The client secret JWT expires after 6 months maximum. If your CI environment does not regenerate it, all Apple auth tests fail silently with a generic 'invalid_client' error.
- Private relay emails bounce if your sending domain is not registered in the Apple Developer portal under Email Communication. There is no error, emails just vanish.
- Apple's 2FA codes are time-based (TOTP). If your CI server's clock drifts by more than 30 seconds, every 2FA code will be rejected.
- The Apple ID login page DOM changes without notice. Selectors like input#account_name_text_field have changed in the past. Pin your selectors and add fallback strategies.
- Running multiple Apple auth tests in parallel causes rate limiting. Apple throttles login attempts per Apple ID. Run auth tests serially.
- Safari vs Chromium: Apple's login page may render differently (iframe vs popup). Test on Chromium for CI, but verify manually on Safari for production parity.
Revoking Authorization for Repeatable First-Login Tests
The biggest testing challenge is that first-login behavior (consent screen, name sharing, email choice) only happens once. To re-test it, you must revoke the Apple ID's authorization for your app. Apple provides a REST API for this using the token revocation endpoint.
Handling Clock Drift for 2FA
Apple's 2FA uses standard TOTP with a 30-second window. If your CI server's system clock is even slightly out of sync, every generated code will be invalid. Use NTP synchronization in your CI pipeline, or account for drift by generating codes with an offset.
10. Writing These Scenarios in Plain English with Assrt
The Playwright code above is robust and battle-tested, but each scenario requires 30 to 60 lines of TypeScript that deal with popup handling, 2FA code generation, DOM selectors for Apple's login page, and conditional logic for consent screens. Assrt lets you express these same scenarios in plain English. The compiler generates the Playwright TypeScript, handles popup management, 2FA integration, and selector maintenance automatically.
Here is the full first-time login with private relay email scenario from Section 4, rewritten as an Assrt file. This is the same test; it compiles to equivalent Playwright code.
Assrt compiles each scenario block into the same Playwright TypeScript you saw in the preceding sections, committed to your repo as real tests you can read, run, and modify. When Apple changes the DOM of their login page or updates the consent screen layout, Assrt detects the failure, analyzes the new DOM, and opens a pull request with updated locators. Your scenario files stay untouched.
Start with the first-time login scenario using real email. Once it is green in your CI, add the private relay scenario, then the returning user flow, then the name sharing edge cases, then the token validation check. In a single afternoon you can have complete Sign in with Apple coverage that most production applications never achieve by hand.
Related Guides
How to Test Auth0 Universal Login
A practical, scenario-by-scenario guide to testing Auth0 Universal Login with Playwright....
How to Test Azure AD Login
A practical, scenario-by-scenario guide to testing Azure AD (Entra ID) login with...
How to Test Clerk Sign-In
A practical, scenario-by-scenario guide to testing Clerk authentication with Playwright....
Ready to automate your testing?
Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.