Accedi con Apple e Google Sign-In in SwiftUI & Backend Laravel Docker: Guida Completa da A a Z

Le linee guida di Apple per l'App Store (App Store Review Guideline 4.8) richiedono obbligatoriamente che qualsiasi applicazione che offre il login tramite terze parti (come Google, Facebook o GitHub) offra come opzione equivalente anche Accedi con Apple (Sign in with Apple).

In questo tutorial vediamo come implementare un'esperienza di login fluida e sicura sia con Apple che con Google in un'app SwiftUI, convalidando crittograficamente i token JWT ricevuti sul backend Laravel Sanctum containerizzato con Docker.

1. Il Flusso di Autenticazione Mobile Sicuro

Un errore di sicurezza gravissimo è inviare dal client solo l'indirizzo email e fidarsi ciecamente della richiesta. Il flusso corretto prevede:

  1. L'utente effettua il login nativo tramite Face ID / Touch ID (Apple) o web sheet OAuth (Google).
  2. Il framework di sistema restituisce un Identity Token crittografato (JWT) firmato direttamente da Apple o Google.
  3. L'app invia questo identity_token alle API del backend.
  4. Il backend Laravel verifica la firma digitale del token contattando gli endpoint pubblici di Apple (JWKS) o Google, estrae l'identificativo univoco (sub) e l'email, crea o aggiorna l'utente nel database PostgreSQL ed emette un Bearer Token Sanctum per le successive richieste autenticate.

2. Implementazione Client SwiftUI

1. Pulsante "Accedi con Apple" in SwiftUI:

import SwiftUI
import AuthenticationServices

struct AppleLoginButtonView: View {
    @ObservedObject var authManager = AuthManager.shared

    var body: some View {
        SignInWithAppleButton(.signIn) { request in
            request.requestedScopes = [.fullName, .email]
        } onCompletion: { result in
            switch result {
            case .success(let authorization):
                if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
                    let userIdentifier = appleIDCredential.user
                    let identityTokenData = appleIDCredential.identityToken
                    let identityToken = identityTokenData.flatMap { String(data: $0, encoding: .utf8) } ?? ""
                    let email = appleIDCredential.email ?? ""
                    let fullName = [appleIDCredential.fullName?.givenName, appleIDCredential.fullName?.familyName]
                        .compactMap { $0 }.joined(separator: " ")

                    Task {
                        await authManager.loginWithApple(
                            identityToken: identityToken,
                            userIdentifier: userIdentifier,
                            email: email,
                            fullName: fullName
                        )
                    }
                }
            case .failure(let error):
                print("Errore Sign in with Apple: \(error.localizedDescription)")
            }
        }
        .signInWithAppleButtonStyle(.white)
        .frame(height: 50)
        .clipShape(RoundedRectangle(cornerRadius: 12))
    }
}

3. Convalida del Token sul Backend Laravel (Docker)

Nel container Docker del backend Laravel, creiamo un controller dedicato per validare i token e creare la sessione utente.

Controller AuthController.php in Laravel:

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;

class AuthController extends Controller
{
    /**
     * Autenticazione con Sign in with Apple
     */
    public function appleLogin(Request $request)
    {
        $validated = $request->validate([
            'identity_token' => 'required|string',
            'user_identifier' => 'required|string',
            'email' => 'nullable|email',
            'full_name' => 'nullable|string',
        ]);

        // Recupera le chiavi pubbliche ufficiali di Apple (JWKS)
        $response = Http::get('https://appleid.apple.com/auth/keys');
        if (!$response->successful()) {
            return response()->json(['error' => 'Impossibile contattare i server Apple'], 500);
        }

        $jwks = $response->json();
        $keys = JWK::parseKeySet($jwks);

        try {
            $decoded = JWT::decode($validated['identity_token'], $keys);
        } catch (\Exception $e) {
            return response()->json(['error' => 'Token Apple non valido o scaduto'], 401);
        }

        $appleSub = $decoded->sub;
        $email = $validated['email'] ?? $decoded->email ?? "{$appleSub}@privaterelay.appleid.com";
        $name = $validated['full_name'] ?: 'Utente Apple';

        // Crea o trova l'utente nel DB PostgreSQL
        $user = User::firstOrCreate(
            ['apple_id' => $appleSub],
            [
                'name' => $name,
                'email' => $email,
                'password' => bcrypt(Str::random(32)),
                'handle' => Str::slug($name) . '_' . Str::random(4),
            ]
        );

        $token = $user->createToken('mobile-ios-app')->plainTextToken;

        return response()->json([
            'token' => $token,
            'user' => $user,
        ]);
    }

    /**
     * Autenticazione con Google Sign-In
     */
    public function googleLogin(Request $request)
    {
        $validated = $request->validate([
            'id_token' => 'required|string',
            'full_name' => 'nullable|string',
        ]);

        // Verifica token direttamente con Google OAuth2
        $response = Http::get('https://oauth2.googleapis.com/tokeninfo', [
            'id_token' => $validated['id_token'],
        ]);

        if (!$response->successful()) {
            return response()->json(['error' => 'Token Google non valido'], 401);
        }

        $payload = $response->json();
        $googleId = $payload['sub'];
        $email = $payload['email'];
        $name = $validated['full_name'] ?: ($payload['name'] ?? 'Utente Google');

        $user = User::firstOrCreate(
            ['google_id' => $googleId],
            [
                'name' => $name,
                'email' => $email,
                'password' => bcrypt(Str::random(32)),
                'handle' => Str::slug($name) . '_' . Str::random(4),
            ]
        );

        $token = $user->createToken('mobile-ios-app')->plainTextToken;

        return response()->json([
            'token' => $token,
            'user' => $user,
        ]);
    }
}

4. Gestione della Private Relay Email & Privacy

Nota Importante su Apple Private Relay

Quando un utente seleziona l'opzione "Nascondi la mia email", Apple genera un indirizzo del tipo xyz@privaterelay.appleid.com. Ricordati che Apple restituisce il nome e l'email solo durante il primissimo login. Salva immediatamente questi dati nel tuo database durante la registrazione iniziale!

5. Sicurezza e Checklist di Rilascio

  • Zero Secret Hardcoded: Configura sempre le chiavi di Bundle ID e Client ID tramite variabili di ambiente .env all'interno dei container Docker.
  • Persistenza Sicura: Salva il token Sanctum nel Keychain di iOS tramite Security Framework o KeychainAccess, mai in chiaro in file di testo non protetti.
  • Revoca delle Sessioni: Implementa l'endpoint /api/auth/logout che revoca il token corrente: $request->user()->currentAccessToken()->delete();.
Autenticazione & Backend Sicuro

Vuoi implementare un sistema di autenticazione moderno e a prova di bomba?

Offriamo sviluppo backend e client nativo iOS con standard di sicurezza elevati, supporto per autenticazione biometrica (Face ID) e conformità alle linee guida Apple App Store.

Diallo Yunus

Diallo Yunus

Sviluppatore iOS Indie & Consulente Cloud

Sviluppatore professionista con oltre 11 app pubblicate su Apple App Store e referente per la monetizzazione digitale internazionale. Con base a Parma (PR), Italia e partnership operativa con Group DMS in Africa.