Guide

Login with Apple

Use Sign in with Apple with Zuvo

Zuvo Auth supports using Sign in with Apple on the web and in native apps for iOS, macOS, watchOS or tvOS.

Overview

To support Sign in with Apple, you need to configure the Apple provider in the Zuvo dashboard for your project.

There are three general ways to use Sign in with Apple, depending on the application you're trying to build:

In some cases you're able to use the OAuth flow within web-based native apps such as with React Native, Expo or other similar frameworks. It is best practice to use native Sign in with Apple capabilities on those platforms instead.

When developing with Expo, you can test Sign in with Apple via the Expo Go app, in all other cases you will need to obtain an Apple Developer account to enable the capability.

Web

Using the OAuth flow for web

Sign in with Apple's OAuth flow is designed for web or browser based sign in methods. It can be used on web-based apps as well as websites, though some users can benefit by using Sign in with Apple JS directly.

Behind the scenes, Zuvo Auth uses the REST APIs provided by Apple.

To initiate sign in, you can use the signInWithOAuth() method from the Zuvo JavaScript library:

    import { createClient } from '@supabase/supabase-js'
    const supabase = createClient('https://your-project-id.supabase.co', 'sb_publishable_...')

    // ---cut---
    supabase.auth.signInWithOAuth({
      provider: 'apple',
    })

This call takes the user to Apple's consent screen. Once the flow ends, the user's profile information is exchanged and validated with Zuvo Auth before it redirects back to your web application with an access and refresh token representing the user's session.

For a PKCE flow, for example in Server-Side Auth, you need an extra step to handle the code exchange. When calling signInWithOAuth, provide a redirectTo URL which points to a callback route. This redirect URL should be added to your redirect allow list.

Client

In the browser, signInWithOAuth automatically redirects to the OAuth provider's authentication endpoint, which then redirects to your endpoint.

import { createClient, type Provider } from '@supabase/supabase-js';
const supabase = createClient('https://your-project-id.supabase.co', 'sb_publishable_...')
const provider = 'provider' as Provider

// ---cut---
await supabase.auth.signInWithOAuth({
  provider,
  options: {
    redirectTo: `http://example.com/auth/callback`,
  },
})
Server

In the server, you need to handle the redirect to the OAuth provider's authentication endpoint. The signInWithOAuth method returns the endpoint URL, which you can redirect to.

import { createClient, type Provider } from '@supabase/supabase-js'
const supabase = createClient('https://your-project-id.supabase.co', 'sb_publishable_...')
const provider = 'provider' as Provider
const redirect = (url: string) => {}

// ---cut---
const { data, error } = await supabase.auth.signInWithOAuth({
  provider,
  options: {
    redirectTo: 'http://example.com/auth/callback',
  },
})

if (data.url) {
  redirect(data.url) // use the redirect API for your server framework
}

At the callback endpoint, handle the code exchange to save the user session.

Next.js

Create a new file at app/auth/callback/route.ts and populate with the following:

import { NextResponse } from 'next/server'

// The client you created from the Server-Side Auth instructions
import { createClient } from '@/utils/supabase/server'

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get('code')
  // if "next" is in param, use it as the redirect URL
  let next = searchParams.get('next') ?? '/'
  if (!next.startsWith('/')) {
    // if "next" is not a relative URL, use the default
    next = '/'
  }

  if (code) {
    const supabase = await createClient()
    const { error } = await supabase.auth.exchangeCodeForSession(code)
    if (!error) {
      const forwardedHost = request.headers.get('x-forwarded-host') // original origin before load balancer
      const isLocalEnv = process.env.NODE_ENV === 'development'
      if (isLocalEnv) {
        // we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
        return NextResponse.redirect(`${origin}${next}`)
      } else if (forwardedHost) {
        return NextResponse.redirect(`https://${forwardedHost}${next}`)
      } else {
        return NextResponse.redirect(`${origin}${next}`)
      }
    }
  }

  // return the user to an error page with instructions
  return NextResponse.redirect(`${origin}/auth/auth-code-error`)
}
SvelteKit

Create a new file at src/routes/auth/callback/+server.js and populate with the following:

import { redirect } from '@sveltejs/kit';

export const GET = async (event) => {
	const {
		url,
		locals: { supabase }
	} = event;
	const code = url.searchParams.get('code') as string;
	const next = url.searchParams.get('next') ?? '/';

  if (code) {
    const { error } = await supabase.auth.exchangeCodeForSession(code)
    if (!error) {
      redirect(303, `/${next.slice(1)}`);
    }
  }

  // return the user to an error page with instructions
  redirect(303, '/auth/auth-code-error');
};
Astro

Create a new file at src/pages/auth/callback.ts and populate with the following:

import { createServerClient, parseCookieHeader } from '@supabase/ssr'
import { type APIRoute } from 'astro'

export const GET: APIRoute = async ({ request, cookies, redirect }) => {
  const requestUrl = new URL(request.url)
  const code = requestUrl.searchParams.get('code')
  const next = requestUrl.searchParams.get('next') || '/'

  if (code) {
    const supabase = createServerClient(
      import.meta.env.PUBLIC_SUPABASE_URL,
      import.meta.env.PUBLIC_SUPABASE_PUBLISHABLE_KEY,
      {
        cookies: {
          getAll() {
            return parseCookieHeader(Astro.request.headers.get('Cookie') ?? '')
          },
          setAll(cookiesToSet, _headers) {
            cookiesToSet.forEach(({ name, value, options }) =>
              Astro.cookies.set(name, value, options)
            )
          },
        },
      }
    )

    const { error } = await supabase.auth.exchangeCodeForSession(code)

    if (!error) {
      return redirect(next)
    }
  }

  // return the user to an error page with instructions
  return redirect('/auth/auth-code-error')
}
Remix

Create a new file at app/routes/auth.callback.tsx and populate with the following:

import { redirect, type LoaderFunctionArgs } from '@remix-run/node'
import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'

export async function loader({ request }: LoaderFunctionArgs) {
  const requestUrl = new URL(request.url)
  const code = requestUrl.searchParams.get('code')
  const next = requestUrl.searchParams.get('next') || '/'
  const responseHeaders = new Headers()

  if (code) {
    const supabase = createServerClient(
      process.env.SUPABASE_URL!,
      process.env.SUPABASE_PUBLISHABLE_KEY!,
      {
        cookies: {
          getAll() {
            return parseCookieHeader(request.headers.get('Cookie') ?? '')
          },
          setAll(cookiesToSet, cacheHeaders) {
            cookiesToSet.forEach(({ name, value, options }) =>
              responseHeaders.append('Set-Cookie', serializeCookieHeader(name, value, options))
            )
            Object.entries(cacheHeaders).forEach(([key, value]) => responseHeaders.set(key, value))
          },
        },
      }
    )

    const { error } = await supabase.auth.exchangeCodeForSession(code)

    if (!error) {
      return redirect(next, { headers: responseHeaders })
    }
  }

  // return the user to an error page with instructions
  return redirect('/auth/auth-code-error', { headers: responseHeaders })
}
Express

Create a new route in your express app and populate with the following:

...
app.get("/auth/callback", async function (req, res) {
  const code = req.query.code
  const next = req.query.next ?? "/"

  if (code) {
    const supabase = createServerClient(
      process.env.SUPABASE_URL,
      process.env.SUPABASE_PUBLISHABLE_KEY, {
    cookies: {
      getAll() {
        return parseCookieHeader(req.headers.cookie ?? '')
      },
      setAll(cookiesToSet, headers) {
        cookiesToSet.forEach(({ name, value, options }) =>
          res.appendHeader('Set-Cookie', serializeCookieHeader(name, value, options))
        )
        Object.entries(headers).forEach(([key, value]) =>
          res.setHeader(key, value)
        )
      },
    },
  })
    await supabase.auth.exchangeCodeForSession(code)
  }

  res.redirect(303, `/${next.slice(1)}`)
})

Configuration [#configuration-web-oauth]

You will require the following information:

  1. Your Apple Developer account's Team ID, which is an alphanumeric string of 10 characters that uniquely identifies the developer of the app. It's often accessible in the upper right-side menu on the Apple Developer Console.
  2. Register email sources for Sign in with Apple for Email Communication which can be found in the Services section of the Apple Developer Console. This enables Apple to send relay emails through your domain when users choose to hide their email addresses.
  3. An App ID which uniquely identifies the app you are building. You can create a new App ID from the Identifiers section in the Apple Developer Console (use the filter menu in the upper right side to see all App IDs). These usually are a reverse domain name string, for example com.example.app. Make sure you configure Sign in with Apple once you create an App ID in the Capabilities list. At this time Zuvo Auth does not support Server-to-Server notification endpoints, so you should leave that setting blank. (In the past App IDs were referred to as bundle IDs.)
  4. A Services ID which uniquely identifies the web services provided by the app you registered in the previous step. You can create a new Services ID from the Identifiers section in the Apple Developer Console (use the filter menu in the upper right side to see all Services IDs). These usually are a reverse domain name string, for example com.example.app.web.
  5. Configure Website URLs for the newly created Services ID. The web domain you should use is the domain your Zuvo project is hosted on. This is usually <project-id>.supabase.co while the redirect URL is https://<project-id>.supabase.co/auth/v1/callback.
  6. Create a signing Key in the Keys section of the Apple Developer Console. You can use this key to generate a secret key using the tool below, which is added to your Zuvo project's Auth configuration. Make sure you safely store the AuthKey_XXXXXXXXXX.p8 file. If you ever lose access to it, or make it public accidentally, revoke it from the Apple Developer Console and create a new one immediately.
  7. Finally, add the information you configured above to the Apple provider configuration in the Zuvo dashboard. If your project also uses native Sign in with Apple (for example on iOS, Expo, or Flutter), list this Services ID as the first entry in the Client IDs field. Zuvo uses the first client ID in the list for the web signInWithOAuth flow, while the native signInWithIdToken flow accepts any client ID in the list as a valid token audience, regardless of order. If a native App ID comes before the Services ID, native sign-in keeps working but web sign-in is rejected by Apple.

You can also configure the Apple auth provider using the Management API:

# Get your access token from https://studio.zuvodev.com/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"

# Configure Apple auth provider
curl -X PATCH "https://api.zuvodev.com/v1/projects/$PROJECT_REF/config/auth" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "external_apple_enabled": true,
    "external_apple_client_id": "your-services-id",
    "external_apple_secret": "your-generated-secret-key"
  }'

Using sign in with Apple JS

Sign in with Apple JS is an official Apple framework for authenticating Apple users on websites. Although it can be used in web-based apps, those use cases will benefit more with the OAuth flow described above. We recommend using this method on classic websites only.

You can use the signInWithIdToken() method from the Zuvo JavaScript library on the website to obtain an access and refresh token once the user has given consent using Sign in with Apple JS:

    async function signIn() {
      try {
        // Generate a nonce for security
        const nonce = crypto.randomUUID() // or use your preferred nonce generation method

        const data = await AppleID.auth.signIn()

        const { data: authData, error } = await supabase.auth.signInWithIdToken({
          provider: 'apple',
          token: data.id_token,
          nonce: nonce,
        })

        if (error) {
          throw error
        }

        // Apple only provides the user's name on the first sign-in
        // The user object contains name information from Apple's response
        if (data.user && data.user.name) {
          const fullName = [
            data.user.name.firstName,
            data.user.name.middleName,
            data.user.name.lastName
          ].filter(Boolean).join(' ')

          // Save the name to user metadata for future use
          await supabase.auth.updateUser({
            data: {
              full_name: fullName,
              given_name: data.user.name.firstName,
              family_name: data.user.name.lastName,
            }
          })
        }
      } catch (error) {
        console.error('Apple sign in failed:', error)
        // Handle sign-in errors appropriately
      }
    }

Alternatively, you can use the AppleIDSignInOnSuccess event with the usePopup option:

    // Generate and store nonce for verification
    const nonce = crypto.randomUUID()

    // Initialize Apple ID with nonce
    AppleID.auth.init({
      clientId: 'your-services-id',
      scope: 'name email',
      redirectURI: 'https://your-domain.com/auth/callback',
      usePopup: true,
      nonce: nonce,
    })

    // Listen for authorization success
    document.addEventListener('AppleIDSignInOnSuccess', async (event) => {
      try {
        const { data: authData, error } = await supabase.auth.signInWithIdToken({
          provider: 'apple',
          token: event.detail.authorization.id_token,
          nonce: nonce,
        })

        if (error) {
          throw error
        }

        // Apple only provides the user's name on the first sign-in
        if (event.detail.user && event.detail.user.name) {
          const fullName = [
            event.detail.user.name.firstName,
            event.detail.user.name.middleName,
            event.detail.user.name.lastName
          ].filter(Boolean).join(' ')

          // Save the name to user metadata for future use
          await supabase.auth.updateUser({
            data: {
              full_name: fullName,
              given_name: event.detail.user.name.firstName,
              family_name: event.detail.user.name.lastName,
            }
          })
        }
      } catch (error) {
        console.error('Apple sign in failed:', error)
      }
    })

Make sure you request the scope name email when initializing the library, as shown in the example above.

Configuration [#configuration-web-apple-js]

To use Sign in with Apple JS you need to configure these options:

  1. Have an App ID which uniquely identifies the app you are building. You can create a new App ID from the Identifiers section in the Apple Developer Console (use the filter menu in the upper right side to see all App IDs). These usually are a reverse domain name string, for example com.example.app. Make sure you configure Sign in with Apple for the App ID you created or already have, in the Capabilities list. At this time Zuvo Auth does not support Server-to-Server notification endpoints, so you should leave that setting blank. (In the past App IDs were referred to as bundle IDs.)
  2. Obtain a Services ID attached to the App ID that uniquely identifies the website. Use this value as the client ID when initializing Sign in with Apple JS. You can create a new Services ID from the Identifiers section in the Apple Developer Console (use the filter menu in the upper right side to see all Services IDs). These usually are a reverse domain name string, for example com.example.app.website.
  3. Configure Website URLs for the newly created Services ID. The web domain you should use is the domain your website is hosted on. The redirect URL must also point to a page on your website that will receive the callback from Apple.
  4. Register the Services ID you created to your project's Apple provider configuration in the Zuvo dashboard under Client IDs.
Expo React Native

Using native sign in with Apple in Expo

When working with Expo, you can use the Expo AppleAuthentication library to obtain an ID token that you can pass to supabase-js signInWithIdToken method.

Follow the Expo docs for installation and configuration instructions. See the supabase-js reference for instructions on initializing the supabase-js client in React Native.

    import { Platform } from 'react-native'
    import * as AppleAuthentication from 'expo-apple-authentication'
    import { supabase } from 'app/utils/supabase'

    export function Auth() {
      if (Platform.OS === 'ios')
        return (
          <AppleAuthentication.AppleAuthenticationButton
            buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN}
            buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
            cornerRadius={5}
            style={{ width: 200, height: 64 }}
            onPress={async () => {
              try {
                const credential = await AppleAuthentication.signInAsync({
                  requestedScopes: [
                    AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
                    AppleAuthentication.AppleAuthenticationScope.EMAIL,
                  ],
                })
                // Sign in via Zuvo Auth.
                if (credential.identityToken) {
                  const {
                    error,
                    data: { user },
                  } = await supabase.auth.signInWithIdToken({
                    provider: 'apple',
                    token: credential.identityToken,
                  })
                  console.log(JSON.stringify({ error, user }, null, 2))
                  if (!error) {
                    // Apple only provides the user's full name on the first sign-in
                    // Save it to user metadata if available
                    if (credential.fullName) {
                      const nameParts = []
                      if (credential.fullName.givenName) nameParts.push(credential.fullName.givenName)
                      if (credential.fullName.middleName) nameParts.push(credential.fullName.middleName)
                      if (credential.fullName.familyName) nameParts.push(credential.fullName.familyName)

                      const fullName = nameParts.join(' ')

                      await supabase.auth.updateUser({
                        data: {
                          full_name: fullName,
                          given_name: credential.fullName.givenName,
                          family_name: credential.fullName.familyName,
                        }
                      })
                    }
                    // User is signed in.
                  }
                } else {
                  throw new Error('No identityToken.')
                }
              } catch (e) {
                if (e.code === 'ERR_REQUEST_CANCELED') {
                  // handle that the user canceled the sign-in flow
                } else {
                  // handle other errors
                }
              }
            }}
          />
        )
      return (
        <>
          
          <Button
            onPress={async () => {
              const { error } = await supabase.auth.signInWithOAuth({
                provider: 'apple',
                options: {
                  redirectTo: 'your-app-scheme://auth/callback',
                  skipBrowserRedirect: false,
                },
              })
              if (error) {
                console.error('Sign in error:', error)
              }
            }}
          >
            Sign in with Apple
          </Button>
        </>
      )
    }

When working with bare React Native, you can use invertase/react-native-apple-authentication to obtain the ID token on iOS. For Android, use the OAuth flow as shown above.

Configuration [#configuration-expo-native]

  1. Have an App ID which uniquely identifies the app you are building. You can create a new App ID from the Identifiers section in the Apple Developer Console (use the filter menu in the upper right side to see all App IDs). These usually are a reverse domain name string, for example com.example.app. Make sure you configure Sign in with Apple for the App ID you created or already have, in the Capabilities list. At this time Zuvo Auth does not support Server-to-Server notification endpoints, so you should leave that setting blank. (In the past App IDs were referred to as bundle IDs.)
  2. Register all of the App IDs that will be using your Zuvo project in the Apple provider configuration in the Zuvo dashboard under Client IDs.
Flutter

Sign in with Apple on iOS and macOS

You can perform Sign in with Apple using the sign_in_with_apple package on Flutter apps running on iOS or macOS. Follow the instructions in the package README to set up native Sign in with Apple on iOS and macOS.

Once the setup is complete on the Flutter app, add the bundle ID of your app to your Zuvo dashboard in Authentication -> Providers -> Apple in order to register your app with Zuvo.

    import 'package:sign_in_with_apple/sign_in_with_apple.dart';
    import 'package:supabase_flutter/supabase_flutter.dart';
    import 'package:crypto/crypto.dart';

    /// Performs Apple sign in on iOS or macOS
    Future<AuthResponse> signInWithApple() async {
      final rawNonce = supabase.auth.generateRawNonce();
      final hashedNonce = sha256.convert(utf8.encode(rawNonce)).toString();

      final credential = await SignInWithApple.getAppleIDCredential(
        scopes: [
          AppleIDAuthorizationScopes.email,
          AppleIDAuthorizationScopes.fullName,
        ],
        nonce: hashedNonce,
      );

      final idToken = credential.identityToken;
      if (idToken == null) {
        throw const AuthException(
            'Could not find ID Token from generated credential.');
      }

      final authResponse = await supabase.auth.signInWithIdToken(
        provider: OAuthProvider.apple,
        idToken: idToken,
        nonce: rawNonce,
      );

      // Apple only provides the user's full name on the first sign-in
      // Save it to user metadata if available
      if (credential.givenName != null || credential.familyName != null) {
        final nameParts = <String>[];
        if (credential.givenName != null) nameParts.add(credential.givenName!);
        if (credential.familyName != null) nameParts.add(credential.familyName!);

        final fullName = nameParts.join(' ');

        await supabase.auth.updateUser(
          UserAttributes(
            data: {
              'full_name': fullName,
              'given_name': credential.givenName,
              'family_name': credential.familyName,
            },
          ),
        );
      }

      return authResponse;
    }

Configuration [#configuration-flutter-native]

  1. Have an App ID which uniquely identifies the app you are building. You can create a new App ID from the Identifiers section in the Apple Developer Console (use the filter menu in the upper right side to see all App IDs). These usually are a reverse domain name string, for example com.example.app. Make sure you configure Sign in with Apple for the App ID you created or already have, in the Capabilities list. At this time Zuvo Auth does not support Server-to-Server notification endpoints, so you should leave that setting blank. (In the past App IDs were referred to as bundle IDs.)
  2. Register all of the App IDs that will be using your Zuvo project in the Apple provider configuration in the Zuvo dashboard under Client IDs.

Sign in with Apple on Android, Web, Windows and Linux

For platforms that don't support native Sign in with Apple, you can use the signInWithOAuth() method to perform Sign in with Apple.

This method of signing in is web based, and will open a browser window to perform the sign in. For non-web platforms, the user is brought back to the app via deep linking.

    await supabase.auth.signInWithOAuth(
      OAuthProvider.apple,
      redirectTo: kIsWeb ? null : 'my.scheme://my-host', // Optionally set the redirect link to bring back the user via deeplink.
      authScreenLaunchMode:
          kIsWeb ? LaunchMode.platformDefault : LaunchMode.externalApplication, // Launch the auth screen in a new webview on mobile.
    );

This call takes the user to Apple's consent screen. Once the flow ends, the user's profile information is exchanged and validated with Zuvo Auth before it redirects back to your Flutter application with an access and refresh token representing the user's session.

Configuration [#configuration-flutter-web]

You will require the following information:

  1. Your Apple Developer account's Team ID, which is an alphanumeric string of 10 characters that uniquely identifies the developer of the app. It's often accessible in the upper right-side menu on the Apple Developer Console.
  2. Register email sources for Sign in with Apple for Email Communication which can be found in the Services section of the Apple Developer Console.
  3. An App ID which uniquely identifies the app you are building. You can create a new App ID from the Identifiers section in the Apple Developer Console (use the filter menu in the upper right side to see all App IDs). These usually are a reverse domain name string, for example com.example.app. Make sure you configure Sign in with Apple once you create an App ID in the Capabilities list. At this time Zuvo Auth does not support Server-to-Server notification endpoints, so you should leave that setting blank. (In the past App IDs were referred to as bundle IDs.)
  4. A Services ID which uniquely identifies the web services provided by the app you registered in the previous step. You can create a new Services ID from the Identifiers section in the Apple Developer Console (use the filter menu in the upper right side to see all Services IDs). These usually are a reverse domain name string, for example com.example.app.web.
  5. Configure Website URLs for the newly created Services ID. The web domain you should use is the domain your Zuvo project is hosted on. This is usually <project-id>.supabase.co while the redirect URL is https://<project-id>.supabase.co/auth/v1/callback.
  6. Create a signing Key in the Keys section of the Apple Developer Console. You can use this key to generate a secret key using the tool below, which is added to your Zuvo project's Auth configuration. Make sure you safely store the AuthKey_XXXXXXXXXX.p8 file. If you ever lose access to it, or make it public accidentally, revoke it from the Apple Developer Console and create a new one immediately. You will have to generate a new secret key using this file every 6 months, so make sure you schedule a recurring reminder in your calendar!
  7. Finally, add the information you configured above to the Apple provider configuration in the Zuvo dashboard. If your app also uses native Sign in with Apple (on iOS or macOS), list this Services ID as the first entry in the Client IDs field. Zuvo uses the first client ID in the list for the web signInWithOAuth flow, while the native signInWithIdToken flow accepts any client ID in the list as a valid token audience, regardless of order. If a native App ID comes before the Services ID, native sign-in keeps working but web sign-in is rejected by Apple.