Εισαγωγή
Ας μιλήσουμε ειλικρινά: η ασφάλεια σε μια mobile εφαρμογή δεν είναι κάτι που μπορείτε να αφήσετε «για μετά». Κάθε μέρα, εκατομμύρια χρήστες εμπιστεύονται τα κινητά τους με κωδικούς, τραπεζικά στοιχεία, ακόμα και ιατρικά αρχεία. Αν η εφαρμογή σας δεν τα προστατεύει σωστά, οι συνέπειες μπορεί να είναι καταστροφικές — και η φήμη σας μαζί τους.
Η καλή είδηση; Στο .NET MAUI, η Microsoft μας δίνει ένα αρκετά ολοκληρωμένο σύνολο εργαλείων ασφαλείας. Από το SecureStorage για κρυπτογραφημένη αποθήκευση, μέχρι τον WebAuthenticator για ροές OAuth 2.0, και ενσωμάτωση βιομετρικής αυθεντικοποίησης μέσω plugins. Σε αυτόν τον οδηγό θα καλύψουμε τα πάντα, με πρακτικά παραδείγματα κώδικα που μπορείτε να εφαρμόσετε αμέσως στα projects σας.
SecureStorage: Κρυπτογραφημένη Αποθήκευση Δεδομένων
Το ISecureStorage API του .NET MAUI σας επιτρέπει να αποθηκεύετε ζεύγη κλειδιού-τιμής με κρυπτογράφηση σε κάθε πλατφόρμα. Σε αντίθεση με τα Preferences (που αποθηκεύουν δεδομένα χωρίς κρυπτογράφηση — ναι, ως plain text), το SecureStorage χρησιμοποιεί τους native μηχανισμούς ασφαλείας κάθε λειτουργικού.
Πώς Λειτουργεί σε Κάθε Πλατφόρμα
- Android: Χρησιμοποιεί
EncryptedSharedPreferencesμε κρυπτογράφηση AES-256 GCM — κλειδιά και τιμές κρυπτογραφούνται αυτόματα - iOS: Αξιοποιεί το Keychain, τον native ασφαλή αποθηκευτικό χώρο της Apple
- Windows: Στα packaged apps, χρησιμοποιεί
ApplicationData.Current.LocalSettingsμε κρυπτογράφηση DPAPI
Βασική Χρήση SecureStorage
Η αποθήκευση και ανάκτηση δεδομένων είναι εξαιρετικά απλή. Δείτε πόσο straightforward είναι:
// Αποθήκευση ευαίσθητου δεδομένου
await SecureStorage.Default.SetAsync("auth_token", jwtToken);
await SecureStorage.Default.SetAsync("refresh_token", refreshToken);
// Ανάκτηση δεδομένου
string token = await SecureStorage.Default.GetAsync("auth_token");
if (string.IsNullOrEmpty(token))
{
// Δεν υπάρχει token - απαιτείται σύνδεση
await NavigateToLoginPage();
}
else
{
// Χρήση του token για API κλήσεις
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
}
// Διαγραφή συγκεκριμένου κλειδιού
SecureStorage.Default.Remove("auth_token");
// Διαγραφή όλων των αποθηκευμένων δεδομένων
SecureStorage.Default.RemoveAll();
SecureStorage με Dependency Injection
Για καλύτερη αρχιτεκτονική και δυνατότητα testing — κάτι που πολλοί developers παραλείπουν στο mobile — χρησιμοποιήστε το ISecureStorage μέσω dependency injection:
// MauiProgram.cs
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
// Εγγραφή του SecureStorage ως service
builder.Services.AddSingleton<ISecureStorage>(SecureStorage.Default);
builder.Services.AddSingleton<ITokenService, TokenService>();
builder.Services.AddTransient<LoginViewModel>();
return builder.Build();
}
// TokenService.cs
public class TokenService : ITokenService
{
private readonly ISecureStorage _secureStorage;
public TokenService(ISecureStorage secureStorage)
{
_secureStorage = secureStorage;
}
public async Task<string?> GetAccessTokenAsync()
{
return await _secureStorage.GetAsync("access_token");
}
public async Task SaveTokensAsync(string accessToken, string refreshToken)
{
await _secureStorage.SetAsync("access_token", accessToken);
await _secureStorage.SetAsync("refresh_token", refreshToken);
}
public void ClearTokens()
{
_secureStorage.Remove("access_token");
_secureStorage.Remove("refresh_token");
}
}
Preferences vs SecureStorage — Πότε Χρησιμοποιείτε Τι
Μια πολύ συχνή σύγχυση, ειδικά σε juniors: πότε χρησιμοποιούμε Preferences και πότε SecureStorage;
- Preferences: Ρυθμίσεις χρήστη (θέμα, γλώσσα, μέγεθος γραμματοσειράς) — δεδομένα που δεν είναι ευαίσθητα
- SecureStorage: Tokens, κωδικοί, API keys, ευαίσθητα δεδομένα χρήστη — οτιδήποτε χρειάζεται κρυπτογράφηση
Ο κανόνας είναι απλός: αν θα σας ενοχλούσε να βρει κάποιος αυτό το δεδομένο σε plain text, τότε πάει στο SecureStorage.
Σημαντικό: Το SecureStorage σχεδιάστηκε για μικρά κείμενα. Αν χρειάζεστε αποθήκευση μεγάλων ευαίσθητων δεδομένων, χρησιμοποιήστε κρυπτογραφημένη βάση δεδομένων (π.χ. SQLCipher).
OAuth 2.0 Authentication με WebAuthenticator
Το .NET MAUI παρέχει τον IWebAuthenticator για browser-based authentication flows. Αυτό σημαίνει ότι μπορείτε να υλοποιήσετε ροές OAuth 2.0 με οποιονδήποτε πάροχο — Google, Microsoft, Facebook, GitHub ή custom IdentityServer. Η λογική είναι η ίδια παντού.
Πώς Λειτουργεί η Ροή
- Η εφαρμογή ανοίγει τον browser του συστήματος στο authorization endpoint
- Ο χρήστης κάνει login στον πάροχο (π.χ. Google)
- Ο πάροχος κάνει redirect στην εφαρμογή μέσω custom URL scheme
- Η εφαρμογή λαμβάνει τον authorization code ή το token
- Τα tokens αποθηκεύονται στο SecureStorage
Αρκετά απλό σαν concept, σωστά; Η υλοποίηση βέβαια θέλει λίγο περισσότερη δουλειά.
Υλοποίηση WebAuthenticator
public class AuthenticationService
{
private const string AuthorizeUrl = "https://your-identity-server.com/connect/authorize";
private const string CallbackUrl = "myapp://callback";
private const string ClientId = "maui-mobile-app";
private readonly ISecureStorage _secureStorage;
public AuthenticationService(ISecureStorage secureStorage)
{
_secureStorage = secureStorage;
}
public async Task<bool> LoginAsync()
{
try
{
// Δημιουργία PKCE code verifier για ασφάλεια
string codeVerifier = GenerateCodeVerifier();
string codeChallenge = GenerateCodeChallenge(codeVerifier);
var authUrl = new Uri(
$"{AuthorizeUrl}?" +
$"client_id={ClientId}&" +
$"response_type=code&" +
$"redirect_uri={Uri.EscapeDataString(CallbackUrl)}&" +
$"scope=openid profile email offline_access&" +
$"code_challenge={codeChallenge}&" +
$"code_challenge_method=S256");
var callbackUri = new Uri(CallbackUrl);
// Εκκίνηση browser-based authentication
WebAuthenticatorResult result =
await WebAuthenticator.Default.AuthenticateAsync(authUrl, callbackUri);
// Ανταλλαγή authorization code με tokens
string authCode = result.Properties["code"];
await ExchangeCodeForTokensAsync(authCode, codeVerifier);
return true;
}
catch (TaskCanceledException)
{
// Ο χρήστης ακύρωσε τη σύνδεση
return false;
}
}
private async Task ExchangeCodeForTokensAsync(
string authCode, string codeVerifier)
{
using var httpClient = new HttpClient();
var tokenRequest = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "authorization_code"),
new KeyValuePair<string, string>("code", authCode),
new KeyValuePair<string, string>("redirect_uri", CallbackUrl),
new KeyValuePair<string, string>("client_id", ClientId),
new KeyValuePair<string, string>("code_verifier", codeVerifier)
});
var response = await httpClient.PostAsync(
"https://your-identity-server.com/connect/token", tokenRequest);
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
var tokenResponse = JsonSerializer.Deserialize<TokenResponse>(json);
// Αποθήκευση tokens στο SecureStorage
await _secureStorage.SetAsync("access_token",
tokenResponse.AccessToken);
await _secureStorage.SetAsync("refresh_token",
tokenResponse.RefreshToken);
}
}
}
Ρύθμιση Platform-Specific για WebAuthenticator
Εδώ είναι που πολλοί κολλάνε. Κάθε πλατφόρμα χρειάζεται ειδική ρύθμιση για να λάβει τα callbacks σωστά:
Android — AndroidManifest.xml:
<activity android:name="myapp.WebAuthenticatorCallbackActivity"
android:exported="true">
<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="myapp" android:host="callback" />
</intent-filter>
</activity>
iOS — Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>myapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
Αν ξεχάσετε αυτές τις ρυθμίσεις, θα σπάσετε το κεφάλι σας ψάχνοντας γιατί ο callback δεν λειτουργεί. Ψιλό tip: ελέγξτε πάντα πρώτα εδώ αν κάτι πάει στραβά.
JWT Token Management και Refresh Strategy
Η σωστή διαχείριση JWT tokens είναι κρίσιμη — και ειλικρινά, είναι από τα πράγματα που λίγοι κάνουν σωστά εξαρχής. Ένα συχνό σενάριο: ο χρήστης ανοίγει την εφαρμογή, πιστεύει ότι είναι συνδεδεμένος, αλλά το access token έχει λήξει και οι API κλήσεις αποτυγχάνουν.
Η λύση; Refresh tokens με αυτόματη ανανέωση.
Αυτόματη Ανανέωση Tokens με HttpClient Handler
public class AuthenticatedHttpClientHandler : DelegatingHandler
{
private readonly ITokenService _tokenService;
private readonly SemaphoreSlim _semaphore = new(1, 1);
public AuthenticatedHttpClientHandler(ITokenService tokenService)
{
_tokenService = tokenService;
InnerHandler = new HttpClientHandler();
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
// Προσθήκη access token στο request
string? token = await _tokenService.GetAccessTokenAsync();
if (!string.IsNullOrEmpty(token))
{
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", token);
}
var response = await base.SendAsync(request, cancellationToken);
// Αν λάβουμε 401, δοκιμάζουμε refresh
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
await _semaphore.WaitAsync(cancellationToken);
try
{
bool refreshed = await _tokenService.RefreshTokenAsync();
if (refreshed)
{
// Επαναποστολή request με νέο token
token = await _tokenService.GetAccessTokenAsync();
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", token);
response = await base.SendAsync(request, cancellationToken);
}
}
finally
{
_semaphore.Release();
}
}
return response;
}
}
Αυτό το pattern εξασφαλίζει ότι τα tokens ανανεώνονται αυτόματα χωρίς ο χρήστης να καταλάβει τίποτα. Το SemaphoreSlim εδώ παίζει σημαντικό ρόλο — αποτρέπει πολλαπλά ταυτόχρονα refresh requests, κάτι που θα ήταν πρόβλημα σε apps με πολλές παράλληλες κλήσεις.
MSAL: Αυθεντικοποίηση με Microsoft Identity
Αν δουλεύετε σε enterprise περιβάλλον και χρειάζεστε ενσωμάτωση με Microsoft Entra ID (πρώην Azure AD), η Microsoft Authentication Library (MSAL) είναι η επίσημη λύση. Υποστηρίζει εταιρικούς λογαριασμούς, προσωπικούς λογαριασμούς Microsoft, και Azure AD B2C.
Εγκατάσταση και Ρύθμιση
// Εγκατάσταση NuGet package
// dotnet add package Microsoft.Identity.Client
// Constants.cs
public static class AuthConstants
{
public const string ClientId = "your-client-id-from-azure-portal";
public static readonly string[] Scopes = { "openid", "profile", "email", "User.Read" };
}
// MsalAuthService.cs
public class MsalAuthService
{
private readonly IPublicClientApplication _pca;
public MsalAuthService()
{
var builder = PublicClientApplicationBuilder
.Create(AuthConstants.ClientId)
.WithRedirectUri($"msal{AuthConstants.ClientId}://auth");
#if ANDROID
builder = builder.WithParentActivityOrWindow(() => Platform.CurrentActivity);
#elif IOS
builder = builder.WithIosKeychainSecurityGroup("com.microsoft.adalcache");
#endif
_pca = builder.Build();
}
public async Task<AuthenticationResult?> SignInAsync()
{
try
{
// Πρώτα δοκιμάζουμε silent authentication
var accounts = await _pca.GetAccountsAsync();
var firstAccount = accounts.FirstOrDefault();
if (firstAccount != null)
{
return await _pca
.AcquireTokenSilent(AuthConstants.Scopes, firstAccount)
.ExecuteAsync();
}
}
catch (MsalUiRequiredException)
{
// Απαιτείται interactive login
}
// Interactive login
return await _pca
.AcquireTokenInteractive(AuthConstants.Scopes)
.ExecuteAsync();
}
public async Task SignOutAsync()
{
var accounts = await _pca.GetAccountsAsync();
foreach (var account in accounts)
{
await _pca.RemoveAsync(account);
}
}
}
Η στρατηγική «silent-first, interactive-fallback» είναι η βέλτιστη πρακτική εδώ. Δοκιμάζετε πρώτα να πάρετε token χωρίς αλληλεπίδραση χρήστη, και μόνο αν αποτύχει, εμφανίζετε τη φόρμα σύνδεσης. Είναι αυτό που περιμένει ο χρήστης — να μην χρειάζεται να κάνει login κάθε φορά που ανοίγει το app.
Βιομετρική Αυθεντικοποίηση (Fingerprint και Face ID)
Η βιομετρική αυθεντικοποίηση προσθέτει ένα επιπλέον επίπεδο ασφαλείας, και ας είμαστε ειλικρινείς — οι χρήστες πλέον το περιμένουν. Αντί να ζητάτε κωδικό πρόσβασης κάθε φορά, χρησιμοποιήστε δακτυλικό αποτύπωμα ή Face ID.
Εγκατάσταση Plugin.Maui.Biometric
Το Plugin.Maui.Biometric είναι ένα δημοφιλές plugin που υποστηρίζει iOS 15+, Android API 26+ (Oreo), macOS 12+ και Windows 10:
// dotnet add package Plugin.Maui.Biometric
// MauiProgram.cs - Εγγραφή στο DI container
builder.Services.AddSingleton<IBiometricAuthenticationService>(
BiometricAuthenticationService.Default);
Ρύθμιση Permissions
Android — AndroidManifest.xml:
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
iOS — Info.plist:
<key>NSFaceIDUsageDescription</key>
<string>Η εφαρμογή χρησιμοποιεί Face ID για ασφαλή πρόσβαση.</string>
Μην ξεχάσετε το NSFaceIDUsageDescription στο iOS — χωρίς αυτό, η Apple θα απορρίψει το app σας στο review.
Υλοποίηση Βιομετρικής Αυθεντικοποίησης
public class BiometricService
{
private readonly IBiometricAuthenticationService _biometric;
public BiometricService(IBiometricAuthenticationService biometric)
{
_biometric = biometric;
}
public async Task<bool> AuthenticateAsync()
{
// Έλεγχος διαθεσιμότητας βιομετρικών
var availability = await _biometric.GetAuthenticationStatusAsync();
if (availability != BiometricAuthenticationStatus.Available)
{
// Βιομετρικά δεν είναι διαθέσιμα σε αυτή τη συσκευή
return false;
}
// Εκκίνηση βιομετρικής αυθεντικοποίησης
var request = new AuthenticationRequest
{
Title = "Επιβεβαίωση Ταυτότητας",
NegativeText = "Χρήση Κωδικού",
AllowPasswordAuth = true // Fallback σε PIN/Password
};
var result = await _biometric.AuthenticateAsync(request);
return result.Status == BiometricAuthenticationStatus.Success;
}
}
// Χρήση στο ViewModel
public partial class LoginViewModel : ObservableObject
{
private readonly BiometricService _biometricService;
private readonly ITokenService _tokenService;
[RelayCommand]
private async Task BiometricLoginAsync()
{
bool authenticated = await _biometricService.AuthenticateAsync();
if (authenticated)
{
// Ο χρήστης επαληθεύτηκε — ανάκτηση αποθηκευμένου token
string? token = await _tokenService.GetAccessTokenAsync();
if (!string.IsNullOrEmpty(token))
{
await Shell.Current.GoToAsync("//main");
}
else
{
// Token δεν υπάρχει, απαιτείται πλήρης σύνδεση
await FullLoginAsync();
}
}
}
}
Βέλτιστες Πρακτικές Ασφαλείας
Πέρα από τα εργαλεία, η ασφάλεια απαιτεί σωστή νοοτροπία. Από προσωπική εμπειρία, οι περισσότερες ευπάθειες σε mobile apps δεν οφείλονται σε σπάνια exploits, αλλά σε βασικά λάθη. Ας δούμε τα πιο κρίσιμα.
1. Ποτέ Μην Αποθηκεύετε API Keys στον Client
Ένα κλασικό λάθος — και ίσως το πιο συνηθισμένο που βλέπω σε code reviews: API keys μέσα στον κώδικα της εφαρμογής. Ακόμα και με obfuscation, ένας αποφασισμένος εισβολέας μπορεί να τα εξάγει.
// ΜΗΝ ΚΑΝΕΤΕ ΑΥΤΟ
public static class ApiConfig
{
public const string ApiKey = "sk-12345-my-secret-key"; // Εκτεθειμένο!
}
// ΣΩΣΤΗ ΠΡΟΣΕΓΓΙΣΗ: Λήψη κλειδιού από τον server μετά την αυθεντικοποίηση
public async Task<string> GetApiKeyAsync()
{
var httpClient = _httpClientFactory.CreateClient("AuthenticatedClient");
var response = await httpClient.GetAsync("/api/config/key");
return await response.Content.ReadAsStringAsync();
}
2. Προσοχή στο Android Auto Backup
Αυτό πιάνει πολλούς εξαπίνης. Στο Android 6.0+, το Auto Backup μπορεί να δημιουργήσει αντίγραφα ασφαλείας των EncryptedSharedPreferences. Τα αντίγραφα αυτά δεν μπορούν να αποκρυπτογραφηθούν σε άλλη συσκευή (τα κλειδιά είναι device-specific). Το .NET MAUI χειρίζεται αυτό αυτόματα, αλλά μπορείτε να εξαιρέσετε ρητά το SecureStorage:
<!-- backup_rules.xml -->
<data-extraction-rules>
<cloud-backup>
<exclude domain="sharedpref"
path="__androidx_security_crypto_encrypted_prefs__"/>
</cloud-backup>
</data-extraction-rules>
3. Χρησιμοποιήστε HTTPS Παντού
Φαίνεται αυτονόητο, αλλά θα εκπλαγείτε πόσοι developers ξεχνούν να αποκλείσουν το HTTP σε production. Στο Android, ενεργοποιήστε το Network Security Configuration:
<!-- network_security_config.xml -->
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>
4. Μην Εμφανίζετε Tokens στα Logs
Ένα ακόμα «κλασικό»: κατά τη διάρκεια του debugging, βάζετε ένα Debug.WriteLine με το token, και μετά ξεχνάτε να το αφαιρέσετε.
// ΜΗΝ ΚΑΝΕΤΕ ΑΥΤΟ
Debug.WriteLine($"Token: {accessToken}");
// ΣΩΣΤΑ: Logging χωρίς ευαίσθητα δεδομένα
Debug.WriteLine("Token retrieved successfully");
Debug.WriteLine($"Token length: {accessToken?.Length ?? 0}");
5. Υλοποιήστε Certificate Pinning για Κρίσιμα APIs
Το certificate pinning αποτρέπει man-in-the-middle επιθέσεις, ακόμα και αν ο εισβολέας έχει εγκαταστήσει ένα rogue CA certificate στη συσκευή. Δεν χρειάζεται παντού — αλλά για banking ή health apps, είναι must:
public class CertificatePinningHandler : HttpClientHandler
{
private readonly string _expectedThumbprint;
public CertificatePinningHandler(string expectedThumbprint)
{
_expectedThumbprint = expectedThumbprint;
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
{
if (cert == null) return false;
var actualThumbprint = cert.GetCertHashString();
return string.Equals(
actualThumbprint,
_expectedThumbprint,
StringComparison.OrdinalIgnoreCase);
};
}
}
Ολοκληρωμένο Παράδειγμα: Ροή Authentication
Λοιπόν, ας δούμε πώς συνδυάζονται όλα τα παραπάνω σε μια πραγματική εφαρμογή. Αυτό είναι ουσιαστικά ο «κορμός» του authentication flow:
// App.xaml.cs — Έλεγχος κατάστασης αυθεντικοποίησης κατά την εκκίνηση
public partial class App : Application
{
private readonly ITokenService _tokenService;
private readonly BiometricService _biometricService;
public App(ITokenService tokenService, BiometricService biometricService)
{
InitializeComponent();
_tokenService = tokenService;
_biometricService = biometricService;
}
protected override Window CreateWindow(IActivationState? activationState)
{
return new Window(new AppShell());
}
// Κατά την εκκίνηση, αποφασίζουμε αν ο χρήστης πρέπει
// να κάνει login ή μπορεί να μπει κατευθείαν
public async Task<bool> TryAutoLoginAsync()
{
string? token = await _tokenService.GetAccessTokenAsync();
if (string.IsNullOrEmpty(token))
return false;
// Αν υπάρχει token, ζητάμε βιομετρική επαλήθευση
bool biometricOk = await _biometricService.AuthenticateAsync();
if (!biometricOk)
return false;
// Επαληθεύουμε ότι το token είναι ακόμα έγκυρο
bool isValid = await _tokenService.ValidateTokenAsync();
if (!isValid)
{
// Δοκιμή refresh
return await _tokenService.RefreshTokenAsync();
}
return true;
}
}
Η σειρά εδώ έχει σημασία: πρώτα ελέγχουμε αν υπάρχει token, μετά βιομετρική επαλήθευση, και τέλος validation. Αν κάτι αποτύχει σε οποιοδήποτε βήμα, ο χρήστης πηγαίνει στο login.
Συχνές Ερωτήσεις (FAQ)
Είναι ασφαλές το SecureStorage στο .NET MAUI για αποθήκευση JWT tokens;
Ναι, και είναι η συνιστώμενη μέθοδος. Χρησιμοποιεί τους native μηχανισμούς κρυπτογράφησης κάθε πλατφόρμας — Keychain στο iOS, EncryptedSharedPreferences με AES-256 GCM στο Android. Απλά μην αποθηκεύετε μεγάλα αντικείμενα σε αυτό, γιατί δεν σχεδιάστηκε για αυτό.
Πώς μπορώ να υλοποιήσω OAuth 2.0 σε .NET MAUI χωρίς backend;
Χρησιμοποιήστε τον WebAuthenticator με τη ροή Authorization Code + PKCE. Η ροή PKCE σχεδιάστηκε ειδικά για public clients (σαν τα mobile apps) που δεν μπορούν να αποθηκεύσουν client secrets με ασφάλεια. Συνδυάστε με δημόσιους παρόχους όπως Auth0, Okta, ή Google Identity — λειτουργεί άψογα.
Το Plugin.Maui.Biometric υποστηρίζει ταυτόχρονα fingerprint και Face ID;
Ναι. Το plugin αναγνωρίζει αυτόματα τους διαθέσιμους βιομετρικούς μηχανισμούς. Σε iPhone χρησιμοποιεί Face ID, σε Android και παλιότερα iPhone χρησιμοποιεί δακτυλικό αποτύπωμα. Μπορείτε επίσης να ενεργοποιήσετε fallback σε PIN/Password με AllowPasswordAuth = true.
Πώς χειρίζομαι τη λήξη refresh tokens;
Όταν ένα refresh token λήξει, δεν υπάρχει τρόπος ανανέωσης χωρίς αλληλεπίδραση χρήστη — αυτό είναι by design. Η σωστή προσέγγιση: αν το refresh αποτύχει, ανακατευθύνετε τον χρήστη στο login. Χρησιμοποιήστε ένα DelegatingHandler (όπως δείξαμε παραπάνω) για αυτόματο refresh, και αν αποτύχει, εκκινήστε νέα ροή σύνδεσης.
Μπορώ να χρησιμοποιήσω certificate pinning σε production;
Ναι, αλλά με προσοχή. Αν αλλάξει το πιστοποιητικό του server χωρίς να ενημερωθεί η εφαρμογή, οι χρήστες δεν θα μπορούν να συνδεθούν — και αυτό δεν είναι ωραίο. Χρησιμοποιήστε pinning στο public key αντί για το πλήρες πιστοποιητικό, και εφαρμόστε μηχανισμό remote configuration για ενημέρωση του pin χωρίς νέο release.