Jak zrobić logowanie przez Facebook w aplikacji .NET MAUI z wykorzystaniem własnego hostingu z PHP
Implementacja logowania przez Facebook w aplikacjach .NET MAUI może sprawiać problemy, szczególnie na iOS. Rozwiązaniem jest OAuth 2.0 Authorization Code Flow, WebAuthenticator oraz własny serwer pośredniczący w PHP.
Wstęp
Wdrożenie logowania przez Facebook w aplikacji mobilnej opartej na .NET MAUI może być wyzwaniem, zwłaszcza gdy chcemy uniknąć instalowania natywnego SDK Facebooka. Nuget z Xamarin.Facebook.iOS i Xamarin.Facebook.Android jest przestarzały, a oficjalne wsparcie dla MAUI wciąż jest ograniczone. Problem ten jest szczególnie widoczny na platformie iOS, gdzie konfiguracja URL Schemes i obsługa callbacków jest bardziej restrykcyjna niż na Androidzie.
Alternatywnym rozwiązaniem jest wykorzystanie mechanizmu OAuth 2.0 Authorization Code Flow wraz z klasą WebAuthenticator dostępną w .NET MAUI oraz własnym serwerem pośredniczącym, czyli Bridge PHP.
Serwer może działać na dowolnym hostingu obsługującym PHP, a jego zadaniem jest odbieranie kodu autoryzacyjnego od Facebooka i przekazywanie go do aplikacji mobilnej przez niestandardowy schemat URI.
Takie rozwiązanie pozwala:
- korzystać z jednego kodu dla Androida i iOS,
- nie walczyć z Facebook SDK,
- zachować pełną kontrolę nad procesem logowania,
- integrować Facebook Login z istniejącym backendem.
Architektura rozwiązania
Przepływ logowania wygląda następująco:
MAUI App
|
| AuthenticateAsync()
v
Facebook OAuth
|
| redirect_uri=https://twojadomena.pl/fb-bridge
v
fb-bridge.php
|
| 302 Redirect
v
appname://auth?code=...
|
v
WebAuthenticator.Callback()
|
v
MAUI App
|
v
Facebook Graph API
|
v
Backend API
Dlaczego nie można użyć appname://auth bezpośrednio?
W praktyce Facebook często nie pozwala dodać własnego schematu URI, takiego jak appname://auth, do listy Valid OAuth Redirect URIs w panelu Meta Developers.
W takiej sytuacji należy zastosować pośredni adres HTTPS, na przykład https://twojadomena.pl/fb-bridge, który Facebook zaakceptuje jako poprawny Redirect URI. Następnie serwer wykonuje przekierowanie do aplikacji mobilnej.
Konfiguracja Facebook Developers
W panelu aplikacji Facebook, w sekcji Facebook Login -> Settings, należy dodać https://twojadomena.pl/fb-bridge do listy Valid OAuth Redirect URIs.
Konfiguracja Apache
W katalogu hostingu appname adres /fb-bridge jest kierowany na skrypt PHP przez .htaccess. Ten sam plik wymusza HTTPS oraz ustawia poprawny typ odpowiedzi dla pliku apple-app-site-association.
RewriteEngine On
RewriteCond %{ENV:HTTPS} !on
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
RewriteRule ^fb-bridge/?$ fb-bridge.php [L,QSA]
<Files "apple-app-site-association">
ForceType application/json
Header set Content-Type "application/json"
</Files>
Dzięki temu adres https://twojadomena.pl/fb-bridge będzie obsługiwany przez skrypt PHP, a plik AASA będzie zwracany jako JSON, czego wymaga iOS.
Minimalny układ plików na hostingu
twojadomena.pl/
├── .htaccess
├── fb-bridge.php
├── fb-bridge.html
└── .well-known/
├── apple-app-site-association
└── assetlinks.json
Implementacja fb-bridge.php
Plik odbiera odpowiedź OAuth od Facebooka i przekazuje ją do aplikacji. Warto obsłużyć nie tylko code i state, ale także błędy zwracane przez Facebooka, na przykład error i error_description.
<?php
// fb-bridge.php
// Cel: odebrać redirect z Facebooka (?code=...&state=... albo ?error=...)
// i natychmiast wykonać 302 do custom schematu: appname://auth?...
// Nigdy nic nie wypisuj przed nagłówkami:
ob_start();
// Zabezpieczenia i nagłówki anty-cache przeglądarki/proxy:
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
// Odbierz parametry z query (code flow)
$code = isset($_GET['code']) ? $_GET['code'] : null;
$state = isset($_GET['state']) ? $_GET['state'] : null;
// Jeśli OAuth zwrócił błąd:
$error = isset($_GET['error']) ? $_GET['error'] : null;
$error_description = isset($_GET['error_description']) ? $_GET['error_description'] : null;
// Zbuduj URL do aplikacji (custom scheme)
$scheme = 'appname://auth';
$params = [];
if (!empty($code)) { $params['code'] = $code; }
if (!empty($state)) { $params['state'] = $state; }
if (!empty($error)) {
$params['error'] = $error;
if (!empty($error_description)) {
$params['error_description'] = $error_description;
}
}
if (empty($params)) {
http_response_code(200);
echo "appname fb-bridge: brak parametrów do przekazania.";
exit;
}
$query = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
$target = $scheme . '?' . $query;
header('Location: ' . $target, true, 302);
exit;
Po wejściu na https://twojadomena.pl/fb-bridge?code=123 użytkownik zostanie przekierowany do appname://auth?code=123. Jeżeli Facebook zwróci błąd, aplikacja dostanie go w callbacku, zamiast czekać bez końca na wynik.
Fallback HTML dla Facebooka
W katalogu musi istnieć także fb-bridge.html, który przekierowuje do aplikacji przez JavaScript. To wariant do weryfikacji dla Facebooka, który nie uruchamia PHP.
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Redirecting...</title></head>
<body>
<script>
const q = new URLSearchParams(location.search);
const code = q.get('code') || '';
const state = q.get('state') || '';
const target = `appname://auth?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`;
location.replace(target);
</script>
</body>
</html>
Przy konfiguracji iOS z Universal Links ten sam adres /fb-bridge pełni dodatkową rolę: iOS może przejąć link HTTPS i przekazać go bezpośrednio do aplikacji. Dlatego dla iOS potrzebna jest konfiguracja Associated Domains oraz plik apple-app-site-association opisany niżej.
Konfiguracja MAUI
AndroidManifest.xml
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="appname"
android:host="auth" />
</intent-filter>
Info.plist
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>appname</string>
<string>fb925001276310234</string>
<string>pl.twojadomena.appname</string>
</array>
</dict>
</array>
Associated Domains i apple-app-site-association na iOS
Na iOS sama obsługa niestandardowego schematu URI może nie wystarczyć, jeżeli callback ma wracać przez adres HTTPS używany jako redirect_uri. W takim wariancie trzeba powiązać domenę z aplikacją przez Universal Links.
Po stronie aplikacji iOS należy dodać Associated Domains dla domeny hostującej bridge:
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:twojadomena.pl</string>
</array>
Po stronie hostingu trzeba wystawić plik .well-known/apple-app-site-association, dostępny pod adresem https://twojadomena.pl/.well-known/apple-app-site-association. Plik nie powinien mieć rozszerzenia .json, nie powinien być zwracany przez przekierowanie i powinien być serwowany jako JSON.
Wartość appID to połączenie Apple Team ID oraz Bundle ID. W wygenerowanym pliku Entitlements.xcent można ją znaleźć pod kluczem application-identifier. Dla appname jest to:
W28L83ZB4A.pl.twojadomena.appname
Docelowy plik .well-known/apple-app-site-association powinien zawierać appID oraz ścieżki obsługiwane przez aplikację:
{
"applinks": {
"apps": [],
"details": [
{
"appID": "W28L83ZB4A.pl.twojadomena.appname",
"paths": ["/fb-bridge", "/fb-bridge/*"]
}
]
}
}
Tablica apps jest historyczna i zostaje pusta. Ważne są appID oraz ścieżki /fb-bridge i /fb-bridge/*, bo to one pozwalają iOS przekazać URL zwrotny do aplikacji. Jeżeli plik zawiera same paths bez identyfikatora aplikacji, iOS nie ma pełnej informacji, do której aplikacji przypisać link.
assetlinks.json dla Android App Links
Dla samego callbacku appname://auth Android używa niestandardowego schematu URI. Jeżeli jednak domena ma obsługiwać również Android App Links, w katalogu .well-known warto utrzymywać assetlinks.json. W hostingu appname plik wskazuje pakiet pl.twojadomena.appname oraz odciski certyfikatów SHA-256.
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "pl.twojadomena.appname",
"sha256_cert_fingerprints": [
"11:CA:1F:A5:1D:43:15:AC:D6:4F:64:51:97:9F:BD:68:AB:EF:31:F5:A6:A5:FD:CA:F5:68:2F:62:D7:C8:A4:C1",
"A2:C5:52:EB:5E:E9:DA:63:DE:CB:BA:B9:45:3F:C4:FD:24:B8:DA:FD:CD:58:CE:9A:0D:31:A2:53:04:A4:FA:03"
]
}
}
]
Użyj wcześniej keytool do wygenerowania odcisków certyfikatów:
keytool -list -v -keystore -alias
Obsługa callback na iOS
W AppDelegate:
public override bool OpenUrl(
UIApplication app,
NSUrl url,
NSDictionary options)
{
return WebAuthenticator.Callback(
new Uri(url.AbsoluteString));
}
Jeżeli iOS zwraca wynik przez Universal Link, trzeba obsłużyć również ContinueUserActivity:
public override bool ContinueUserActivity(
UIApplication application,
NSUserActivity userActivity,
UIApplicationRestorationHandler completionHandler)
{
if (userActivity.ActivityType == NSUserActivityType.BrowsingWeb &&
userActivity.WebPageUrl is not null)
{
return WebAuthenticator.Callback(
new Uri(userActivity.WebPageUrl.AbsoluteString));
}
return base.ContinueUserActivity(
application,
userActivity,
completionHandler);
}
Rozpoczęcie logowania
const string fbAppId = "925001276310234";
const string facebookRedirectUri = "https://twojadomena.pl/fb-bridge";
const string appCallbackUri = "appname://auth";
var pkce = PkceUtil.CreateS256();
Budowanie URL:
var authUri = new Uri(
"https://www.facebook.com/v19.0/dialog/oauth" +
$"?client_id={fbAppId}" +
$"&redirect_uri={Uri.EscapeDataString(facebookRedirectUri)}" +
$"&response_type=code" +
$"&code_challenge={pkce.CodeChallenge}" +
$"&code_challenge_method=S256" +
$"&scope=public_profile,email");
Uruchomienie logowania:
var callbackUri =
DeviceInfo.Platform == DevicePlatform.iOS
? new Uri(facebookRedirectUri)
: new Uri(appCallbackUri);
var result =
await WebAuthenticator.Default.AuthenticateAsync(
authUri,
callbackUri);
Po poprawnym logowaniu:
var authCode = result.Properties["code"];
Wymiana code na access_token
Facebook wymaga wykonania dodatkowego wywołania:
var tokenUrl =
"https://graph.facebook.com/v19.0/oauth/access_token" +
$"?client_id={fbAppId}" +
$"&redirect_uri={Uri.EscapeDataString(facebookRedirectUri)}" +
$"&code={Uri.EscapeDataString(authCode)}" +
$"&code_verifier={Uri.EscapeDataString(pkce.CodeVerifier)}";
Pobranie tokena:
var tokenJson =
await http.GetStringAsync(tokenUrl);
Pobranie danych użytkownika
var meJson =
await http.GetStringAsync(
$"https://graph.facebook.com/me?fields=id,email&access_token={accessToken}");
Przykładowa odpowiedź:
{
"id": "1307126283021094",
"email": "user@example.com"
}
Logowanie do własnego backendu
Po uzyskaniu facebookUserId i accessToken można przekazać je do własnego API:
ProfileManagerInstance.LoginBySocialConnect(
userId,
accessToken,
SocialConnectRequestModel.SocialConnect.Facebook);
Backend powinien:
- Zweryfikować token przez Graph API.
- Pobrać dane użytkownika.
- Utworzyć konto lub odnaleźć istniejące.
- Zalogować użytkownika.
- Wysłać komunikat powitalny do aplikacji.
Typowe problemy
Facebook zwraca: Brak adresu URL przekierowania w parametrach
Przyczyną jest zwykle redirect_uri, które nie jest zgodne z wpisem w Valid OAuth Redirect URIs.
AuthenticateAsync nigdy nie wraca
Najczęstsze przyczyny:
- brak konfiguracji URL Scheme,
- brak
OpenUrlwAppDelegate, - brak Associated Domains albo pliku
.well-known/apple-app-site-associationdla callbacku HTTPS na iOS, - brak obsługi
ContinueUserActivitydla Universal Links na iOS, - bridge nie wykonuje przekierowania,
- redirect trafia na stronę HTML zamiast do aplikacji.
Android działa, iOS nie
Najczęściej oznacza to brak WebAuthenticator.Callback(...) w AppDelegate, brak obsługi Universal Links albo niepoprawny appID w pliku apple-app-site-association.
Zalety rozwiązania
- brak Facebook SDK,
- wspólny kod Android/iOS,
- prostsza migracja Xamarin -> MAUI,
- możliwość integracji z własnym backendem,
- pełna kontrola nad procesem OAuth.
Podsumowanie
Połączenie .NET MAUI, WebAuthenticator oraz własnego skryptu fb-bridge.php pozwala wdrożyć logowanie Facebook bez używania natywnego SDK Facebooka. Rozwiązanie działa zarówno na Androidzie, jak i na iOS, a jednocześnie pozwala zachować pełną kontrolę nad wymianą tokenów oraz integracją z własnym systemem użytkowników.
W przypadku aplikacji MAUI taki model jest szczególnie wygodny, ponieważ backend PHP już weryfikuje token Facebooka przez Graph API i integruje logowanie z istniejącym systemem użytkowników oraz komunikacją ProtoBuf. Dzięki temu Facebook pełni wyłącznie rolę dostawcy tożsamości, a cała logika autoryzacji pozostaje po stronie własnego serwera.