# Social sign-in setup

The Google and Apple credentials to create, their return URLs, the Worker secrets and the mobile side of Continue with Apple and Continue with Google.

## How it works [#how-it-works]

Continue with Apple and Continue with Google are built on web (`/login`, `/signup`) and in the app (P-Entrar, sign-up), and stay hidden until the server has credentials. `appConfig.get` lists a provider in `socialProviders` only when its id **and** secret are set ([`social-providers.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/auth/src/social-providers.ts)). Without any credentials nothing changes on either surface.

| Surface | Flow                                                                                                                                                                                                                                                                                                                                                                                                          |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Web     | The browser leaves for the provider, returns to `https://<origin>/api/auth/callback/<provider>`, then goes to the page that sent it to `/login` (`?redirect=`) or to `/dashboard`. A refusal returns to the same page with `?error=`; the page explains `account_not_linked` and offers a retry for anything else. A cancel shows nothing.                                                                    |
| App     | Better Auth's Expo plugin opens the same server flow in a system auth session (`ASWebAuthenticationSession` on iOS, a Custom Tab on Android). The callback returns to the app scheme (`autofolio:///`, `autofolio-dev:///` in the dev build) with the session, and `useEnterApp` lands on the garage. A refusal also returns to the scheme, so the sheet closes; the app shows no message for it (see below). |

On iOS Google shows only beside Apple (App Store guideline 4.8) and Apple shows only on iOS ([`auth.social.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/apps/mobile/src/features/auth/auth.social.ts)); the web offers both.

**Account linking.** The first sign-in with a new email creates the account and its Organization. An email that already has an account is joined only when the provider vouches for the email **and** the account's own email is verified (`account.accountLinking` with `requireLocalEmailVerified`, no trusted providers). Anything else answers `account_not_linked`, so a password sign-up squatting someone's address is never joined. Apple's _Hide My Email_ addresses are new accounts. Linked accounts are managed from the app's Security → Apple and Google. [`social-sign-in.test.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/auth/src/social-sign-in.test.ts) proves the redirect start, the app-scheme return, the refused foreign `callbackURL` and the three linking cases.

## Google [#google]

In [Google Cloud console](https://console.cloud.google.com/) for the AutoFolio project:

<Steps>
<Step>

**OAuth consent screen** (Google Auth Platform → Branding/Audience): user type _External_, app name AutoFolio, support email, authorized domain `its-live.dev`, home page `https://autofolio.its-live.dev`, privacy policy `https://autofolio.its-live.dev/privacy`, terms `https://autofolio.its-live.dev/terms`. Scopes: `openid`, `email`, `profile` only (no verification review needed). Publish the app to _In production_, or only test users can sign in.

</Step>
<Step>

**One OAuth client, type _Web application_** (Clients → Create client):

- Authorized JavaScript origins: `https://autofolio.its-live.dev`, `http://localhost:3005`
- Authorized redirect URIs: `https://autofolio.its-live.dev/api/auth/callback/google`, `http://localhost:3005/api/auth/callback/google`

Its client id and secret become `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`.

</Step>
</Steps>

<Callout>
  No iOS or Android OAuth client is needed. The app signs in through the server's web client in a
  system browser session, so Google only ever sees the web client and its
  `https://…/callback/google` redirect. iOS and Android clients (bundle `io.zietec.autofolio`;
  package `io.zietec.autofolio` with the EAS signing SHA-1) are for the native Google Sign-In SDK,
  which this app does not use. Create them only if that changes; the server would then also have to
  accept their ids as ID-token audiences.
</Callout>

## Apple [#apple]

In [Apple Developer → Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list), team of the App Store account (its **Team ID** is at the top right):

<Steps>
<Step>

**App ID** `io.zietec.autofolio`: enable the _Sign in with Apple_ capability, as a primary App ID.

</Step>
<Step>

**Services ID** (Identifiers → + → Services IDs), e.g. `io.zietec.autofolio.signin`. Enable _Sign in with Apple_ → Configure: primary App ID `io.zietec.autofolio`; _Domains and Subdomains_ `autofolio.its-live.dev`; _Return URLs_ `https://autofolio.its-live.dev/api/auth/callback/apple`. Apple refuses `localhost` and plain `http`: test Apple on the production origin or an HTTPS tunnel registered here too. The Services ID becomes `APPLE_CLIENT_ID`.

</Step>
<Step>

**Key** (Keys → +): name it, enable _Sign in with Apple_, Configure → primary App ID `io.zietec.autofolio`. Download `AuthKey_<KEY_ID>.p8` (Apple lets you download it once; keep it in the password manager, never in the repo) and note the **Key ID**.

</Step>
<Step>

**Client secret.** Apple takes a JWT signed with that key as the client secret, valid for at most six months. Mint it with [`scripts/apple-client-secret.mjs`](https://github.com/CarlosZiegler/autofolio/blob/main/scripts/apple-client-secret.mjs) (180 days) and pipe it straight into the Worker secret, so it never lands in a file or the terminal history:

```sh
node scripts/apple-client-secret.mjs --team-id <TEAM_ID> --key-id <KEY_ID> \
  --client-id io.zietec.autofolio.signin --key ~/secure/AuthKey_<KEY_ID>.p8 \
  | pnpm --filter web exec wrangler secret put APPLE_CLIENT_SECRET --name autofolio-web
```

Re-run it before the 180 days run out (set a reminder); an expired secret makes every Apple sign-in fail with `invalid_client`.

</Step>
<Step>

**Email relay** (Services → Sign in with Apple for Email Communication → Configure): register the sending domain `its-live.dev` and the `EMAIL_FROM` address `noreply@its-live.dev`. Without it, mail to a _Hide My Email_ address (`…@privaterelay.appleid.com`) bounces: verification, sign-in alerts and reminders never arrive.

</Step>
</Steps>

## Worker secrets [#worker-secrets]

The values go to the `autofolio-web` Worker as secrets. Run from the repository root; each command prompts for the value (or reads it from a pipe, as above). `wrangler secret put` deploys a new version at once, so the buttons appear as soon as both values of a provider are set.

| Name                          | Value                                                                       |
| ----------------------------- | --------------------------------------------------------------------------- |
| `GOOGLE_CLIENT_ID`            | The web client's id (`….apps.googleusercontent.com`)                        |
| `GOOGLE_CLIENT_SECRET`        | The web client's secret                                                     |
| `APPLE_CLIENT_ID`             | The Services ID (`io.zietec.autofolio.signin`)                              |
| `APPLE_CLIENT_SECRET`         | The JWT from `scripts/apple-client-secret.mjs`                              |
| `APPLE_APP_BUNDLE_IDENTIFIER` | `io.zietec.autofolio`, the audience of a native Apple ID token (not secret) |

```sh
pnpm --filter web exec wrangler secret put GOOGLE_CLIENT_ID --name autofolio-web
pnpm --filter web exec wrangler secret put GOOGLE_CLIENT_SECRET --name autofolio-web
pnpm --filter web exec wrangler secret put APPLE_CLIENT_ID --name autofolio-web
pnpm --filter web exec wrangler secret put APPLE_APP_BUNDLE_IDENTIFIER --name autofolio-web
# APPLE_CLIENT_SECRET: the pipe in the Apple steps above.
```

`BETTER_AUTH_URL` (`https://autofolio.its-live.dev`) and `NATIVE_APP_SCHEME` (`autofolio`) are already plain vars in `apps/web/wrangler.jsonc`; the callback URLs above derive from the first, the trusted app scheme from the second. Pull request previews get none of these secrets, so they show no social buttons (their origins are not registered with Google or Apple anyway).

Locally, add `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` to `apps/web/.env` (commented in `.env.example`) to try Google on `http://localhost:3005`.

## Mobile (EAS) [#mobile-eas]

Nothing to add. The app reads no Apple or Google value: it calls the server at `EXPO_PUBLIC_SERVER_URL`, already `https://autofolio.its-live.dev` in the `production` profile of `apps/mobile/eas.json`, and asks `appConfig.get` which providers to show. No EAS environment variable, secret or new binary is needed; the buttons appear in installed builds as soon as the Worker has the secrets. The browser flow needs no _Sign in with Apple_ entitlement in the app.

## Check it [#check-it]

```sh
curl -s -X POST https://autofolio.its-live.dev/api/rpc/appConfig/get \
  -H 'content-type: application/json' -d '{"json":{"platform":"web"}}'
```

`socialProviders` lists `apple` and `google`; `/login` shows both buttons above the email form, and each opens the provider's consent page. Then sign in once per provider on the web and in the app.

## Known limits [#known-limits]

- **Native Sign in with Apple.** On iOS, Apple runs in the system auth session, not the native Apple sheet. If App Review asks for the native sheet, add `expo-apple-authentication` (new binary, `usesAppleSignIn`), pass its ID token as `authClient.signIn.social({ provider: "apple", idToken: { token } })`; the server already accepts it through `APPLE_APP_BUNDLE_IDENTIFIER`.
- **Refusals in the app.** The Expo client drops the URL the auth session returns, so the app cannot tell a refusal (`account_not_linked`) from a cancel and shows nothing; the web explains it.

